@dwast/verifactu-lib 0.1.0-beta.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/aeat/client.d.ts +162 -0
- package/dist/cjs/aeat/client.d.ts.map +1 -0
- package/dist/cjs/aeat/client.js +267 -0
- package/dist/cjs/aeat/client.js.map +1 -0
- package/dist/cjs/aeat/index.d.ts +3 -0
- package/dist/cjs/aeat/index.d.ts.map +1 -0
- package/dist/cjs/aeat/index.js +19 -0
- package/dist/cjs/aeat/index.js.map +1 -0
- package/dist/cjs/aeat/response.d.ts +78 -0
- package/dist/cjs/aeat/response.d.ts.map +1 -0
- package/dist/cjs/aeat/response.js +117 -0
- package/dist/cjs/aeat/response.js.map +1 -0
- package/dist/{core → cjs/core}/hash/chain.d.ts +42 -0
- package/dist/cjs/core/hash/chain.d.ts.map +1 -0
- package/dist/cjs/core/hash/chain.js +167 -0
- package/dist/cjs/core/hash/chain.js.map +1 -0
- package/dist/cjs/core/hash/index.d.ts.map +1 -0
- package/dist/cjs/core/hash/index.js +18 -0
- package/dist/cjs/core/hash/index.js.map +1 -0
- package/dist/{core → cjs/core}/index.d.ts +2 -0
- package/dist/cjs/core/index.d.ts.map +1 -0
- package/dist/cjs/core/index.js +23 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/core/models/event.d.ts.map +1 -0
- package/dist/cjs/core/models/event.js +3 -0
- package/dist/{core → cjs/core}/models/event.js.map +1 -1
- package/dist/cjs/core/models/index.d.ts.map +1 -0
- package/dist/cjs/core/models/index.js +19 -0
- package/dist/cjs/core/models/index.js.map +1 -0
- package/dist/{core → cjs/core}/models/invoice.d.ts +42 -17
- package/dist/cjs/core/models/invoice.d.ts.map +1 -0
- package/dist/cjs/core/models/invoice.js +3 -0
- package/dist/{core → cjs/core}/models/invoice.js.map +1 -1
- package/dist/cjs/core/qr/index.d.ts.map +1 -0
- package/dist/cjs/core/qr/index.js +18 -0
- package/dist/cjs/core/qr/index.js.map +1 -0
- package/dist/cjs/core/qr/qr.d.ts.map +1 -0
- package/dist/cjs/core/qr/qr.js +19 -0
- package/dist/cjs/core/qr/qr.js.map +1 -0
- package/dist/cjs/core/utils/date.d.ts +16 -0
- package/dist/cjs/core/utils/date.d.ts.map +1 -0
- package/dist/cjs/core/utils/date.js +27 -0
- package/dist/cjs/core/utils/date.js.map +1 -0
- package/dist/cjs/core/utils/index.d.ts +2 -0
- package/dist/cjs/core/utils/index.d.ts.map +1 -0
- package/dist/cjs/core/utils/index.js +18 -0
- package/dist/cjs/core/utils/index.js.map +1 -0
- package/dist/cjs/core/validation/index.d.ts +2 -0
- package/dist/cjs/core/validation/index.d.ts.map +1 -0
- package/dist/cjs/core/validation/index.js +18 -0
- package/dist/cjs/core/validation/index.js.map +1 -0
- package/dist/cjs/core/validation/validate.d.ts +18 -0
- package/dist/cjs/core/validation/validate.d.ts.map +1 -0
- package/dist/cjs/core/validation/validate.js +65 -0
- package/dist/cjs/core/validation/validate.js.map +1 -0
- package/dist/cjs/core/xml/generator.d.ts +140 -0
- package/dist/cjs/core/xml/generator.d.ts.map +1 -0
- package/dist/cjs/core/xml/generator.js +269 -0
- package/dist/cjs/core/xml/generator.js.map +1 -0
- package/dist/cjs/core/xml/index.d.ts.map +1 -0
- package/dist/cjs/core/xml/index.js +18 -0
- package/dist/cjs/core/xml/index.js.map +1 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integration/accountability.d.ts +41 -0
- package/dist/cjs/integration/accountability.d.ts.map +1 -0
- package/dist/cjs/integration/accountability.js +92 -0
- package/dist/cjs/integration/accountability.js.map +1 -0
- package/dist/cjs/integration/index.d.ts +5 -0
- package/dist/cjs/integration/index.d.ts.map +1 -0
- package/dist/cjs/integration/index.js +21 -0
- package/dist/cjs/integration/index.js.map +1 -0
- package/dist/cjs/integration/p12.d.ts +58 -0
- package/dist/cjs/integration/p12.d.ts.map +1 -0
- package/dist/cjs/integration/p12.js +127 -0
- package/dist/cjs/integration/p12.js.map +1 -0
- package/dist/{integration → cjs/integration}/repository.d.ts +2 -0
- package/dist/cjs/integration/repository.d.ts.map +1 -0
- package/dist/cjs/integration/repository.js +3 -0
- package/dist/cjs/integration/repository.js.map +1 -0
- package/dist/{integration → cjs/integration}/service.d.ts +11 -2
- package/dist/cjs/integration/service.d.ts.map +1 -0
- package/dist/cjs/integration/service.js +230 -0
- package/dist/cjs/integration/service.js.map +1 -0
- package/dist/cjs/integration/tenant-credentials.d.ts +44 -0
- package/dist/cjs/integration/tenant-credentials.d.ts.map +1 -0
- package/dist/cjs/integration/tenant-credentials.js +62 -0
- package/dist/cjs/integration/tenant-credentials.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/aeat/client.d.ts +162 -0
- package/dist/esm/aeat/client.d.ts.map +1 -0
- package/dist/esm/aeat/client.js +259 -0
- package/dist/esm/aeat/client.js.map +1 -0
- package/dist/esm/aeat/index.d.ts +3 -0
- package/dist/esm/aeat/index.d.ts.map +1 -0
- package/dist/esm/aeat/index.js +3 -0
- package/dist/esm/aeat/index.js.map +1 -0
- package/dist/esm/aeat/response.d.ts +78 -0
- package/dist/esm/aeat/response.d.ts.map +1 -0
- package/dist/esm/aeat/response.js +114 -0
- package/dist/esm/aeat/response.js.map +1 -0
- package/dist/esm/core/hash/chain.d.ts +139 -0
- package/dist/esm/core/hash/chain.d.ts.map +1 -0
- package/dist/{core → esm/core}/hash/chain.js +37 -0
- package/dist/esm/core/hash/chain.js.map +1 -0
- package/dist/esm/core/hash/index.d.ts +2 -0
- package/dist/esm/core/hash/index.d.ts.map +1 -0
- package/dist/esm/core/hash/index.js.map +1 -0
- package/dist/esm/core/index.d.ts +7 -0
- package/dist/esm/core/index.d.ts.map +1 -0
- package/dist/{core → esm/core}/index.js +2 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/models/event.d.ts +25 -0
- package/dist/esm/core/models/event.d.ts.map +1 -0
- package/dist/esm/core/models/event.js.map +1 -0
- package/dist/esm/core/models/index.d.ts +3 -0
- package/dist/esm/core/models/index.d.ts.map +1 -0
- package/dist/esm/core/models/index.js.map +1 -0
- package/dist/esm/core/models/invoice.d.ts +115 -0
- package/dist/esm/core/models/invoice.d.ts.map +1 -0
- package/dist/esm/core/models/invoice.js.map +1 -0
- package/dist/esm/core/qr/index.d.ts +2 -0
- package/dist/esm/core/qr/index.d.ts.map +1 -0
- package/dist/esm/core/qr/index.js.map +1 -0
- package/dist/esm/core/qr/qr.d.ts +13 -0
- package/dist/esm/core/qr/qr.d.ts.map +1 -0
- package/dist/esm/core/qr/qr.js.map +1 -0
- package/dist/esm/core/utils/date.d.ts +16 -0
- package/dist/esm/core/utils/date.d.ts.map +1 -0
- package/dist/esm/core/utils/date.js +24 -0
- package/dist/esm/core/utils/date.js.map +1 -0
- package/dist/esm/core/utils/index.d.ts +2 -0
- package/dist/esm/core/utils/index.d.ts.map +1 -0
- package/dist/esm/core/utils/index.js +2 -0
- package/dist/esm/core/utils/index.js.map +1 -0
- package/dist/esm/core/validation/index.d.ts +2 -0
- package/dist/esm/core/validation/index.d.ts.map +1 -0
- package/dist/esm/core/validation/index.js +2 -0
- package/dist/esm/core/validation/index.js.map +1 -0
- package/dist/esm/core/validation/validate.d.ts +18 -0
- package/dist/esm/core/validation/validate.d.ts.map +1 -0
- package/dist/esm/core/validation/validate.js +62 -0
- package/dist/esm/core/validation/validate.js.map +1 -0
- package/dist/esm/core/xml/generator.d.ts +140 -0
- package/dist/esm/core/xml/generator.d.ts.map +1 -0
- package/dist/esm/core/xml/generator.js +259 -0
- package/dist/esm/core/xml/generator.js.map +1 -0
- package/dist/esm/core/xml/index.d.ts +2 -0
- package/dist/esm/core/xml/index.d.ts.map +1 -0
- package/dist/esm/core/xml/index.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/integration/accountability.d.ts +41 -0
- package/dist/esm/integration/accountability.d.ts.map +1 -0
- package/dist/esm/integration/accountability.js +88 -0
- package/dist/esm/integration/accountability.js.map +1 -0
- package/dist/esm/integration/index.d.ts +5 -0
- package/dist/esm/integration/index.d.ts.map +1 -0
- package/dist/esm/integration/index.js +5 -0
- package/dist/esm/integration/index.js.map +1 -0
- package/dist/esm/integration/p12.d.ts +58 -0
- package/dist/esm/integration/p12.d.ts.map +1 -0
- package/dist/esm/integration/p12.js +119 -0
- package/dist/esm/integration/p12.js.map +1 -0
- package/dist/esm/integration/repository.d.ts +79 -0
- package/dist/esm/integration/repository.d.ts.map +1 -0
- package/dist/esm/integration/repository.js.map +1 -0
- package/dist/esm/integration/service.d.ts +45 -0
- package/dist/esm/integration/service.d.ts.map +1 -0
- package/dist/{integration → esm/integration}/service.js +101 -15
- package/dist/esm/integration/service.js.map +1 -0
- package/dist/esm/integration/tenant-credentials.d.ts +44 -0
- package/dist/esm/integration/tenant-credentials.d.ts.map +1 -0
- package/dist/esm/integration/tenant-credentials.js +58 -0
- package/dist/esm/integration/tenant-credentials.js.map +1 -0
- package/package.json +45 -13
- package/readme.md +566 -42
- package/src/aeat/client.ts +296 -55
- package/src/aeat/index.ts +1 -0
- package/src/aeat/response.ts +195 -0
- package/src/core/hash/chain.ts +68 -0
- package/src/core/index.ts +2 -0
- package/src/core/models/invoice.ts +40 -18
- package/src/core/utils/date.ts +27 -0
- package/src/core/utils/index.ts +1 -0
- package/src/core/validation/index.ts +1 -0
- package/src/core/validation/validate.ts +71 -0
- package/src/core/xml/generator.ts +306 -73
- package/src/integration/accountability.ts +125 -0
- package/src/integration/index.ts +2 -0
- package/src/integration/p12.ts +155 -0
- package/src/integration/repository.ts +2 -0
- package/src/integration/service.ts +113 -14
- package/dist/aeat/client.d.ts +0 -57
- package/dist/aeat/client.d.ts.map +0 -1
- package/dist/aeat/client.js +0 -87
- package/dist/aeat/client.js.map +0 -1
- package/dist/aeat/index.d.ts +0 -2
- package/dist/aeat/index.d.ts.map +0 -1
- package/dist/aeat/index.js +0 -2
- package/dist/aeat/index.js.map +0 -1
- package/dist/core/hash/chain.d.ts.map +0 -1
- package/dist/core/hash/chain.js.map +0 -1
- package/dist/core/hash/index.d.ts.map +0 -1
- package/dist/core/hash/index.js.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/models/event.d.ts.map +0 -1
- package/dist/core/models/index.d.ts.map +0 -1
- package/dist/core/models/index.js.map +0 -1
- package/dist/core/models/invoice.d.ts.map +0 -1
- package/dist/core/qr/index.d.ts.map +0 -1
- package/dist/core/qr/index.js.map +0 -1
- package/dist/core/qr/qr.d.ts.map +0 -1
- package/dist/core/qr/qr.js.map +0 -1
- package/dist/core/xml/generator.d.ts +0 -32
- package/dist/core/xml/generator.d.ts.map +0 -1
- package/dist/core/xml/generator.js +0 -103
- package/dist/core/xml/generator.js.map +0 -1
- package/dist/core/xml/index.d.ts.map +0 -1
- package/dist/core/xml/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integration/index.d.ts +0 -3
- package/dist/integration/index.d.ts.map +0 -1
- package/dist/integration/index.js +0 -3
- package/dist/integration/index.js.map +0 -1
- package/dist/integration/repository.d.ts.map +0 -1
- package/dist/integration/repository.js.map +0 -1
- package/dist/integration/service.d.ts.map +0 -1
- package/dist/integration/service.js.map +0 -1
- /package/dist/{core → cjs/core}/hash/index.d.ts +0 -0
- /package/dist/{core → cjs/core}/models/event.d.ts +0 -0
- /package/dist/{core → cjs/core}/models/index.d.ts +0 -0
- /package/dist/{core → cjs/core}/qr/index.d.ts +0 -0
- /package/dist/{core → cjs/core}/qr/qr.d.ts +0 -0
- /package/dist/{core → cjs/core}/xml/index.d.ts +0 -0
- /package/dist/{index.d.ts → cjs/index.d.ts} +0 -0
- /package/dist/{core → esm/core}/hash/index.js +0 -0
- /package/dist/{core → esm/core}/models/event.js +0 -0
- /package/dist/{core → esm/core}/models/index.js +0 -0
- /package/dist/{core → esm/core}/models/invoice.js +0 -0
- /package/dist/{core → esm/core}/qr/index.js +0 -0
- /package/dist/{core → esm/core}/qr/qr.js +0 -0
- /package/dist/{core → esm/core}/xml/index.js +0 -0
- /package/dist/{index.js → esm/index.js} +0 -0
- /package/dist/{integration → esm/integration}/repository.js +0 -0
package/readme.md
CHANGED
|
@@ -1,89 +1,613 @@
|
|
|
1
1
|
# @dwast/verifactu-lib
|
|
2
2
|
|
|
3
|
-
Librería privada
|
|
3
|
+
Librería npm privada para la gestión de **Verifactu** (Ley 18/2022 y RD 1007/2023) en un stack MERN (JS/TS). Genera facturas en formato Verifactu, calcula el encadenado hash (huella), genera el QR, se comunica con AEAT (SOAP + mTLS) y define la capa de integración que el sistema consumidor debe implementar.
|
|
4
|
+
|
|
5
|
+
> **Verifactu es obligatorio desde el 1 de julio de 2025** para todos los sistemas de facturación en España.
|
|
4
6
|
|
|
5
7
|
## ¿Qué resuelve?
|
|
6
8
|
|
|
7
|
-
Verifactu no es solo "endpoints": el núcleo es la generación de facturas en
|
|
9
|
+
Verifactu no es solo "endpoints": el núcleo es la generación de facturas en XML con los campos obligatorios, el **encadenado hash** (SHA-256) que garantiza integridad e inmutabilidad, el **QR Verifactu** y el registro de eventos. La comunicación con AEAT es solo una parte.
|
|
8
10
|
|
|
9
11
|
La librería se divide en tres capas:
|
|
10
12
|
|
|
11
13
|
- **`core`** — Núcleo puro, sin dependencias de red ni de framework. Fácil de testear y de usar en cualquier parte del stack.
|
|
12
|
-
- **`aeat`** — Cliente para los servicios de AEAT (
|
|
13
|
-
- **`integration`** — Capa de orquestación
|
|
14
|
+
- **`aeat`** — Cliente para los servicios de AEAT (SOAP, soporta mTLS con certificado digital).
|
|
15
|
+
- **`integration`** — Capa de orquestación: persistencia, servicio de facturación, lectura de `.p12` y accountability (contabilidad).
|
|
16
|
+
|
|
17
|
+
## Instalación
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @dwast/verifactu-lib
|
|
21
|
+
```
|
|
14
22
|
|
|
15
23
|
## Estructura
|
|
16
24
|
|
|
17
25
|
```
|
|
18
26
|
src/
|
|
19
27
|
├── core/
|
|
20
|
-
│ ├── models/
|
|
21
|
-
│ ├── hash/
|
|
22
|
-
│ ├── xml/
|
|
23
|
-
│
|
|
24
|
-
|
|
25
|
-
├──
|
|
28
|
+
│ ├── models/ # Tipos: Invoice, Emisor, Desglose, SistemaInformatico, Event
|
|
29
|
+
│ ├── hash/ # Encadenado hash (SHA-256)
|
|
30
|
+
│ ├── xml/ # Generación XML Verifactu (RegistroAlta, Anulacion, Evento, Suministro)
|
|
31
|
+
│ ├── qr/ # Generación QR Verifactu
|
|
32
|
+
│ └── utils/ # Utilidades (fecha/hora con huso)
|
|
33
|
+
├── aeat/ # Cliente de servicios AEAT (SOAP + mTLS)
|
|
34
|
+
├── integration/ # Repositorio, servicio, p12, accountability
|
|
26
35
|
└── index.ts
|
|
27
36
|
```
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
**Subpaths de importación** (definidos en `package.json`):
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
| Subpath | Contenido |
|
|
41
|
+
|---------|-----------|
|
|
42
|
+
| `@dwast/verifactu-lib` | Todo (re-exporta core + aeat + integration) |
|
|
43
|
+
| `@dwast/verifactu-lib/core` | Núcleo puro (modelos, hash, xml, qr, utils) |
|
|
44
|
+
| `@dwast/verifactu-lib/aeat` | Cliente AEAT + parser de respuesta |
|
|
45
|
+
| `@dwast/verifactu-lib/integration` | Repositorio, servicio, p12, accountability |
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
2. **Cliente AEAT configurado.** El sistema DEBE proporcionar un `VerifactuClient` con las credenciales y el entorno adecuados.
|
|
47
|
+
---
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
## Núcleo (`core`)
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
import { VerifactuService } from '@dwast/verifactu-lib/integration';
|
|
40
|
-
import { VerifactuClient } from '@dwast/verifactu-lib/aeat';
|
|
41
|
-
import { MyMongoRepository } from './my-repo'; // implementa VerifactuRepository
|
|
51
|
+
### Modelos (`core/models`)
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const service = new VerifactuService(repo, client);
|
|
53
|
+
```ts
|
|
54
|
+
import type { Invoice } from '@dwast/verifactu-lib/core';
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
id: { serie: 'A', numero: '2025-001', fechaExpedicion: '
|
|
49
|
-
emisor: { nif: '
|
|
50
|
-
tipoFactura: 'F1',
|
|
51
|
-
tipoDesglose: 'S1',
|
|
56
|
+
const invoice: Invoice = {
|
|
57
|
+
id: { serie: 'A', numero: '2025-001', fechaExpedicion: '07-09-2026' },
|
|
58
|
+
emisor: { nif: 'B18579458', nombreRazon: 'MOVVIENDO TOURISM GROUP SL' },
|
|
59
|
+
tipoFactura: 'F1', // F1–F9
|
|
60
|
+
tipoDesglose: 'S1', // S1–S6
|
|
61
|
+
descripcion: 'Servicios de consultoria',
|
|
62
|
+
destinatarios: [{ nombreRazon: 'CLIENTE SL', nif: 'B12345678' }],
|
|
52
63
|
desglose: {
|
|
53
|
-
|
|
54
|
-
{
|
|
64
|
+
detalles: [
|
|
65
|
+
{
|
|
66
|
+
calificacionOperacion: 'S1', // S1/S2/N1/N2...
|
|
67
|
+
claveRegimen: '01', // obligatorio si Impuesto es IVA/IPSI/IGIC
|
|
68
|
+
tipoImpositivo: 21,
|
|
69
|
+
baseImponible: 100,
|
|
70
|
+
cuotaRepercutida: 21,
|
|
71
|
+
},
|
|
55
72
|
],
|
|
56
73
|
cuotaTotal: 21,
|
|
57
74
|
importeTotal: 121,
|
|
58
75
|
},
|
|
76
|
+
sistemaInformatico: {
|
|
77
|
+
nombreRazon: 'MOVVIENDO TOURISM GROUP SL',
|
|
78
|
+
nif: 'B18579458',
|
|
79
|
+
nombreSistemaInformatico: 'SIF-TEST',
|
|
80
|
+
idSistemaInformatico: '01', // TextMax2Type: máx 2 caracteres
|
|
81
|
+
version: '1.0',
|
|
82
|
+
numeroInstalacion: '1',
|
|
83
|
+
},
|
|
84
|
+
fechaHoraHusoGenRegistro: '2026-09-07T10:00:00+02:00',
|
|
85
|
+
huella: 'ABC123', // se rellena con computeRegistroAlta
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> **⚠️ Fechas.** El XSD `fecha` de AEAT usa el formato **`DD-MM-YYYY`** (no ISO). `FechaExpedicionFactura` y `FechaExpedicionFacturaAnulada` van en `DD-MM-YYYY`. En cambio `FechaHoraHusoGenRegistro` es `dateTime` (ISO 8601 con huso).
|
|
90
|
+
|
|
91
|
+
> **⚠️ Importes.** AEAT **sí pide los importes**: el XML incluye el bloque `Desglose` con `BaseImponibleOimporteNoSujeto`, `CuotaRepercutida`, `CuotaTotal` e `ImporteTotal`. Además, la **huella (hash)** se calcula sobre `CuotaTotal` e `ImporteTotal` (los totales), no sobre el desglose línea a línea. Ver [Hash](#hash-corehash).
|
|
92
|
+
|
|
93
|
+
### Hash (`core/hash`)
|
|
94
|
+
|
|
95
|
+
La huella **no se calcula sobre el XML**, sino sobre una cadena canónica de campos concatenados (`nombreCampo=valor&...`). Resultado: **SHA-256 en hexadecimal MAYÚSCULA** (64 caracteres). Verificado contra los [vectores oficiales de la AEAT](https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Veri-Factu_especificaciones_huella_hash_registros.pdf).
|
|
96
|
+
|
|
97
|
+
**Campos por tipo de registro:**
|
|
98
|
+
|
|
99
|
+
| Registro | Campos (en orden) |
|
|
100
|
+
|----------|-------------------|
|
|
101
|
+
| **Alta (8)** | `IDEmisorFactura`, `NumSerieFactura`, `FechaExpedicionFactura`, `TipoFactura`, `CuotaTotal`, `ImporteTotal`, `Huella`, `FechaHoraHusoGenRegistro` |
|
|
102
|
+
| **Anulación (5)** | `IDEmisorFacturaAnulada`, `NumSerieFacturaAnulada`, `FechaExpedicionFacturaAnulada`, `Huella`, `FechaHoraHusoGenRegistro` |
|
|
103
|
+
| **Evento (9)** | `NIF`, `ID`, `IdSistemaInformatico`, `Version`, `NumeroInstalacion`, `NIF`, `TipoEvento`, `HuellaEvento`, `FechaHoraHusoGenEvento` (el `NIF` aparece dos veces: sistema informático y obligado a emitir) |
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import {
|
|
107
|
+
computeRegistroAlta,
|
|
108
|
+
computeRegistroAnulacion,
|
|
109
|
+
computeRegistroEvento,
|
|
110
|
+
verifyRegistroAlta,
|
|
111
|
+
concatenateRegistroAlta,
|
|
112
|
+
HashChain,
|
|
113
|
+
} from '@dwast/verifactu-lib/core';
|
|
114
|
+
|
|
115
|
+
// Huella de un registro de alta
|
|
116
|
+
const hash = computeRegistroAlta({
|
|
117
|
+
idEmisorFactura: 'B18579458',
|
|
118
|
+
numSerieFactura: 'A2025-001',
|
|
119
|
+
fechaExpedicionFactura: '07-09-2026', // DD-MM-YYYY
|
|
120
|
+
tipoFactura: 'F1',
|
|
121
|
+
cuotaTotal: '21.00',
|
|
122
|
+
importeTotal: '121.00',
|
|
123
|
+
huellaAnterior: null, // hash del registro anterior (o null si es el primero)
|
|
124
|
+
fechaHoraHusoGenRegistro: '2026-09-07T10:00:00+02:00',
|
|
59
125
|
});
|
|
126
|
+
|
|
127
|
+
// Verificar que un hash coincide
|
|
128
|
+
const ok = verifyRegistroAlta({ ... }, hash);
|
|
129
|
+
|
|
130
|
+
// Cadena con estado (encadena automáticamente)
|
|
131
|
+
const chain = new HashChain();
|
|
132
|
+
const h1 = chain.pushAlta({ /* ... */ });
|
|
133
|
+
const h2 = chain.pushAlta({ /* ... */ }); // encadena con h1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Funciones exportadas:**
|
|
137
|
+
|
|
138
|
+
| Función | Descripción |
|
|
139
|
+
|---------|-------------|
|
|
140
|
+
| `sha256(data)` | SHA-256 en hex MAYÚSCULA |
|
|
141
|
+
| `concatenateFields(fields)` | Concatena `nombreCampo=valor&...` (trim, vacíos como `campo=`) |
|
|
142
|
+
| `concatenateRegistroAlta(input)` | Cadena canónica de un alta |
|
|
143
|
+
| `concatenateRegistroAnulacion(input)` | Cadena canónica de una anulación |
|
|
144
|
+
| `concatenateRegistroEvento(input)` | Cadena canónica de un evento |
|
|
145
|
+
| `computeRegistroAlta(input)` | Huella de un alta |
|
|
146
|
+
| `computeRegistroAnulacion(input)` | Huella de una anulación |
|
|
147
|
+
| `computeRegistroEvento(input)` | Huella de un evento |
|
|
148
|
+
| `verifyRegistroAlta(input, hash)` | Comprueba la huella de un alta |
|
|
149
|
+
| `verifyRegistroAnulacion(input, hash)` | Comprueba la huella de una anulación |
|
|
150
|
+
| `verifyRegistroEvento(input, hash)` | Comprueba la huella de un evento |
|
|
151
|
+
| `HashChain` | Cadena con estado (`pushAlta`, `pushAnulacion`, `pushEvento`, `reset`) |
|
|
152
|
+
|
|
153
|
+
### XML (`core/xml`)
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import {
|
|
157
|
+
generateInvoiceXml, // alias de generateRegistroAltaXml
|
|
158
|
+
generateRegistroAltaXml,
|
|
159
|
+
generateAnulacionXml,
|
|
160
|
+
generateEventoXml,
|
|
161
|
+
generateSuministroXml,
|
|
162
|
+
buildCabecera,
|
|
163
|
+
buildRemisionEnvelope,
|
|
164
|
+
} from '@dwast/verifactu-lib/core';
|
|
165
|
+
|
|
166
|
+
// 1. XML del registro de alta (RegistroAlta)
|
|
167
|
+
const registroAltaXml = generateInvoiceXml(invoice, previousHash);
|
|
168
|
+
|
|
169
|
+
// 2. Cabecera (ObligadoEmision + Representante si el certificado es de representante)
|
|
170
|
+
const cabecera = buildCabecera(
|
|
171
|
+
{ nombreRazon: invoice.emisor.nombreRazon, nif: invoice.emisor.nif },
|
|
172
|
+
certificatePem, // opcional
|
|
173
|
+
extractCertificateInfo, // opcional, para detectar representante
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// 3. Envolver en el suministro (RegFactuSistemaFacturacion)
|
|
177
|
+
const xml = generateSuministroXml(registroAltaXml, cabecera);
|
|
178
|
+
|
|
179
|
+
// 4. Sobre SOAP para el envío a AEAT
|
|
180
|
+
const envelope = buildRemisionEnvelope(xml);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Funciones exportadas:**
|
|
184
|
+
|
|
185
|
+
| Función | Descripción |
|
|
186
|
+
|---------|-------------|
|
|
187
|
+
| `generateRegistroAltaXml(invoice, previousHash)` | XML `<sf:RegistroAlta>` de un alta |
|
|
188
|
+
| `generateInvoiceXml(invoice, previousHash)` | Alias de `generateRegistroAltaXml` |
|
|
189
|
+
| `generateAnulacionXml(anulacion, previousHash)` | XML `<sf:RegistroAnulacion>` |
|
|
190
|
+
| `generateEventoXml(evento)` | XML `<sf:RegistroEvento>` (estructura `EventosSIF.xsd`) |
|
|
191
|
+
| `generateSuministroXml(registroXml, cabecera)` | Envuelve en `<sfLR:RegFactuSistemaFacturacion>` con `Cabecera` |
|
|
192
|
+
| `buildCabecera(obligado, certPem?, extractInfo?)` | Construye la cabecera; añade `Representante` si el NIF del cert difiere del obligado |
|
|
193
|
+
| `buildRemisionEnvelope(operationXml)` | Sobre SOAP Envelope/Body |
|
|
194
|
+
|
|
195
|
+
**Namespaces (críticos):**
|
|
196
|
+
|
|
197
|
+
| Prefijo | Namespace |
|
|
198
|
+
|---------|-----------|
|
|
199
|
+
| `sfLR` | `.../tike/cont/ws/SuministroLR.xsd` — `RegFactuSistemaFacturacion`, `Cabecera`, `RegistroFactura` |
|
|
200
|
+
| `sf` | `.../tike/cont/ws/SuministroInformacion.xsd` — `ObligadoEmision`, `Representante`, `RegistroAlta` y todos sus hijos |
|
|
201
|
+
| `sf` (eventos) | `.../tike/cont/ws/EventosSIF.xsd` — `RegistroEvento` (suministro independiente, no va dentro de `RegFactuSistemaFacturacion`) |
|
|
202
|
+
|
|
203
|
+
**Eventos (`RegistroEvento`):** se envían como un suministro independiente (no dentro de `RegFactuSistemaFacturacion`). Estructura: `IDVersion` + `Evento` (`SistemaInformatico`, `ObligadoEmision`, `FechaHoraHusoGenEvento`, `TipoEvento`, `Encadenamiento`, `TipoHuella`, `HuellaEvento`). El hash del evento usa `NIF`, `ID` (= `IDOtro` del software, vacío si usa NIF), `IdSistemaInformatico`, `Version`, `NumeroInstalacion`, `NIF` (obligado), `TipoEvento`, `HuellaEvento` (anterior), `FechaHoraHusoGenEvento`.
|
|
204
|
+
|
|
205
|
+
### QR (`core/qr`)
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { buildQrPayload } from '@dwast/verifactu-lib/core';
|
|
209
|
+
|
|
210
|
+
const qr = buildQrPayload(hash); // URL de verificación con el hash
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Fecha/hora (`core/utils`)
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { nowIsoWithOffset } from '@dwast/verifactu-lib/core';
|
|
217
|
+
|
|
218
|
+
// Fecha/hora actual con el huso detectado automáticamente (no asume +02:00)
|
|
219
|
+
const ts = nowIsoWithOffset(); // → 2026-09-07T10:29:05+02:00
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
AEAT exige que `FechaHoraHusoGenRegistro` sea la **fecha/hora actual** del sistema (margen de 240 segundos). Usa este helper en vez de hardcodear un huso.
|
|
223
|
+
|
|
224
|
+
### Validación (`core/validation`)
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { validateInvoice } from '@dwast/verifactu-lib/core';
|
|
228
|
+
|
|
229
|
+
const { valid, errors } = validateInvoice(invoice);
|
|
230
|
+
if (!valid) {
|
|
231
|
+
console.error(errors); // lista de campos obligatorios o con formato incorrecto
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Comprueba campos obligatorios, el formato `DD-MM-YYYY` de las fechas, `destinatarios` para F1/F3, y `claveRegimen` cuando hay `tipoImpositivo`. `VerifactuService.registerInvoice` la llama automáticamente y lanza un error si la factura es inválida.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Cliente AEAT (`aeat`)
|
|
240
|
+
|
|
241
|
+
Soporta autenticación por **certificado digital (mTLS)** o por token, y **dos entornos** (producción y pruebas). Por defecto se usa **producción**; los tests fijan `env: 'test'` para usar desarrollo.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { VerifactuClient, AEAT_TEST_SOAP_URL } from '@dwast/verifactu-lib/aeat';
|
|
245
|
+
|
|
246
|
+
const client = new VerifactuClient({
|
|
247
|
+
testBaseUrl: AEAT_TEST_SOAP_URL, // https://prewww1.aeat.es/.../VerifactuSOAP
|
|
248
|
+
env: 'test', // 'production' | 'test' | 'development'
|
|
249
|
+
cert, // PEM (mTLS)
|
|
250
|
+
key, // PEM (mTLS)
|
|
251
|
+
timeoutMs: 15_000,
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Config (`VerifactuClientConfig`):**
|
|
256
|
+
|
|
257
|
+
| Campo | Descripción |
|
|
258
|
+
|-------|-------------|
|
|
259
|
+
| `baseUrl` | URL del endpoint SOAP en producción |
|
|
260
|
+
| `testBaseUrl` | URL del endpoint SOAP en pruebas |
|
|
261
|
+
| `env` | `production` (default) \| `test` \| `development` |
|
|
262
|
+
| `authToken` | Token OAuth alternativo al certificado |
|
|
263
|
+
| `cert` / `key` | Certificado y clave PEM (mTLS) |
|
|
264
|
+
| `operation` | Operación SOAP (default `RegFactuSistemaFacturacion`) |
|
|
265
|
+
| `namespace` | Namespace del servicio |
|
|
266
|
+
| `timeoutMs` | Timeout de petición (default 10s) |
|
|
267
|
+
|
|
268
|
+
**Regla de entorno:** por defecto se usa **producción**. Si `env` es `production` (o no se indica), usa `baseUrl`. Si `env` es `test`/`development`, usa `testBaseUrl` (o lanza error si no hay). Los tests fijan `env: 'test'` para usar desarrollo.
|
|
269
|
+
|
|
270
|
+
### Métodos
|
|
271
|
+
|
|
272
|
+
| Método | Descripción |
|
|
273
|
+
|--------|-------------|
|
|
274
|
+
| `sendRecord(xml)` | Envía un registro (alta/anulación/evento) a AEAT |
|
|
275
|
+
| `getOperationStatus(operationId)` | ⚠️ **Deprecado**: la consulta por ID no está soportada por AEAT (error 4118). Usa `queryInvoice` |
|
|
276
|
+
| `queryInvoice({ obligado, numSerieFactura, ejercicio, periodo })` | Consulta un registro por número de factura y devuelve su huella |
|
|
277
|
+
|
|
278
|
+
### `queryInvoice` — obtener la huella de un registro enviado
|
|
279
|
+
|
|
280
|
+
En vez de guardar la huella localmente, puedes consultar AEAT y obtener la huella confirmada de un registro enviado:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const res = await client.queryInvoice({
|
|
284
|
+
obligado: { nombreRazon: 'MOVVIENDO TOURISM GROUP SL', nif: 'B18579458' },
|
|
285
|
+
numSerieFactura: 'A2026-...',
|
|
286
|
+
ejercicio: '2026',
|
|
287
|
+
periodo: '09', // 01–12 (meses); no existe '13'
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const huella = res.consulta?.huella; // hash confirmado por AEAT
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
> **Nota:** `periodo` solo admite `01`–`12` (meses). Usa el mes real de la factura.
|
|
294
|
+
|
|
295
|
+
> **⚠️ ¿Guardar la huella o pedirla a AEAT?** La cadena de huellas es **por obligado + sistema informático**. Para encadenar la siguiente factura necesitas la huella del último registro. **Lo recomendable es guardarla localmente** (en la BD, vía `VerifactuRepository.getLastRecord()`). Puedes recuperarla de AEAT con `queryInvoice`, pero necesitas saber el **número de factura** del último registro (que igualmente tendrías que guardar). Consultar "por periodo" sin número es poco fiable, porque devuelve registros de todos los sistemas y ejecuciones.
|
|
296
|
+
|
|
297
|
+
### Formato de respuesta (`SendResult`)
|
|
298
|
+
|
|
299
|
+
`sendRecord`, `getOperationStatus` y `queryInvoice` devuelven **todo lo recibido de AEAT**, sea OK o error:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
interface SendResult {
|
|
303
|
+
operationId: string; // ID de la operación devuelto por AEAT
|
|
304
|
+
status: 'OK' | 'ERROR'; // estado HTTP/registro
|
|
305
|
+
message?: string; // mensaje de AEAT
|
|
306
|
+
errorCode?: string; // código de error de AEAT
|
|
307
|
+
rawResponse?: string; // cuerpo crudo (para auditoría)
|
|
308
|
+
data?: Record<string, unknown>; // body completo parseado
|
|
309
|
+
response?: SoapResponse; // respuesta SOAP normalizada a JSON
|
|
310
|
+
registro?: RegistroResponse; // si la operación es de registro
|
|
311
|
+
consulta?: ConsultaResponse; // si la operación es de consulta
|
|
312
|
+
evento?: EventoResponse; // si la operación es de evento
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Interfaces tipadas:**
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
interface RegistroResponse {
|
|
320
|
+
estadoRegistro?: string;
|
|
321
|
+
codigoErrorRegistro?: string;
|
|
322
|
+
descripcionErrorRegistro?: string;
|
|
323
|
+
csv?: string; // Código Seguro de Verificación
|
|
324
|
+
huella?: string; // hash confirmado por AEAT
|
|
325
|
+
fechaHoraHusoGenRegistro?: string;
|
|
326
|
+
idFactura?: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface ConsultaResponse {
|
|
330
|
+
estado?: string;
|
|
331
|
+
codigoError?: string;
|
|
332
|
+
descripcionError?: string;
|
|
333
|
+
csv?: string;
|
|
334
|
+
huella?: string; // hash del primer registro devuelto
|
|
335
|
+
registros?: RegistroResponse[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface EventoResponse {
|
|
339
|
+
estadoEvento?: string;
|
|
340
|
+
codigoErrorEvento?: string;
|
|
341
|
+
descripcionErrorEvento?: string;
|
|
342
|
+
csv?: string;
|
|
343
|
+
huella?: string;
|
|
344
|
+
}
|
|
60
345
|
```
|
|
61
346
|
|
|
347
|
+
**Ejemplo de respuesta OK (registro):**
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"operationId": "",
|
|
352
|
+
"status": "OK",
|
|
353
|
+
"response": {
|
|
354
|
+
"operation": "RespuestaRegFactuSistemaFacturacion",
|
|
355
|
+
"body": {
|
|
356
|
+
"CSV": "A-UAGWU4Y3GE47KR",
|
|
357
|
+
"EstadoEnvio": "Correcto",
|
|
358
|
+
"RespuestaLinea": {
|
|
359
|
+
"EstadoRegistro": "Correcto",
|
|
360
|
+
"IDFactura": { "NumSerieFactura": "A2026-..." }
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
"registro": { "estadoRegistro": "Correcto", "csv": "A-UAGWU4Y3GE47KR" }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Ejemplo de respuesta ERROR (validación de AEAT):**
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{
|
|
372
|
+
"status": "OK",
|
|
373
|
+
"response": {
|
|
374
|
+
"operation": "RespuestaRegFactuSistemaFacturacion",
|
|
375
|
+
"body": {
|
|
376
|
+
"EstadoEnvio": "Incorrecto",
|
|
377
|
+
"RespuestaLinea": {
|
|
378
|
+
"EstadoRegistro": "Incorrecto",
|
|
379
|
+
"CodigoErrorRegistro": "1189",
|
|
380
|
+
"DescripcionErrorRegistro": "Si TipoFactura es F1 o F3 o R1 o R2 o R3 o R4 el bloque Destinatarios tiene que estar cumplimentado."
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
"registro": {
|
|
384
|
+
"estadoRegistro": "Incorrecto",
|
|
385
|
+
"codigoErrorRegistro": "1189",
|
|
386
|
+
"descripcionErrorRegistro": "Si TipoFactura es F1 o F3 o R1 o R2 o R3 o R4 el bloque Destinatarios tiene que estar cumplimentado."
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
> **Nota:** `data` contiene **todo** lo que AEAT devolvió. `rawResponse` es el cuerpo crudo tal cual, para conservarlo en auditoría. La AEAT exige guardar la respuesta de cada envío.
|
|
393
|
+
|
|
394
|
+
### Parser XML→JSON (`parseSoapResponse`)
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { parseSoapResponse } from '@dwast/verifactu-lib/aeat';
|
|
398
|
+
|
|
399
|
+
const soap = parseSoapResponse(xmlResponse); // → SoapResponse | null
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Convierte el SOAP XML de AEAT a JSON normalizado, mapeando a las interfaces tipadas. Devuelve `null` si la entrada no es XML.
|
|
403
|
+
|
|
404
|
+
### URLs oficiales
|
|
405
|
+
|
|
406
|
+
| Entorno | Endpoint SOAP (remisión voluntaria) |
|
|
407
|
+
|---------|-------------------------------------|
|
|
408
|
+
| Producción | `https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP` |
|
|
409
|
+
| Pruebas | `https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP` |
|
|
410
|
+
|
|
411
|
+
| Entorno | Endpoint SOAP (bajo requerimiento) |
|
|
412
|
+
|---------|-------------------------------------|
|
|
413
|
+
| Producción | `https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP` |
|
|
414
|
+
| Pruebas | `https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP` |
|
|
415
|
+
|
|
416
|
+
WSDL: `.../tikeV1.0/cont/ws/SistemaFacturacion.wsdl`. La operación de consulta (`ConsultaFactuSistemaFacturacion`) usa el **mismo endpoint** `VerifactuSOAP`.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Integración (`integration`)
|
|
421
|
+
|
|
422
|
+
### Requisitos del sistema consumidor
|
|
423
|
+
|
|
424
|
+
1. **Persistencia.** El sistema DEBE implementar `VerifactuRepository` y disponer de las tablas de `src/integration/schema.sql` (`verifactu_records`, `verifactu_responses`, `verifactu_events`). La AEAT exige conservar la respuesta de cada envío.
|
|
425
|
+
2. **Credenciales.** Para el envío real, el sistema aporta el certificado (`.p12`) y el NIF.
|
|
426
|
+
|
|
427
|
+
### Repositorio (`VerifactuRepository`)
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import type { VerifactuRepository, StoredRecord, StoredResponse, RecordStatus } from '@dwast/verifactu-lib/integration';
|
|
431
|
+
|
|
432
|
+
// El sistema consumidor implementa esta interfaz (Mongoose, Prisma, etc.)
|
|
433
|
+
const repo: VerifactuRepository = {
|
|
434
|
+
saveRecord(record) { /* ... */ },
|
|
435
|
+
updateRecordStatus(id, status) { /* ... */ },
|
|
436
|
+
getRecord(id) { /* ... */ },
|
|
437
|
+
getLastRecord() { /* ... */ },
|
|
438
|
+
saveResponse(response) { /* ... */ },
|
|
439
|
+
getResponses(recordId) { /* ... */ },
|
|
440
|
+
saveEvent(event) { /* ... */ },
|
|
441
|
+
};
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Estados (`RecordStatus`):** `PENDING` → `SENT` → `CONFIRMED` / `REJECTED` → `ANNULLED`.
|
|
445
|
+
|
|
446
|
+
### Servicio de facturación (`VerifactuService`)
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { VerifactuService } from '@dwast/verifactu-lib/integration';
|
|
450
|
+
import { VerifactuClient } from '@dwast/verifactu-lib/aeat';
|
|
451
|
+
|
|
452
|
+
const service = new VerifactuService(repo, client, certificatePem?);
|
|
453
|
+
|
|
454
|
+
await service.registerInvoice(invoice); // emitir (alta)
|
|
455
|
+
await service.annulInvoice(invoiceId, invoice?); // anular
|
|
456
|
+
await service.modifyInvoice(invoiceId, corrected); // modificar (anula + alta)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
| Método | Descripción |
|
|
460
|
+
|--------|-------------|
|
|
461
|
+
| `registerInvoice(invoice)` | Valida, genera XML + hash, guarda el registro, lo envía a AEAT y persiste la respuesta |
|
|
462
|
+
| `annulInvoice(invoiceId, invoice?)` | Genera un registro de anulación y marca la original como `ANNULLED` |
|
|
463
|
+
| `modifyInvoice(invoiceId, correctedInvoice)` | Verifactu no tiene "modificación": anula la original + emite la corregida |
|
|
464
|
+
| `registerEvent(evento)` | Genera el XML de un evento, calcula su huella encadenada, lo persiste y lo envía |
|
|
465
|
+
|
|
466
|
+
### Lectura del `.p12` (`readP12`)
|
|
467
|
+
|
|
468
|
+
Lee el `.p12` e itera **todos** sus campos, devolviéndolos. No interpreta qué campo es cada cosa.
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { readP12, extractCertificateInfo, extractNifFromCertificate } from '@dwast/verifactu-lib/integration';
|
|
472
|
+
|
|
473
|
+
const content = readP12(buffer, password);
|
|
474
|
+
// content.certificates[0] → cert PEM
|
|
475
|
+
// content.keys[0] → key PEM
|
|
476
|
+
// content.bags → todos los campos
|
|
477
|
+
|
|
478
|
+
// Utilidades explícitas (para detección de representante)
|
|
479
|
+
const info = extractCertificateInfo(certPem); // { nif, nombreRazon } | null
|
|
480
|
+
const nif = extractNifFromCertificate(certPem); // NIF del certificado | null
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Detección de representante
|
|
484
|
+
|
|
485
|
+
Si el NIF del certificado difiere del NIF del obligado a emitir, el certificado actúa como **representante** y se añade el bloque `Representante` en la cabecera:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { buildCabecera } from '@dwast/verifactu-lib/core';
|
|
489
|
+
import { extractCertificateInfo } from '@dwast/verifactu-lib/integration';
|
|
490
|
+
|
|
491
|
+
const cabecera = buildCabecera(
|
|
492
|
+
{ nombreRazon: 'MOVVIENDO TOURISM GROUP SL', nif: 'B18579458' },
|
|
493
|
+
certificatePem,
|
|
494
|
+
extractCertificateInfo,
|
|
495
|
+
);
|
|
496
|
+
// cabecera.representante = { nombreRazon: 'ANTONIO JOSE RECHE MARTÍNEZ', nif: '74654958V' }
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Accountability (contabilidad) (`AccountabilityService`)
|
|
500
|
+
|
|
501
|
+
Combina credenciales + cliente mTLS + persistencia. El NIF se aporta en el config (la librería no interpreta los campos del certificado).
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
import { AccountabilityService } from '@dwast/verifactu-lib/integration';
|
|
505
|
+
|
|
506
|
+
const service = new AccountabilityService(repo, {
|
|
507
|
+
baseUrl: 'https://...aeat...',
|
|
508
|
+
nif: 'B18579458',
|
|
509
|
+
p12Buffer, // o cert/key PEM
|
|
510
|
+
p12Password: '...',
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await service.registerInvoice(invoice);
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Credenciales por tenant
|
|
517
|
+
|
|
518
|
+
La librería **no gestiona el multitenancy**: solo recibe los datos (el `.p12`, la contraseña, la factura) y los envía. Cómo cada app guarda y administra las credenciales de cada tenant es problema suyo.
|
|
519
|
+
|
|
520
|
+
Un patrón habitual en apps multitenant es pedir la contraseña del `.p12` **una vez** al iniciar el proceso de facturación y cachearla en memoria (nunca en la BD). Eso se implementa en la app, no en la librería.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Tests
|
|
525
|
+
|
|
526
|
+
La suite usa **vitest**. Ejecuta:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
npm test
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Tests unitarios (sin red)
|
|
533
|
+
|
|
534
|
+
| Archivo | Qué cubre |
|
|
535
|
+
|---------|-----------|
|
|
536
|
+
| `test/hash.test.ts` | Cálculo de huella (alta, anulación, evento), concatenación, verificación, cadena `HashChain` |
|
|
537
|
+
| `test/xml.test.ts` | Generación de XML (RegistroAlta, Suministro, Cabecera, Representante) |
|
|
538
|
+
| `test/qr.test.ts` | Generación del payload QR |
|
|
539
|
+
| `test/p12.test.ts` | Lectura del `.p12`, extracción de NIF e info del certificado, detección de representante |
|
|
540
|
+
| `test/client.test.ts` | Resolución de URL por entorno, parseo de respuesta |
|
|
541
|
+
| `test/response.test.ts` | Parser SOAP→JSON y mapeo a interfaces tipadas |
|
|
542
|
+
| `test/validation.test.ts` | Validación de facturas (campos obligatorios, formato de fechas) |
|
|
543
|
+
| `test/integration.test.ts` | Servicio de orquestación (con repositorio mock) |
|
|
544
|
+
|
|
545
|
+
### Test de integración real contra AEAT (`test/aeat-integration.test.ts`)
|
|
546
|
+
|
|
547
|
+
Envía registros reales al **entorno de pruebas** de AEAT (`https://prewww1.aeat.es`). Requiere un certificado FNMT válido. Si la conexión falla por autenticación o red, el test se **salta** (no falla).
|
|
548
|
+
|
|
549
|
+
**Casos cubiertos:**
|
|
550
|
+
|
|
551
|
+
| Caso | Descripción |
|
|
552
|
+
|------|-------------|
|
|
553
|
+
| Envía 1 factura | Alta con `PrimerRegistro` |
|
|
554
|
+
| Envía 2 facturas encadenadas | La 2ª usa la huella de la 1ª (`RegistroAnterior`) |
|
|
555
|
+
| Anula una factura y emite una nueva | Alta → Anulación → Nueva alta |
|
|
556
|
+
| Consulta la huella de un registro enviado | Usa `queryInvoice` en vez de guardar la huella localmente |
|
|
557
|
+
|
|
558
|
+
**Detalles del test de integración:**
|
|
559
|
+
|
|
560
|
+
- **Número de factura único** `YYYYMMDDHHMMSS` para evitar duplicados entre ejecuciones.
|
|
561
|
+
- **Espera de 1s** al inicio de cada test para que los números no colisionen.
|
|
562
|
+
- **Sistema informático único por test** (`numeroInstalacion`) para que `PrimerRegistro` sea válido aunque el entorno ya tenga facturas.
|
|
563
|
+
- **`FechaHoraHusoGenRegistro`** = hora actual con huso detectado (`nowIsoWithOffset`).
|
|
564
|
+
- **Cada test vuelca en `test/responses/aeat-integration.log`** (TXT, gitignored) lo que se **envía** (XML) y lo que se **recibe** de AEAT, para poder depurar si algo falla. Se añade (append) en cada ejecución, acumulando el historial completo.
|
|
565
|
+
|
|
566
|
+
> **Nota:** `IdSistemaInformatico` es `TextMax2Type` (máx 2 caracteres). La unicidad por test va en `NumeroInstalacion` (hasta 100 caracteres).
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Autenticación ante AEAT
|
|
571
|
+
|
|
572
|
+
La AEAT sabe de quién es cada factura por **tres mecanismos**:
|
|
573
|
+
|
|
574
|
+
| Mecanismo | Qué garantiza |
|
|
575
|
+
|-----------|---------------|
|
|
576
|
+
| **NIF en el XML** (`IDEmisorFactura`) | De quién es la factura (identidad en el contenido) |
|
|
577
|
+
| **Certificado digital en la conexión** (mTLS) | Quién envía (autenticación del canal) |
|
|
578
|
+
| **Huella/hash** | Que no se ha manipulado (integridad) |
|
|
579
|
+
|
|
580
|
+
El `.p12` se usa para el **handshake TLS (mTLS)**: se extraen `cert` + `key` y se pasan al cliente. El NIF se aporta por la app.
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Seguridad de credenciales
|
|
585
|
+
|
|
586
|
+
| Enfoque | Seguridad |
|
|
587
|
+
|---------|-----------|
|
|
588
|
+
| `.p12` + contraseña en la BD | ❌ Mala (atacante lo tiene todo) |
|
|
589
|
+
| `.p12` en la BD, contraseña en env/secret manager | ✅ Buena |
|
|
590
|
+
| `.p12` en la BD, contraseña pedida en runtime y cacheada en memoria | ✅ Buena |
|
|
591
|
+
| Solo certificado en la BD, clave privada en secret manager | ✅✅ La más segura |
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
62
595
|
## Scripts
|
|
63
596
|
|
|
64
597
|
```bash
|
|
65
|
-
npm run build # compila a dist/
|
|
598
|
+
npm run build # compila ESM + CJS a dist/
|
|
66
599
|
npm run typecheck # typecheck sin emitir
|
|
67
600
|
npm test # ejecuta vitest
|
|
68
601
|
```
|
|
69
602
|
|
|
70
603
|
## Publicación
|
|
71
604
|
|
|
72
|
-
Paquete
|
|
605
|
+
Paquete público en npm. Instálalo con:
|
|
73
606
|
|
|
74
607
|
```bash
|
|
75
|
-
npm install
|
|
608
|
+
npm install @dwast/verifactu-lib
|
|
76
609
|
```
|
|
77
610
|
|
|
78
|
-
## Algoritmo de huella (hash)
|
|
79
|
-
|
|
80
|
-
La huella de un registro Verifactu **no se calcula sobre el XML**, sino sobre una cadena canónica de campos concatenados en el formato `nombreCampo=valor&...`:
|
|
81
|
-
|
|
82
|
-
- **Alta (8 campos):** `IDEmisorFactura`, `NumSerieFactura`, `FechaExpedicionFactura`, `TipoFactura`, `CuotaTotal`, `ImporteTotal`, `Huella` (hash anterior), `FechaHoraHusoGenRegistro`.
|
|
83
|
-
- **Anulación (5 campos):** `IDEmisorFacturaAnulada`, `NumSerieFacturaAnulada`, `FechaExpedicionFacturaAnulada`, `Huella`, `FechaHoraHusoGenRegistro`.
|
|
84
|
-
|
|
85
|
-
Reglas: se recortan espacios al inicio/final, los campos vacíos van como `nombreCampo=`, **no se aplica URL-encoding**, y el resultado es **SHA-256 en hexadecimal MAYÚSCULA** (64 caracteres). El hash anterior se coloca en el campo `Huella` en su posición exacta. La implementación está verificada contra los [vectores oficiales de la AEAT](https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Veri-Factu_especificaciones_huella_hash_registros.pdf).
|
|
86
|
-
|
|
87
611
|
## Nota sobre el esquema oficial
|
|
88
612
|
|
|
89
|
-
El XML generado sigue la estructura del RD 1007/2023 y la documentación de la AEAT,
|
|
613
|
+
El XML generado sigue la estructura del RD 1007/2023 y la documentación de la AEAT, y se ha validado contra el entorno de pruebas real. No obstante, **debe validarse contra el XSD oficial** (descargable desde el [portal de desarrolladores de la AEAT](https://www.agenciatributaria.es/AEAT.desarrolladores/Desarrolladores/_menu_/Documentacion/Sistemas_Informaticos_de_Facturacion_y_Sistemas_VERI_FACTU/Sistemas_Informaticos_de_Facturacion_y_Sistemas_VERI_FACTU.html)) antes de producción.
|