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": {
"number": "FA2024/001",
"date": "2024-09-13T00:00:00Z",
"totalHt": 100.00,
"totalTax": 21.00,
"total": 121.00,
"notePublic": "Factura de prueba",
"lines": [
{"description": "Servicio", "quantity": 1, "unitPrice": 100, "taxRate": 21, "total": 121.00}
{"description": "Servicio", "quantity": 1, "unitPrice": 100, "taxRate": 21}
]
},
"client": {

View File

@ -46,20 +46,17 @@ Formato propio de la API. Se detecta por la presencia del campo `factura`.
## 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
{
"invoice": {
"number": "FA2024/001",
"date": "2024-09-13T00:00:00Z",
"totalHt": 100.00,
"totalTax": 21.00,
"total": 121.00,
"notePublic": "Servicios de consultoría",
"lines": [
{"description": "Servicio A", "quantity": 1, "unitPrice": 60, "taxRate": 21, "total": 72.60},
{"description": "Servicio B", "quantity": 1, "unitPrice": 40, "taxRate": 21, "total": 48.40}
{"description": "Servicio A", "quantity": 1, "unitPrice": 60, "taxRate": 21},
{"description": "Servicio B", "quantity": 2, "unitPrice": 40, "taxRate": 10}
]
},
"client": {
@ -83,16 +80,18 @@ Compatible con el BFF de Dolibarr. Se detecta por la presencia del campo `invoic
| Dolibarr | VeriFactu |
|---|---|
| `invoice.number` | `num_serie` |
| `invoice.date` | `fecha_expedicion` (convierte ISO → dd-mm-yyyy) |
| `invoice.date` | `fecha_expedicion` (ISO → dd-mm-yyyy) |
| `invoice.notePublic` | `descripcion` |
| `lines[].quantity × unitPrice` | calcula `base = total / (1 + rate/100)`, `cuota = total - base` |
| `lines[].taxRate` | agrupa por tipo → `iva[].tipo` |
| `lines[].total` | calcula `base = total / (1 + rate/100)`, `cuota = total - base` |
| `client.name` | `destinatario.nombre` |
| `client.vatNumber` | `destinatario.nif` |
| `invoice.total` | `importe_total` |
| suma de líneas | `importe_total` (calculado automáticamente) |
| `emisor.nif` | `emisor_nif` |
| `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

View File

@ -18,20 +18,17 @@ type Transformer struct{}
func (t *Transformer) Name() string { return "dolibarr" }
type Input struct {
Invoice InvoiceInput `json:"invoice"`
Client *ClientInput `json:"client,omitempty"`
Emisor EmisorInput `json:"emisor"`
Sistema SistemaInput `json:"sistema"`
Invoice InvoiceInput `json:"invoice"`
Client *ClientInput `json:"client,omitempty"`
Emisor EmisorInput `json:"emisor"`
Sistema SistemaInput `json:"sistema"`
}
type InvoiceInput struct {
Number string `json:"number"`
Date string `json:"date"`
TotalHt float64 `json:"totalHt"`
TotalTax float64 `json:"totalTax"`
Total float64 `json:"total"`
NotePublic string `json:"notePublic,omitempty"`
Lines []LineInput `json:"lines"`
Number string `json:"number"`
Date string `json:"date"`
NotePublic string `json:"notePublic,omitempty"`
Lines []LineInput `json:"lines"`
}
type LineInput struct {
@ -39,11 +36,10 @@ type LineInput struct {
Quantity float64 `json:"quantity"`
UnitPrice float64 `json:"unitPrice"`
TaxRate float64 `json:"taxRate"`
Total float64 `json:"total"`
}
type ClientInput struct {
Name string `json:"name"`
Name string `json:"name"`
VatNumber string `json:"vatNumber"`
}
@ -64,6 +60,10 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
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)
if err != nil {
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)
for _, line := range in.Invoice.Lines {
rate := line.TaxRate
base := line.Total / (1 + rate/100)
cuota := line.Total - base
lineTotal := line.Quantity * line.UnitPrice
base := lineTotal / (1 + rate/100)
cuota := lineTotal - base
if existing, ok := ivaMap[rate]; ok {
existing.Base += base
@ -88,9 +89,11 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
}
iva := make([]formats.IVAData, 0, len(ivaMap))
var importeTotal float64
for _, v := range ivaMap {
v.Base = round2(v.Base)
v.Cuota = round2(v.Cuota)
importeTotal += v.Base + v.Cuota
iva = append(iva, *v)
}
@ -107,11 +110,6 @@ func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult,
desc = "Factura"
}
importeTotal := in.Invoice.Total
if importeTotal == 0 {
importeTotal = in.Invoice.TotalHt + in.Invoice.TotalTax
}
return &formats.TransformResult{
EmisorNIF: in.Emisor.NIF,
EmisorNombre: in.Emisor.Nombre,