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 port: 6789
verifactu: verifactu:
environment: test production: false # true para producción
certificates: certificates:
storage_path: ./certs/ storage_path: ./data/certs/
cert_file: ./data/certs/personal.p12
cert_password: TU_CONTRASEÑA
crypto: crypto:
keys_path: ./keys/ keys_path: ./keys/
@ -81,6 +83,7 @@ Authorization: Bearer <token>
"tipo": "alta", "tipo": "alta",
"factura": { "factura": {
"emisor_nif": "A12345678", "emisor_nif": "A12345678",
"emisor_nombre": "NOMBRE REGISTRADO EN AEAT",
"num_serie": "2024-001", "num_serie": "2024-001",
"fecha_expedicion": "13-09-2024", "fecha_expedicion": "13-09-2024",
"tipo_factura": "F1", "tipo_factura": "F1",
@ -95,7 +98,7 @@ Authorization: Bearer <token>
"importe_total": 121.00 "importe_total": 121.00
}, },
"sistema": { "sistema": {
"nombre": "Software", "nombre": "NOMBRE REGISTRADO EN AEAT PARA EL NIF PROVEEDOR",
"nif_proveedor": "A12345678", "nif_proveedor": "A12345678",
"version": "1.0.0" "version": "1.0.0"
} }
@ -126,13 +129,17 @@ Authorization: Bearer <token>
| Campo | Tipo | Descripción | | Campo | Tipo | Descripción |
|-------|------|-------------| |-------|------|-------------|
| `tipo` | string | `alta` o `anulacion` | | `tipo` | string | `alta` o `anulacion` |
| `emisor_nif` | string | NIF emisor (9 caracteres) | | `factura.emisor_nif` | string | NIF emisor (9 caracteres) |
| `num_serie` | string | Número de serie | | `factura.emisor_nombre` | string | Nombre del emisor exactamente como figura en el censo AEAT |
| `fecha_expedicion` | string | Fecha (`dd-mm-yyyy`) | | `factura.num_serie` | string | Número de serie |
| `tipo_factura` | string | `F1`, `F2`, `R1`-`R5` | | `factura.fecha_expedicion` | string | Fecha (`dd-mm-yyyy`) |
| `iva[]` | array | Al menos un registro | | `factura.tipo_factura` | string | `F1`, `F2`, `R1`-`R5` |
| `importe_total` | number | > 0 | | `factura.destinatario` | object | Obligatorio para F1, F3 y rectificativas |
| `sistema.*` | object | Datos del software | | `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 ## 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. **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.
1. Ir a https://www.fnmt.es/consultas-y-resolucion-de-incidentes/centros-de-emision-y-oficinas 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.
2. Buscar una oficina cercana FNMT 3. **Videollamada** (coste 2,99 € + IVA): servicio de identificación remota sin desplazamiento.
3. Acudir presencialmente con DNI
4. Solicitar certificado de persona física
5. Te lo emiten en el acto
### 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:** ```powershell
1. Ir a https://preportal.aeat.es $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(".\data\certs\personal.p12", "TU_CONTRASEÑA")
2. Registro como usuario Write-Host "Válido desde:" $cert.NotBefore
3. Solicitar certificado de prueba Write-Host "Válido hasta:" $cert.NotAfter
4. Descargar archivo .p12 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. El campo `Subject` debe contener `OU=CIUDADANOS` (persona física) y el NIF del titular.
### 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)
## Registrar Nuevo Certificado ## Registrar Nuevo Certificado
@ -96,17 +87,6 @@ VerifactuMidAPI/
└── config.yml # Configuración └── 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 ## Problemas Comunes

View File

@ -33,36 +33,42 @@ Buscar facturas previamente enviadas.
## Encadenamiento de Hash ## 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 ## URLs
### Testing (Preproducción) ### Testing (Preproducción)
``` ```
https://prewww2.aeat.es/.../SistemaFacturacion https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
``` ```
### Producción ### Producción
``` ```
https://www2.agenciatributaria.gob.es/.../SistemaFacturacion https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
``` ```
## Formato ## Formato
SOAP 1.1 sobre HTTPS con: SOAP 1.1 sobre HTTPS con mTLS (certificado cliente cualificado). Namespaces requeridos:
- Certificado cliente cualificado
- XML con namespaces específicos - `xmlns:sum``https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd`
- Response con CSV (Código de Verificación) - `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 ## 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 ## Fallback Local

View File

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

View File

@ -12,6 +12,7 @@ import (
"time" "time"
) )
type HashService struct { type HashService struct {
lastRecord *LastRecord lastRecord *LastRecord
} }
@ -37,9 +38,10 @@ func (s *HashService) GetLastRecord() *LastRecord {
} }
func (s *HashService) CalculateHash(data *InvoiceData, previousHash string) string { 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.EmisorNIF,
data.NumSerie, data.NumSerie,
data.Fecha.Format("02-01-2006"), data.Fecha.Format("02-01-2006"),

View File

@ -33,6 +33,7 @@ type InvoiceInput struct {
type FacturaInput struct { type FacturaInput struct {
EmisorNIF string `json:"emisor_nif"` EmisorNIF string `json:"emisor_nif"`
EmisorNombre string `json:"emisor_nombre"`
NumSerie string `json:"num_serie"` NumSerie string `json:"num_serie"`
FechaExpedicion string `json:"fecha_expedicion"` FechaExpedicion string `json:"fecha_expedicion"`
TipoFactura string `json:"tipo_factura"` 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 { type SOAPBody struct {
XMLName xml.Name `xml:"soap:Body"` XMLName xml.Name `xml:"soap:Body"`
Content interface{} `xml:",any"` Content interface{} `xml:",any"`
Id string `xml:"wsu:Id,attr"`
} }
func BuildSOAPEnvelope(payload interface{}) *SOAPEnvelope { func BuildSOAPEnvelope(payload interface{}) *SOAPEnvelope {
return &SOAPEnvelope{ return &SOAPEnvelope{
XmlnsSOAP: "http://schemas.xmlsoap.org/soap/envelope/", XmlnsSOAP: "http://schemas.xmlsoap.org/soap/envelope/",
XmlnsSUM: "https://www.agenciatributaria.gob.es/static/files/common/xsd/sum/information.xsd", XmlnsSUM: "https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd",
XmlnsSUM1: "https://www.agenciatributaria.gob.es/static/files/common/xsd/sum/suministroInformacion.xsd", XmlnsSUM1: "https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd",
Body: SOAPBody{ Body: SOAPBody{
Content: payload, Content: payload,
}, },
@ -136,23 +135,11 @@ type EnviarAnulacionRequest struct {
} }
func BuildAltaSOAPRequest(data AltaData) (*SOAPEnvelope, error) { func BuildAltaSOAPRequest(data AltaData) (*SOAPEnvelope, error) {
altaReq := BuildAltaRequest(data) return BuildSOAPEnvelope(BuildAltaRequest(data)), nil
env := BuildSOAPEnvelope(EnviarFacturaRequest{
AltaReq: altaReq,
})
return env, nil
} }
func BuildAnulacionSOAPRequest(data AltaData) (*SOAPEnvelope, error) { func BuildAnulacionSOAPRequest(data AltaData) (*SOAPEnvelope, error) {
anulReq := BuildAnulacionRequest(data) return BuildSOAPEnvelope(BuildAnulacionRequest(data)), nil
env := BuildSOAPEnvelope(EnviarAnulacionRequest{
AnulReq: anulReq,
})
return env, nil
} }
func ParseResponse(data []byte) (*Response, error) { func ParseResponse(data []byte) (*Response, error) {

View File

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