Implementa las 4 fases de la especificación de automatización: módulo de plantillas/tarifas editable por el equipo, generación de PDF (HTML+JS vía Chrome headless) para cotizaciones/contratos/arquitecturas/cuentas de cobro, chat propio en el dashboard reutilizando el mismo motor y tools del bot de Telegram, y nuevas tools del agente para crear estos documentos end-to-end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
137 lines
4.2 KiB
Go
137 lines
4.2 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// GenerarDocumento es el punto único que usan cotizaciones, contratos, actas de
|
|
// proyecto y cuentas de cobro para producir su PDF: toma la plantilla activa del
|
|
// tipo pedido (Capa 1, fuente de verdad editable en /plantillas-documento), la
|
|
// renderiza con los datos que arma cada endpoint, la convierte a PDF y la deja
|
|
// guardada en el historial (DocumentoGenerado) asociada al cliente/proyecto.
|
|
func GenerarDocumento(tipo string, datos map[string]interface{}, clienteID, proyectoID *uint, generadoPor string) (*models.DocumentoGenerado, []byte, error) {
|
|
plantilla, err := models.GetPlantillaDocumentoActiva(tipo)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("no hay una plantilla activa para el tipo '%s': %w", tipo, err)
|
|
}
|
|
|
|
tmpl, err := template.New("documento").Parse(plantilla.ContenidoHTML)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("plantilla HTML inválida: %w", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, datos); err != nil {
|
|
return nil, nil, fmt.Errorf("error al renderizar plantilla: %w", err)
|
|
}
|
|
|
|
pdfBytes, err := RenderHTMLToPDF(buf.String())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
filename := fmt.Sprintf("%s-%s.pdf", tipo, uuid.NewString())
|
|
subdir := tipo
|
|
if clienteID != nil {
|
|
subdir = fmt.Sprintf("%s/%d", tipo, *clienteID)
|
|
}
|
|
path, err := SaveGeneratedFile(subdir, filename, pdfBytes)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
doc := &models.DocumentoGenerado{
|
|
Tipo: tipo,
|
|
ClienteID: clienteID,
|
|
ProyectoID: proyectoID,
|
|
PlantillaID: &plantilla.ID,
|
|
Nombre: filename,
|
|
Archivo: path,
|
|
TipoMime: "application/pdf",
|
|
Tamanio: int64(len(pdfBytes)),
|
|
DatosJSON: marshalDatos(datos),
|
|
GeneradoPor: generadoPor,
|
|
}
|
|
if err := models.CreateDocumentoGenerado(doc); err != nil {
|
|
return nil, nil, fmt.Errorf("documento generado pero no se pudo guardar en el historial: %w", err)
|
|
}
|
|
|
|
return doc, pdfBytes, nil
|
|
}
|
|
|
|
// ItemCotizacion es un renglón de una cotización con el precio ya calculado
|
|
// por quien llama (Claude, tras consultar /api/v2/tarifas). El backend solo suma.
|
|
type ItemCotizacion struct {
|
|
Descripcion string `json:"descripcion"`
|
|
Cantidad float64 `json:"cantidad"`
|
|
ValorUnitario float64 `json:"valor_unitario"`
|
|
Unidad string `json:"unidad"`
|
|
}
|
|
|
|
// CrearCotizacion es la única implementación de "generar cotización": la usan por
|
|
// igual el endpoint POST /api/v2/cotizaciones y la tool crear_cotizacion del
|
|
// agente de Telegram/Claude, para que el comportamiento sea idéntico sin importar el canal.
|
|
func CrearCotizacion(clienteID uint, alcance, tipoProyecto string, items []ItemCotizacion, generadoPor string) (*models.DocumentoGenerado, float64, error) {
|
|
if clienteID == 0 {
|
|
return nil, 0, fmt.Errorf("cliente_id requerido")
|
|
}
|
|
if alcance == "" {
|
|
return nil, 0, fmt.Errorf("alcance requerido")
|
|
}
|
|
if len(items) == 0 {
|
|
return nil, 0, fmt.Errorf("items requerido (al menos un ítem con descripcion, cantidad y valor_unitario)")
|
|
}
|
|
|
|
cliente, err := models.GetClienteByID(clienteID)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("cliente no encontrado: %w", err)
|
|
}
|
|
|
|
var total float64
|
|
for _, it := range items {
|
|
total += it.Cantidad * it.ValorUnitario
|
|
}
|
|
|
|
datos := DatosBaseDocumento(map[string]interface{}{
|
|
"Cliente": cliente,
|
|
"Alcance": alcance,
|
|
"TipoProyecto": tipoProyecto,
|
|
"Items": items,
|
|
"Total": total,
|
|
})
|
|
|
|
doc, _, err := GenerarDocumento("cotizacion", datos, &clienteID, nil, generadoPor)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return doc, total, nil
|
|
}
|
|
|
|
// DatosBaseDocumento agrega los campos comunes a cualquier plantilla (fecha,
|
|
// datos fiscales de U-SITE) al mapa de datos específicos de cada tipo de documento.
|
|
func DatosBaseDocumento(extra map[string]interface{}) map[string]interface{} {
|
|
base := map[string]interface{}{
|
|
"Fecha": time.Now().Format("02/01/2006"),
|
|
"EmpresaNombre": "U-SITE S.A.S BIC",
|
|
"EmpresaWeb": "https://u-site.app",
|
|
}
|
|
for k, v := range extra {
|
|
base[k] = v
|
|
}
|
|
return base
|
|
}
|
|
|
|
func marshalDatos(datos map[string]interface{}) string {
|
|
b, err := json.Marshal(datos)
|
|
if err != nil {
|
|
return "{}"
|
|
}
|
|
return string(b)
|
|
}
|