"Avisame el 15 de marzo que vence la póliza de Acme, y todos los años" queda programado desde la conversación, visible y cancelable en el panel. Tres tools: programar, listar, cancelar. El aviso vuelve por donde se pidió — Telegram a ese chat, correo a esa casilla — o al dueño del espacio por correo y campanita. Nunca a una dirección que dicte la conversación: eso sería un cañón de spam con destinatario libre. Y las tools de aviso solo se le OFRECEN al modelo cuando la sesión es interna: el chat de prueba del panel (que corre autenticado) o un canal marcado como línea privada del dueño. En el widget público escribe cualquiera, y cualquiera no puede programarle recordatorios ni gastarle el plan a otro. El gate está en dos capas: la tool no se declara, y si igual la pide, el ejecutor la rechaza. Tres detalles que se pagan una sola vez: - El cron corre en memoria del proceso, sin lock distribuido: con dos instancias cada aviso saldría dos veces. El reclamo es un UPDATE condicional — la base ya es el árbitro, no hace falta traer otro. - Si el servidor estuvo caído, una repetición diaria se saltea los ciclos perdidos en vez de disparar diez avisos viejos de golpe. - Un fallo de SMTP devuelve el aviso a pendiente: una caída de correo no puede perder un vencimiento de póliza. Una fecha sin hora se entrega a las 9, no a medianoche, que es cuando nadie mira el teléfono. De paso: los archivos de los espacios uMind nunca se sirven por el estático de /uploads. El guard genérico solo sabe si hay sesión de panel, no de quién es el archivo — salen por su endpoint, que sí valida propiedad. Cerrado por construcción y no por acordarse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
2.4 KiB
Go
92 lines
2.4 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestProyectoTicketTableName(t *testing.T) {
|
|
if got := (ProyectoTicket{}).TableName(); got != "proyecto_tickets" {
|
|
t.Errorf("TableName() = %q, want %q", got, "proyecto_tickets")
|
|
}
|
|
}
|
|
|
|
func TestTicketMensajeTableName(t *testing.T) {
|
|
if got := (TicketMensaje{}).TableName(); got != "ticket_mensajes" {
|
|
t.Errorf("TableName() = %q, want %q", got, "ticket_mensajes")
|
|
}
|
|
}
|
|
|
|
func TestProyectoTicketDefaults(t *testing.T) {
|
|
ticket := ProyectoTicket{
|
|
Titulo: "Test ticket",
|
|
Descripcion: "Descripción",
|
|
AutorNombre: "Cliente",
|
|
}
|
|
if ticket.Estado != "" {
|
|
t.Errorf("Estado debe ser vacío por defecto (GORM default), got %q", ticket.Estado)
|
|
}
|
|
if ticket.Prioridad != "" {
|
|
t.Errorf("Prioridad debe ser vacía por defecto (GORM default), got %q", ticket.Prioridad)
|
|
}
|
|
if ticket.Origen != "" {
|
|
t.Errorf("Origen debe ser vacío por defecto (GORM default), got %q", ticket.Origen)
|
|
}
|
|
if ticket.ProyectoID != nil {
|
|
t.Errorf("ProyectoID debe ser nil por defecto, got %v", ticket.ProyectoID)
|
|
}
|
|
if ticket.PortalUserID != nil {
|
|
t.Errorf("PortalUserID debe ser nil por defecto, got %v", ticket.PortalUserID)
|
|
}
|
|
}
|
|
|
|
func TestProyectoTicketEmailFrom(t *testing.T) {
|
|
ticket := ProyectoTicket{
|
|
EmailFrom: "cliente@ejemplo.com",
|
|
AutorNombre: "Cliente",
|
|
Titulo: "Soporte",
|
|
Origen: "email",
|
|
}
|
|
if ticket.EmailFrom != "cliente@ejemplo.com" {
|
|
t.Errorf("EmailFrom = %q, want %q", ticket.EmailFrom, "cliente@ejemplo.com")
|
|
}
|
|
if ticket.Origen != "email" {
|
|
t.Errorf("Origen = %q, want %q", ticket.Origen, "email")
|
|
}
|
|
}
|
|
|
|
func TestProyectoTicketAsignado(t *testing.T) {
|
|
uid := uint(42)
|
|
ticket := ProyectoTicket{
|
|
AsignadoA: &uid,
|
|
}
|
|
if ticket.AsignadoA == nil {
|
|
t.Fatal("AsignadoA debe ser no nil")
|
|
}
|
|
if *ticket.AsignadoA != 42 {
|
|
t.Errorf("AsignadoA = %d, want %d", *ticket.AsignadoA, 42)
|
|
}
|
|
}
|
|
|
|
func TestProyectoTicketAsignadoNil(t *testing.T) {
|
|
ticket := ProyectoTicket{}
|
|
if ticket.AsignadoA != nil {
|
|
t.Errorf("AsignadoA debe ser nil por defecto, got %v", *ticket.AsignadoA)
|
|
}
|
|
}
|
|
|
|
func TestTicketMensajeDefaults(t *testing.T) {
|
|
msg := TicketMensaje{
|
|
TicketID: 1,
|
|
Contenido: "Hola",
|
|
AutorNombre: "Admin",
|
|
}
|
|
if msg.EsAdmin {
|
|
t.Error("EsAdmin debe ser false por defecto")
|
|
}
|
|
if msg.LeidoPortal {
|
|
t.Error("LeidoPortal debe ser false por defecto")
|
|
}
|
|
}
|
|
|
|
// CountTicketsByEstado requiere DB real — probado en integration tests
|