package models import ( "fmt" "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" ) // UmindAviso es un recordatorio que el propio dueño le pidió al agente: // "avisame el 15 de marzo que vence la póliza de Acme, y todos los años". // Vuelve por donde se pidió — nunca a una dirección que dicte la conversación. type UmindAviso struct { gorm.Model AgenteID uint `json:"agente_id" gorm:"column:agente_id;index;not null"` TenantID uint `json:"tenant_id" gorm:"column:tenant_id;index"` // desnormalizado: listar por espacio sin join Titulo string `json:"titulo" gorm:"column:titulo;size:200;not null"` Detalle string `json:"detalle" gorm:"column:detalle;type:text"` // ProximoAt es cuándo toca. En repeticiones se corre solo hacia adelante. ProximoAt time.Time `json:"proximo_at" gorm:"column:proximo_at;index;not null"` Repetir string `json:"repetir" gorm:"column:repetir;size:20"` // "" | diario | semanal | mensual | anual // Destino es el sessionID de donde salió el pedido ("tg:123", "mail:a@b"). // Vacío = al dueño del espacio por correo. Destino string `json:"destino" gorm:"column:destino;size:120"` Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente';index"` UltimoEnvioAt *time.Time `json:"ultimo_envio_at" gorm:"column:ultimo_envio_at"` Error string `json:"error" gorm:"column:error;type:text"` } func (UmindAviso) TableName() string { return "umind_avisos" } // UmindAvisoMax acota cuántos avisos pendientes puede tener un agente. Sin // tope, una conversación en bucle programa mil recordatorios. const UmindAvisoMax = 50 func CreateUmindAviso(a *UmindAviso) error { var n int64 app.Http.Database.DB.Model(&UmindAviso{}). Where("agente_id = ? AND estado = ? AND deleted_at IS NULL", a.AgenteID, "pendiente").Count(&n) if n >= UmindAvisoMax { return fmt.Errorf("este agente ya tiene el máximo de %d avisos programados", UmindAvisoMax) } return app.Http.Database.DB.Create(a).Error } func GetUmindAvisosByAgente(agenteID uint) ([]UmindAviso, error) { var items []UmindAviso err := app.Http.Database.DB.Where("agente_id = ?", agenteID). Order("proximo_at ASC").Limit(100).Find(&items).Error return items, err } func GetUmindAvisoByID(id uint) (*UmindAviso, error) { var a UmindAviso if err := app.Http.Database.DB.First(&a, id).Error; err != nil { return nil, err } return &a, nil } // CancelarUmindAviso solo cancela los pendientes: cancelar uno ya enviado no // significa nada y ocultaría un error de identificación del agente. func CancelarUmindAviso(id, agenteID uint) error { return app.Http.Database.DB.Model(&UmindAviso{}). Where("id = ? AND agente_id = ? AND estado = ?", id, agenteID, "pendiente"). Update("estado", "cancelado").Error } func DeleteUmindAviso(id uint) error { return app.Http.Database.DB.Delete(&UmindAviso{}, id).Error } // GetUmindAvisosVencidos devuelve los que ya deberían haber salido. func GetUmindAvisosVencidos() ([]UmindAviso, error) { var items []UmindAviso err := app.Http.Database.DB. Where("estado = ? AND proximo_at <= ? AND deleted_at IS NULL", "pendiente", time.Now()). Limit(200).Find(&items).Error return items, err } // ReclamarAviso marca el aviso como "enviando" solo si seguía pendiente, y // dice si el reclamo fue de este proceso. El cron corre en memoria del // proceso, sin lock distribuido: con dos instancias del binario levantadas // cada aviso saldría dos veces. Este UPDATE condicional es el lock — la base // ya es el árbitro, no hace falta traer otro. func ReclamarAviso(id uint) bool { res := app.Http.Database.DB.Model(&UmindAviso{}). Where("id = ? AND estado = ?", id, "pendiente"). Update("estado", "enviando") return res.Error == nil && res.RowsAffected == 1 } // ReprogramarAviso corre la fecha hacia adelante según la repetición y lo // deja pendiente otra vez. Si no repite, queda enviado. func ReprogramarAviso(a *UmindAviso) error { ahora := time.Now() updates := map[string]interface{}{"ultimo_envio_at": &ahora, "error": ""} proximo := siguienteFecha(a.ProximoAt, a.Repetir) if proximo.IsZero() { updates["estado"] = "enviado" } else { // Si el servidor estuvo caído varios ciclos, se salta los perdidos en // vez de disparar una ráfaga de avisos viejos. for !proximo.After(ahora) { proximo = siguienteFecha(proximo, a.Repetir) } updates["estado"] = "pendiente" updates["proximo_at"] = proximo } return app.Http.Database.DB.Model(&UmindAviso{}).Where("id = ?", a.ID).Updates(updates).Error } func siguienteFecha(desde time.Time, repetir string) time.Time { switch repetir { case "diario": return desde.AddDate(0, 0, 1) case "semanal": return desde.AddDate(0, 0, 7) case "mensual": return desde.AddDate(0, 1, 0) case "anual": return desde.AddDate(1, 0, 0) } return time.Time{} } // MarcarAvisoFallido lo devuelve a pendiente para el próximo ciclo: una caída // de SMTP no puede perder un vencimiento de póliza. func MarcarAvisoFallido(id uint, motivo string) { app.Http.Database.DB.Model(&UmindAviso{}).Where("id = ?", id). Updates(map[string]interface{}{"estado": "pendiente", "error": motivo}) } // SiguienteFechaAviso se exporta solo para poder probar el cálculo de // repeticiones sin base de datos de por medio. func SiguienteFechaAviso(desde time.Time, repetir string) time.Time { return siguienteFecha(desde, repetir) }