105 lines
3.7 KiB
JavaScript
105 lines
3.7 KiB
JavaScript
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
|