Files
mercado.clube67.com/backend/models/RaffleResult.js
T

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