feat: add format system with auto-detection
- Add internal/formats/registry.go with Transformer interface - Add internal/formats/native/ for current API format - Add internal/formats/dolibarr/ for Dolibarr BFF format - Refactor factura.go to use TransformAuto() for auto-detection - Remove format query param — detection is now automatic
This commit is contained in:
parent
74487015df
commit
16049fa3ef
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue