diff --git a/README.md b/README.md index a6a96e0..d115381 100644 --- a/README.md +++ b/README.md @@ -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 "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 "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 | 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 | --- diff --git a/documentacion/certificado_pruebas.md b/documentacion/certificado_pruebas.md index ca5ffad..7c846c3 100644 --- a/documentacion/certificado_pruebas.md +++ b/documentacion/certificado_pruebas.md @@ -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 diff --git a/documentacion/verifactu.md b/documentacion/verifactu.md index 2a44e51..b99f965 100644 --- a/documentacion/verifactu.md +++ b/documentacion/verifactu.md @@ -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 `S`. Para el resto, se envía `` 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 diff --git a/internal/factura.go b/internal/factura.go index 3bf4873..6e21e1f 100644 --- a/internal/factura.go +++ b/internal/factura.go @@ -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, @@ -227,8 +240,10 @@ func ToAltaData(in *InvoiceInput, data *InvoiceData, huella, prevHash string) ve NumeroInstalacion: data.Sistema.NumeroInstalacion, TipoUsoVerifactu: data.Sistema.TipoUsoVerifactu, }, - Huella: huella, - PrevHash: prevHash, - FechaGen: data.FechaGen, + Huella: huella, + PrevHash: prevHash, + PrevNumSerie: prevNumSerie, + PrevFecha: prevFecha, + FechaGen: data.FechaGen, } } diff --git a/internal/hash.go b/internal/hash.go index 25c3ee7..f154aef 100644 --- a/internal/hash.go +++ b/internal/hash.go @@ -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"), diff --git a/internal/models.go b/internal/models.go index 0b1294d..5801870 100644 --- a/internal/models.go +++ b/internal/models.go @@ -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"` diff --git a/test_directo_aeat.py b/test_directo_aeat.py new file mode 100644 index 0000000..73d9162 --- /dev/null +++ b/test_directo_aeat.py @@ -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""" + + + + + + + {NOMBRE} + {NIF} + + + + + 1.0 + + {NIF} + {NUM_SERIE} + {FECHA_EXP} + + {NOMBRE} + {TIPO_FACTURA} + Factura de prueba directa + + + {NOMBRE} + {NIF} + + + + + 01 + S1 + 21.00 + {BASE:.2f} + {CUOTA:.2f} + + + {CUOTA:.2f} + {TOTAL:.2f} + + + {NIF} + {PREV_NUM_SERIE} + {PREV_FECHA_EXP} + {PREV_HUELLA} + + + + {NOMBRE} + {NIF} + VerifactuMidAPI + 01 + 1.0.0 + 1 + S + N + N + + {FECHA_GEN} + 01 + {huella} + + + + +""" + +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}") diff --git a/verifactu/soap.go b/verifactu/soap.go index c68e0db..e2f3f5c 100644 --- a/verifactu/soap.go +++ b/verifactu/soap.go @@ -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) { diff --git a/verifactu/xml.go b/verifactu/xml.go index 8d370ca..6b6a99d 100644 --- a/verifactu/xml.go +++ b/verifactu/xml.go @@ -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 { @@ -114,9 +125,9 @@ type Response struct { } type ResponseBody struct { - XMLName xml.Name `xml:"Body"` - Fault *Fault `xml:"Fault,omitempty"` - RegistroRespuesta *RegistroRespuesta `xml:"RegistroRespuesta,omitempty"` + XMLName xml.Name `xml:"Body"` + 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 { - CSV string `xml:"CSV"` - Estado string `xml:"Estado"` +type RespuestaRegFactu struct { + CSV string `xml:"CSV"` + 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,36 +229,39 @@ func BuildAltaRequest(data AltaData) *AltaRequest { NIF: data.EmisorNIF, }, }, - RegistroAlta: RegistroAlta{ - IDVersion: "1.0", - IDFactura: IDFactura{ - IDEmisorFactura: data.EmisorNIF, - NumSerieFactura: data.NumSerie, - FechaExpedicionFactura: fechaExp, + RegistroFactura: AltaRegistroFactura{ + RegistroAlta: RegistroAlta{ + IDVersion: "1.0", + IDFactura: IDFactura{ + IDEmisorFactura: data.EmisorNIF, + 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 { 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