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(®la, 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(®la, 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") }