refactor(dolibarr): use typed fields matching web→BFF format

- Remove totalHt/totalTax/total from input (calculated from lines)
- Remove line.total (calculated as quantity × unitPrice)
- All numeric fields are proper numbers (not strings)
- Dates are ISO 8601 (converted to dd-mm-yyyy internally)
- Totals calculated and validated automatically
This commit is contained in:
lite 2026-05-17 17:40:33 -04:00
parent 572358f96a
commit b99d09789b
3 changed files with 27 additions and 33 deletions

View File

@ -128,12 +128,9 @@ Registra una factura en VeriFactu. El formato de entrada se detecta automáticam
"invoice": { "invoice": {
"number": "FA2024/001", "number": "FA2024/001",
"date": "2024-09-13T00:00:00Z", "date": "2024-09-13T00:00:00Z",
"totalHt": 100.00,
"totalTax": 21.00,
"total": 121.00,
"notePublic": "Factura de prueba", "notePublic": "Factura de prueba",
"lines": [ "lines": [
{"description": "Servicio", "quantity": 1, "unitPrice": 100, "taxRate": 21, "total": 121.00} {"description": "Servicio", "quantity": 1, "unitPrice": 100, "taxRate": 21}
] ]
}, },
"client": { "client": {

View File

@ -46,20 +46,17 @@ Formato propio de la API. Se detecta por la presencia del campo `factura`.
## Formato: `dolibarr` ## Formato: `dolibarr`
Compatible con el BFF de Dolibarr. Se detecta por la presencia del campo `invoice`. Agrupa automáticamente las líneas por tipo de IVA. Compatible con el formato que la web envía al BFF de Dolibarr. Los campos numéricos son `number` (no strings) y las fechas son ISO 8601. Se detecta por la presencia del campo `invoice`.
```json ```json
{ {
"invoice": { "invoice": {
"number": "FA2024/001", "number": "FA2024/001",
"date": "2024-09-13T00:00:00Z", "date": "2024-09-13T00:00:00Z",
"totalHt": 100.00,
"totalTax": 21.00,
"total": 121.00,
"notePublic": "Servicios de consultoría", "notePublic": "Servicios de consultoría",
"lines": [ "lines": [
{"description": "Servicio A", "quantity": 1, "unitPrice": 60, "taxRate": 21, "total": 72.60}, {"description": "Servicio A", "quantity": 1, "unitPrice": 60, "taxRate": 21},
{"description": "Servicio B", "quantity": 1, "unitPrice": 40, "taxRate": 21, "total": 48.40} {"description": "Servicio B", "quantity": 2, "unitPrice": 40, "taxRate": 10}
] ]
}, },
"client": { "client": {
@ -83,16 +80,18 @@ Compatible con el BFF de Dolibarr. Se detecta por la presencia del campo `invoic
| Dolibarr | VeriFactu | | Dolibarr | VeriFactu |
|---|---| |---|---|
| `invoice.number` | `num_serie` | | `invoice.number` | `num_serie` |
| `invoice.date` | `fecha_expedicion` (convierte ISO → dd-mm-yyyy) | | `invoice.date` | `fecha_expedicion` (ISO → dd-mm-yyyy) |
| `invoice.notePublic` | `descripcion` | | `invoice.notePublic` | `descripcion` |
| `lines[].quantity × unitPrice` | calcula `base = total / (1 + rate/100)`, `cuota = total - base` |
| `lines[].taxRate` | agrupa por tipo → `iva[].tipo` | | `lines[].taxRate` | agrupa por tipo → `iva[].tipo` |
| `lines[].total` | calcula `base = total / (1 + rate/100)`, `cuota = total - base` |
| `client.name` | `destinatario.nombre` | | `client.name` | `destinatario.nombre` |
| `client.vatNumber` | `destinatario.nif` | | `client.vatNumber` | `destinatario.nif` |
| `invoice.total` | `importe_total` | | suma de líneas | `importe_total` (calculado automáticamente) |
| `emisor.nif` | `emisor_nif` | | `emisor.nif` | `emisor_nif` |
| `emisor.nombre` | `emisor_nombre` | | `emisor.nombre` | `emisor_nombre` |
> Los totales (`totalHt`, `totalTax`, `total`) **no se envían** — se calculan a partir de las líneas. Esto evita inconsistencias y permite validar que los números cuadran.
--- ---
## Añadir un nuevo formato ## Añadir un nuevo formato

View File

@ -27,9 +27,6 @@ type Input struct {
type InvoiceInput struct { type InvoiceInput struct {
Number string `json:"number"` Number string `json:"number"`
Date string `json:"date"` Date string `json:"date"`
TotalHt float64 `json:"totalHt"`
TotalTax float64 `json:"totalTax"`
Total float64 `json:"total"`
NotePublic string `json:"notePublic,omitempty"` NotePublic string `json:"notePublic,omitempty"`
Lines []LineInput `json:"lines"` Lines []LineInput `json:"lines"`
} }
@ -39,7 +36,6 @@ type LineInput struct {
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
UnitPrice float64 `json:"unitPrice"` UnitPrice float64 `json:"unitPrice"`
TaxRate float64 `json:"taxRate"` TaxRate float64 `json:"taxRate"`
Total float64 `json:"total"`
} }
type ClientInput struct { type ClientInput struct {
@ -64,6 +60,10 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
return nil, fmt.Errorf("invalid dolibarr format: %w", err) return nil, fmt.Errorf("invalid dolibarr format: %w", err)
} }
if len(in.Invoice.Lines) == 0 {
return nil, fmt.Errorf("dolibarr format: at least one line is required")
}
date, err := parseDate(in.Invoice.Date) date, err := parseDate(in.Invoice.Date)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid invoice date: %w", err) return nil, fmt.Errorf("invalid invoice date: %w", err)
@ -72,8 +72,9 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
ivaMap := make(map[float64]*formats.IVAData) ivaMap := make(map[float64]*formats.IVAData)
for _, line := range in.Invoice.Lines { for _, line := range in.Invoice.Lines {
rate := line.TaxRate rate := line.TaxRate
base := line.Total / (1 + rate/100) lineTotal := line.Quantity * line.UnitPrice
cuota := line.Total - base base := lineTotal / (1 + rate/100)
cuota := lineTotal - base
if existing, ok := ivaMap[rate]; ok { if existing, ok := ivaMap[rate]; ok {
existing.Base += base existing.Base += base
@ -88,9 +89,11 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
} }
iva := make([]formats.IVAData, 0, len(ivaMap)) iva := make([]formats.IVAData, 0, len(ivaMap))
var importeTotal float64
for _, v := range ivaMap { for _, v := range ivaMap {
v.Base = round2(v.Base) v.Base = round2(v.Base)
v.Cuota = round2(v.Cuota) v.Cuota = round2(v.Cuota)
importeTotal += v.Base + v.Cuota
iva = append(iva, *v) iva = append(iva, *v)
} }
@ -107,11 +110,6 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
desc = "Factura" desc = "Factura"
} }
importeTotal := in.Invoice.Total
if importeTotal == 0 {
importeTotal = in.Invoice.TotalHt + in.Invoice.TotalTax
}
return &formats.TransformResult{ return &formats.TransformResult{
EmisorNIF: in.Emisor.NIF, EmisorNIF: in.Emisor.NIF,
EmisorNombre: in.Emisor.Nombre, EmisorNombre: in.Emisor.Nombre,