package services import ( "bytes" "encoding/json" "fmt" "html/template" "log" "time" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // DatosPlantilla es el contexto inyectado al renderizar una plantilla de correo type DatosPlantilla struct { ClienteNombre string ClienteEmpresa string ClienteEmail string Servicios []ItemServicio Total float64 FechaVencimiento string DiasRestantes int Asunto string } type ItemServicio struct { Nombre string Precio float64 Moneda string FechaVenc string } // DatosEjemplo devuelve datos de prueba para la previsualización de plantillas func DatosEjemplo() DatosPlantilla { return DatosPlantilla{ ClienteNombre: "Juan Pérez", ClienteEmpresa: "Empresa Demo S.A.", ClienteEmail: "cliente@ejemplo.com", Servicios: []ItemServicio{ {Nombre: "Hosting Basic", Precio: 29.99, Moneda: "USD", FechaVenc: "2025-12-31"}, {Nombre: "Dominio .com", Precio: 14.99, Moneda: "USD", FechaVenc: "2025-12-31"}, }, Total: 44.98, FechaVencimiento: "31/12/2025", DiasRestantes: 15, Asunto: "Recordatorio de vencimiento", } } // RenderPlantilla renderiza el CuerpoHTML de una PlantillaCorreo con los datos dados func RenderPlantilla(p *models.PlantillaCorreo, datos DatosPlantilla) (string, error) { tmpl, err := template.New("correo").Parse(p.CuerpoHTML) if err != nil { return "", fmt.Errorf("plantilla HTML inválida: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, datos); err != nil { return "", fmt.Errorf("error al renderizar plantilla: %w", err) } return buf.String(), nil } // EnviarCorreoPrueba envía la plantilla con datos de ejemplo al email dado func EnviarCorreoPrueba(email string, p *models.PlantillaCorreo) error { html, err := RenderPlantilla(p, DatosEjemplo()) if err != nil { return err } return app.Http.Mail.Send(email, "[PRUEBA] "+p.Asunto, html) } // EnviarCorreoManual envía correo de aviso para un contrato específico func EnviarCorreoManual(contrato *models.Contrato) error { // Usar la primera plantilla activa de tipo renovacion/vencimiento como fallback plantillas, err := models.GetAllPlantillasSelect() if err != nil || len(plantillas) == 0 { return fmt.Errorf("no hay plantillas de correo disponibles") } p, err := models.GetPlantillaByID(plantillas[0].ID) if err != nil { return err } dias := int(contrato.FechaVencimiento.Sub(time.Now()).Hours() / 24) datos := DatosPlantilla{ ClienteNombre: contrato.Cliente.Nombre, ClienteEmpresa: contrato.Cliente.Empresa, ClienteEmail: contrato.Cliente.Email, FechaVencimiento: contrato.FechaVencimiento.Format("02/01/2006"), DiasRestantes: dias, Total: contrato.PrecioAcordado, Servicios: []ItemServicio{{ Nombre: contrato.Servicio.Nombre, Precio: contrato.PrecioAcordado, Moneda: contrato.Servicio.Moneda, FechaVenc: contrato.FechaVencimiento.Format("02/01/2006"), }}, } html, err := RenderPlantilla(p, datos) if err != nil { return err } return app.Http.Mail.Send(contrato.Cliente.Email, p.Asunto, html) } // EnviarNotificacionGrupo envía un correo agrupado para un cliente con múltiples contratos func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cliente, contratos []models.Contrato) error { p, err := models.GetPlantillaByID(regla.PlantillaID) if err != nil { return fmt.Errorf("plantilla no encontrada: %w", err) } var items []ItemServicio var total float64 var fechaVenc time.Time for _, c := range contratos { items = append(items, ItemServicio{ Nombre: c.Servicio.Nombre, Precio: c.PrecioAcordado, Moneda: c.Servicio.Moneda, FechaVenc: c.FechaVencimiento.Format("02/01/2006"), }) total += c.PrecioAcordado if fechaVenc.IsZero() || c.FechaVencimiento.Before(fechaVenc) { fechaVenc = c.FechaVencimiento } } dias := int(fechaVenc.Sub(time.Now()).Hours() / 24) datos := DatosPlantilla{ ClienteNombre: cliente.Nombre, ClienteEmpresa: cliente.Empresa, ClienteEmail: cliente.Email, Servicios: items, Total: total, FechaVencimiento: fechaVenc.Format("02/01/2006"), DiasRestantes: dias, Asunto: p.Asunto, } html, err := RenderPlantilla(p, datos) if err != nil { return err } // Construir JSON de IDs de contratos para el log var ids []uint for _, c := range contratos { ids = append(ids, c.ID) } idsJSON, _ := json.Marshal(ids) // Crear log previo (pendiente) logEntry := models.NotificacionLog{ ClienteID: cliente.ID, ReglaID: regla.ID, ContratosIDs: string(idsJSON), FechaEnvio: time.Now(), Estado: "pendiente", Asunto: p.Asunto, PreviewHTML: html, } savedLog, err := models.CreateNotificacionLog(logEntry) if err != nil { log.Printf("Error guardando log de notificación: %v", err) } // Enviar correo (también CC si está definido) sendErr := app.Http.Mail.Send(cliente.Email, p.Asunto, html) if cliente.EmailCC != "" { _ = app.Http.Mail.Send(cliente.EmailCC, "[CC] "+p.Asunto, html) } // Actualizar estado del log if savedLog != nil { if sendErr != nil { savedLog.Estado = "fallido" savedLog.ErrorMsg = sendErr.Error() } else { savedLog.Estado = "enviado" } models.UpdateNotificacionLog(*savedLog) } return sendErr } // ReenviarLog reenvía un correo ya registrado en el historial func ReenviarLog(logEntry *models.NotificacionLog) error { if logEntry.PreviewHTML == "" { return fmt.Errorf("no hay HTML guardado para este envío") } sendErr := app.Http.Mail.Send(logEntry.Cliente.Email, logEntry.Asunto, logEntry.PreviewHTML) if sendErr != nil { logEntry.Estado = "fallido" logEntry.ErrorMsg = sendErr.Error() } else { logEntry.Estado = "enviado" logEntry.ErrorMsg = "" } models.UpdateNotificacionLog(*logEntry) return sendErr }