Fix integración VeriFactu AEAT: XML, hash y encadenamiento

Bugs corregidos tras validación directa contra la plataforma de pruebas:

- Hash: formato key=valor& en lugar de separador |
- TipoHuella: código 01 en lugar de literal SHA-256
- FechaHoraHusoGenRegistro: formato ISO 8601 con huso horario
- IdSistemaInformatico: 2 caracteres (01)
- Namespaces SOAP: URLs correctas de www2.agenciatributaria.gob.es
- Estructura XML: eliminados wrappers Content/AltaReq sobrantes
- Encadenamiento: xs:choice correcto (PrimerRegistro XOR RegistroAnterior)
- RegistroAnterior: referencia datos de la factura anterior, no la actual
- Destinatarios: bloque obligatorio para facturas F1/F3/rectificativas
- wsu:Id: eliminado atributo con namespace no declarado
- Parser respuesta: mapeado a RespuestaRegFactuSistemaFacturacion real
- Campo emisor_nombre añadido al input de factura

La API envía y recibe correctamente contra prewww1.aeat.es
con respuesta EstadoEnvio=Correcto y CSV asignado por la AEAT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
admin 2026-05-13 01:13:09 +02:00
parent b9899684cc
commit 1ad84a3b14
9 changed files with 323 additions and 141 deletions

View File

@ -21,10 +21,12 @@ server:
port: 6789
verifactu:
environment: test
production: false # true para producción
certificates:
storage_path: ./certs/
storage_path: ./data/certs/
cert_file: ./data/certs/personal.p12
cert_password: TU_CONTRASEÑA
crypto:
keys_path: ./keys/
@ -81,6 +83,7 @@ Authorization: Bearer <token>
"tipo": "alta",
"factura": {
"emisor_nif": "A12345678",
"emisor_nombre": "NOMBRE REGISTRADO EN AEAT",
"num_serie": "2024-001",
"fecha_expedicion": "13-09-2024",
"tipo_factura": "F1",
@ -95,7 +98,7 @@ Authorization: Bearer <token>
"importe_total": 121.00
},
"sistema": {
"nombre": "Software",
"nombre": "NOMBRE REGISTRADO EN AEAT PARA EL NIF PROVEEDOR",
"nif_proveedor": "A12345678",
"version": "1.0.0"
}
@ -126,13 +129,17 @@ Authorization: Bearer <token>
| Campo | Tipo | Descripción |
|-------|------|-------------|
| `tipo` | string | `alta` o `anulacion` |
| `emisor_nif` | string | NIF emisor (9 caracteres) |
| `num_serie` | string | Número de serie |
| `fecha_expedicion` | string | Fecha (`dd-mm-yyyy`) |
| `tipo_factura` | string | `F1`, `F2`, `R1`-`R5` |
| `iva[]` | array | Al menos un registro |
| `importe_total` | number | > 0 |
| `sistema.*` | object | Datos del software |
| `factura.emisor_nif` | string | NIF emisor (9 caracteres) |
| `factura.emisor_nombre` | string | Nombre del emisor exactamente como figura en el censo AEAT |
| `factura.num_serie` | string | Número de serie |
| `factura.fecha_expedicion` | string | Fecha (`dd-mm-yyyy`) |
| `factura.tipo_factura` | string | `F1`, `F2`, `R1`-`R5` |
| `factura.destinatario` | object | Obligatorio para F1, F3 y rectificativas |
| `factura.iva[]` | array | Al menos un registro |
| `factura.importe_total` | number | > 0 |
| `sistema.nombre` | string | Nombre del proveedor del software exactamente como figura en el censo AEAT para su NIF |
| `sistema.nif_proveedor` | string | NIF del proveedor del software |
| `sistema.version` | string | Versión del software |
---

View File

