package controllers import ( "encoding/json" "fmt" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) type PasoDiagnostico struct { Paso string `json:"paso"` OK bool `json:"ok"` Mensaje string `json:"mensaje"` Ms int64 `json:"ms"` } // EjecutarTestCiclo ejecuta el ciclo completo de notificaciones sobre datos temporales: // 1. Envío de bienvenida // 2. Marcar contrato como pagado + enviar confirmación de pago // 3. Idempotencia (no reenviar si ya se envió hoy) // // Crea y destruye los datos de prueba dentro de la misma transacción de limpieza. // POST /app/api/diagnostico/ciclo-notificaciones func EjecutarTestCiclo(c *fiber.Ctx) error { var body struct { TestEmail string `json:"test_email"` } if err := c.BodyParser(&body); err != nil || body.TestEmail == "" { return c.Status(400).JSON(fiber.Map{"error": "test_email requerido"}) } db := app.Http.Database.DB suffix := fmt.Sprintf("_diag_%d", time.Now().UnixNano()) pasos := []PasoDiagnostico{} allOK := true paso := func(nombre string, fn func() error) { t0 := time.Now() err := fn() ms := time.Since(t0).Milliseconds() ok := err == nil msg := "OK" if !ok { msg = err.Error() allOK = false } pasos = append(pasos, PasoDiagnostico{Paso: nombre, OK: ok, Mensaje: msg, Ms: ms}) } // ── Datos temporales ────────────────────────────────────────────────────── var plantBienvenida, plantPago models.PlantillaCorreo var reglaBienvenida, reglaPago models.NotificacionRegla var cliente models.Cliente var contrato models.Contrato // ── Cleanup siempre al finalizar ────────────────────────────────────────── defer func() { if contrato.ID > 0 { db.Unscoped().Where("cliente_id = ?", cliente.ID).Delete(&models.NotificacionLog{}) db.Unscoped().Delete(&contrato) } if cliente.ID > 0 { db.Unscoped().Delete(&cliente) } if reglaBienvenida.ID > 0 { db.Unscoped().Delete(®laBienvenida) } if reglaPago.ID > 0 { db.Unscoped().Delete(®laPago) } if plantBienvenida.ID > 0 { db.Unscoped().Delete(&plantBienvenida) } if plantPago.ID > 0 { db.Unscoped().Delete(&plantPago) } }() // ── SETUP: crear datos de prueba ────────────────────────────────────────── paso("setup: plantilla bienvenida", func() error { plantBienvenida = models.PlantillaCorreo{ Nombre: "DIAG Bienvenida" + suffix, Asunto: "[TEST] Bienvenido al sistema", CuerpoHTML: "

Hola {{.ClienteNombre}}, bienvenido. Tu vencimiento es {{.FechaVencimiento}}.

", Tipo: "bienvenida", } return db.Create(&plantBienvenida).Error }) paso("setup: plantilla pago confirmado", func() error { plantPago = models.PlantillaCorreo{ Nombre: "DIAG Pago Confirmado" + suffix, Asunto: "[TEST] Tu pago fue recibido", CuerpoHTML: "

Hola {{.ClienteNombre}}, recibimos tu pago de ${{.Total}}. ¡Gracias!

", Tipo: "pago", } return db.Create(&plantPago).Error }) paso("setup: regla bienvenida", func() error { if plantBienvenida.ID == 0 { return fmt.Errorf("plantilla bienvenida no creada") } reglaBienvenida = models.NotificacionRegla{ Nombre: "DIAG Regla Bienvenida" + suffix, TipoEvento: "bienvenida", PlantillaID: plantBienvenida.ID, Activo: true, AplicaA: "todos", PasarelaEnlace: "ninguna", } return db.Create(®laBienvenida).Error }) paso("setup: regla pago_recibido", func() error { if plantPago.ID == 0 { return fmt.Errorf("plantilla pago no creada") } reglaPago = models.NotificacionRegla{ Nombre: "DIAG Regla Pago" + suffix, TipoEvento: "pago_recibido", PlantillaID: plantPago.ID, Activo: true, AplicaA: "todos", PasarelaEnlace: "ninguna", } return db.Create(®laPago).Error }) paso("setup: cliente y contrato temporales", func() error { cliente = models.Cliente{ Nombre: "DIAG Cliente" + suffix, Email: body.TestEmail, Empresa: "Diagnóstico Sistema", Activo: true, } if err := db.Create(&cliente).Error; err != nil { return err } now := time.Now() contrato = models.Contrato{ ClienteID: cliente.ID, Cliente: cliente, FechaInicio: now, FechaVencimiento: now.AddDate(1, 0, 0), PrecioAcordado: 99.00, Estado: "activo", } if err := db.Create(&contrato).Error; err != nil { return err } contrato.Cliente = cliente return nil }) // Si el setup falló, no continuar con los pasos funcionales if !allOK { return c.JSON(fiber.Map{"pasos": pasos, "ok": false, "resumen": "Setup falló — ver pasos anteriores"}) } // ── PASO 1: Enviar correo de bienvenida ─────────────────────────────────── paso("paso 1 — envío de bienvenida", func() error { return services.EnviarNotificacionGrupo(®laBienvenida, &cliente, []models.Contrato{contrato}, "") }) // Verificar log en DB paso("paso 1 — verificar log en historial", func() error { var count int64 db.Model(&models.NotificacionLog{}). Where("cliente_id = ? AND regla_id = ? AND estado = 'enviado'", cliente.ID, reglaBienvenida.ID). Count(&count) if count == 0 { return fmt.Errorf("no se encontró log con estado='enviado' para la regla bienvenida") } return nil }) // ── PASO 2: Simular recepción de pago ───────────────────────────────────── paso("paso 2 — marcar contrato como pagado", func() error { _, err := models.MarcarContratoPagado(contrato.ID) return err }) paso("paso 2 — verificar pago_confirmado en DB", func() error { var c models.Contrato db.Select("pago_confirmado, fecha_pago").First(&c, contrato.ID) if !c.PagoConfirmado { return fmt.Errorf("pago_confirmado sigue siendo false tras MarcarContratoPagado") } if c.FechaPago == nil { return fmt.Errorf("fecha_pago es nil tras MarcarContratoPagado") } return nil }) paso("paso 2 — envío de confirmación de pago", func() error { // EnviarCorreoConfirmacionPago es void — usar EnviarNotificacionGrupo directamente // para capturar el error (la función void loguea pero no retorna) reloadContrato, err := models.GetContratoByID(contrato.ID) if err != nil { return err } return services.EnviarNotificacionGrupo(®laPago, &cliente, []models.Contrato{*reloadContrato}, "") }) paso("paso 2 — verificar log de pago en historial", func() error { var count int64 db.Model(&models.NotificacionLog{}). Where("cliente_id = ? AND regla_id = ? AND estado = 'enviado'", cliente.ID, reglaPago.ID). Count(&count) if count == 0 { return fmt.Errorf("no se encontró log con estado='enviado' para la regla pago_recibido") } return nil }) // ── PASO 3: Idempotencia ────────────────────────────────────────────────── paso("paso 3 — idempotencia (no reenviar si ya enviado hoy)", func() error { // Verificar que YaEnviadoHoy retorna true para ambas reglas if !models.YaEnviadoHoy(cliente.ID, reglaBienvenida.ID) { return fmt.Errorf("YaEnviadoHoy debería ser true para bienvenida, pero retornó false") } if !models.YaEnviadoHoy(cliente.ID, reglaPago.ID) { return fmt.Errorf("YaEnviadoHoy debería ser true para pago_recibido, pero retornó false") } return nil }) // Contar emails en historial para el resumen var totalLogs int64 db.Model(&models.NotificacionLog{}).Where("cliente_id = ?", cliente.ID).Count(&totalLogs) // Serializar IDs para debug idsJSON, _ := json.Marshal(map[string]uint{ "cliente_id": cliente.ID, "contrato_id": contrato.ID, "regla_bienvenida_id": reglaBienvenida.ID, "regla_pago_id": reglaPago.ID, "plantilla_bienvenida": plantBienvenida.ID, "plantilla_pago": plantPago.ID, }) resumen := fmt.Sprintf("Ciclo completado — %d emails enviados al historial, destinatario: %s", totalLogs, body.TestEmail) if !allOK { resumen = "Ciclo con errores — revisar pasos" } return c.JSON(fiber.Map{ "pasos": pasos, "ok": allOK, "resumen": resumen, "ids_tmp": string(idsJSON), }) }