diff --git a/internal/factura.go b/internal/factura.go index 6e21e1f..c47ee0c 100644 --- a/internal/factura.go +++ b/internal/factura.go @@ -1,10 +1,12 @@ package internal import ( + "encoding/json" "fmt" "log" "time" + "VerifactuMidAPI/internal/formats" "VerifactuMidAPI/verifactu" ) @@ -23,11 +25,6 @@ func (s *FacturaService) SetVerifactuClient(client *verifactu.Client) { s.verifactu = client } -type AltaInput struct { - InvoiceInput - EmisorNombre string -} - type AltaOutput struct { Success bool `json:"success"` CSV string `json:"csv,omitempty"` @@ -35,39 +32,81 @@ type AltaOutput struct { Error string `json:"error,omitempty"` } -func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) { +func (s *FacturaService) ProcessAlta(raw json.RawMessage) (*AltaOutput, error) { defer func() { if r := recover(); r != nil { log.Printf("PANIC: %v", r) } }() - errs := ValidateInvoiceInput(&input.InvoiceInput) - if len(errs) > 0 { - errMsg := "" - for _, e := range errs { - errMsg += e.Error() + "; " - } - log.Printf("validation errors: %s", errMsg) + result, formatName, err := formats.TransformAuto(raw) + if err != nil { return &AltaOutput{ Success: false, - Error: "validation_failed: " + errMsg, + Error: "transform_error: " + err.Error(), }, nil } - data, err := TransformToInvoiceData(&input.InvoiceInput) - if err != nil { - log.Printf("transform error: %v", err) - return &AltaOutput{ - Success: false, - Error: err.Error(), - }, nil + log.Printf("detected format: %s", formatName) + return s.processInvoiceData(result) +} + +func (s *FacturaService) processInvoiceData(result *formats.TransformResult) (*AltaOutput, error) { + fecha, _ := time.Parse("02-01-2006", result.FechaExpedicion) + + ivaList := make([]IVARegularizacion, len(result.IVA)) + for i, v := range result.IVA { + ivaList[i] = IVARegularizacion{ + Base: v.Base, + Cuota: v.Cuota, + Tipo: v.Tipo, + ClaveRegimen: "01", + Calificacion: "S1", + } + } + + cuotaTotal := 0.0 + for _, v := range result.IVA { + cuotaTotal += v.Cuota + } + + var dest *Destinatario + if result.Destinatario != nil { + dest = &Destinatario{ + Nombre: result.Destinatario.Nombre, + NIF: result.Destinatario.NIF, + } + } + + data := &InvoiceData{ + Tipo: "alta", + EmisorNIF: result.EmisorNIF, + NumSerie: result.NumSerie, + Fecha: fecha, + TipoFactura: result.TipoFactura, + Descripcion: result.Descripcion, + Destinatario: dest, + IVA: ivaList, + CuotaTotal: cuotaTotal, + ImporteTotal: result.ImporteTotal, + Sistema: Sistema{ + Nombre: result.Sistema.Nombre, + NIFProveedor: result.Sistema.NIFProveedor, + NombreSistema: result.Sistema.Nombre, + IDSistema: "01", + Version: result.Sistema.Version, + NumeroInstalacion: "1", + TipoUsoVerifactu: "S", + TipoUsoMultiOT: "N", + IndicadorMultiOT: "N", + }, + FechaGen: time.Now(), } var prevHash, prevNumSerie string var prevFecha time.Time if s.hashStorage != nil { - record, err := s.hashStorage.GetLastRecord(input.Factura.EmisorNIF) + record, err := s.hashStorage.GetLastRecord(result.EmisorNIF) if err != nil { return &AltaOutput{ Success: false, @@ -96,14 +135,38 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) { data.Huella = currentHash data.FechaGen = now - altaData := ToAltaData(&input.InvoiceInput, data, currentHash, prevHash, prevNumSerie, prevFecha) + altaData := verifactu.AltaData{ + EmisorNombre: result.EmisorNombre, + EmisorNIF: data.EmisorNIF, + NumSerie: data.NumSerie, + FechaExpedicion: data.Fecha, + TipoFactura: data.TipoFactura, + Descripcion: data.Descripcion, + DestinatarioNombre: destNombre(data.Destinatario), + DestinatarioNIF: destNIF(data.Destinatario), + IVA: toIVAData(data.IVA), + CuotaTotal: data.CuotaTotal, + ImporteTotal: data.ImporteTotal, + Sistema: verifactu.SistemaData{ + Nombre: data.Sistema.Nombre, + NIFProveedor: data.Sistema.NIFProveedor, + NombreSistema: data.Sistema.NombreSistema, + Version: data.Sistema.Version, + NumeroInstalacion: data.Sistema.NumeroInstalacion, + TipoUsoVerifactu: data.Sistema.TipoUsoVerifactu, + }, + Huella: currentHash, + PrevHash: prevHash, + PrevNumSerie: prevNumSerie, + PrevFecha: prevFecha, + FechaGen: data.FechaGen, + } if s.verifactu != nil { log.Printf("Sending to AEAT...") resp, err := s.verifactu.SendAlta(altaData) if err != nil { log.Printf("AEAT error: %v", err) - log.Printf("AEAT error, falling back to local: %v", err) goto saveLocal } @@ -163,6 +226,34 @@ saveLocal: }, nil } +func destNombre(d *Destinatario) string { + if d == nil { + return "" + } + return d.Nombre +} + +func destNIF(d *Destinatario) string { + if d == nil { + return "" + } + return d.NIF +} + +func toIVAData(list []IVARegularizacion) []verifactu.IVARegularizacionData { + out := make([]verifactu.IVARegularizacionData, len(list)) + for i, v := range list { + out[i] = verifactu.IVARegularizacionData{ + Base: v.Base, + Cuota: v.Cuota, + Tipo: v.Tipo, + ClaveRegimen: v.ClaveRegimen, + Calificacion: v.Calificacion, + } + } + return out +} + type AnulacionInput struct { InvoiceInput EmisorNombre string @@ -200,50 +291,3 @@ func (s *FacturaService) ProcessAnulacion(input AnulacionInput) (*AnulacionOutpu Estado: "Anulada", }, nil } - -func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash, prevNumSerie string, prevFecha time.Time) verifactu.AltaData { - ivaData := make([]verifactu.IVARegularizacionData, len(data.IVA)) - for i, iva := range data.IVA { - ivaData[i] = verifactu.IVARegularizacionData{ - Base: iva.Base, - Cuota: iva.Cuota, - Tipo: iva.Tipo, - ClaveRegimen: iva.ClaveRegimen, - Calificacion: iva.Calificacion, - } - } - - destNombre := "" - destNIF := "" - if data.Destinatario != nil { - destNombre = data.Destinatario.Nombre - destNIF = data.Destinatario.NIF - } - - return verifactu.AltaData{ - EmisorNombre: in.Factura.EmisorNombre, - EmisorNIF: data.EmisorNIF, - NumSerie: data.NumSerie, - FechaExpedicion: data.Fecha, - TipoFactura: data.TipoFactura, - Descripcion: data.Descripcion, - DestinatarioNombre: destNombre, - DestinatarioNIF: destNIF, - IVA: ivaData, - CuotaTotal: data.CuotaTotal, - ImporteTotal: data.ImporteTotal, - Sistema: verifactu.SistemaData{ - Nombre: data.Sistema.Nombre, - NIFProveedor: data.Sistema.NIFProveedor, - NombreSistema: data.Sistema.NombreSistema, - Version: data.Sistema.Version, - NumeroInstalacion: data.Sistema.NumeroInstalacion, - TipoUsoVerifactu: data.Sistema.TipoUsoVerifactu, - }, - Huella: huella, - PrevHash: prevHash, - PrevNumSerie: prevNumSerie, - PrevFecha: prevFecha, - FechaGen: data.FechaGen, - } -} diff --git a/internal/formats/dolibarr/format.go b/internal/formats/dolibarr/format.go new file mode 100644 index 0000000..f059d32 --- /dev/null +++ b/internal/formats/dolibarr/format.go @@ -0,0 +1,151 @@ +package dolibarr + +import ( + "encoding/json" + "fmt" + "math" + "time" + + "VerifactuMidAPI/internal/formats" +) + +func init() { + formats.Register(&Transformer{}) +} + +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"` +} + +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"` +} + +type LineInput struct { + Description string `json:"description"` + Quantity float64 `json:"quantity"` + UnitPrice float64 `json:"unitPrice"` + TaxRate float64 `json:"taxRate"` + Total float64 `json:"total"` +} + +type ClientInput struct { + Name string `json:"name"` + VatNumber string `json:"vatNumber"` +} + +type EmisorInput struct { + NIF string `json:"nif"` + Nombre string `json:"nombre"` +} + +type SistemaInput struct { + Nombre string `json:"nombre"` + NIFProveedor string `json:"nif_proveedor"` + Version string `json:"version"` +} + +func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult, error) { + var in Input + if err := json.Unmarshal(raw, &in); err != nil { + return nil, fmt.Errorf("invalid dolibarr format: %w", err) + } + + date, err := parseDate(in.Invoice.Date) + if err != nil { + return nil, fmt.Errorf("invalid invoice date: %w", err) + } + + 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 + + if existing, ok := ivaMap[rate]; ok { + existing.Base += base + existing.Cuota += cuota + } else { + ivaMap[rate] = &formats.IVAData{ + Base: base, + Cuota: cuota, + Tipo: rate, + } + } + } + + iva := make([]formats.IVAData, 0, len(ivaMap)) + for _, v := range ivaMap { + v.Base = round2(v.Base) + v.Cuota = round2(v.Cuota) + iva = append(iva, *v) + } + + var dest *formats.DestinatarioData + if in.Client != nil && in.Client.VatNumber != "" { + dest = &formats.DestinatarioData{ + Nombre: in.Client.Name, + NIF: in.Client.VatNumber, + } + } + + desc := in.Invoice.NotePublic + if desc == "" { + 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, + NumSerie: in.Invoice.Number, + FechaExpedicion: date, + TipoFactura: "F1", + Descripcion: desc, + Destinatario: dest, + IVA: iva, + ImporteTotal: round2(importeTotal), + Sistema: formats.SistemaData{ + Nombre: in.Sistema.Nombre, + NIFProveedor: in.Sistema.NIFProveedor, + Version: in.Sistema.Version, + }, + }, nil +} + +func parseDate(s string) (string, error) { + formats := []string{ + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05", + "2006-01-02", + } + for _, f := range formats { + t, err := time.Parse(f, s) + if err == nil { + return t.Format("02-01-2006"), nil + } + } + return "", fmt.Errorf("cannot parse date %q", s) +} + +func round2(v float64) float64 { + return math.Round(v*100) / 100 +} diff --git a/internal/formats/native/format.go b/internal/formats/native/format.go new file mode 100644 index 0000000..b858eff --- /dev/null +++ b/internal/formats/native/format.go @@ -0,0 +1,94 @@ +package native + +import ( + "encoding/json" + "fmt" + "time" + + "VerifactuMidAPI/internal/formats" +) + +func init() { + formats.Register(&Transformer{}) +} + +type Transformer struct{} + +func (t *Transformer) Name() string { return "native" } + +type Input struct { + Tipo string `json:"tipo"` + Factura FacturaInput `json:"factura"` + Sistema SistemaInput `json:"sistema"` +} + +type FacturaInput struct { + EmisorNIF string `json:"emisor_nif"` + EmisorNombre string `json:"emisor_nombre"` + NumSerie string `json:"num_serie"` + FechaExpedicion string `json:"fecha_expedicion"` + TipoFactura string `json:"tipo_factura"` + Descripcion string `json:"descripcion"` + Destinatario *DestinatarioInput `json:"destinatario,omitempty"` + IVA []IVAInput `json:"iva"` + ImporteTotal float64 `json:"importe_total"` +} + +type DestinatarioInput struct { + Nombre string `json:"nombre"` + NIF string `json:"nif"` +} + +type IVAInput struct { + Base float64 `json:"base"` + Cuota float64 `json:"cuota"` + Tipo float64 `json:"tipo"` +} + +type SistemaInput struct { + Nombre string `json:"nombre"` + NIFProveedor string `json:"nif_proveedor"` + Version string `json:"version"` +} + +func (t *Transformer) Transform(raw json.RawMessage) (*formats.TransformResult, error) { + var in Input + if err := json.Unmarshal(raw, &in); err != nil { + return nil, fmt.Errorf("invalid native format: %w", err) + } + + var dest *formats.DestinatarioData + if in.Factura.Destinatario != nil { + dest = &formats.DestinatarioData{ + Nombre: in.Factura.Destinatario.Nombre, + NIF: in.Factura.Destinatario.NIF, + } + } + + iva := make([]formats.IVAData, len(in.Factura.IVA)) + for i, v := range in.Factura.IVA { + iva[i] = formats.IVAData{Base: v.Base, Cuota: v.Cuota, Tipo: v.Tipo} + } + + _, err := time.Parse("02-01-2006", in.Factura.FechaExpedicion) + if err != nil { + return nil, fmt.Errorf("invalid fecha_expedicion: %w", err) + } + + return &formats.TransformResult{ + EmisorNIF: in.Factura.EmisorNIF, + EmisorNombre: in.Factura.EmisorNombre, + NumSerie: in.Factura.NumSerie, + FechaExpedicion: in.Factura.FechaExpedicion, + TipoFactura: in.Factura.TipoFactura, + Descripcion: in.Factura.Descripcion, + Destinatario: dest, + IVA: iva, + ImporteTotal: in.Factura.ImporteTotal, + Sistema: formats.SistemaData{ + Nombre: in.Sistema.Nombre, + NIFProveedor: in.Sistema.NIFProveedor, + Version: in.Sistema.Version, + }, + }, nil +} diff --git a/internal/formats/registry.go b/internal/formats/registry.go new file mode 100644 index 0000000..10619d3 --- /dev/null +++ b/internal/formats/registry.go @@ -0,0 +1,71 @@ +package formats + +import ( + "encoding/json" + "fmt" + "sort" +) + +type Transformer interface { + Name() string + Transform(raw json.RawMessage) (*TransformResult, error) +} + +type TransformResult struct { + EmisorNIF string + EmisorNombre string + NumSerie string + FechaExpedicion string + TipoFactura string + Descripcion string + Destinatario *DestinatarioData + IVA []IVAData + ImporteTotal float64 + Sistema SistemaData +} + +type DestinatarioData struct { + Nombre string + NIF string +} + +type IVAData struct { + Base float64 + Cuota float64 + Tipo float64 +} + +type SistemaData struct { + Nombre string + NIFProveedor string + Version string +} + +var registry = make(map[string]Transformer) +var order []string + +func Register(t Transformer) { + name := t.Name() + if _, exists := registry[name]; !exists { + order = append(order, name) + } + registry[name] = t +} + +func TransformAuto(raw json.RawMessage) (*TransformResult, string, error) { + for _, name := range order { + t := registry[name] + result, err := t.Transform(raw) + if err == nil { + return result, name, nil + } + } + return nil, "", fmt.Errorf("no matching format found (available: %v)", Available()) +} + +func Available() []string { + names := make([]string, len(order)) + copy(names, order) + sort.Strings(names) + return names +} diff --git a/main.go b/main.go index 52d0822..a26ea35 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,8 @@ import ( "time" "VerifactuMidAPI/api" + _ "VerifactuMidAPI/internal/formats/dolibarr" + _ "VerifactuMidAPI/internal/formats/native" "VerifactuMidAPI/internal" "VerifactuMidAPI/internal/cert" "VerifactuMidAPI/internal/config"