This commit is contained in:
Lizandro Guarnizo
2026-05-12 19:41:17 -05:00
parent 55c68b470b
commit 25e3cd6254
9 changed files with 270 additions and 3 deletions
+2 -1
View File
@@ -118,8 +118,9 @@ func BoldWebhook(c *fiber.Ctx) error {
log.Printf("[BOLD] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
} else {
log.Printf("[BOLD] Webhook: contrato %d marcado como pagado", contratoID)
// Notificar SaaS externos asociados al contrato (goroutine, no bloquea respuesta)
// Notificar SaaS externos + enviar correo de confirmación (goroutine, no bloquea respuesta)
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "bold", float64(monto), "COP")
go services.EnviarCorreoConfirmacionPago(contratoID)
}
}
}
+3 -2
View File
@@ -152,7 +152,7 @@ func SeePlanes(c *fiber.Ctx) error {
response, err := services.SeePlanes(*dlocalConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Error al obtener los planes", err),
"error": fmt.Sprintf("Error al obtener los planes: %v", err),
})
}
@@ -324,8 +324,9 @@ func DlocalWebhook(c *fiber.Ctx) error {
log.Printf("[DLOCAL] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
} else {
log.Printf("[DLOCAL] Webhook: contrato %d marcado como pagado", contratoID)
// Notificar SaaS externos asociados al contrato (goroutine, no bloquea respuesta)
// Notificar SaaS externos + enviar correo de confirmación (goroutine, no bloquea respuesta)
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "dlocal", monto, moneda)
go services.EnviarCorreoConfirmacionPago(contratoID)
}
}
}
+3
View File
@@ -118,6 +118,7 @@ func verificarPago(ref string) (confirmado bool, fechaPago string) {
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dlocal_payment_log (id=%d)", contratoID, l.ID)
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID)
return true, l.CreatedAt.Format("02/01/2006 15:04")
}
}
@@ -133,6 +134,7 @@ func verificarPago(ref string) (confirmado bool, fechaPago string) {
} else if paid {
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via Bold API (payment_id=%s)", contratoID, paymentID)
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID)
if paymentID != "" && !models.IsBoldNotificationDuplicate("api-check-"+paymentID) {
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
NotificationID: "api-check-" + paymentID,
@@ -156,6 +158,7 @@ func verificarPago(ref string) (confirmado bool, fechaPago string) {
} else if paid {
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dLocal API (payment_id=%s)", contratoID, paymentID)
_ = models.MarcarContratoPagado(contratoID)
go services.EnviarCorreoConfirmacionPago(contratoID)
notifID := "api-check-" + ref
if !models.IsDlocalNotificationDuplicate(notifID) {
_ = models.SaveDlocalPaymentLog(models.DlocalPaymentLog{
+117
View File
@@ -228,6 +228,123 @@ func DeleteContrato(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
// GetHistorialContrato devuelve la línea de tiempo de actividad de un contrato
func GetHistorialContrato(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
contrato, err := models.GetContratoByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
notifLogs, _ := models.GetLogsByContratoID(uint(id))
pagoLogs, _ := models.GetDispatchLogsByContratoID(uint(id))
// Construir timeline unificado
type Evento struct {
Tipo string `json:"tipo"` // creacion | renovacion | notificacion | pago
Icono string `json:"icono"`
Titulo string `json:"titulo"`
Detalle string `json:"detalle"`
Estado string `json:"estado"` // ok | error | info
FechaISO string `json:"fecha"`
}
var timeline []Evento
// Evento: creación del contrato
timeline = append(timeline, Evento{
Tipo: "creacion",
Icono: "document",
Titulo: "Contrato creado",
Detalle: "Inicio: " + contrato.FechaInicio.Format("02/01/2006") + " · Vence: " + contrato.FechaVencimiento.Format("02/01/2006"),
Estado: "info",
FechaISO: contrato.CreatedAt.Format(time.RFC3339),
})
// Evento: renovaciones (updated_at con estado renovado — heurístico por estado)
if contrato.Estado == "renovado" {
timeline = append(timeline, Evento{
Tipo: "renovacion",
Icono: "refresh",
Titulo: "Contrato renovado",
Detalle: "Nuevo vencimiento: " + contrato.FechaVencimiento.Format("02/01/2006"),
Estado: "ok",
FechaISO: contrato.UpdatedAt.Format(time.RFC3339),
})
}
// Eventos: notificaciones enviadas
for _, n := range notifLogs {
estado := "ok"
if n.Estado == "fallido" {
estado = "error"
} else if n.Estado == "pendiente" {
estado = "info"
}
detalle := n.Asunto
if n.Estado == "fallido" && n.ErrorMsg != "" {
detalle += " · Error: " + n.ErrorMsg
}
reglaLabel := ""
if n.Regla.ID > 0 {
reglaLabel = " (" + n.Regla.Nombre + ")"
}
timeline = append(timeline, Evento{
Tipo: "notificacion",
Icono: "mail",
Titulo: "Notificación enviada" + reglaLabel,
Detalle: detalle,
Estado: estado,
FechaISO: n.CreatedAt.Format(time.RFC3339),
})
}
// Eventos: pagos / dispatches
for _, p := range pagoLogs {
estado := "ok"
if p.Estado == "failed" {
estado = "error"
}
detalle := p.Referencia
if p.PayerEmail != "" {
detalle += " · " + p.PayerEmail
}
if p.Fuente != "" {
detalle += " · " + p.Fuente
}
timeline = append(timeline, Evento{
Tipo: "pago",
Icono: "currency",
Titulo: "Pago recibido",
Detalle: detalle,
Estado: estado,
FechaISO: p.CreatedAt.Format(time.RFC3339),
})
}
// Ordenar por fecha desc
for i := 0; i < len(timeline)-1; i++ {
for j := i + 1; j < len(timeline); j++ {
if timeline[j].FechaISO > timeline[i].FechaISO {
timeline[i], timeline[j] = timeline[j], timeline[i]
}
}
}
return c.JSON(fiber.Map{
"contrato": fiber.Map{
"id": contrato.ID,
"cliente": contrato.Cliente.Nombre,
"estado": contrato.Estado,
},
"timeline": timeline,
"total": len(timeline),
})
}
func calcularFechaVencimiento(desde time.Time, periodicidad string) time.Time {
switch periodicidad {
case "mensual":