diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go index 95e9a38..3216659 100644 --- a/pkg/models/contrato.go +++ b/pkg/models/contrato.go @@ -115,6 +115,18 @@ func GetContratosYaVencidos() ([]Contrato, error) { return items, nil } +// GetContratosConEnlacePendiente devuelve contratos que tienen un enlace de pago Bold generado +// pero cuyo pago aún no ha sido confirmado. Usado para polling periódico de la API de Bold. +func GetContratosConEnlacePendiente() ([]Contrato, error) { + var items []Contrato + if err := app.Http.Database.DB.Preload("Cliente"). + Where("enlace_pago_link_id != '' AND pago_confirmado = false AND deleted_at IS NULL"). + Find(&items).Error; err != nil { + return nil, err + } + return items, nil +} + // syncContratoServicios sincroniza la tabla join contrato_servicios usando SQL directo // para evitar que GORM intente hacer upsert de los servicios existentes. func syncContratoServicios(contratoID uint, servicioIDs []uint) error { diff --git a/pkg/services/cron_service.go b/pkg/services/cron_service.go index c9ddad6..a564acd 100644 --- a/pkg/services/cron_service.go +++ b/pkg/services/cron_service.go @@ -1,6 +1,7 @@ package services import ( + "fmt" "log" "github.com/robfig/cron/v3" @@ -25,8 +26,14 @@ func IniciarCron() { 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") + log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min") } // DetenerCron para graceful shutdown @@ -141,3 +148,63 @@ func procesarContratos(regla *models.NotificacionRegla, contratos []models.Contr } } } + +// 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") +}