332 lines
8.0 KiB
Go
332 lines
8.0 KiB
Go
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *FacturaService) ReloadCertificate(path, password string) error {
|
||
|
|
if s.verifactu == nil {
|
||
|
|
return fmt.Errorf("verifactu client not initialized")
|
||
|
|
}
|
||
|
|
return s.verifactu.SetCertificate(path, password)
|
||
|
|
}
|
||
|
|
|
||
|
|
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("[Transform] formato=%s NumSerie=%q Fecha=%q Total=%.2f IVA=%d tramos",
|
||
|
|
formatName, result.NumSerie, result.FechaExpedicion, result.ImporteTotal, len(result.IVA))
|
||
|
|
|
||
|
|
if s.verifactu != nil {
|
||
|
|
if result.EmisorNIF == "" {
|
||
|
|
nif, nombre := s.verifactu.CertSubject()
|
||
|
|
result.EmisorNIF = nif
|
||
|
|
if result.EmisorNombre == "" {
|
||
|
|
result.EmisorNombre = nombre
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if result.Sistema.NIFProveedor == "" {
|
||
|
|
result.Sistema.NIFProveedor = result.EmisorNIF
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if result.Sistema.Nombre == "" {
|
||
|
|
result.Sistema.Nombre = "VerifactuMidAPI"
|
||
|
|
}
|
||
|
|
if result.Sistema.Version == "" {
|
||
|
|
result.Sistema.Version = "1.0"
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Printf("[Emisor] NIF=%q Nombre=%q", result.EmisorNIF, result.EmisorNombre)
|
||
|
|
log.Printf("[Sistema] Nombre=%q NIFProveedor=%q Version=%q", result.Sistema.Nombre, result.Sistema.NIFProveedor, result.Sistema.Version)
|
||
|
|
|
||
|
|
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 {
|
||
|
|
return &AltaOutput{
|
||
|
|
Success: false,
|
||
|
|
Error: "verifactu_not_configured: no hay certificado registrado",
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Printf("Sending to AEAT...")
|
||
|
|
resp, err := s.verifactu.SendAlta(altaData)
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("AEAT error: %v", err)
|
||
|
|
return &AltaOutput{
|
||
|
|
Success: false,
|
||
|
|
Error: "aeat_connection_error: " + err.Error(),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if resp.Body.Fault != nil {
|
||
|
|
log.Printf("AEAT fault: %v", resp.Body.Fault.FaultString)
|
||
|
|
return &AltaOutput{
|
||
|
|
Success: false,
|
||
|
|
Error: "aeat_fault: " + resp.Body.Fault.FaultString,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if resp.Body.Respuesta == nil {
|
||
|
|
return &AltaOutput{
|
||
|
|
Success: false,
|
||
|
|
Error: "aeat_error: respuesta vacía",
|
||
|
|
}, 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
|
||
|
|
}
|
||
|
|
|
||
|
|
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 == "" && s.verifactu != nil {
|
||
|
|
nif, nombre := s.verifactu.CertSubject()
|
||
|
|
input.Factura.EmisorNIF = nif
|
||
|
|
if input.EmisorNombre == "" {
|
||
|
|
input.EmisorNombre = nombre
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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
|
||
|
|
}
|