From 5e0b6ae2f0a2018da679dc5d4a79622c44352c36 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 1 May 2026 12:02:21 -0500 Subject: [PATCH] up --- main.go | 10 ++++- migrations/migrate.go | 2 +- pkg/models/contrato.go | 22 +++++++++ pkg/services/renovacion_service.go | 59 +++++++++++++++++++++++++ rest/controllers/api/bold_controller.go | 16 +++++-- 5 files changed, 103 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index fb732bc..c4052ce 100755 --- a/main.go +++ b/main.go @@ -45,11 +45,17 @@ func main() { } else { // Crear/actualizar tablas de Renovaciones (idempotente) migrations.MigrateRenovaciones() + // Crear tablas de integraciones y pasarelas si no existen + app.Http.Database.DB.AutoMigrate( + &models.QueryHistory{}, + &models.HostingerConfig{}, + &models.CloudflareConfig{}, + &models.BoldConfig{}, + &models.BoldWebhookLog{}, + ) // Seed automático (idempotente) de módulos del sistema migrations.SeedRenovaciones() migrations.SeedIntegraciones() - // Auto-migrar query_history si no existe - app.Http.Database.DB.AutoMigrate(&models.QueryHistory{}) // Iniciar cron de vencimientos services.IniciarCron() defer services.DetenerCron() diff --git a/migrations/migrate.go b/migrations/migrate.go index e5f93f5..39eb7c0 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -98,7 +98,7 @@ func MigrateRenovaciones() { &models.Cliente{}, &models.PlantillaCorreo{}, &models.NotificacionRegla{}, - &models.Contrato{}, + &models.Contrato{}, // incluye columnas enlace_pago y enlace_pago_link_id &models.NotificacionLog{}, &models.SmtpConfig{}, ); err != nil { diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go index 9b59a10..fc844a7 100644 --- a/pkg/models/contrato.go +++ b/pkg/models/contrato.go @@ -19,6 +19,11 @@ type Contrato struct { Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"` Notas string `json:"notas" gorm:"column:notas"` + // Enlace de pago Bold: único por ciclo de pago. + // Se reutiliza en múltiples notificaciones del mismo ciclo. + // Se anula (vacía) cuando SALE_APPROVED llega y se registra el pago. + EnlacePago string `json:"enlace_pago" gorm:"column:enlace_pago;type:text"` + EnlacePagoLinkID string `json:"enlace_pago_link_id" gorm:"column:enlace_pago_link_id;type:varchar(64)"` } func (Contrato) TableName() string { return "contratos" } @@ -134,3 +139,20 @@ func DeleteContrato(id uint) error { db.Exec("DELETE FROM contrato_servicios WHERE contrato_id = ?", id) return db.Delete(&c).Error } + +// GuardarEnlacePago persiste el enlace de pago Bold en el contrato. +func GuardarEnlacePago(contratoID uint, linkID, url string) error { + return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(map[string]interface{}{ + "enlace_pago": url, + "enlace_pago_link_id": linkID, + }).Error +} + +// LimpiarEnlacePago borra el enlace vigente cuando se registra un pago aprobado, +// para que la próxima notificación genere un link nuevo. +func LimpiarEnlacePago(contratoID uint) error { + return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(map[string]interface{}{ + "enlace_pago": "", + "enlace_pago_link_id": "", + }).Error +} diff --git a/pkg/services/renovacion_service.go b/pkg/services/renovacion_service.go index 4ed333c..f461cf3 100644 --- a/pkg/services/renovacion_service.go +++ b/pkg/services/renovacion_service.go @@ -22,6 +22,9 @@ type DatosPlantilla struct { FechaVencimiento string DiasRestantes int Asunto string + // EnlacePago es la URL de pago Bold para el ciclo actual. + // Mismo link en todas las notificaciones hasta que el pago sea aprobado. + EnlacePago string } type ItemServicio struct { @@ -45,6 +48,7 @@ func DatosEjemplo() DatosPlantilla { FechaVencimiento: "31/12/2025", DiasRestantes: 15, Asunto: "Recordatorio de vencimiento", + EnlacePago: "https://checkout.bold.co/payment/LNK_ejemplo123", } } @@ -134,6 +138,13 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl } dias := int(fechaVenc.Sub(time.Now()).Hours() / 24) + + // Generar o reutilizar el enlace de pago para el primer contrato del grupo + enlacePago := "" + if len(contratos) > 0 { + enlacePago = ObtenerOCrearEnlacePago(&contratos[0]) + } + datos := DatosPlantilla{ ClienteNombre: cliente.Nombre, ClienteEmpresa: cliente.Empresa, @@ -143,6 +154,7 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl FechaVencimiento: fechaVenc.Format("02/01/2006"), DiasRestantes: dias, Asunto: p.Asunto, + EnlacePago: enlacePago, } html, err := RenderPlantilla(p, datos) @@ -208,3 +220,50 @@ func ReenviarLog(logEntry *models.NotificacionLog) error { models.UpdateNotificacionLog(*logEntry) return sendErr } + +// ObtenerOCrearEnlacePago devuelve el enlace Bold vigente del contrato. +// Si ya existe uno (no ha sido pagado aún), lo reutiliza. +// Si no existe, crea uno nuevo en Bold y lo guarda en el contrato. +// Cuando Bold notifica SALE_APPROVED, llamar models.LimpiarEnlacePago(contratoID) +// para que la próxima notificación genere un link nuevo. +func ObtenerOCrearEnlacePago(contrato *models.Contrato) string { + // Reutilizar si ya hay uno vigente + if contrato.EnlacePago != "" { + return contrato.EnlacePago + } + + cfg, err := models.GetBoldConfig() + if err != nil { + log.Printf("[BOLD] ObtenerOCrearEnlacePago: sin config activa para contrato %d", contrato.ID) + return "" + } + + // Nombre de servicios en la descripción + desc := fmt.Sprintf("Renovación contrato #%d", contrato.ID) + if len(contrato.Servicios) > 0 { + desc = fmt.Sprintf("Renovación: %s", contrato.Servicios[0].Nombre) + } + + req := BoldPaymentLinkRequest{ + AmountType: "CLOSE", + Amount: BoldAmountField{ + Currency: "COP", + TotalAmount: int64(contrato.PrecioAcordado), + }, + Description: desc, + Reference: fmt.Sprintf("contrato-%d", contrato.ID), + CallbackURL: cfg.CallbackUrl, + } + + result, err := CreateBoldPaymentLink(cfg, req) + if err != nil { + log.Printf("[BOLD] ObtenerOCrearEnlacePago: error creando link para contrato %d: %v", contrato.ID, err) + return "" + } + + // Persistir en BD + _ = models.GuardarEnlacePago(contrato.ID, result.Payload.PaymentLink, result.Payload.URL) + contrato.EnlacePago = result.Payload.URL + contrato.EnlacePagoLinkID = result.Payload.PaymentLink + return result.Payload.URL +} diff --git a/rest/controllers/api/bold_controller.go b/rest/controllers/api/bold_controller.go index ff31beb..7e8dceb 100644 --- a/rest/controllers/api/bold_controller.go +++ b/rest/controllers/api/bold_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "fmt" "log" "github.com/gofiber/fiber/v2" @@ -91,9 +92,18 @@ func BoldWebhook(c *fiber.Ctx) error { log.Printf("[BOLD] Webhook: SALE_APPROVED — payment_id=%s referencia=%s email=%s monto=%d", paymentID, referencia, payerEmail, monto) - // ─── 7. Aquí se conecta la lógica de negocio ───────────────────────────── - // Ejemplo: actualizar estado de pago de un contrato de renovación. - // procesarPagoBold(paymentID, referencia, payerEmail, monto) + // ─── 7. Limpiar enlace de pago del contrato (para generar uno nuevo en la próxima notificación) + // La referencia tiene formato "contrato-{id}" + if referencia != "" { + var contratoID uint + if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 { + if err := models.LimpiarEnlacePago(contratoID); err != nil { + log.Printf("[BOLD] Webhook: error limpiando enlace pago contrato %d: %v", contratoID, err) + } else { + log.Printf("[BOLD] Webhook: enlace pago limpiado para contrato %d", contratoID) + } + } + } models.MarkBoldWebhookProcessed(notificationID)