up
This commit is contained in:
@@ -88,6 +88,22 @@ func GetContratosProximosVencer(diasAntes int) ([]Contrato, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetContratosYaVencidos retorna contratos cuya fecha de vencimiento es hoy o anterior
|
||||
// y su estado sigue siendo 'activo' (aún no han sido marcados como vencidos)
|
||||
func GetContratosYaVencidos() ([]Contrato, error) {
|
||||
var items []Contrato
|
||||
hoy := time.Now().UTC()
|
||||
startOfDay := time.Date(hoy.Year(), hoy.Month(), hoy.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").
|
||||
Where("estado = 'activo' AND fecha_vencimiento < ?", startOfDay).
|
||||
Find(&items).Error; err != nil {
|
||||
log.Printf("Error getting contratos vencidos: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// syncContratoServicios sincroniza la tabla join contrato_servicios usando SQL directo
|
||||
// para evitar que GORM intente hacer upsert de los servicios existentes.
|
||||
func syncContratoServicios(contratoID uint, servicioIDs []uint) error {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type NotificacionRegla struct {
|
||||
gorm.Model
|
||||
Nombre string `json:"nombre" gorm:"column:nombre"`
|
||||
TipoEvento string `json:"tipo_evento" gorm:"column:tipo_evento;default:'vencimiento_proximo'"` // vencimiento_proximo | ya_vencido | bienvenida | pago_recibido | manual
|
||||
DiasAntes int `json:"dias_antes" gorm:"column:dias_antes"`
|
||||
PlantillaID uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
||||
Plantilla PlantillaCorreo `json:"plantilla" gorm:"foreignKey:PlantillaID"`
|
||||
@@ -35,6 +36,16 @@ func GetReglasActivas() ([]NotificacionRegla, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func GetReglasByTipoEvento(tipoEvento string) ([]NotificacionRegla, error) {
|
||||
var items []NotificacionRegla
|
||||
if err := app.Http.Database.DB.Preload("Plantilla").
|
||||
Where("activo = ? AND tipo_evento = ?", true, tipoEvento).
|
||||
Order("dias_antes DESC").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func GetReglaByID(id uint) (*NotificacionRegla, error) {
|
||||
var item NotificacionRegla
|
||||
if err := app.Http.Database.DB.Preload("Plantilla").First(&item, id).Error; err != nil {
|
||||
@@ -50,6 +61,7 @@ func CreateRegla(r NotificacionRegla) error {
|
||||
func UpdateRegla(r NotificacionRegla) error {
|
||||
return app.Http.Database.DB.Model(&r).Updates(map[string]interface{}{
|
||||
"nombre": r.Nombre,
|
||||
"tipo_evento": r.TipoEvento,
|
||||
"dias_antes": r.DiasAntes,
|
||||
"plantilla_id": r.PlantillaID,
|
||||
"activo": r.Activo,
|
||||
|
||||
+100
-61
@@ -13,15 +13,20 @@ var cronScheduler *cron.Cron
|
||||
func IniciarCron() {
|
||||
cronScheduler = cron.New()
|
||||
|
||||
// Ejecutar todos los días a las 8:00 AM
|
||||
_, err := cronScheduler.AddFunc("0 8 * * *", ProcesarVencimientos)
|
||||
if err != nil {
|
||||
log.Printf("[CRON] Error registrando tarea: %v", err)
|
||||
// Vencimientos próximos — todos los días a las 8:00 AM
|
||||
if _, err := cronScheduler.AddFunc("0 8 * * *", ProcesarVencimientosProximos); err != nil {
|
||||
log.Printf("[CRON] Error registrando tarea vencimientos_proximo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Contratos ya vencidos — todos los días a las 9:00 AM
|
||||
if _, err := cronScheduler.AddFunc("0 9 * * *", ProcesarYaVencidos); err != nil {
|
||||
log.Printf("[CRON] Error registrando tarea ya_vencido: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cronScheduler.Start()
|
||||
log.Println("[CRON] Scheduler iniciado — verificando vencimientos diariamente a las 8:00 AM")
|
||||
log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM")
|
||||
}
|
||||
|
||||
// DetenerCron para graceful shutdown
|
||||
@@ -31,13 +36,19 @@ func DetenerCron() {
|
||||
}
|
||||
}
|
||||
|
||||
// ProcesarVencimientos es la función principal del cron
|
||||
// ProcesarVencimientos mantiene compatibilidad para llamadas manuales
|
||||
func ProcesarVencimientos() {
|
||||
log.Println("[CRON] Iniciando procesamiento de vencimientos...")
|
||||
ProcesarVencimientosProximos()
|
||||
ProcesarYaVencidos()
|
||||
}
|
||||
|
||||
reglas, err := models.GetReglasActivas()
|
||||
// ProcesarVencimientosProximos procesa reglas de tipo "vencimiento_proximo"
|
||||
func ProcesarVencimientosProximos() {
|
||||
log.Println("[CRON] Procesando vencimientos próximos...")
|
||||
|
||||
reglas, err := models.GetReglasByTipoEvento("vencimiento_proximo")
|
||||
if err != nil {
|
||||
log.Printf("[CRON] Error obteniendo reglas: %v", err)
|
||||
log.Printf("[CRON] Error obteniendo reglas vencimiento_proximo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,58 +58,86 @@ func ProcesarVencimientos() {
|
||||
log.Printf("[CRON] Error obteniendo contratos para regla %d: %v", regla.ID, err)
|
||||
continue
|
||||
}
|
||||
if len(contratos) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filtrar por AplicaA
|
||||
var filtrados []models.Contrato
|
||||
for _, c := range contratos {
|
||||
switch regla.AplicaA {
|
||||
case "renovable":
|
||||
for _, s := range c.Servicios {
|
||||
if s.Tipo == "renovable" {
|
||||
filtrados = append(filtrados, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "unico":
|
||||
for _, s := range c.Servicios {
|
||||
if s.Tipo == "unico" {
|
||||
filtrados = append(filtrados, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
filtrados = append(filtrados, c)
|
||||
}
|
||||
}
|
||||
if len(filtrados) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Agrupar contratos por cliente
|
||||
porCliente := make(map[uint][]models.Contrato)
|
||||
for _, c := range filtrados {
|
||||
porCliente[c.ClienteID] = append(porCliente[c.ClienteID], c)
|
||||
}
|
||||
|
||||
for clienteID, grupoContratos := range porCliente {
|
||||
// Evitar duplicados: ya enviado hoy para esta regla + cliente
|
||||
if models.YaEnviadoHoy(clienteID, regla.ID) {
|
||||
log.Printf("[CRON] Ya enviado hoy a cliente %d para regla %d — saltando", clienteID, regla.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
cliente := &grupoContratos[0].Cliente
|
||||
if err := EnviarNotificacionGrupo(®la, cliente, grupoContratos); err != nil {
|
||||
log.Printf("[CRON] Error enviando a cliente %d: %v", clienteID, err)
|
||||
} else {
|
||||
log.Printf("[CRON] Enviado a cliente %d (%s) — %d contrato(s)",
|
||||
clienteID, cliente.Email, len(grupoContratos))
|
||||
}
|
||||
}
|
||||
procesarContratos(®la, contratos)
|
||||
}
|
||||
|
||||
log.Println("[CRON] Procesamiento de vencimientos finalizado")
|
||||
log.Println("[CRON] Vencimientos próximos finalizado")
|
||||
}
|
||||
|
||||
// ProcesarYaVencidos procesa reglas de tipo "ya_vencido"
|
||||
func ProcesarYaVencidos() {
|
||||
log.Println("[CRON] Procesando contratos ya vencidos...")
|
||||
|
||||
reglas, err := models.GetReglasByTipoEvento("ya_vencido")
|
||||
if err != nil {
|
||||
log.Printf("[CRON] Error obteniendo reglas ya_vencido: %v", err)
|
||||
return
|
||||
}
|
||||
if len(reglas) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
contratos, err := models.GetContratosYaVencidos()
|
||||
if err != nil {
|
||||
log.Printf("[CRON] Error obteniendo contratos vencidos: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, regla := range reglas {
|
||||
procesarContratos(®la, contratos)
|
||||
}
|
||||
|
||||
log.Println("[CRON] Ya vencidos finalizado")
|
||||
}
|
||||
|
||||
// procesarContratos aplica filtros y envía notificaciones para una regla y lista de contratos
|
||||
func procesarContratos(regla *models.NotificacionRegla, contratos []models.Contrato) {
|
||||
if len(contratos) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Filtrar por AplicaA
|
||||
var filtrados []models.Contrato
|
||||
for _, c := range contratos {
|
||||
switch regla.AplicaA {
|
||||
case "renovable":
|
||||
for _, s := range c.Servicios {
|
||||
if s.Tipo == "renovable" {
|
||||
filtrados = append(filtrados, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "unico":
|
||||
for _, s := range c.Servicios {
|
||||
if s.Tipo == "unico" {
|
||||
filtrados = append(filtrados, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
filtrados = append(filtrados, c)
|
||||
}
|
||||
}
|
||||
if len(filtrados) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Agrupar por cliente
|
||||
porCliente := make(map[uint][]models.Contrato)
|
||||
for _, c := range filtrados {
|
||||
porCliente[c.ClienteID] = append(porCliente[c.ClienteID], c)
|
||||
}
|
||||
|
||||
for clienteID, grupoContratos := range porCliente {
|
||||
if models.YaEnviadoHoy(clienteID, regla.ID) {
|
||||
log.Printf("[CRON] Ya enviado hoy a cliente %d para regla %d — saltando", clienteID, regla.ID)
|
||||
continue
|
||||
}
|
||||
cliente := &grupoContratos[0].Cliente
|
||||
if err := EnviarNotificacionGrupo(regla, cliente, grupoContratos); err != nil {
|
||||
log.Printf("[CRON] Error enviando a cliente %d: %v", clienteID, err)
|
||||
} else {
|
||||
log.Printf("[CRON] Enviado a cliente %d (%s) — %d contrato(s)", clienteID, cliente.Email, len(grupoContratos))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +80,13 @@ func EnviarCorreoPrueba(email string, p *models.PlantillaCorreo) error {
|
||||
func EnviarCorreoManual(contrato *models.Contrato) error {
|
||||
// Intentar obtener plantilla y pasarela desde la primera regla activa
|
||||
var p *models.PlantillaCorreo
|
||||
var reglaID uint
|
||||
gateway := "bold"
|
||||
|
||||
reglas, err := models.GetReglasActivas()
|
||||
if err == nil && len(reglas) > 0 {
|
||||
regla := reglas[0]
|
||||
reglaID = regla.ID
|
||||
gateway = regla.PasarelaEnlace
|
||||
p, err = models.GetPlantillaByID(regla.PlantillaID)
|
||||
if err != nil {
|
||||
@@ -107,7 +109,7 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
|
||||
// Generar o reutilizar el enlace de pago
|
||||
enlacePago := ObtenerOCrearEnlacePago(contrato, gateway)
|
||||
|
||||
dias := int(contrato.FechaVencimiento.Sub(time.Now()).Hours() / 24)
|
||||
dias := int(time.Until(contrato.FechaVencimiento).Hours() / 24)
|
||||
var items []ItemServicio
|
||||
for _, s := range contrato.Servicios {
|
||||
items = append(items, ItemServicio{
|
||||
@@ -131,7 +133,34 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Http.Mail.Send(contrato.Cliente.Email, p.Asunto, html)
|
||||
|
||||
// Crear log previo (pendiente)
|
||||
idsJSON, _ := json.Marshal([]uint{contrato.ID})
|
||||
logEntry := models.NotificacionLog{
|
||||
ClienteID: contrato.ClienteID,
|
||||
ReglaID: reglaID,
|
||||
ContratosIDs: string(idsJSON),
|
||||
FechaEnvio: time.Now(),
|
||||
Estado: "pendiente",
|
||||
Asunto: p.Asunto,
|
||||
PreviewHTML: html,
|
||||
}
|
||||
savedLog, _ := models.CreateNotificacionLog(logEntry)
|
||||
|
||||
sendErr := app.Http.Mail.Send(contrato.Cliente.Email, 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
|
||||
}
|
||||
|
||||
// EnviarNotificacionGrupo envía un correo agrupado para un cliente con múltiples contratos
|
||||
@@ -160,7 +189,7 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl
|
||||
}
|
||||
}
|
||||
|
||||
dias := int(fechaVenc.Sub(time.Now()).Hours() / 24)
|
||||
dias := int(time.Until(fechaVenc).Hours() / 24)
|
||||
|
||||
// Generar o reutilizar el enlace de pago para el primer contrato del grupo
|
||||
enlacePago := ""
|
||||
|
||||
Reference in New Issue
Block a user