141 lines
5.3 KiB
JavaScript
141 lines
5.3 KiB
JavaScript
'use strict'
|
||
|
||
const cron = require('node-cron')
|
||
const { query, execute } = require('../database/postgres')
|
||
const RaffleResult = require('../models/RaffleResult')
|
||
const Ticket = require('../models/Ticket')
|
||
const WhatsappCore = require('../services/whatsappService')
|
||
|
||
const BASE_URL = process.env.APP_URL || 'http://localhost:4001'
|
||
|
||
function startWorker() {
|
||
console.log('⏳ Worker de Sorteio Automático iniciado (node-cron)')
|
||
|
||
// ── A cada minuto: sorteio + liberar reservas + alerta expiração ───────────
|
||
cron.schedule('* * * * *', async () => {
|
||
try {
|
||
await Ticket.releaseExpiredReservations()
|
||
await _alertReservationExpiring()
|
||
|
||
const raffles = await query("SELECT id, total_numeros, end_at FROM raffles WHERE status = 'active'")
|
||
for (const raffle of raffles) {
|
||
let shouldDraw = false
|
||
let reason = ''
|
||
|
||
if (raffle.end_at && new Date() >= new Date(raffle.end_at)) {
|
||
shouldDraw = true
|
||
reason = 'Data limite atingida'
|
||
}
|
||
|
||
if (!shouldDraw) {
|
||
const totalSold = await Ticket.countByRaffle(raffle.id)
|
||
if (totalSold >= raffle.total_numeros) {
|
||
shouldDraw = true
|
||
reason = 'Lotação máxima atingida (100% vendido)'
|
||
}
|
||
}
|
||
|
||
if (shouldDraw) {
|
||
console.log(`🤖 [Worker] Iniciando sorteio automático #${raffle.id} - Motivo: ${reason}`)
|
||
try {
|
||
const result = await RaffleResult.draw(raffle.id, '127.0.0.1')
|
||
console.log(`🎉 [Worker] Sorteio #${raffle.id} finalizado! Ganhador: Ticket #${result.winner_ticket_id} (Número ${result.winner_number})`)
|
||
} catch (drawError) {
|
||
if (drawError.message === 'Nenhum ticket vendido para este sorteio') {
|
||
console.log(`⚠️ [Worker] Sorteio #${raffle.id} ignorado: nenhum ticket vendido.`)
|
||
} else {
|
||
console.error(`❌ [Worker] Erro ao executar sorteio #${raffle.id}:`, drawError.message)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('❌ [Worker] Falha na checagem cron (minuto):', e.message)
|
||
}
|
||
})
|
||
|
||
// ── A cada 10 minutos: notificação "Últimas Horas" ─────────────────────────
|
||
cron.schedule('*/10 * * * *', async () => {
|
||
try {
|
||
await _alertLastHours()
|
||
} catch (e) {
|
||
console.error('❌ [Worker] Falha na checagem last_hours:', e.message)
|
||
}
|
||
})
|
||
}
|
||
|
||
// ─── Alerta "Últimas Horas" ───────────────────────────────────────────────────
|
||
// Dispara 1× por sorteio cujo end_at está entre agora e +2h,
|
||
// e que ainda não foi notificado (last_hours_notif_sent IS NULL).
|
||
async function _alertLastHours() {
|
||
const rows = await query(
|
||
`SELECT id, titulo FROM raffles
|
||
WHERE status = 'active'
|
||
AND end_at IS NOT NULL
|
||
AND end_at BETWEEN NOW() AND NOW() + INTERVAL '2 hours'
|
||
AND last_hours_notif_sent IS NULL`
|
||
)
|
||
|
||
for (const raffle of (rows ?? [])) {
|
||
// Marca antes de enviar — evita duplos em caso de erro parcial
|
||
await execute(`UPDATE raffles SET last_hours_notif_sent = NOW() WHERE id = $1`, [raffle.id])
|
||
|
||
const users = await query(
|
||
`SELECT DISTINCT u.id, u.telefone, u.cpf, u.nome
|
||
FROM users u
|
||
WHERE u.telefone IS NOT NULL
|
||
AND u.whatsapp_optin = TRUE
|
||
AND u.cpf NOT IN (
|
||
SELECT DISTINCT t.usuario_cpf FROM tickets t
|
||
WHERE t.raffle_id = $1 AND t.status = 'sold'
|
||
)`,
|
||
[raffle.id]
|
||
)
|
||
|
||
const link = `${BASE_URL}/sorteio`
|
||
for (const u of (users ?? [])) {
|
||
WhatsappCore.sendNotification({
|
||
userId: u.id, phone: u.telefone, type: 'last_hours', userCpf: u.cpf,
|
||
variables: { raffleName: raffle.titulo, link },
|
||
}).catch(() => {})
|
||
}
|
||
console.log(`⏰ [Worker] last_hours rifa #${raffle.id} — ${(users ?? []).length} notificação(ões) enfileirada(s)`)
|
||
}
|
||
}
|
||
|
||
// ─── Alerta "Reserva Expirando" ───────────────────────────────────────────────
|
||
// Reservas com reserved_until entre 50s e 70s no futuro (≈ 1 min restante).
|
||
async function _alertReservationExpiring() {
|
||
const rows = await query(
|
||
`SELECT DISTINCT t.usuario_telefone, t.usuario_cpf, t.usuario_nome,
|
||
o.pix_copy_paste, o.qr_code_url, r.titulo as raffle_titulo
|
||
FROM tickets t
|
||
JOIN orders o ON o.id = t.order_id
|
||
JOIN raffles r ON r.id = t.raffle_id
|
||
WHERE t.status = 'reserved'
|
||
AND o.status = 'pending'
|
||
AND t.usuario_telefone IS NOT NULL
|
||
AND t.reserved_until BETWEEN NOW() + INTERVAL '50 seconds'
|
||
AND NOW() + INTERVAL '70 seconds'`
|
||
)
|
||
|
||
for (const t of (rows ?? [])) {
|
||
WhatsappCore.sendNotification({
|
||
userId: null,
|
||
phone: t.usuario_telefone,
|
||
type: 'reservation_expiring',
|
||
userCpf: t.usuario_cpf,
|
||
variables: {
|
||
raffleName: t.raffle_titulo,
|
||
link: t.qr_code_url || `${BASE_URL}/area-do-usuario`,
|
||
},
|
||
}).catch(() => {})
|
||
}
|
||
|
||
if ((rows ?? []).length > 0) {
|
||
console.log(`⚠️ [Worker] reservation_expiring: ${rows.length} alerta(s) enviado(s)`)
|
||
}
|
||
}
|
||
|
||
module.exports = { startWorker }
|