'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