@ -1,41 +1,32 @@
# Obtener Certificado de Pruebas AEAT
# Certificado Digital para VeriFactu
## Introducción
Para enviar facturas al entorno de pruebas de VeriFactu (no en producción), necesitas un certificado digital de pruebas registrado en el sistema de la AEAT.
VeriFactu requiere autenticación mTLS: la API presenta tu certificado al conectarse con la AEAT. Tanto en pruebas como en producción se usa el mismo certificado real — no existe un certificado especial de pruebas.
## Opciones de Certificados
## Certificado válido: FNMT Persona Física
### Opción 1: Certificado FNMT (Fábrica Nacional de Moneda y Timbre)
El **Certificado de Ciudadano (Persona Física) de la FNMT** es gratuito y válido tanto para el entorno de pruebas como para producción.
Es el más común para empresas y autónomos en España.
**Obtención gratuita — opciones:**
**Pasos:**
1. Ir a https://www.fnmt.es/consultas-y-resolucion-de-incidentes/centros-de-emision-y-oficinas
2. Buscar una oficina cercana FNMT
3. Acudir presencialmente con DNI
4. Solicitar certificado de persona física
5. Te lo emiten en el acto
1. **Presencial en oficina AEAT** (recomendado): llevar el DNI a cualquier delegación de la Agencia Tributaria. Trámite en 2 minutos, sin cita previa en la mayoría.
2. **Con DNIe + NFC**: si tu DNI es posterior a 2015, puedes hacer todo el proceso online desde `sede.fnmt.gob.es` usando el chip NFC del DNI con tu móvil o un lector de tarjetas.
3. **Videollamada** (coste 2,99 € + IVA): servicio de identificación remota sin desplazamiento.
### Opción 2: Certificado de la AEAT (solo pruebas)
> La renovación online es gratuita mientras el certificado esté vigente (hasta 60 días antes de su vencimiento).
La AEAT ofrece certificados de pruebas:
## Verificar el certificado (PowerShell)
**Pasos:**
1. Ir a https://preportal.aeat.es
2. Registro como usuario
3. Solicitar certificado de prueba
4. Descargar archivo .p12
```powershell
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(".\data\certs\personal.p12", "TU_CONTRASEÑA")
Write-Host "Válido desde:" $cert.NotBefore
Write-Host "Válido hasta:" $cert.NotAfter
Write-Host "Asunto:" $cert.Subject
Write-Host "Vigente:" ($cert.NotAfter -gt (Get-Date))
```
> **Nota**: Los certificados de prueba de la AEAT suelen tener validez limitada.
### Opción 3: Certificado personal (prod)
Si ya tienes tu certificado personal (el que usaste antes), puede que funcione en producción.
**Verificar:**
1. ¿Está vigente? (no expired)
2. ¿Es cualificado? (firmado por autoridad reconocida)
El campo `Subject` debe contener `OU=CIUDADANOS` (persona física) y el NIF del titular.
## Registrar Nuevo Certificado
@ -96,17 +87,6 @@ VerifactuMidAPI/
└── config.yml # Configuración
```
## Verificar Certificado
Para verificar que el certificado es válido:
```powershell
openssl pkcs12 -in .\data\certs\personal.p12 -passin pass:TU_PASS -nokeys -clcerts | openssl x509 -noout -dates
```
Muestra:
- Not Before: fecha de inicio de validez
- Not After: fecha de caducidad
## Problemas Comunes

View File

@ -33,36 +33,42 @@ Buscar facturas previamente enviadas.
## Encadenamiento de Hash
Cada factura incluye el hash de la anterior:
Cada factura incluye el hash de la anterior. El formato exacto validado por la AEAT es:
```
Hash(N) = SHA256(Datos(N) + Hash(N-1))
SHA256("IDEmisorFactura=NIF&NumSerieFactura=SERIE&FechaExpedicionFactura=dd-mm-yyyy&TipoFactura=F1&CuotaTotal=21.00&ImporteTotal=121.00&Huella=HASH_ANTERIOR&FechaHoraHusoGenRegistro=yyyy-mm-ddThh:mm:ss+hh:mm")
```
Esto garantiza la integridad del registro histórico.
Para el primer registro de un emisor, `Huella` se deja vacío y en el XML se envía `<PrimerRegistro>S</PrimerRegistro>`. Para el resto, se envía `<RegistroAnterior>` con los datos de la factura anterior (nunca ambos a la vez).
## URLs
### Testing (Preproducción)
```
https://prewww2.aeat.es/.../SistemaFacturacion
https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
```
### Producción
```
https://www2.agenciatributaria.gob.es/.../SistemaFacturacion
https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
```
## Formato
SOAP 1.1 sobre HTTPS con:
- Certificado cliente cualificado
- XML con namespaces específicos
- Response con CSV (Código de Verificación)
SOAP 1.1 sobre HTTPS con mTLS (certificado cliente cualificado). Namespaces requeridos:
- `xmlns:sum``https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd`
- `xmlns:sum1``https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd`
Campos específicos a tener en cuenta:
- `TipoHuella`: valor `01` (no `SHA-256`)
- `FechaHoraHusoGenRegistro`: formato ISO 8601 con huso horario (`yyyy-mm-ddThh:mm:ss+hh:mm`)
- `IdSistemaInformatico`: 2 caracteres (`01`, no `1`)
- `Encadenamiento`: usa `xs:choice` — o `PrimerRegistro` o `RegistroAnterior`, nunca los dos
## CSV
Código de Verificación de 13 caracteres (o hash SHA-256 en desarrollo) que acredita el registro en Hacienda.
Código de Verificación asignado por la AEAT con formato `A-XXXXXXXXXXXXX`. Acredita el registro de la factura en Hacienda. Cuando la API opera en modo fallback local devuelve el hash SHA-256 en su lugar y el estado indica `Correcto (local)`.
## Fallback Local

View File

@ -1,6 +1,7 @@
package internal
import (
"fmt"
"log"
"time"
@ -63,7 +64,8 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
}, nil
}
prevHash := ""
var prevHash, prevNumSerie string
var prevFecha time.Time
if s.hashStorage != nil {
record, err := s.hashStorage.GetLastRecord(input.Factura.EmisorNIF)
if err != nil {
@ -74,6 +76,8 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
}
if record != nil {
prevHash = record.Huella
prevNumSerie = record.NumSerie
prevFecha = record.Fecha
}
}
@ -92,7 +96,7 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
data.Huella = currentHash
data.FechaGen = now
altaData := ToAltaData(&input.InvoiceInput, data, currentHash, prevHash)
altaData := ToAltaData(&input.InvoiceInput, data, currentHash, prevHash, prevNumSerie, prevFecha)
if s.verifactu != nil {
log.Printf("Sending to AEAT...")
@ -108,8 +112,12 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
goto saveLocal
}
if resp.Body.RegistroRespuesta != nil {
if resp.Body.RegistroRespuesta.Estado == "Correcto" {
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{
@ -118,17 +126,22 @@ func (s *FacturaService) ProcessAlta(input AltaInput) (*AltaOutput, error) {
}, nil
}
}
return &AltaOutput{
Success: true,
CSV: resp.Body.RegistroRespuesta.CSV,
Estado: resp.Body.RegistroRespuesta.Estado,
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:" + resp.Body.RegistroRespuesta.Estado,
Error: "aeat_error: " + errMsg,
}, nil
}
}
@ -188,7 +201,7 @@ func (s *FacturaService) ProcessAnulacion(input AnulacionInput) (*AnulacionOutpu
}, nil
}
func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) verifactu.AltaData {
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{
@ -208,7 +221,7 @@ func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) ve
}
return verifactu.AltaData{
EmisorNombre: in.Factura.EmisorNIF,
EmisorNombre: in.Factura.EmisorNombre,
EmisorNIF: data.EmisorNIF,
NumSerie: data.NumSerie,
FechaExpedicion: data.Fecha,
@ -229,6 +242,8 @@ func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) ve
},
Huella: huella,
PrevHash: prevHash,
PrevNumSerie: prevNumSerie,
PrevFecha: prevFecha,
FechaGen: data.FechaGen,
}
}

View File

@ -12,6 +12,7 @@ import (
"time"
)
type HashService struct {
lastRecord *LastRecord
}
@ -37,9 +38,10 @@ func (s *HashService) GetLastRecord() *LastRecord {
}
func (s *HashService) CalculateHash(data *InvoiceData, previousHash string) string {
fechaGen := data.FechaGen.Format(time.RFC3339)
fechaGen := data.FechaGen.Format("2006-01-02T15:04:05-07:00")
fields := fmt.Sprintf("%s|%s|%s|%s|%.2f|%.2f|%s|%s",
fields := fmt.Sprintf(
"IDEmisorFactura=%s&NumSerieFactura=%s&FechaExpedicionFactura=%s&TipoFactura=%s&CuotaTotal=%.2f&ImporteTotal=%.2f&Huella=%s&FechaHoraHusoGenRegistro=%s",
data.EmisorNIF,
data.NumSerie,
data.Fecha.Format("02-01-2006"),

View File

@ -33,6 +33,7 @@ type InvoiceInput struct {
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"`

