2026-04-17 11:03:06 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-12 23:13:09 +00:00
|
|
|
"fmt"
|
2026-04-17 11:03:06 +00:00
|
|
|
"log"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"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 AltaInput struct {
|
|
|
|
|
InvoiceInput
|
|
|
|
|
EmisorNombre string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AltaOutput struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
CSV string `json:"csv,omitempty"`
|
|
|
|
|
Estado string `json:"estado,omitempty"`
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *FacturaService) ProcessAlta(input AltaInput) (*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)
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: "validation_failed: " + errMsg,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data, err := TransformToInvoiceData(&input.InvoiceInput)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("transform error: %v", err)
|
|
|
|
|
return &AltaOutput{
|
|
|
|
|
Success: false,
|
|
|
|
|
Error: err.Error(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
record, err := s.hashStorage.GetLastRecord(input.Factura.EmisorNIF)
|
|
|
|
|
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-12 23:13:09 +00:00
|
|
|
altaData := ToAltaData(&input.InvoiceInput, data, currentHash, prevHash, prevNumSerie, prevFecha)
|
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)
|
|
|
|
|
log.Printf("AEAT error, falling back to local: %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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:13:09 +00:00
|
|
|
func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash, prevNumSerie string, prevFecha time.Time) verifactu.AltaData {
|
2026-04-17 11:03:06 +00:00
|
|
|
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{
|
2026-05-12 23:13:09 +00:00
|
|
|
EmisorNombre: in.Factura.EmisorNombre,
|
2026-04-17 11:03:06 +00:00
|
|
|
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,
|
|
|
|
|
},
|
2026-05-12 23:13:09 +00:00
|
|
|
Huella: huella,
|
|
|
|
|
PrevHash: prevHash,
|
|
|
|
|
PrevNumSerie: prevNumSerie,
|
|
|
|
|
PrevFecha: prevFecha,
|
|
|
|
|
FechaGen: data.FechaGen,
|
2026-04-17 11:03:06 +00:00
|
|
|
}
|
|
|
|
|
}
|