package models import ( "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" ) // UmindAccionPendiente es una acción que el agente preparó pero no ejecutó, // esperando el visto bueno de una persona. // // No es un motor de workflows: es una tabla y una regla. Las acciones que // salen hacia afuera (mandar un correo, entregar una cotización) pedidas por // alguien que no es el dueño no se ejecutan — se encolan. type UmindAccionPendiente 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"` Tipo string `json:"tipo" gorm:"column:tipo;size:20"` // correo | documento | herramienta // Herramienta y ArgumentosJSON son el payload EXACTO que se va a ejecutar. // Al aprobar se ejecuta esto y no se le vuelve a preguntar al modelo: si // se regenerara, aprobarías una cosa y saldría otra. Herramienta string `json:"herramienta" gorm:"column:herramienta;size:60;not null"` ArgumentosJSON string `json:"-" gorm:"column:argumentos_json;type:text"` // Resumen es lo que lee la persona. Un botón "Aprobar" sobre un JSON es // una firma en blanco. Resumen string `json:"resumen" gorm:"column:resumen;type:text"` SessionID string `json:"session_id" gorm:"column:session_id;size:120"` // para poder responderle a quien lo pidió // ArchivoID apunta a la vista previa cuando la acción produce un documento. ArchivoID *uint `json:"archivo_id" gorm:"column:archivo_id"` Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente';index"` ResueltaPor string `json:"resuelta_por" gorm:"column:resuelta_por;size:120"` ResueltaAt *time.Time `json:"resuelta_at" gorm:"column:resuelta_at"` Motivo string `json:"motivo" gorm:"column:motivo;type:text"` ExpiraAt time.Time `json:"expira_at" gorm:"column:expira_at;index"` ResultadoJSON string `json:"resultado_json" gorm:"column:resultado_json;type:text"` } func (UmindAccionPendiente) TableName() string { return "umind_acciones_pendientes" } // UmindAccionVigencia es cuánto vale un pendiente. Una cotización aprobada // tres semanas tarde llega con precios de otro mes: es peor que ninguna. const UmindAccionVigencia = 7 * 24 * time.Hour // UmindAccionMax acota los pendientes por agente. Sin tope, un visitante // insistente llena la bandeja del dueño y la vuelve inútil. const UmindAccionMax = 100 func CreateUmindAccion(a *UmindAccionPendiente) error { var n int64 app.Http.Database.DB.Model(&UmindAccionPendiente{}). Where("agente_id = ? AND estado = ? AND deleted_at IS NULL", a.AgenteID, "pendiente").Count(&n) if n >= UmindAccionMax { return gorm.ErrInvalidData } if a.ExpiraAt.IsZero() { a.ExpiraAt = time.Now().Add(UmindAccionVigencia) } return app.Http.Database.DB.Create(a).Error } func GetUmindAccionByID(id uint) (*UmindAccionPendiente, error) { var a UmindAccionPendiente if err := app.Http.Database.DB.First(&a, id).Error; err != nil { return nil, err } return &a, nil } // GetUmindAccionesPendientes lista la bandeja del espacio. Es del espacio y no // de un agente: aprobar es tarea de una persona, y las tareas van en una sola // bandeja — con tres agentes, tres pantallas significan que nadie revisa. func GetUmindAccionesPendientes(tenantID uint, incluirResueltas bool) ([]UmindAccionPendiente, error) { var items []UmindAccionPendiente q := app.Http.Database.DB.Where("tenant_id = ?", tenantID) if !incluirResueltas { q = q.Where("estado = ?", "pendiente") } err := q.Order("created_at DESC").Limit(200).Find(&items).Error return items, err } func ContarUmindAccionesPendientes(tenantID uint) int64 { var n int64 app.Http.Database.DB.Model(&UmindAccionPendiente{}). Where("tenant_id = ? AND estado = ? AND deleted_at IS NULL", tenantID, "pendiente").Count(&n) return n } // ReclamarAccion toma la acción para ejecutarla, una sola vez. Dos personas // mirando la misma bandeja pueden apretar "Aprobar" a la vez; sin esto la // cotización sale dos veces. func ReclamarAccion(id uint) bool { res := app.Http.Database.DB.Model(&UmindAccionPendiente{}). Where("id = ? AND estado = ?", id, "pendiente"). Update("estado", "ejecutando") return res.Error == nil && res.RowsAffected == 1 } func CerrarAccion(id uint, estado, quien, motivo, resultado string) error { ahora := time.Now() return app.Http.Database.DB.Model(&UmindAccionPendiente{}).Where("id = ?", id). Updates(map[string]interface{}{ "estado": estado, "resuelta_por": quien, "resuelta_at": &ahora, "motivo": motivo, "resultado_json": resultado, }).Error } // VencerAccionesViejas cierra las que nadie miró a tiempo. func VencerAccionesViejas() int64 { res := app.Http.Database.DB.Model(&UmindAccionPendiente{}). Where("estado = ? AND expira_at < ?", "pendiente", time.Now()). Update("estado", "vencida") return res.RowsAffected }