up
This commit is contained in:
@@ -45,11 +45,17 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
// Crear/actualizar tablas de Renovaciones (idempotente)
|
// Crear/actualizar tablas de Renovaciones (idempotente)
|
||||||
migrations.MigrateRenovaciones()
|
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
|
// Seed automático (idempotente) de módulos del sistema
|
||||||
migrations.SeedRenovaciones()
|
migrations.SeedRenovaciones()
|
||||||
migrations.SeedIntegraciones()
|
migrations.SeedIntegraciones()
|
||||||
// Auto-migrar query_history si no existe
|
|
||||||
app.Http.Database.DB.AutoMigrate(&models.QueryHistory{})
|
|
||||||
// Iniciar cron de vencimientos
|
// Iniciar cron de vencimientos
|
||||||
services.IniciarCron()
|
services.IniciarCron()
|
||||||
defer services.DetenerCron()
|
defer services.DetenerCron()
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func MigrateRenovaciones() {
|
|||||||
&models.Cliente{},
|
&models.Cliente{},
|
||||||
&models.PlantillaCorreo{},
|
&models.PlantillaCorreo{},
|
||||||
&models.NotificacionRegla{},
|
&models.NotificacionRegla{},
|
||||||
&models.Contrato{},
|
&models.Contrato{}, // incluye columnas enlace_pago y enlace_pago_link_id
|
||||||
&models.NotificacionLog{},
|
&models.NotificacionLog{},
|
||||||
&models.SmtpConfig{},
|
&models.SmtpConfig{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ type Contrato struct {
|
|||||||
Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado
|
Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado
|
||||||
AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"`
|
AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"`
|
||||||
Notas string `json:"notas" gorm:"column:notas"`
|
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" }
|
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)
|
db.Exec("DELETE FROM contrato_servicios WHERE contrato_id = ?", id)
|
||||||
return db.Delete(&c).Error
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ type DatosPlantilla struct {
|
|||||||
FechaVencimiento string
|
FechaVencimiento string
|
||||||
DiasRestantes int
|
DiasRestantes int
|
||||||
Asunto string
|
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 {
|
type ItemServicio struct {
|
||||||
@@ -45,6 +48,7 @@ func DatosEjemplo() DatosPlantilla {
|
|||||||
FechaVencimiento: "31/12/2025",
|
FechaVencimiento: "31/12/2025",
|
||||||
DiasRestantes: 15,
|
DiasRestantes: 15,
|
||||||
Asunto: "Recordatorio de vencimiento",
|
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)
|
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{
|
datos := DatosPlantilla{
|
||||||
ClienteNombre: cliente.Nombre,
|
ClienteNombre: cliente.Nombre,
|
||||||
ClienteEmpresa: cliente.Empresa,
|
ClienteEmpresa: cliente.Empresa,
|
||||||
@@ -143,6 +154,7 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl
|
|||||||
FechaVencimiento: fechaVenc.Format("02/01/2006"),
|
FechaVencimiento: fechaVenc.Format("02/01/2006"),
|
||||||
DiasRestantes: dias,
|
DiasRestantes: dias,
|
||||||
Asunto: p.Asunto,
|
Asunto: p.Asunto,
|
||||||
|
EnlacePago: enlacePago,
|
||||||
}
|
}
|
||||||
|
|
||||||
html, err := RenderPlantilla(p, datos)
|
html, err := RenderPlantilla(p, datos)
|
||||||
@@ -208,3 +220,50 @@ func ReenviarLog(logEntry *models.NotificacionLog) error {
|
|||||||
models.UpdateNotificacionLog(*logEntry)
|
models.UpdateNotificacionLog(*logEntry)
|
||||||
return sendErr
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"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",
|
log.Printf("[BOLD] Webhook: SALE_APPROVED — payment_id=%s referencia=%s email=%s monto=%d",
|
||||||
paymentID, referencia, payerEmail, monto)
|
paymentID, referencia, payerEmail, monto)
|
||||||
|
|
||||||
// ─── 7. Aquí se conecta la lógica de negocio ─────────────────────────────
|
// ─── 7. Limpiar enlace de pago del contrato (para generar uno nuevo en la próxima notificación)
|
||||||
// Ejemplo: actualizar estado de pago de un contrato de renovación.
|
// La referencia tiene formato "contrato-{id}"
|
||||||
// procesarPagoBold(paymentID, referencia, payerEmail, monto)
|
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)
|
models.MarkBoldWebhookProcessed(notificationID)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user