154
test_directo_aeat.py Normal file
View File

@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Peticion directa al endpoint VeriFactu de la AEAT (sin pasar por la API).
Usa mTLS con los PEM ya convertidos en data/certs/.
"""
import hashlib
from datetime import datetime, timezone
import requests
# Certificado (los PEM no tienen contrasena)
CERT_PEM = "data/certs/cert_cert.pem"
KEY_PEM = "data/certs/cert_key.pem"
# Identidad
NIF = "53950250R"
NOMBRE = "JOSEP VICENT MESTRE LLOBELL"
# Endpoint de pruebas AEAT
URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
# Datos de la factura de prueba
NUM_SERIE = "TEST-DIRECTO-003"
FECHA_EXP = "13-05-2026"
TIPO_FACTURA = "F1"
BASE = 100.00
CUOTA = 21.00
TOTAL = 121.00
# Registro anterior (encadenamiento)
PREV_NUM_SERIE = "TEST-DIRECTO-002"
PREV_FECHA_EXP = "13-05-2026"
PREV_HUELLA = "B4F12C2C6407438501BBB5C81A8443E78860CD2D736D614C032CEDB4CC521D90"
# Timestamp generacion (mismo formato para hash y XML)
_now = datetime.now(timezone.utc)
FECHA_GEN = _now.strftime("%Y-%m-%dT%H:%M:%S+00:00")
def calcular_huella(nif, num_serie, fecha_exp, tipo, cuota, total, prev_hash, fecha_gen):
"""SHA-256 segun la especificacion VeriFactu (formato key=value&)."""
campos = (
f"IDEmisorFactura={nif}&"
f"NumSerieFactura={num_serie}&"
f"FechaExpedicionFactura={fecha_exp}&"
f"TipoFactura={tipo}&"
f"CuotaTotal={cuota:.2f}&"
f"ImporteTotal={total:.2f}&"
f"Huella={prev_hash}&"
f"FechaHoraHusoGenRegistro={fecha_gen}"
)
print(f" Campos huella : {campos}")
return hashlib.sha256(campos.encode()).hexdigest().upper()
huella = calcular_huella(NIF, NUM_SERIE, FECHA_EXP, TIPO_FACTURA, CUOTA, TOTAL, PREV_HUELLA, FECHA_GEN)
SOAP = f"""<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd"
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
<soapenv:Header/>
<soapenv:Body>
<sum:RegFactuSistemaFacturacion>
<sum:Cabecera>
<sum1:ObligadoEmision>
<sum1:NombreRazon>{NOMBRE}</sum1:NombreRazon>
<sum1:NIF>{NIF}</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta>
<sum1:IDVersion>1.0</sum1:IDVersion>
<sum1:IDFactura>
<sum1:IDEmisorFactura>{NIF}</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>{NUM_SERIE}</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>{FECHA_EXP}</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<sum1:NombreRazonEmisor>{NOMBRE}</sum1:NombreRazonEmisor>
<sum1:TipoFactura>{TIPO_FACTURA}</sum1:TipoFactura>
<sum1:DescripcionOperacion>Factura de prueba directa</sum1:DescripcionOperacion>
<sum1:Destinatarios>
<sum1:IDDestinatario>
<sum1:NombreRazon>{NOMBRE}</sum1:NombreRazon>
<sum1:NIF>{NIF}</sum1:NIF>
</sum1:IDDestinatario>
</sum1:Destinatarios>
<sum1:Desglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>{BASE:.2f}</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>{CUOTA:.2f}</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
</sum1:Desglose>
<sum1:CuotaTotal>{CUOTA:.2f}</sum1:CuotaTotal>
<sum1:ImporteTotal>{TOTAL:.2f}</sum1:ImporteTotal>
<sum1:Encadenamiento>
<sum1:RegistroAnterior>
<sum1:IDEmisorFactura>{NIF}</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>{PREV_NUM_SERIE}</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>{PREV_FECHA_EXP}</sum1:FechaExpedicionFactura>
<sum1:Huella>{PREV_HUELLA}</sum1:Huella>
</sum1:RegistroAnterior>
</sum1:Encadenamiento>
<sum1:SistemaInformatico>
<sum1:NombreRazon>{NOMBRE}</sum1:NombreRazon>
<sum1:NIF>{NIF}</sum1:NIF>
<sum1:NombreSistemaInformatico>VerifactuMidAPI</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>01</sum1:IdSistemaInformatico>
<sum1:Version>1.0.0</sum1:Version>
<sum1:NumeroInstalacion>1</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>S</sum1:TipoUsoPosibleSoloVerifactu>
<sum1:TipoUsoPosibleMultiOT>N</sum1:TipoUsoPosibleMultiOT>
<sum1:IndicadorMultiplesOT>N</sum1:IndicadorMultiplesOT>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>{FECHA_GEN}</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>01</sum1:TipoHuella>
<sum1:Huella>{huella}</sum1:Huella>
</sum1:RegistroAlta>
</sum:RegistroFactura>
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>"""
print(f"\n{'='*60}")
print(f"Endpoint : {URL}")
print(f"NIF : {NIF}")
print(f"Num serie : {NUM_SERIE}")
print(f"Huella : {huella}")
print(f"Fecha gen : {FECHA_GEN}")
print(f"{'='*60}\n")
try:
resp = requests.post(
URL,
data=SOAP.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": ""},
cert=(CERT_PEM, KEY_PEM),
verify=True,
timeout=30,
)
print(f"HTTP Status : {resp.status_code}")
print(f"\nRespuesta AEAT:\n{resp.text}")
except requests.exceptions.SSLError as e:
print(f"[ERROR SSL] Problema con el certificado: {e}")
except requests.exceptions.ConnectionError as e:
print(f"[ERROR CONEXION] No se pudo conectar: {e}")
except Exception as e:
print(f"[ERROR] {type(e).__name__}: {e}")

View File

@ -97,14 +97,13 @@ type Reference2 struct {
type SOAPBody struct {
XMLName xml.Name `xml:"soap:Body"`
Content interface{} `xml:",any"`
Id string `xml:"wsu:Id,attr"`
}
func BuildSOAPEnvelope(payload interface{}) *SOAPEnvelope {
return &SOAPEnvelope{
XmlnsSOAP: "http://schemas.xmlsoap.org/soap/envelope/",
XmlnsSUM: "https://www.agenciatributaria.gob.es/static/files/common/xsd/sum/information.xsd",
XmlnsSUM1: "https://www.agenciatributaria.gob.es/static/files/common/xsd/sum/suministroInformacion.xsd",
XmlnsSUM: "https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd",
XmlnsSUM1: "https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd",
Body: SOAPBody{
Content: payload,
},
@ -136,23 +135,11 @@ type EnviarAnulacionRequest struct {
}
func BuildAltaSOAPRequest(data AltaData) (*SOAPEnvelope, error) {
altaReq := BuildAltaRequest(data)
env := BuildSOAPEnvelope(EnviarFacturaRequest{
AltaReq: altaReq,
})
return env, nil
return BuildSOAPEnvelope(BuildAltaRequest(data)), nil
}
func BuildAnulacionSOAPRequest(data AltaData) (*SOAPEnvelope, error) {
anulReq := BuildAnulacionRequest(data)
env := BuildSOAPEnvelope(EnviarAnulacionRequest{
AnulReq: anulReq,
})
return env, nil
return BuildSOAPEnvelope(BuildAnulacionRequest(data)), nil
}
func ParseResponse(data []byte) (*Response, error) {

View File

@ -7,8 +7,13 @@ import (
)
type AltaRequest struct {
Cabecera Cabecera `xml:"sum:RegFactuSistemaFacturacion>sum:Cabecera"`
RegistroAlta RegistroAlta `xml:"sum:RegFactuSistemaFacturacion>sum:RegistroFactura>sum1:RegistroAlta"`
XMLName xml.Name `xml:"sum:RegFactuSistemaFacturacion"`
Cabecera Cabecera `xml:"sum:Cabecera"`
RegistroFactura AltaRegistroFactura `xml:"sum:RegistroFactura"`
}
type AltaRegistroFactura struct {
RegistroAlta RegistroAlta `xml:"sum1:RegistroAlta"`
}
type Cabecera struct {
@ -26,6 +31,7 @@ type RegistroAlta struct {
NombreRazonEmisor string `xml:"sum1:NombreRazonEmisor"`
TipoFactura string `xml:"sum1:TipoFactura"`
DescripcionOperacion string `xml:"sum1:DescripcionOperacion"`
Destinatarios *Destinatarios `xml:"sum1:Destinatarios,omitempty"`
Desglose Desglose `xml:"sum1:Desglose"`
CuotaTotal string `xml:"sum1:CuotaTotal"`
ImporteTotal string `xml:"sum1:ImporteTotal"`
@ -88,8 +94,13 @@ type SistemaInformatico struct {
}
type AnulacionRequest struct {
Cabecera Cabecera `xml:"sum:RegFactuSistemaFacturacion>sum:Cabecera"`
RegistroAnulacion RegistroAnulacion `xml:"sum:RegFactuSistemaFacturacion>sum:RegistroFactura>sum1:RegistroAnulacion"`
XMLName xml.Name `xml:"sum:RegFactuSistemaFacturacion"`
Cabecera Cabecera `xml:"sum:Cabecera"`
RegistroFactura AnulacionRegistroFactura `xml:"sum:RegistroFactura"`
}
type AnulacionRegistroFactura struct {
RegistroAnulacion RegistroAnulacion `xml:"sum1:RegistroAnulacion"`
}
type RegistroAnulacion struct {
@ -115,8 +126,8 @@ type Response struct {
type ResponseBody struct {
XMLName xml.Name `xml:"Body"`
Fault *Fault `xml:"Fault,omitempty"`
RegistroRespuesta *RegistroRespuesta `xml:"RegistroRespuesta,omitempty"`
Fault *Fault `xml:"Fault"`
Respuesta *RespuestaRegFactu `xml:"RespuestaRegFactuSistemaFacturacion"`
}
type Fault struct {
@ -124,9 +135,16 @@ type Fault struct {
FaultString string `xml:"faultstring"`
}
type RegistroRespuesta struct {
type RespuestaRegFactu struct {
CSV string `xml:"CSV"`
Estado string `xml:"Estado"`
EstadoEnvio string `xml:"EstadoEnvio"`
RespuestaLineas []RespuestaLinea `xml:"RespuestaLinea"`
}
type RespuestaLinea struct {
EstadoRegistro string `xml:"EstadoRegistro"`
CodigoError string `xml:"CodigoErrorRegistro"`
DescripcionError string `xml:"DescripcionErrorRegistro"`
}
type AltaData struct {
@ -145,6 +163,8 @@ type AltaData struct {
Huella string
FechaGen time.Time
PrevHash string
PrevNumSerie string
PrevFecha time.Time
}
type IVARegularizacionData struct {
@ -166,7 +186,7 @@ type SistemaData struct {
func BuildAltaRequest(data AltaData) *AltaRequest {
fechaExp := data.FechaExpedicion.Format("02-01-2006")
fechaGen := data.FechaGen.Format("02-01-2006T15:04:05")
fechaGen := data.FechaGen.Format("2006-01-02T15:04:05-07:00")
ivaList := make([]DetalleDesglose, len(data.IVA))
for i, iva := range data.IVA {
@ -184,16 +204,24 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
enc = Encadenamiento{PrimerRegistro: "S"}
} else {
enc = Encadenamiento{
PrimerRegistro: "N",
RegistroAnterior: &RegistroAnterior{
IDEmisorFactura: data.EmisorNIF,
NumSerieFactura: data.NumSerie,
FechaExpedicionFactura: fechaExp,
NumSerieFactura: data.PrevNumSerie,
FechaExpedicionFactura: data.PrevFecha.Format("02-01-2006"),
Huella: data.PrevHash,
},
}
}
var destinatarios *Destinatarios
if data.DestinatarioNIF != "" {
destinatarios = &Destinatarios{
IDDestinatario: []IDDestinatario{
{NombreRazon: data.DestinatarioNombre, NIF: data.DestinatarioNIF},
},
}
}
req := &AltaRequest{
Cabecera: Cabecera{
ObligadoEmision: ObligadoEmision{
@ -201,6 +229,7 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
NIF: data.EmisorNIF,
},
},
RegistroFactura: AltaRegistroFactura{
RegistroAlta: RegistroAlta{
IDVersion: "1.0",
IDFactura: IDFactura{
@ -211,6 +240,7 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
NombreRazonEmisor: data.EmisorNombre,
TipoFactura: data.TipoFactura,
DescripcionOperacion: data.Descripcion,
Destinatarios: destinatarios,
Desglose: Desglose{
DetalleDesglose: ivaList,
},
@ -221,7 +251,7 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
NombreRazon: data.Sistema.Nombre,
NIF: data.Sistema.NIFProveedor,
NombreSistemaInformatico: data.Sistema.NombreSistema,
IdSistemaInformatico: "1",
IdSistemaInformatico: "01",
Version: data.Sistema.Version,
NumeroInstalacion: data.Sistema.NumeroInstalacion,
TipoUsoPosibleSoloVerifactu: data.Sistema.TipoUsoVerifactu,
@ -229,9 +259,10 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
IndicadorMultiplesOT: "N",
},
FechaHoraHusoGenRegistro: fechaGen,
TipoHuella: "SHA-256",
TipoHuella: "01",
Huella: data.Huella,
},
},
}
return req
@ -239,18 +270,17 @@ func BuildAltaRequest(data AltaData) *AltaRequest {
func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
fechaExp := data.FechaExpedicion.Format("02-01-2006")
fechaGen := data.FechaGen.Format("02-01-2006T15:04:05")
fechaGen := data.FechaGen.Format("2006-01-02T15:04:05-07:00")
var enc Encadenamiento
if data.PrevHash == "" {
enc = Encadenamiento{PrimerRegistro: "S"}
} else {
enc = Encadenamiento{
PrimerRegistro: "N",
RegistroAnterior: &RegistroAnterior{
IDEmisorFactura: data.EmisorNIF,
NumSerieFactura: data.NumSerie,
FechaExpedicionFactura: fechaExp,
NumSerieFactura: data.PrevNumSerie,
FechaExpedicionFactura: data.PrevFecha.Format("02-01-2006"),
Huella: data.PrevHash,
},
}
@ -263,7 +293,7 @@ func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
NIF: data.EmisorNIF,
},
},
RegistroAnulacion: RegistroAnulacion{
RegistroFactura: AnulacionRegistroFactura{RegistroAnulacion: RegistroAnulacion{
IDVersion: "1.0",
IDFacturaAnulada: IDFacturaAnulada{
IDEmisorFacturaAnulada: data.EmisorNIF,
@ -275,7 +305,7 @@ func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
NombreRazon: data.Sistema.Nombre,
NIF: data.Sistema.NIFProveedor,
NombreSistemaInformatico: data.Sistema.NombreSistema,
IdSistemaInformatico: "1",
IdSistemaInformatico: "01",
Version: data.Sistema.Version,
NumeroInstalacion: data.Sistema.NumeroInstalacion,
TipoUsoPosibleSoloVerifactu: data.Sistema.TipoUsoVerifactu,
@ -283,9 +313,9 @@ func BuildAnulacionRequest(data AltaData) *AnulacionRequest {
IndicadorMultiplesOT: "N",
},
FechaHoraHusoGenRegistro: fechaGen,
TipoHuella: "SHA-256",
TipoHuella: "01",
Huella: data.Huella,
},
}},
}
return req