diff --git a/pkg/services/telegram_agent_service.go b/pkg/services/telegram_agent_service.go index da0c9a2..b1c39fd 100644 --- a/pkg/services/telegram_agent_service.go +++ b/pkg/services/telegram_agent_service.go @@ -207,6 +207,15 @@ func agentTools() []agentTool { "search": str("Búsqueda por número, cliente o descripción"), "estado": agentToolParam{Type: "string", Description: "pendiente | pagada | vencida | cancelada", Enum: []string{"pendiente", "pagada", "vencida", "cancelada", ""}}, }, nil)), + tool("crear_factura", "Registra una factura de VENTA a partir de datos escritos, SIN archivo adjunto: U-SITE le factura/cobra al cliente. Usa esta cuando el usuario dicta los datos de la factura. Si en cambio acaba de enviar un PDF o una foto de la factura, usa adjuntar_factura.", + obj(map[string]agentToolParam{ + "cliente_id": num("ID del cliente al que se le factura (usa listar_clientes si no lo sabes)"), + "monto": num("Monto total de la factura"), + "numero": str("Número de factura, ej: FEV 92"), + "descripcion": str("Concepto, ej: 'Hosting por 1 año'"), + "fecha_emision": str("Fecha de emisión YYYY-MM-DD; si no se indica, hoy"), + "fecha_vencimiento": str("Fecha de vencimiento YYYY-MM-DD, opcional"), + }, []string{"cliente_id", "monto"})), tool("adjuntar_factura", "Guarda como factura de VENTA el documento (PDF/foto) que el usuario acaba de enviar por Telegram: U-SITE es quien factura/cobra al cliente. Solo funciona si hay un archivo adjunto pendiente. Si el documento es al revés (un proveedor le factura a U-SITE), usa adjuntar_factura_compra en su lugar, no esta.", obj(map[string]agentToolParam{ "cliente_id": num("ID del cliente al que pertenece la factura (usa listar_clientes si no lo sabes)"), @@ -663,6 +672,48 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{}, } return map[string]interface{}{"items": items, "total": total, "page": page}, nil + case "crear_factura": + clienteID := uint(getInt("cliente_id", 0)) + if clienteID == 0 { + return nil, fmt.Errorf("cliente_id requerido") + } + monto := getFloat("monto", 0) + if monto <= 0 { + return nil, fmt.Errorf("el monto debe ser mayor a cero") + } + emision := time.Now() + if v := getStr("fecha_emision"); v != "" { + if t, err := time.Parse("2006-01-02", v); err == nil { + emision = t + } + } + var vence *time.Time + if v := getStr("fecha_vencimiento"); v != "" { + if t, err := time.Parse("2006-01-02", v); err == nil { + vence = &t + } + } + f := &models.Factura{ + ClienteID: clienteID, + Numero: getStr("numero"), + Descripcion: getStr("descripcion"), + Monto: monto, + Moneda: "COP", + Estado: "pendiente", + FechaEmision: emision, + FechaVencimiento: vence, + Visible: true, + } + if err := models.CreateFactura(f); err != nil { + return nil, err + } + // Se devuelven los valores guardados, no los pedidos: es lo que el + // prompt le exige informar al usuario. + return map[string]interface{}{ + "ok": true, "factura_id": f.ID, "monto_guardado": f.Monto, + "numero": f.Numero, "cliente_id": f.ClienteID, + }, nil + case "adjuntar_factura": att, ok := PopTelegramAttachment(chatID) if !ok { diff --git a/pkg/services/telegram_tools_test.go b/pkg/services/telegram_tools_test.go new file mode 100644 index 0000000..2585f76 --- /dev/null +++ b/pkg/services/telegram_tools_test.go @@ -0,0 +1,45 @@ +package services + +import ( + "strings" + "testing" +) + +// Registrar una factura dictando los datos no era posible: la única +// herramienta de venta era adjuntar_factura, que exige un archivo pendiente. +// Sin una alternativa, el modelo no tenía con qué guardar y el usuario recibía +// un "guardado exitosamente" sobre algo que nunca se creó. +func TestExisteHerramientaParaFacturaSinAdjunto(t *testing.T) { + tools := agentTools() + + nombres := map[string]agentTool{} + for _, tl := range tools { + nombres[tl.Function.Name] = tl + } + + crear, ok := nombres["crear_factura"] + if !ok { + t.Fatal("falta crear_factura: no habría forma de registrar una factura de venta sin archivo adjunto") + } + if _, ok := nombres["adjuntar_factura"]; !ok { + t.Error("adjuntar_factura debe seguir existiendo para el caso con documento") + } + + // La descripción tiene que distinguirlas, o el modelo elige la equivocada + // y falla por falta de adjunto — que es justo el problema original. + d := strings.ToLower(crear.Function.Description) + if !strings.Contains(d, "sin archivo adjunto") { + t.Error("la descripción debe dejar claro que no necesita adjunto") + } + if !strings.Contains(d, "adjuntar_factura") { + t.Error("la descripción debe remitir a adjuntar_factura cuando sí hay documento") + } + + // cliente_id y monto son obligatorios: sin ellos la factura no sirve. + req := strings.Join(crear.Function.Parameters.Required, ",") + for _, campo := range []string{"cliente_id", "monto"} { + if !strings.Contains(req, campo) { + t.Errorf("%q debería ser obligatorio en crear_factura", campo) + } + } +}