Files
soft_usite/pkg/services/cron_service.go
T
2026-05-13 22:36:09 -05:00

211 lines
5.9 KiB
Go

package services
import (
"fmt"
"log"
"github.com/robfig/cron/v3"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
var cronScheduler *cron.Cron
// IniciarCron arranca el scheduler de tareas. Llamar desde app.go o main.go.
func IniciarCron() {
cronScheduler = cron.New()
// Vencimientos próximos — todos los días a las 8:00 AM
if _, err := cronScheduler.AddFunc("0 8 * * *", ProcesarVencimientosProximos); err != nil {
log.Printf("[CRON] Error registrando tarea vencimientos_proximo: %v", err)
return
}
// Contratos ya vencidos — todos los días a las 9:00 AM
if _, err := cronScheduler.AddFunc("0 9 * * *", ProcesarYaVencidos); err != nil {
log.Printf("[CRON] Error registrando tarea ya_vencido: %v", err)
return
}
// Polling Bold — cada 15 minutos para detectar pagos sin webhook
if _, err := cronScheduler.AddFunc("*/15 * * * *", VerificarPagosBoldPendientes); err != nil {
log.Printf("[CRON] Error registrando tarea bold_polling: %v", err)
return
}
cronScheduler.Start()
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min")
}
// DetenerCron para graceful shutdown
func DetenerCron() {
if cronScheduler != nil {
cronScheduler.Stop()
}
}
// ProcesarVencimientos mantiene compatibilidad para llamadas manuales
func ProcesarVencimientos() {
ProcesarVencimientosProximos()
ProcesarYaVencidos()
}
// ProcesarVencimientosProximos procesa reglas de tipo "vencimiento_proximo"
func ProcesarVencimientosProximos() {
log.Println("[CRON] Procesando vencimientos próximos...")
reglas, err := models.GetReglasByTipoEvento("vencimiento_proximo")
if err != nil {
log.Printf("[CRON] Error obteniendo reglas vencimiento_proximo: %v", err)
return
}
for _, regla := range reglas {
contratos, err := models.GetContratosProximosVencer(regla.DiasAntes)
if err != nil {
log.Printf("[CRON] Error obteniendo contratos para regla %d: %v", regla.ID, err)
continue
}
procesarContratos(&regla, contratos)
}
log.Println("[CRON] Vencimientos próximos finalizado")
}
// ProcesarYaVencidos procesa reglas de tipo "ya_vencido"
func ProcesarYaVencidos() {
log.Println("[CRON] Procesando contratos ya vencidos...")
reglas, err := models.GetReglasByTipoEvento("ya_vencido")
if err != nil {
log.Printf("[CRON] Error obteniendo reglas ya_vencido: %v", err)
return
}
if len(reglas) == 0 {
return
}
contratos, err := models.GetContratosYaVencidos()
if err != nil {
log.Printf("[CRON] Error obteniendo contratos vencidos: %v", err)
return
}
for _, regla := range reglas {
procesarContratos(&regla, contratos)
}
log.Println("[CRON] Ya vencidos finalizado")
}
// procesarContratos aplica filtros y envía notificaciones para una regla y lista de contratos
func procesarContratos(regla *models.NotificacionRegla, contratos []models.Contrato) {
if len(contratos) == 0 {
return
}
// Filtrar por AplicaA
var filtrados []models.Contrato
for _, c := range contratos {
switch regla.AplicaA {
case "renovable":
for _, s := range c.Servicios {
if s.Tipo == "renovable" {
filtrados = append(filtrados, c)
break
}
}
case "unico":
for _, s := range c.Servicios {
if s.Tipo == "unico" {
filtrados = append(filtrados, c)
break
}
}
default:
filtrados = append(filtrados, c)
}
}
if len(filtrados) == 0 {
return
}
// Agrupar por cliente
porCliente := make(map[uint][]models.Contrato)
for _, c := range filtrados {
porCliente[c.ClienteID] = append(porCliente[c.ClienteID], c)
}
for clienteID, grupoContratos := range porCliente {
if models.YaEnviadoHoy(clienteID, regla.ID) {
log.Printf("[CRON] Ya enviado hoy a cliente %d para regla %d — saltando", clienteID, regla.ID)
continue
}
cliente := &grupoContratos[0].Cliente
if err := EnviarNotificacionGrupo(regla, cliente, grupoContratos, ""); err != nil {
log.Printf("[CRON] Error enviando a cliente %d: %v", clienteID, err)
} else {
log.Printf("[CRON] Enviado a cliente %d (%s) — %d contrato(s)", clienteID, cliente.Email, len(grupoContratos))
}
}
}
// VerificarPagosBoldPendientes consulta la API de Bold para cada contrato que tiene un
// enlace de pago generado pero aún no confirmado. Se ejecuta cada 15 minutos via cron.
// Esto compensa la ausencia de webhooks de Bold.
func VerificarPagosBoldPendientes() {
contratos, err := models.GetContratosConEnlacePendiente()
if err != nil {
log.Printf("[BOLD-POLL] Error obteniendo contratos pendientes: %v", err)
return
}
if len(contratos) == 0 {
return
}
boldCfg, err := models.GetBoldConfig()
if err != nil {
log.Printf("[BOLD-POLL] Sin configuración Bold activa: %v", err)
return
}
log.Printf("[BOLD-POLL] Verificando %d contrato(s) con enlace pendiente...", len(contratos))
for _, c := range contratos {
paid, paymentID, monto, err := CheckBoldLinkPaid(boldCfg, c.EnlacePagoLinkID)
if err != nil {
log.Printf("[BOLD-POLL] Error consultando link %s (contrato %d): %v", c.EnlacePagoLinkID, c.ID, err)
continue
}
if !paid {
continue
}
ref := fmt.Sprintf("contrato-%d", c.ID)
log.Printf("[BOLD-POLL] Pago detectado para %s (payment_id=%s, monto=%d)", ref, paymentID, monto)
if ok, _ := models.MarcarContratoPagado(c.ID); ok {
go EnviarCorreoConfirmacionPago(c.ID, "bold")
log.Printf("[BOLD-POLL] Contrato %d marcado como pagado", c.ID)
}
// Enriquecer callback log si existe
go models.EnrichBoldCallbackLog(ref, c.Cliente.Email, monto)
// Guardar en webhook log como API_CHECK para trazabilidad
notifID := "api-check-" + paymentID
if paymentID != "" && !models.IsBoldNotificationDuplicate(notifID) {
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
NotificationID: notifID,
Tipo: "API_CHECK",
PaymentID: paymentID,
Referencia: ref,
PayerEmail: c.Cliente.Email,
Monto: monto,
Procesado: true,
})
}
}
log.Println("[BOLD-POLL] Verificación completada")
}