const { execute, queryOne, query, transaction } = require('../database/postgres') const crypto = require('crypto') const NotificationService = require('../services/notificationService') class RaffleResult { /** * Executa o sorteio de um raffle de forma segura e auditável. */ static async draw(raffle_id, executedByIp = '127.0.0.1') { const txResult = await transaction(async (client) => { // 1. Lock do sorteio const raffleResult = await client.query( "SELECT id, titulo, end_at, status FROM raffles WHERE id = $1 AND status != 'finished' FOR UPDATE", [raffle_id] ) const raffle = raffleResult.rows[0] if (!raffle) throw new Error('Sorteio não encontrado ou já finalizado') // 2. Busca todos os tickets vendidos, ordenados pela data e id (ordem determinística) const ticketsResult = await client.query( 'SELECT id, numero FROM tickets WHERE raffle_id = $1 ORDER BY id ASC', [raffle_id] ) const tickets = ticketsResult.rows if (tickets.length === 0) throw new Error('Nenhum ticket vendido para este sorteio') const totalSold = tickets.length const lastTicketId = tickets[tickets.length - 1].id // 3. Gerar SEED fixo e auditável (SHA256) // Baseado em: id do sorteio + end_at + total vendido + id do último ticket const endAtISO = raffle.end_at ? new Date(raffle.end_at).toISOString() : 'no-end-date' const seedString = `${raffle_id}-${endAtISO}-${totalSold}-${lastTicketId}` const seed = crypto.createHash('sha256').update(seedString).digest('hex') // 4. Transformar seed (hex) em um número inteiro grande usando BigInt para matemática justa const seedBigInt = BigInt('0x' + seed) const winnerIndex = Number(seedBigInt % BigInt(totalSold)) const winnerTicket = tickets[winnerIndex] // 5. Salvar resultado e logs await client.query( `INSERT INTO raffle_results (raffle_id, winner_ticket_id, seed, algorithm_version) VALUES ($1, $2, $3, $4)`, [raffle_id, winnerTicket.id, seed, '1.0'] ) await client.query( `INSERT INTO raffle_draw_logs (raffle_id, seed, result_number, ip) VALUES ($1, $2, $3, $4)`, [raffle_id, seed, winnerTicket.numero, executedByIp] ) // 6. Atualizar status do sorteio await client.query( "UPDATE raffles SET status = 'finished', ativo = 0 WHERE id = $1", [raffle_id] ) return { winner_ticket_id: winnerTicket.id, winner_number: winnerTicket.numero, seed, winner_cpf: winnerTicket.usuario_cpf, winner_nome: winnerTicket.usuario_nome, winner_telefone: winnerTicket.usuario_telefone, raffle_titulo: raffle.titulo, } }) // Notificação ao ganhador FORA da transação — garante que só dispara após COMMIT if (txResult.winner_cpf) { setImmediate(() => { NotificationService.notifyWinner( txResult.winner_cpf, txResult.winner_nome, txResult.winner_telefone, txResult.raffle_titulo || 'Sorteio #' + raffle_id, txResult.winner_number ).catch(console.error) }) } return { winner_ticket_id: txResult.winner_ticket_id, winner_number: txResult.winner_number, seed: txResult.seed, } } static findByRaffleId(raffle_id) { return queryOne(` SELECT rr.*, t.numero as winner_number, t.usuario_nome, t.usuario_telefone, t.usuario_cpf FROM raffle_results rr JOIN tickets t ON t.id = rr.winner_ticket_id WHERE rr.raffle_id = $1 `, [raffle_id]) } } module.exports = RaffleResult