-- envio de email al admini en caso de error

This commit is contained in:
MSI\migue 2025-10-29 10:36:36 -04:00
parent bbc517d256
commit e4f4c2294d
11 changed files with 383 additions and 15 deletions

11
.env
View File

@ -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

4
.gitignore vendored
View File

@ -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

View File

@ -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

17
go.mod
View File

@ -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
)

24
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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
}

View File

@ -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 = `<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{.AppName}} Error de Sincronización</title>
<style>@media (max-width:620px){.container{width:100% !important}.p-24{padding:20px !important}.h1{font-size:22px !important;line-height:28px !important}}</style>
</head>
<body style="margin:0;padding:0;background:#f4f6f8;color:#1c1c1c;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f4f6f8;">
<tr><td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" class="container" style="width:600px;max-width:600px;background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.06);">
<tr><td style="background:{{if .ColorHeaderBG}}{{.ColorHeaderBG}}{{else}}#0e1a2b{{end}};padding:20px 24px;" align="left">
{{if .LogoURL}}<img src="{{.LogoURL}}" width="140" alt="{{.AppName}}" style="display:block;border:0;max-width:100%">{{else}}<div style="font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;color:#fff;">{{.AppName}}</div>{{end}}
</td></tr>
<tr><td class="p-24" style="padding:28px 32px 8px 32px;">
<h1 class="h1" style="margin:0 0 8px 0;font-family:Arial,Helvetica,sans-serif;font-size:24px;line-height:32px;color:#0e1a2b;">
Error de sincronización en {{.AppName}}
</h1>
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:22px;color:#4a4a4a;">
Hola <strong>{{.Nombre}}</strong>, hemos detectado un problema al intentar sincronizar los datos. Aquí están los detalles:
</p>
</td></tr>
<tr><td class="p-24" style="padding:8px 32px 0 32px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f9fafb;border:1px solid #e6e8eb;border-radius:8px;">
<tr><td style="padding:16px 20px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr><td style="font-family:Arial,Helvetica,sans-serif;font-size:13px;color:#6b7280;padding:4px 0;width:160px;">Descripción del Error</td>
<td style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;padding:4px 0;"><strong>{{.ErrorDescripcion}}</strong></td></tr>
<tr><td style="font-family:Arial,Helvetica,sans-serif;font-size:13px;color:#6b7280;padding:4px 0;width:160px;">Hora del Error</td>
<td style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111827;padding:4px 0;"><strong>{{.FechaError}}</strong></td></tr>
</table>
</td></tr>
</table>
</td></tr>
<br/><br/><br/><br/>
<tr><td style="background:{{if .ColorHeaderBG}}{{.ColorHeaderBG}}{{else}}#0e1a2b{{end}};padding:18px 24px;" align="left">
<p style="margin:0 0 6px 0;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;color:#ffffff;">{{.AppName}}</p>
<p style="margin:0;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:18px;color:#bfc6d1;">
{{.Direccion}}{{if .Telefono}} · Tel. {{.Telefono}}{{end}}{{if .EmailContacto}} · {{.EmailContacto}}{{end}} · © {{.Year}} {{.AppName}}
</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>`

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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 {