diff --git a/.env b/.env index 1f764ba..6992bbb 100644 --- a/.env +++ b/.env @@ -48,3 +48,14 @@ AUTH_METHOD=POST TM_HEADER_ORIGIN=https://azure-function.timemanagerweb.com TM_HEADER_TENANT_NAME=pruebas-dos TM_HEADER_USER_AGENT='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' + + +# Configuración de correo SMTP +EMAIL_HOST=mail.myapps.bo +EMAIL_PORT=465 +EMAIL_USER=noreply@myapps.bo +EMAIL_PASSWORD=sb6+3vchj11e +EMAIL_FROM=noreply@myapps.bo +EMAIL_ADMIN=sureflatron3@gmail.com + +APP_NAME=Rendición de Gastos \ No newline at end of file diff --git a/.gitignore b/.gitignore index a9846c4..a278863 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ *.dll *.so *.dylib - +*.log # Test binary, built with `go test -c` *.test @@ -29,6 +29,8 @@ go.work # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff +.idea/** +.logs/*.log .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml diff --git a/cmd/go-sync-service/main.go b/cmd/go-sync-service/main.go index 66cc46d..04f9ffc 100644 --- a/cmd/go-sync-service/main.go +++ b/cmd/go-sync-service/main.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/tuusuario/go-sync-service/internal/config" + email "github.com/tuusuario/go-sync-service/internal/email" "github.com/tuusuario/go-sync-service/internal/scheduler" "github.com/tuusuario/go-sync-service/metrics" ) @@ -26,6 +27,8 @@ func main() { config.InitLogger(conf) config.Log.Info("🚀 Iniciando servicio: go-sync-service") + emailService := email.NewSMTPEmailService(conf) + // Conexión a Redis redisClient := config.GetRedisClient(conf) if err := redisClient.Ping(context.Background()).Err(); err != nil { @@ -44,7 +47,7 @@ func main() { } config.Log.Info("✅ Conexión a base de datos establecida") - scheduler.Start(context.Background(), redisClient, redisConfigProvider, database) + scheduler.Start(context.Background(), redisClient, redisConfigProvider, database, *emailService) config.Log.Info("✅ Scheduler en ejecución y escuchando recargas") //metrics Grafana diff --git a/go.mod b/go.mod index 9ea30a1..72b7fda 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/gorm v1.30.0 ) @@ -41,9 +41,9 @@ require ( github.com/prometheus/client_golang v1.22.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.20.1 - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/text v0.29.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/postgres v1.6.0 ) @@ -52,9 +52,16 @@ require github.com/elastic/go-elasticsearch v0.0.0 require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect ) diff --git a/go.sum b/go.sum index 0be3901..456c3c9 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= @@ -44,6 +52,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -92,22 +102,36 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index f650e95..9ca2b35 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -55,6 +55,16 @@ type Config struct { TM_HEADER_ORIGIN string `mapstructure:"TM_HEADER_ORIGIN"` TM_HEADER_TENANT_NAME string `mapstructure:"TM_HEADER_TENANT_NAME"` TM_HEADER_USER_AGENT string `mapstructure:"TM_HEADER_USER_AGENT"` + + // Email + EmailHost string `mapstructure:"EMAIL_HOST"` + EmailPort int `mapstructure:"EMAIL_PORT"` + EmailUser string `mapstructure:"EMAIL_USER"` + EmailPassword string `mapstructure:"EMAIL_PASSWORD"` + EmailFrom string `mapstructure:"EMAIL_FROM"` + EmailAdmin string `mapstructure:"EMAIL_ADMIN"` + + AppName string `mapstructure:"APP_NAME"` } var GlobalConfig *Config diff --git a/internal/email/email_service.go b/internal/email/email_service.go new file mode 100644 index 0000000..8c6f69c --- /dev/null +++ b/internal/email/email_service.go @@ -0,0 +1,139 @@ +package email + +import ( + "fmt" + "github.com/tuusuario/go-sync-service/internal/config" + "github.com/tuusuario/go-sync-service/internal/domain/ports" + "log" + "time" + + "github.com/go-playground/validator/v10" + "gopkg.in/gomail.v2" +) + +// EmailService define la interfaz para el envío de correos +type EmailService interface { + SendEmail(email EmailRequest) error +} + +// SMTPEmailService implementa EmailService con SMTP +type SMTPEmailService struct { + Config *config.Config + Validator *validator.Validate +} + +// NewSMTPEmailService retorna una nueva instancia del servicio de email con validación +func NewSMTPEmailService(cfg *config.Config) *SMTPEmailService { + return &SMTPEmailService{ + Config: cfg, + Validator: validator.New(), // Inicializa el validador + } +} + +// EmailRequest estructura para la validación del email +type EmailRequest struct { + To string `json:"correoElectronico" validate:"required,email"` + Subject string `json:"asunto" validate:"required,min=3,max=100"` + Body string `json:"cuerpo" validate:"required"` +} + +// SendEmail valida y envía un correo electrónico usando SMTP +func (s *SMTPEmailService) SendEmail(email EmailRequest) error { + // Validar la estructura antes de enviarla + /* + err := s.Validator.Struct(email) + if err != nil { + for _, err := range err.(validator.ValidationErrors) { + log.Printf("❌ Error de validación en %s: %s", err.Field(), err.Tag()) + } + return fmt.Errorf("❌ Error en la validación del email: %v", err) + } + */ + smtpHost := s.Config.EmailHost + smtpPort := s.Config.EmailPort + smtpUser := s.Config.EmailUser + smtpPass := s.Config.EmailPassword + emailFrom := s.Config.EmailFrom + + if smtpHost == "" || smtpUser == "" || smtpPass == "" || emailFrom == "" || smtpPort == 0 { + log.Println("❌ ERROR: Faltan variables de entorno para SMTP") + return fmt.Errorf("faltan variables de entorno para el envío de correos") + } + + log.Printf("📧 Enviando correo desde: %s", emailFrom) + + mail := gomail.NewMessage() + mail.SetHeader("From", emailFrom) + mail.SetHeader("To", email.To) + mail.SetHeader("Subject", email.Subject) + mail.SetBody("text/html", email.Body) + dialer := gomail.NewDialer(smtpHost, smtpPort, smtpUser, smtpPass) + err := dialer.DialAndSend(mail) + if err != nil { + log.Printf("❌ Error enviando correo a %s: %s", email.To, err) + return err + } + + log.Printf("✅ Correo enviado con éxito a %s", email.To) + return nil +} + +func (s *SMTPEmailService) PrepareEmail(asunto string, mensaje string, cfg ports.RedisConfigProvider) error { + // Validar la estructura antes de enviarla + branding := LoadBrandingAsignation(cfg, s.Config.AppName) + + // Cargar destinatarios del entorno + destinos := s.LoadDestinosCorreo(cfg) + + // Cargar datos para la plantilla + data := TaskTplData{ + AppName: branding.AppName, + Nombre: "Administrador", + TareaNombre: asunto, // Ejemplo + Descripcion: mensaje, // Ejemplo + FechaError: time.Now().Format("02/01/2006 15:04"), // Ejemplo + ErrorDescripcion: mensaje, + LinkTarea: "", + LogoURL: branding.LogoURL, + Direccion: branding.Direccion, + Telefono: branding.Telefono, + EmailContacto: branding.EmailContacto, + Year: branding.Year, + ColorHeaderBG: branding.ColorHeaderBG, + ColorAccent: branding.ColorAccent, + ColorFooterText: branding.ColorFooterText, + ColorFooterMuted: branding.ColorFooterMuted, + ColorBodyBG: branding.ColorBodyBG, + } + + // Renderizar el correo + body, err := RenderTaskEmailHTML("", data) + if err != nil { + return fmt.Errorf("error al renderizar la plantilla de tarea: %v", err) + } + + // Crear el correo + subject := BuildTaskSubject(branding.AppName) + reqMail := EmailRequest{ + To: *destinos, // Usamos la lista de destinos cargada + Subject: subject, + Body: body, + } + + // Enviar el correo + if err := s.SendEmail(reqMail); err != nil { + return fmt.Errorf("error al enviar el correo de notificación de tarea: %v", err) + } + + return nil +} + +// Cargar los correos electrónicos desde la configuración +func (s *SMTPEmailService) LoadDestinosCorreo(cfg ports.RedisConfigProvider) *string { + destinos, err := cfg.GetString("SYNCRONIZADOR_EMAIL_DESTINOS") // Asumiendo que usas RedisConfigProvider + if err != nil { + log.Printf(" ERROR no se pudo cargar la lista de destinatarios: %v", err) + destinos = s.Config.EmailAdmin + } + return &destinos +} diff --git a/internal/email/task_asignation.go b/internal/email/task_asignation.go new file mode 100644 index 0000000..240ba14 --- /dev/null +++ b/internal/email/task_asignation.go @@ -0,0 +1,155 @@ +package email + +import ( + "bytes" + "fmt" + "github.com/tuusuario/go-sync-service/internal/domain/ports" + "html/template" + "strings" + "time" +) + +// TaskTplData contiene los datos que se inyectarán en la plantilla HTML de la notificación de tarea. +// Se usan como {{.Campo}} dentro del template. +type TaskTplData struct { + AppName string + Nombre string + TareaNombre string + Descripcion string + FechaVencimiento string + ErrorDescripcion string + FechaError string + LinkTarea string + LogoURL string + Direccion string + Telefono string + EmailContacto string + Year int + ColorHeaderBG string + ColorAccent string + ColorFooterText string + ColorFooterMuted string + ColorBodyBG string +} + +type Branding struct { + AppName string + LogoURL string + ColorHeaderBG string + ColorAccent string + ColorFooterText string + ColorFooterMuted string + ColorBodyBG string + Direccion string + Telefono string + EmailContacto string + Year int +} + +// Código del parámetro en tu tabla `parametros` que guarda la PLANTILLA HTML para la tarea. +// Ej: SELECT valor FROM parametros WHERE codigo = 'EMAIL_TEMPLATE_TASK'; +const ParamEmailTaskTemplate = "EMAIL_TEMPLATE_TASK" + +// BuildTaskSubject arma el asunto con el nombre de la app. +func BuildTaskSubject(appName string) string { + appName = strings.TrimSpace(appName) + if appName == "" { + return "Tarea asignada" + } + return fmt.Sprintf("[%s] Tarea asignada", appName) +} + +// RenderTaskEmailHTML recibe la plantilla HTML (tplStr) y los datos (data) para la notificación de tarea. +// Si tplStr viene vacío, usa DefaultTaskTemplate como fallback. +func RenderTaskEmailHTML(tplStr string, data TaskTplData) (string, error) { + if strings.TrimSpace(tplStr) == "" { + tplStr = DefaultErrorSyncTemplate + } + tpl, err := template.New("email_task").Parse(tplStr) + if err != nil { + return "", fmt.Errorf("parse template: %w", err) + } + var buf bytes.Buffer + if err := tpl.Execute(&buf, data); err != nil { + return "", fmt.Errorf("execute template: %w", err) + } + return buf.String(), nil +} + +func LoadBrandingAsignation(cfg ports.RedisConfigProvider, appName string) Branding { + get := func(key, def string) string { + if cfg == nil { + return def + } + v, _ := cfg.GetString("parametros:" + key) + v = strings.TrimSpace(v) + if v == "" { + return def + } + return v + } + if strings.TrimSpace(appName) == "" { + appName = get("EMAIL_APP_NAME", appName) + } + return Branding{ + AppName: appName, + LogoURL: get("EMAIL_LOGO_URL", ""), + ColorHeaderBG: get("EMAIL_COLOR_HEADER_BG", "#0e1a2b"), + ColorAccent: get("EMAIL_COLOR_ACCENT", "#CBA135"), + ColorFooterText: get("EMAIL_COLOR_FOOTER_TEXT", "#ffffff"), + ColorFooterMuted: get("EMAIL_COLOR_FOOTER_MUTED", "#bfc6d1"), + ColorBodyBG: get("EMAIL_COLOR_BODY_BG", "#f4f6f8"), + Direccion: get("EMAIL_FOOTER_ADDRESS", ""), + Telefono: get("EMAIL_FOOTER_PHONE", ""), + EmailContacto: get("EMAIL_FOOTER_EMAIL", ""), + Year: time.Now().Year(), + } +} + +// --- Plantilla por defecto para la notificación de tarea (Go template con {{.Campo}}) --- +const DefaultErrorSyncTemplate = ` + + + + {{.AppName}} – Error de Sincronización + + + + + +
+ + + + +



+ + +
+` diff --git a/internal/scheduler/listener.go b/internal/scheduler/listener.go index 8f91450..3cc474d 100644 --- a/internal/scheduler/listener.go +++ b/internal/scheduler/listener.go @@ -2,6 +2,7 @@ package scheduler import ( "context" + "github.com/tuusuario/go-sync-service/internal/email" "github.com/redis/go-redis/v9" "github.com/tuusuario/go-sync-service/internal/config" @@ -9,7 +10,7 @@ import ( "gorm.io/gorm" ) -func listenCronReload(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, dbConn *gorm.DB) { +func listenCronReload(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, dbConn *gorm.DB, email email.SMTPEmailService) { pubsub := redisClient.Subscribe(ctx, config.GlobalConfig.RedisSubscribe) ch := pubsub.Channel() @@ -18,8 +19,10 @@ func listenCronReload(ctx context.Context, redisClient *redis.Client, cfg ports. for msg := range ch { if msg.Payload == "reload" { config.Log.Info("🔄 Recargando configuración de cron...") - if err := loadAndStartJobs(ctx, cfg, dbConn); err != nil { + if err := loadAndStartJobs(ctx, cfg, dbConn, email); err != nil { config.Log.Errorf("❌ Error al recargar cron jobs: %v", err) + _ = email.PrepareEmail("ERROR AL OBTENER CRONS EN EL REOLAD", err.Error(), cfg) + } } } diff --git a/internal/scheduler/manager.go b/internal/scheduler/manager.go index e824310..770b7ba 100644 --- a/internal/scheduler/manager.go +++ b/internal/scheduler/manager.go @@ -2,6 +2,7 @@ package scheduler import ( "context" + email "github.com/tuusuario/go-sync-service/internal/email" "github.com/redis/go-redis/v9" "github.com/robfig/cron/v3" @@ -17,16 +18,17 @@ import ( var currentCron *cron.Cron -func Start(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, database *gorm.DB) { +func Start(ctx context.Context, redisClient *redis.Client, cfg ports.RedisConfigProvider, database *gorm.DB, email email.SMTPEmailService) { config.Log.Info("🚀 Iniciando Scheduler...") - if err := loadAndStartJobs(ctx, cfg, database); err != nil { + if err := loadAndStartJobs(ctx, cfg, database, email); err != nil { config.Log.Errorf("❌ Error inicializando jobs: %v", err) + _ = email.PrepareEmail("ERROR AL OBTENER CRONS", err.Error(), cfg) } - go listenCronReload(ctx, redisClient, cfg, database) + go listenCronReload(ctx, redisClient, cfg, database, email) } -func loadAndStartJobs(ctx context.Context, cfg ports.RedisConfigProvider, dbConn *gorm.DB) error { +func loadAndStartJobs(ctx context.Context, cfg ports.RedisConfigProvider, dbConn *gorm.DB, email email.SMTPEmailService) error { lista, err := utils.CargarDesdeRedis[dto.CronConfigList](cfg, config.CronConfig) if err != nil { return err @@ -48,10 +50,11 @@ func loadAndStartJobs(ctx context.Context, cfg ports.RedisConfigProvider, dbConn _, err := newCron.AddFunc(job.Configuracion.Ejecucion, func() { config.Log.Infof("🚀 Ejecutando job: %s", job.Nombre) //cargar Job - fetcher.SyncData(cfg, dbConn, job) + fetcher.SyncData(cfg, dbConn, job, email) }) if err != nil { config.Log.Errorf("❌ Error registrando job %s: %v", job.Nombre, err) + _ = email.PrepareEmail("ERROR AL REGISTRAR EL JOB: "+job.Nombre, err.Error(), cfg) } } diff --git a/internal/sync/fetcher.go b/internal/sync/fetcher.go index ce7d5c7..5b17a68 100644 --- a/internal/sync/fetcher.go +++ b/internal/sync/fetcher.go @@ -3,7 +3,9 @@ package fetcher import ( "encoding/json" "fmt" + "github.com/tuusuario/go-sync-service/internal/email" "strconv" + "strings" "time" "github.com/tuusuario/go-sync-service/internal/config" @@ -16,13 +18,14 @@ import ( "gorm.io/gorm" ) -func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJob) { +func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJob, email email.SMTPEmailService) { start := time.Now() logPrefix := fmt.Sprintf("[🧩 Job: %s] ", job.Nombre) config.Log.Printf("%s Iniciando sincronización...", logPrefix) var dbcore ports.Database = db.NewGormDatabase(database) var hasError bool + var errorMessages []string // Collects all error messages http.InitClient() @@ -37,6 +40,7 @@ func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJo jobIndividual, err := utils.CargarDesdeRedis[dto.JobConfig](redis, proceso) if err != nil { config.Log.Printf(logPrefix+" ❌ Error al obtener configuración del proceso: %v", err) + errorMessages = append(errorMessages, fmt.Sprintf("Error en proceso %s: %v", proceso, err)) hasError = true continue } @@ -44,11 +48,13 @@ func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJo session, err := http.GetSession(redis, job, jobIndividual.Auth, dbcore, logPrefix) if err != nil { config.Log.Println(logPrefix + " ❌ No se pudo obtener sesión") + errorMessages = append(errorMessages, fmt.Sprintf("❌ No se pudo obtener sesión para proceso %s: %v", proceso, err)) hasError = true continue } if session == nil || session.Headers == nil { config.Log.Println(logPrefix + " ❌ Sesión inválida o vacía") + errorMessages = append(errorMessages, fmt.Sprintf("❌ Sesión inválida o vacía para proceso %s", proceso)) hasError = true continue } @@ -63,6 +69,7 @@ func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJo response, err := FetchAllPaginatedManual[map[string]interface{}](session.EndPoint, jobIndividual.Service, logPrefix) if err != nil { config.Log.Printf(logPrefix+" ❌ Error al obtener data: %v", err) + errorMessages = append(errorMessages, fmt.Sprintf("❌ Error al obtener data para proceso %s: %v", proceso, err)) hasError = true continue } @@ -72,6 +79,7 @@ func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJo err = dbcore.SyncRows(jobIndividual.Persistencia, response, job.UnidadNegocio.CompanyDB) if err != nil { config.Log.Printf(logPrefix+" ❌ Error al guardar en base de datos: %v", err) + errorMessages = append(errorMessages, fmt.Sprintf("❌ Error al guardar en base de datos para proceso %s: %v", proceso, err)) hasError = true } } @@ -83,6 +91,9 @@ END: metrics.CronDuration.WithLabelValues(jobName).Observe(duration) if hasError { + errMessage := strings.Join(errorMessages, "\n") // Combine all errors into one message + _ = email.PrepareEmail("ERROR AL EJECUTAR EL JOB: "+job.Nombre, errMessage, redis) + metrics.CronError.WithLabelValues(jobName).Inc() metrics.CronLastError.WithLabelValues(jobName).Set(float64(time.Now().Unix())) } else {