VerifactuMidAPI/internal/factura.go

294 lines
6.8 KiB
Go
Raw Normal View History

package internal
import (
"encoding/json"
"fmt"
"log"
"time"
"VerifactuMidAPI/internal/formats"
"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"`
}
func (s *FacturaService) ProcessAlta(raw json.RawMessage) (*AltaOutput, error) {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC: %v", r)
}
}()
result, formatName, err := formats.TransformAuto(raw)
if err != nil {
return &AltaOutput{
Success: false,
Error: "transform_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(result.EmisorNIF)
if err != nil {
return &AltaOutput{
Success: false,
Error: "hash_storage_error",
}, nil
}
if record != nil {
prevHash = record.Huella
prevNumSerie = record.NumSerie
prevFecha = record.Fecha
}
}
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
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)
goto saveLocal
}
if resp.Body.Fault != nil {
log.Printf("AEAT fault, falling back to local: %v", resp.Body.Fault.FaultString)
goto saveLocal
}
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" {
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: csv,
Estado: "Correcto",
}, nil
}
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)
return &AltaOutput{
Success: false,
Error: "aeat_error: " + errMsg,
}, 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
}
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
}
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
}