2026-04-17 11:03:06 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-17 20:27:27 +00:00
|
|
|
"encoding/json"
|
2026-05-12 23:13:09 +00:00
|
|
|
"fmt"
|
2026-04-17 11:03:06 +00:00
|
|
|
"log"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-05-17 20:27:27 +00:00
|
|
|
"VerifactuMidAPI/internal/formats"
|
2026-04-17 11:03:06 +00:00
|
|
|
"VerifactuMidAPI/verifactu"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type FacturaService struct {
|
|
|
|
|
hashStorage LastRecordStorage
|
|
|
|
|
verifactu *verifactu.Client
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewFacturaService(storage LastRecordStorage) *FacturaService {
|
|
|
|
|
return &FacturaService{
|
|
|
|
|
hashStorage: storage,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *FacturaService) SetVerifactuClient(client *verifactu.Client) {
|
|
|
|
|
s.verifactu = client
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AltaOutput struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
CSV string `json:"csv,omitempty"`
|
|
|
|
|
Estado string `json:"estado,omitempty"`
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 20:27:27 +00:00
|
|
|
func (s *FacturaService) ProcessAlta(raw json.RawMessage) (*AltaOutput, error) {
|
2026-04-17 11:03:06 +00:00
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
log.Printf("PANIC: %v", r)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2026-05-17 20:27:27 +00:00
|
|
|
result, formatName, err := formats.TransformAuto(raw)
|
|
|
|
|
if err != nil {
|
2026-04-17 11:03:06 +00:00
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
2026-05-17 20:27:27 +00:00
|
|
|
Error: "transform_error: " + err.Error(),
|
2026-04-17 11:03:06 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 20:27:27 +00:00
|
|
|
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(),
|
2026-04-17 11:03:06 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:13:09 +00:00
|
|
|
var prevHash, prevNumSerie string
|
|
|
|
|
var prevFecha time.Time
|
2026-04-17 11:03:06 +00:00
|
|
|
if s.hashStorage != nil {
|
2026-05-17 20:27:27 +00:00
|
|
|
record, err := s.hashStorage.GetLastRecord(result.EmisorNIF)
|
2026-04-17 11:03:06 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "hash_storage_error",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
if record != nil {
|
|
|
|
|
prevHash = record.Huella
|
2026-05-12 23:13:09 +00:00
|
|
|
prevNumSerie = record.NumSerie
|
|
|
|
|
prevFecha = record.Fecha
|
2026-04-17 11:03:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hashService := NewHashService()
|
|
|
|
|
currentHash := hashService.CalculateHash(data, prevHash)
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
lastRecord := &LastRecord{
|
|
|
|
|
EmisorNIF: data.EmisorNIF,
|
|
|
|
|
NumSerie: data.NumSerie,
|
|
|
|
|
Fecha: data.Fecha,
|
|
|
|
|
Huella: currentHash,
|
|
|
|
|
FechaGen: now,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.Huella = currentHash
|
|
|
|
|
data.FechaGen = now
|
|
|
|
|
|
2026-05-17 20:27:27 +00:00
|
|
|
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,
|
|
|
|
|
}
|
2026-04-17 11:03:06 +00:00
|
|
|
|
|
|
|
|
if s.verifactu != nil {
|
|
|
|
|
log.Printf("Sending to AEAT...")
|
|
|
|
|
resp, err := s.verifactu.SendAlta(altaData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("AEAT error: %v", err)
|
|
|
|
|
goto saveLocal
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.Body.Fault != nil {
|
|
|
|
|
log.Printf("AEAT fault, falling back to local: %v", resp.Body.Fault.FaultString)
|
|
|
|
|
goto saveLocal
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:13:09 +00:00
|
|
|
if resp.Body.Respuesta != nil {
|
|
|
|
|
estado := resp.Body.Respuesta.EstadoEnvio
|
|
|
|
|
csv := resp.Body.Respuesta.CSV
|
|
|
|
|
log.Printf("AEAT EstadoEnvio: %s CSV: %s", estado, csv)
|
|
|
|
|
|
|
|
|
|
if estado == "Correcto" {
|
2026-04-17 11:03:06 +00:00
|
|
|
if s.hashStorage != nil {
|
|
|
|
|
if err := s.hashStorage.SaveLastRecord(lastRecord); err != nil {
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "hash_save_error",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: true,
|
2026-05-12 23:13:09 +00:00
|
|
|
CSV: csv,
|
|
|
|
|
Estado: "Correcto",
|
2026-04-17 11:03:06 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:13:09 +00:00
|
|
|
errMsg := estado
|
|
|
|
|
if len(resp.Body.Respuesta.RespuestaLineas) > 0 {
|
|
|
|
|
l := resp.Body.Respuesta.RespuestaLineas[0]
|
|
|
|
|
errMsg = fmt.Sprintf("%s [%s] %s", estado, l.CodigoError, l.DescripcionError)
|
|
|
|
|
}
|
|
|
|
|
log.Printf("AEAT error: %s", errMsg)
|
2026-04-17 11:03:06 +00:00
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
2026-05-12 23:13:09 +00:00
|
|
|
Error: "aeat_error: " + errMsg,
|
2026-04-17 11:03:06 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveLocal:
|
|
|
|
|
if s.hashStorage != nil {
|
|
|
|
|
if err := s.hashStorage.SaveLastRecord(lastRecord); err != nil {
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "hash_save_error",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: true,
|
|
|
|
|
CSV: currentHash,
|
|
|
|
|
Estado: "Correcto (local)",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 20:27:27 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
type AnulacionInput struct {
|
|
|
|
|
InvoiceInput
|
|
|
|
|
EmisorNombre string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AnulacionOutput struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
CSV string `json:"csv,omitempty"`
|
|
|
|
|
Estado string `json:"estado,omitempty"`
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *FacturaService) ProcessAnulacion(input AnulacionInput) (*AnulacionOutput, error) {
|
|
|
|
|
if input.Factura.EmisorNIF == "" {
|
|
|
|
|
return &AnulacionOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "emisor_nif_required",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
if input.Factura.NumSerie == "" {
|
|
|
|
|
return &AnulacionOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "num_serie_required",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
if input.Factura.FechaExpedicion == "" {
|
|
|
|
|
return &AnulacionOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "fecha_expedicion_required",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &AnulacionOutput{
|
|
|
|
|
Success: true,
|
|
|
|
|
Estado: "Anulada",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|