136 lines
4.4 KiB
JavaScript
136 lines
4.4 KiB
JavaScript
'use strict'
|
|
|
|
const { execute, queryOne, query, transaction } = require('../database/postgres')
|
|
|
|
class Ticket {
|
|
/**
|
|
* Reserva atômica: verifica disponibilidade e insere em uma única transação com status reservado.
|
|
*/
|
|
static async reserve({ raffle_id, numeros, usuario_nome, usuario_telefone, usuario_cpf, order_id, preco_pago }) {
|
|
return transaction(async (client) => {
|
|
const raffle = await client.query(
|
|
'SELECT id, ativo, total_numeros FROM raffles WHERE id = $1 FOR UPDATE',
|
|
[raffle_id]
|
|
)
|
|
if (!raffle.rows[0]) throw new Error('Sorteio não encontrado')
|
|
if (!raffle.rows[0].ativo) throw new Error('Sorteio encerrado')
|
|
|
|
const insertIds = []
|
|
|
|
// Permitir reservar múltiplos números
|
|
const numsToReserve = Array.isArray(numeros) ? numeros : [numeros]
|
|
|
|
for (const numero of numsToReserve) {
|
|
if (numero < 1 || numero > raffle.rows[0].total_numeros) throw new Error(`Número inválido: ${numero}`)
|
|
|
|
// Verifica se número já foi vendido ou reservado ativamente
|
|
const existing = await client.query(
|
|
"SELECT id FROM tickets WHERE raffle_id = $1 AND numero = $2 AND (status = 'sold' OR (status = 'reserved' AND reserved_until > NOW()))",
|
|
[raffle_id, numero]
|
|
)
|
|
if (existing.rows.length > 0) throw new Error(`Número ${numero} já está reservado ou vendido`)
|
|
|
|
// Remove reserva expirada se existir
|
|
await client.query(
|
|
"DELETE FROM tickets WHERE raffle_id = $1 AND numero = $2 AND status = 'reserved' AND reserved_until <= NOW()",
|
|
[raffle_id, numero]
|
|
)
|
|
|
|
// Insere o ticket como reservado (+5 min TTL)
|
|
const result = await client.query(
|
|
`INSERT INTO tickets (raffle_id, numero, usuario_nome, usuario_telefone, usuario_cpf, status, reserved_until, order_id, price_paid)
|
|
VALUES ($1, $2, $3, $4, $5, 'reserved', NOW() + INTERVAL '5 minutes', $6, $7) RETURNING id`,
|
|
[raffle_id, numero, usuario_nome, usuario_telefone || null, usuario_cpf || null, order_id, preco_pago]
|
|
)
|
|
insertIds.push(result.rows[0].id)
|
|
}
|
|
|
|
return { ids: insertIds }
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Confirma o pagamento convertendo reservados em vendidos
|
|
*/
|
|
static async confirmOrder(order_id) {
|
|
return execute(
|
|
"UPDATE tickets SET status = 'sold', reserved_until = NULL WHERE order_id = $1 AND status = 'reserved'",
|
|
[order_id]
|
|
)
|
|
}
|
|
|
|
static getByOrderId(order_id) {
|
|
return query(
|
|
'SELECT t.*, r.titulo as raffle_nome FROM tickets t JOIN raffles r ON r.id = t.raffle_id WHERE t.order_id = $1',
|
|
[order_id]
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Libera tickets reservados não pagos
|
|
*/
|
|
static async releaseExpiredReservations() {
|
|
return execute("DELETE FROM tickets WHERE status = 'reserved' AND reserved_until <= NOW()")
|
|
}
|
|
|
|
static findByRaffle(raffle_id) {
|
|
return query(
|
|
'SELECT * FROM tickets WHERE raffle_id = $1 ORDER BY numero ASC',
|
|
[raffle_id]
|
|
)
|
|
}
|
|
|
|
static findByNumber(raffle_id, numero) {
|
|
return queryOne(
|
|
'SELECT * FROM tickets WHERE raffle_id = $1 AND numero = $2',
|
|
[raffle_id, numero]
|
|
)
|
|
}
|
|
|
|
static getSoldNumbers(raffle_id) {
|
|
return query("SELECT numero FROM tickets WHERE raffle_id = $1 AND (status = 'sold' OR (status = 'reserved' AND reserved_until > NOW()))", [raffle_id])
|
|
.then(rows => rows.map(r => r.numero))
|
|
}
|
|
|
|
static async countByRaffle(raffle_id) {
|
|
const row = await queryOne(
|
|
"SELECT COUNT(*) as total FROM tickets WHERE raffle_id = $1 AND (status = 'sold' OR (status = 'reserved' AND reserved_until > NOW()))",
|
|
[raffle_id]
|
|
)
|
|
return row ? parseInt(row.total) : 0
|
|
}
|
|
|
|
static getAll() {
|
|
return query(`
|
|
SELECT t.*, r.titulo AS raffle_name
|
|
FROM tickets t
|
|
JOIN raffles r ON t.raffle_id = r.id
|
|
ORDER BY t.data_compra DESC
|
|
`)
|
|
}
|
|
|
|
static deleteById(id) {
|
|
return execute('DELETE FROM tickets WHERE id = $1', [id])
|
|
}
|
|
|
|
static deleteByIdAndTenant(id, tenantId) {
|
|
return execute(
|
|
'DELETE FROM tickets t USING raffles r WHERE t.id = $1 AND t.raffle_id = r.id AND r.tenant_id = $2',
|
|
[id, tenantId]
|
|
)
|
|
}
|
|
|
|
static findByUserCpf(cpf) {
|
|
const cleanCpf = cpf.replace(/[^\d]+/g, '')
|
|
return query(`
|
|
SELECT t.*, r.titulo AS raffle_name
|
|
FROM tickets t
|
|
JOIN raffles r ON t.raffle_id = r.id
|
|
WHERE t.usuario_cpf = $1
|
|
ORDER BY t.data_compra DESC
|
|
`, [cleanCpf])
|
|
}
|
|
}
|
|
|
|
module.exports = Ticket
|