Files

265 lines
7.3 KiB
JavaScript

'use strict'
const { Queue } = require('bullmq')
const Redis = require('ioredis')
const WhatsappLog = require('../models/WhatsappLog')
const { queryOne } = require('../database/postgres')
const connection = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
maxRetriesPerRequest: null
})
const whatsappQueue = new Queue('whatsapp-notifications', { connection })
// Tipos críticos — enviados mesmo sem opt-in explícito (transacionais)
const CRITICAL_TYPES = new Set([
'purchase_confirmed',
'purchase_pending',
'winner',
'reservation_expiring',
'security_login',
'club_welcome',
'club_payment_confirmed',
'club_payment_overdue',
'club_suspended',
'club_canceled',
'club_payment_reminder',
])
function parseTemplate(type, vars) {
const templates = {
'purchase_confirmed':
`🎟️ *Compra confirmada!*
Olá! Recebemos seu pagamento com sucesso.
Números Garantidos: *{numeros}*
Valor: *R$ {valor}*
Acesse sua conta para ver os detalhes:
{link}
Boa sorte! 🍀`,
'purchase_pending':
`⏳ *Finalize seu pagamento*
Seus números no Alemão Conveniências estão reservados por tempo limitado!
Para garantir sua participação, efetue o pagamento usando a chave PIX abaixo:
Chave copia-e-cola:
{pixCopiaECola}
Ou acesse o QRCode no link: {link}`,
'winner':
`🎉 *PARABÉNS, {nome}!* 🎉
Seu ticket *{numero}* foi SORTEADO na rifa *{raffleName}*!
Acesse nossa Área do Cliente para resgatar seu prêmio agora mesmo:
{link}`,
'last_hours':
`⏰ *Últimas horas!*
O sorteio da rifa *{raffleName}* encerra em breve.
Ainda dá tempo de participar!
👉 {link}`,
'price_drop':
`⚡ *QUEDA DE PREÇO FLASH* ⚡
Os números da rifa *{raffleName}* baixaram para apenas *R$ {novo_valor}*!
Corra antes que acabe:
👉 {link}`,
'price_drop_local':
`🔔 *Seu número ficou mais barato!*
Você tem uma reserva na rifa *{raffleName}*.
O preço caiu para *R$ {novo_valor}* — finalize agora antes que expire:
👉 {link}`,
'reservation_expiring':
`⚠️ *Sua reserva vai expirar!*
Você tem apenas *1 minuto* para finalizar o pagamento e garantir seu(s) número(s) na rifa *{raffleName}*.
Pague agora: {link}`,
'welcome':
`👋 *Bem-vindo ao Alemão Conveniências!*
Olá, *{nome}*! Seu cadastro foi realizado com sucesso.
Aqui você encontra promoções exclusivas e participa dos nossos sorteios. Acesse sua área:
{link}`,
'claim_received':
`✅ *Resgate solicitado!*
Oi, *{nome}*! Recebemos sua solicitação de resgate do prêmio da rifa *{raffleName}* (ticket #{numero}).
Nossa equipe analisará e entrará em contato em breve. 🎁`,
'claim_approved':
`🏆 *Prêmio aprovado!*
Boa notícia, *{nome}*! Seu prêmio da rifa *{raffleName}* foi *aprovado* pela nossa equipe.
Em breve entraremos em contato para combinar a entrega. 📦`,
'claim_delivered':
`🎁 *Prêmio entregue!*
Parabéns, *{nome}*! Seu prêmio da rifa *{raffleName}* foi marcado como *entregue*.
Obrigado por participar do Alemão Conveniências! Até o próximo sorteio! 🍀`,
'security_login':
`🔐 *Novo acesso à sua conta*
Olá, *{nome}*! Detectamos um acesso à sua conta no Alemão Conveniências em *{data}* às *{hora}*.
Se não foi você, acesse a loja ou entre em contato conosco imediatamente.`,
'password_reset':
`🔑 *Redefinição de senha*
Olá, *{nome}*! Recebemos uma solicitação para redefinir sua senha.
Clique no link abaixo para criar uma nova senha (válido por 1 hora):
{link}
Se não foi você, ignore esta mensagem.`,
'club_welcome':
`🎉 *Bem-vindo ao {clubName}!*
Olá, *{nome}*! Sua assinatura do plano *{planName}* foi recebida e está sendo processada.
Assim que o pagamento for confirmado, sua carteirinha digital estará disponível:
{link}`,
'club_payment_confirmed':
`✅ *Pagamento confirmado — {clubName}*
Oi, *{nome}*! Seu pagamento mensal do *{clubName}* foi confirmado.
Acesse sua carteirinha digital e aproveite todos os benefícios! 😊`,
'club_payment_overdue':
`⚠️ *Pagamento em atraso — {clubName}*
Olá, *{nome}*! Identificamos que seu pagamento mensal do *{clubName}* está em atraso.
Regularize agora para não perder seus benefícios:
{link}`,
'club_payment_reminder':
`🔔 *Lembrete de vencimento — {clubName}*
Oi, *{nome}*! Seu pagamento mensal do *{clubName}* vence em *{dueDate}*.
Pague com antecedência e mantenha seus benefícios ativos:
{link}`,
'club_suspended':
`⛔ *Plano suspenso — {clubName}*
Olá, *{nome}*. Seu plano no *{clubName}* foi *suspenso* por falta de pagamento.
Para reativar, regularize seu pagamento:
{link}`,
'club_canceled':
`📋 *Cancelamento confirmado — {clubName}*
Olá, *{nome}*. Sua assinatura do *{clubName}* foi cancelada conforme solicitado.
Se mudar de ideia, você pode assinar novamente a qualquer momento. Obrigado por ter sido nosso cliente! 🙏`,
'club_medico_partner_new':
`🏥 *Novo parceiro na rede — {clubName}*
Oi, *{nome}*! Temos um novo parceiro na rede do *{clubName}*:
*{partnerName}*
🩺 {specialty}
📍 {address}
Agende sua consulta com desconto exclusivo para membros!`,
}
let text = templates[type] || 'Notificação do Sistema Nuvem'
for (const [key, value] of Object.entries(vars)) {
text = text.replace(new RegExp(`{${key}}`, 'g'), value ?? '')
}
return text
}
const WhatsappCore = {
queue: whatsappQueue,
async sendNotification({ userId, phone, type, variables, userCpf, tenantId }) {
if (!phone) return
// Opt-in: mensagens não-críticas só vão para usuários que aceitaram
if (!CRITICAL_TYPES.has(type)) {
const optin = await this._checkOptin(userCpf, phone)
if (!optin) return null
}
let cleanPhone = phone.replace(/\D/g, '')
if (cleanPhone.length === 10 || cleanPhone.length === 11) cleanPhone = `55${cleanPhone}`
const parsedMessage = parseTemplate(type, variables)
const payload = { number: cleanPhone, body: parsedMessage }
const logResult = await WhatsappLog.create({ user_id: userId, phone: cleanPhone, type, payload, tenant_id: tenantId || 1 })
const logId = logResult.rows?.[0]?.id || logResult.lastInsertRowid
// Compradores recorrentes têm prioridade máxima nas mensagens de engajamento
const isCritical = CRITICAL_TYPES.has(type)
let priority = isCritical ? 1 : 10
if (!isCritical && userCpf) {
const isRepeat = await this._isRepeatBuyer(userCpf)
if (isRepeat) priority = 3
}
await whatsappQueue.add('send-msg', { logId, payload }, {
priority,
attempts: 3,
backoff: { type: 'exponential', delay: 5000 }
})
return logId
},
async _checkOptin(userCpf, phone) {
if (!userCpf) return true // sem CPF = não conseguimos checar, assume permitido
const user = await queryOne(
'SELECT whatsapp_optin FROM users WHERE cpf = $1',
[userCpf.replace(/\D/g, '')]
).catch(() => null)
if (!user) return false // CPF fornecido mas não encontrado — não assume opt-in
return user.whatsapp_optin !== false
},
async _isRepeatBuyer(userCpf) {
const row = await queryOne(
`SELECT COUNT(*) as cnt FROM orders
WHERE user_cpf = $1 AND status = 'paid'`,
[userCpf.replace(/\D/g, '')]
).catch(() => null)
return parseInt(row?.cnt ?? 0) > 1
},
}
module.exports = WhatsappCore