@dwast/verifactu-lib 0.1.0-beta.1 → 0.1.0-beta.3

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.
Files changed (157) hide show
  1. package/dist/cjs/aeat/client.d.ts +115 -14
  2. package/dist/cjs/aeat/client.d.ts.map +1 -1
  3. package/dist/cjs/aeat/client.js +223 -47
  4. package/dist/cjs/aeat/client.js.map +1 -1
  5. package/dist/cjs/aeat/index.d.ts +1 -0
  6. package/dist/cjs/aeat/index.d.ts.map +1 -1
  7. package/dist/cjs/aeat/index.js +1 -0
  8. package/dist/cjs/aeat/index.js.map +1 -1
  9. package/dist/cjs/aeat/response.d.ts +80 -0
  10. package/dist/cjs/aeat/response.d.ts.map +1 -0
  11. package/dist/cjs/aeat/response.js +118 -0
  12. package/dist/cjs/aeat/response.js.map +1 -0
  13. package/dist/cjs/core/hash/chain.d.ts +42 -0
  14. package/dist/cjs/core/hash/chain.d.ts.map +1 -1
  15. package/dist/cjs/core/hash/chain.js +40 -0
  16. package/dist/cjs/core/hash/chain.js.map +1 -1
  17. package/dist/cjs/core/index.d.ts +2 -0
  18. package/dist/cjs/core/index.d.ts.map +1 -1
  19. package/dist/cjs/core/index.js +2 -0
  20. package/dist/cjs/core/index.js.map +1 -1
  21. package/dist/cjs/core/models/index.d.ts +0 -1
  22. package/dist/cjs/core/models/index.d.ts.map +1 -1
  23. package/dist/cjs/core/models/index.js +0 -1
  24. package/dist/cjs/core/models/index.js.map +1 -1
  25. package/dist/cjs/core/models/invoice.d.ts +42 -17
  26. package/dist/cjs/core/models/invoice.d.ts.map +1 -1
  27. package/dist/cjs/core/utils/date.d.ts +16 -0
  28. package/dist/cjs/core/utils/date.d.ts.map +1 -0
  29. package/dist/cjs/core/utils/date.js +27 -0
  30. package/dist/cjs/core/utils/date.js.map +1 -0
  31. package/dist/cjs/core/utils/index.d.ts +2 -0
  32. package/dist/cjs/core/utils/index.d.ts.map +1 -0
  33. package/dist/cjs/core/utils/index.js +18 -0
  34. package/dist/cjs/core/utils/index.js.map +1 -0
  35. package/dist/cjs/core/validation/index.d.ts +2 -0
  36. package/dist/cjs/core/validation/index.d.ts.map +1 -0
  37. package/dist/cjs/core/validation/index.js +18 -0
  38. package/dist/cjs/core/validation/index.js.map +1 -0
  39. package/dist/cjs/core/validation/validate.d.ts +18 -0
  40. package/dist/cjs/core/validation/validate.d.ts.map +1 -0
  41. package/dist/cjs/core/validation/validate.js +65 -0
  42. package/dist/cjs/core/validation/validate.js.map +1 -0
  43. package/dist/cjs/core/xml/generator.d.ts +117 -11
  44. package/dist/cjs/core/xml/generator.d.ts.map +1 -1
  45. package/dist/cjs/core/xml/generator.js +239 -64
  46. package/dist/cjs/core/xml/generator.js.map +1 -1
  47. package/dist/cjs/integration/accountability.d.ts +41 -0
  48. package/dist/cjs/integration/accountability.d.ts.map +1 -0
  49. package/dist/cjs/integration/accountability.js +92 -0
  50. package/dist/cjs/integration/accountability.js.map +1 -0
  51. package/dist/cjs/integration/index.d.ts +2 -0
  52. package/dist/cjs/integration/index.d.ts.map +1 -1
  53. package/dist/cjs/integration/index.js +2 -0
  54. package/dist/cjs/integration/index.js.map +1 -1
  55. package/dist/cjs/integration/p12.d.ts +58 -0
  56. package/dist/cjs/integration/p12.d.ts.map +1 -0
  57. package/dist/cjs/integration/p12.js +127 -0
  58. package/dist/cjs/integration/p12.js.map +1 -0
  59. package/dist/cjs/integration/repository.d.ts +4 -3
  60. package/dist/cjs/integration/repository.d.ts.map +1 -1
  61. package/dist/cjs/integration/service.d.ts +11 -2
  62. package/dist/cjs/integration/service.d.ts.map +1 -1
  63. package/dist/cjs/integration/service.js +115 -15
  64. package/dist/cjs/integration/service.js.map +1 -1
  65. package/dist/cjs/integration/tenant-credentials.d.ts +44 -0
  66. package/dist/cjs/integration/tenant-credentials.d.ts.map +1 -0
  67. package/dist/cjs/integration/tenant-credentials.js +62 -0
  68. package/dist/cjs/integration/tenant-credentials.js.map +1 -0
  69. package/dist/esm/aeat/client.d.ts +115 -14
  70. package/dist/esm/aeat/client.d.ts.map +1 -1
  71. package/dist/esm/aeat/client.js +218 -46
  72. package/dist/esm/aeat/client.js.map +1 -1
  73. package/dist/esm/aeat/index.d.ts +1 -0
  74. package/dist/esm/aeat/index.d.ts.map +1 -1
  75. package/dist/esm/aeat/index.js +1 -0
  76. package/dist/esm/aeat/index.js.map +1 -1
  77. package/dist/esm/aeat/response.d.ts +80 -0
  78. package/dist/esm/aeat/response.d.ts.map +1 -0
  79. package/dist/esm/aeat/response.js +115 -0
  80. package/dist/esm/aeat/response.js.map +1 -0
  81. package/dist/esm/core/hash/chain.d.ts +42 -0
  82. package/dist/esm/core/hash/chain.d.ts.map +1 -1
  83. package/dist/esm/core/hash/chain.js +37 -0
  84. package/dist/esm/core/hash/chain.js.map +1 -1
  85. package/dist/esm/core/index.d.ts +2 -0
  86. package/dist/esm/core/index.d.ts.map +1 -1
  87. package/dist/esm/core/index.js +2 -0
  88. package/dist/esm/core/index.js.map +1 -1
  89. package/dist/esm/core/models/index.d.ts +0 -1
  90. package/dist/esm/core/models/index.d.ts.map +1 -1
  91. package/dist/esm/core/models/index.js +0 -1
  92. package/dist/esm/core/models/index.js.map +1 -1
  93. package/dist/esm/core/models/invoice.d.ts +42 -17
  94. package/dist/esm/core/models/invoice.d.ts.map +1 -1
  95. package/dist/esm/core/utils/date.d.ts +16 -0
  96. package/dist/esm/core/utils/date.d.ts.map +1 -0
  97. package/dist/esm/core/utils/date.js +24 -0
  98. package/dist/esm/core/utils/date.js.map +1 -0
  99. package/dist/esm/core/utils/index.d.ts +2 -0
  100. package/dist/esm/core/utils/index.d.ts.map +1 -0
  101. package/dist/esm/core/utils/index.js +2 -0
  102. package/dist/esm/core/utils/index.js.map +1 -0
  103. package/dist/esm/core/validation/index.d.ts +2 -0
  104. package/dist/esm/core/validation/index.d.ts.map +1 -0
  105. package/dist/esm/core/validation/index.js +2 -0
  106. package/dist/esm/core/validation/index.js.map +1 -0
  107. package/dist/esm/core/validation/validate.d.ts +18 -0
  108. package/dist/esm/core/validation/validate.d.ts.map +1 -0
  109. package/dist/esm/core/validation/validate.js +62 -0
  110. package/dist/esm/core/validation/validate.js.map +1 -0
  111. package/dist/esm/core/xml/generator.d.ts +117 -11
  112. package/dist/esm/core/xml/generator.d.ts.map +1 -1
  113. package/dist/esm/core/xml/generator.js +233 -64
  114. package/dist/esm/core/xml/generator.js.map +1 -1
  115. package/dist/esm/integration/accountability.d.ts +41 -0
  116. package/dist/esm/integration/accountability.d.ts.map +1 -0
  117. package/dist/esm/integration/accountability.js +88 -0
  118. package/dist/esm/integration/accountability.js.map +1 -0
  119. package/dist/esm/integration/index.d.ts +2 -0
  120. package/dist/esm/integration/index.d.ts.map +1 -1
  121. package/dist/esm/integration/index.js +2 -0
  122. package/dist/esm/integration/index.js.map +1 -1
  123. package/dist/esm/integration/p12.d.ts +58 -0
  124. package/dist/esm/integration/p12.d.ts.map +1 -0
  125. package/dist/esm/integration/p12.js +119 -0
  126. package/dist/esm/integration/p12.js.map +1 -0
  127. package/dist/esm/integration/repository.d.ts +4 -3
  128. package/dist/esm/integration/repository.d.ts.map +1 -1
  129. package/dist/esm/integration/service.d.ts +11 -2
  130. package/dist/esm/integration/service.d.ts.map +1 -1
  131. package/dist/esm/integration/service.js +117 -17
  132. package/dist/esm/integration/service.js.map +1 -1
  133. package/dist/esm/integration/tenant-credentials.d.ts +44 -0
  134. package/dist/esm/integration/tenant-credentials.d.ts.map +1 -0
  135. package/dist/esm/integration/tenant-credentials.js +58 -0
  136. package/dist/esm/integration/tenant-credentials.js.map +1 -0
  137. package/package.json +3 -1
  138. package/readme.md +731 -38
  139. package/src/aeat/client.ts +298 -57
  140. package/src/aeat/index.ts +1 -0
  141. package/src/aeat/response.ts +198 -0
  142. package/src/core/hash/chain.ts +68 -0
  143. package/src/core/index.ts +2 -0
  144. package/src/core/models/index.ts +0 -1
  145. package/src/core/models/invoice.ts +40 -18
  146. package/src/core/utils/date.ts +27 -0
  147. package/src/core/utils/index.ts +1 -0
  148. package/src/core/validation/index.ts +1 -0
  149. package/src/core/validation/validate.ts +71 -0
  150. package/src/core/xml/generator.ts +312 -73
  151. package/src/integration/accountability.ts +125 -0
  152. package/src/integration/index.ts +2 -0
  153. package/src/integration/p12.ts +155 -0
  154. package/src/integration/repository.ts +4 -3
  155. package/src/integration/schema.sql +0 -14
  156. package/src/integration/service.ts +134 -16
  157. package/src/core/models/event.ts +0 -25
package/readme.md CHANGED
@@ -1,89 +1,782 @@
1
1
  # @dwast/verifactu-lib
2
2
 
3
- Librería privada (npm) para la gestión de **Verifactu** (Ley 18/2022 y RD 1007/2023) en un stack MERN (JS/TS).
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 formato 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.
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 (depende de credenciales y entornos).
13
- - **`integration`** — Capa de orquestación que define los **requisitos del sistema consumidor** (persistencia de registros y respuestas en tablas) y une núcleo + AEAT.
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/ # Tipos: Invoice, Emisor, Desglose, Event, etc.
21
- │ ├── hash/ # Encadenado hash (SHA-256)
22
- │ ├── xml/ # Generación XML Verifactu (RegistroFactura)
23
- └── qr/ # Generación QR Verifactu
24
- ├── aeat/ # Cliente de servicios AEAT
25
- ├── integration/ # Repositorio, servicio de orquestación y schema.sql
28
+ │ ├── models/ # Tipos: Invoice, Emisor, Desglose, SistemaInformatico
29
+ │ ├── hash/ # Encadenado hash (SHA-256)
30
+ │ ├── xml/ # Generación XML Verifactu (RegistroAlta, Anulacion, Evento, Suministro)
31
+ ├── qr/ # Generación QR Verifactu
32
+ ├── validation/ # Validación de facturas
33
+ │ └── utils/ # Utilidades (fecha/hora con huso)
34
+ ├── aeat/ # Cliente de servicios AEAT (SOAP + mTLS)
35
+ ├── integration/ # Repositorio, servicio, p12, accountability
26
36
  └── index.ts
27
37
  ```
28
38
 
29
- ## Requisitos del sistema consumidor
39
+ **Subpaths de importación** (definidos en `package.json`):
40
+
41
+ | Subpath | Contenido |
42
+ |---------|-----------|
43
+ | `@dwast/verifactu-lib` | Todo (re-exporta core + aeat + integration) |
44
+ | `@dwast/verifactu-lib/core` | Núcleo puro (modelos, hash, xml, qr, utils) |
45
+ | `@dwast/verifactu-lib/aeat` | Cliente AEAT + parser de respuesta |
46
+ | `@dwast/verifactu-lib/integration` | Repositorio, servicio, p12, accountability |
47
+
48
+ ---
49
+
50
+ ## Cómo usar
30
51
 
31
- La capa `integration` define qué debe aportar el sistema que use la librería:
52
+ Guía práctica paso a paso. La receta recomendada es **`VerifactuService`** (se encarga de validar, generar el hash, el XML, encadenar con la huella anterior, enviar y persistir). Para casos "de bajo nivel" usa el `VerifactuClient` directamente.
32
53
 
33
- 1. **Persistencia de registros y respuestas.** El sistema DEBE implementar la interfaz `VerifactuRepository` (ver `src/integration/repository.ts`) 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 para acreditar la remisión.
34
- 2. **Cliente AEAT configurado.** El sistema DEBE proporcionar un `VerifactuClient` con las credenciales y el entorno adecuados.
54
+ ### 0. Configuración inicial (certificado `.p12` cliente)
35
55
 
36
- ### Ejemplo de uso
56
+ Extrae `cert` + `key` del `.p12` y construye el cliente (aquí en entorno de **pruebas**; en producción usa `baseUrl` y no pases `env`).
37
57
 
38
58
  ```ts
59
+ import { readFileSync } from 'node:fs';
60
+ import { readP12 } from '@dwast/verifactu-lib/integration';
61
+ import { VerifactuClient, AEAT_TEST_SOAP_URL } from '@dwast/verifactu-lib/aeat';
39
62
  import { VerifactuService } from '@dwast/verifactu-lib/integration';
40
- import { VerifactuClient } from '@dwast/verifactu-lib/aeat';
41
- import { MyMongoRepository } from './my-repo'; // implementa VerifactuRepository
63
+ import type { VerifactuRepository } from '@dwast/verifactu-lib/integration';
64
+
65
+ // 1. Leer el .p12 y extraer certificado + clave (PEM).
66
+ const { certificates, keys } = readP12(
67
+ readFileSync('mi-certificado.p12'),
68
+ 'contraseña-del-p12',
69
+ );
70
+
71
+ // 2. Cliente AEAT con mTLS. env:'test' → usa testBaseUrl (pruebas).
72
+ const client = new VerifactuClient({
73
+ testBaseUrl: AEAT_TEST_SOAP_URL, // https://prewww1.aeat.es/.../VerifactuSOAP
74
+ env: 'test',
75
+ cert: certificates[0],
76
+ key: keys[0],
77
+ timeoutMs: 15_000,
78
+ });
79
+
80
+ // 3. Servicio de orquestación (repositorio implementado por tu app + cliente).
81
+ // El 3er arg (cert PEM) sirve para detectar representante en la cabecera.
82
+ const repo: VerifactuRepository = /* tu implementación (Mongoose, Prisma...) */;
83
+ const service = new VerifactuService(repo, client, certificates[0]);
84
+ ```
85
+
86
+ > **Producción:** omite `env` y `testBaseUrl`, usa `baseUrl`:
87
+ > `new VerifactuClient({ baseUrl: AEAT_PRODUCTION_SOAP_URL, cert, key })`.
42
88
 
43
- const repo = new MyMongoRepository();
44
- const client = new VerifactuClient({ baseUrl: 'https://...', authToken: '...' });
45
- const service = new VerifactuService(repo, client);
89
+ ### 1. Enviar 1 factura (alta)
46
90
 
47
- await service.registerInvoice({
48
- id: { serie: 'A', numero: '2025-001', fechaExpedicion: '2025-07-01' },
49
- emisor: { nif: 'B12345678', nombreRazon: 'Empresa S.L.' },
91
+ ```ts
92
+ import type { Invoice } from '@dwast/verifactu-lib/core';
93
+
94
+ const invoice: Invoice = {
95
+ id: { serie: 'A', numero: '2026-0001', fechaExpedicion: '07-09-2026' }, // DD-MM-YYYY
96
+ emisor: { nif: 'B18579458', nombreRazon: 'MOVVIENDO TOURISM GROUP SL' },
50
97
  tipoFactura: 'F1',
51
98
  tipoDesglose: 'S1',
99
+ descripcion: 'Servicios de consultoria',
100
+ destinatarios: [{ nombreRazon: 'CLIENTE SL', nif: 'B12345678' }],
52
101
  desglose: {
53
- tipoOperaciones: [
54
- { tipoOperacion: '01', detallesIva: [{ tipoImpositivo: 21, baseImponible: 100, cuotaRepercutida: 21 }] },
102
+ detalles: [
103
+ { calificacionOperacion: 'S1', claveRegimen: '01', tipoImpositivo: 21, baseImponible: 100, cuotaRepercutida: 21 },
55
104
  ],
56
105
  cuotaTotal: 21,
57
106
  importeTotal: 121,
58
107
  },
108
+ sistemaInformatico: {
109
+ nombreRazon: 'MOVVIENDO TOURISM GROUP SL',
110
+ nif: 'B18579458',
111
+ nombreSistemaInformatico: 'SIF-PROD',
112
+ idSistemaInformatico: '01', // máx 2 caracteres
113
+ version: '1.0',
114
+ numeroInstalacion: '1',
115
+ },
116
+ fechaHoraHusoGenRegistro: new Date().toISOString().replace(/\.\d+Z/, 'Z'), // o nowIsoWithOffset()
117
+ huella: '', // la calcula el servicio
118
+ };
119
+
120
+ const record = await service.registerInvoice(invoice);
121
+
122
+ console.log(record.status); // 'CONFIRMED' | 'REJECTED'
123
+ console.log(record.hash); // huella SHA-256 (64 hex)
124
+ console.log(record.hashVerified); // true/false (si AEAT devolvió huella que coincide)
125
+ console.log(record.qrPayload); // URL del QR
126
+ console.log(record.xml); // XML Verifactu que se envió (persistir)
127
+ ```
128
+
129
+ > La **fecha/hora** debe ser la actual (margen AEAT de 240s). Usa `nowIsoWithOffset()` de `@dwast/verifactu-lib/core` en vez de `toISOString()`.
130
+
131
+ ### 2. Enviar varias facturas encadenadas
132
+
133
+ `VerifactuService` **encadena automáticamente** la huella: cada alta usa como `huellaAnterior` el hash del último registro guardado en `getLastRecord()`. Solo encadena **dentro del mismo obligado + sistema informático**.
134
+
135
+ ```ts
136
+ const f1 = await service.registerInvoice(invoice1); // PrimerRegistro
137
+ const f2 = await service.registerInvoice(invoice2); // usa f1.hash como huellaAnterior
138
+ const f3 = await service.registerInvoice(invoice3); // usa f2.hash
139
+ console.log(f2.previousHash === f1.hash); // true
140
+ ```
141
+
142
+ > **Importante:** el sistema informático (`numeroInstalacion`/`idSistemaInformatico`) debe ser **el mismo** en toda la cadena. Para empezar una cadena nueva por cada sistema, usa un `numeroInstalacion` único.
143
+
144
+ ### 3. Anular una factura
145
+
146
+ ```ts
147
+ await service.annulInvoice(invoiceId, invoice);
148
+ // - Genera el registro de anulación (encadena con el último hash).
149
+ // - Envía a AEAT.
150
+ // - Marca la factura original como 'ANNULLED'.
151
+ ```
152
+
153
+ `invoiceId` es la **referencia de la original** (serie + número, p. ej. `A2026-0001`), que debe existir en el repositorio. Pasa la `invoice` completa si quieres que use su `sistemaInformatico` y sus fechas; si no, usa los valores por defecto de la librería.
154
+
155
+ ### 4. Modificar una factura
156
+
157
+ Verifactu **no tiene "modificación"**: se hace como anulación + nuevo alta. `modifyInvoice` lo hace por ti.
158
+
159
+ ```ts
160
+ const original: Invoice = /* la factura ya enviada */;
161
+ await service.registerInvoice(original);
162
+
163
+ const corregida: Invoice = { ...original, id: { ...original.id, numero: '2026-0002' }, /* cambios */ };
164
+ await service.modifyInvoice(`${original.id.serie ?? ''}${original.id.numero}`, corregida);
165
+ // Anula la original y emite la corregida como nuevo alta (CONFIRMED si AEAT la acepta).
166
+ ```
167
+
168
+ ### 5. Consultar la huella de un registro enviado
169
+
170
+ En lugar de guardar la huella, puedes pedirla a AEAT. Necesitas el **número de factura** y el **mes** real.
171
+
172
+ ```ts
173
+ const res = await client.queryInvoice({
174
+ obligado: { nombreRazon: 'MOVVIENDO TOURISM GROUP SL', nif: 'B18579458' },
175
+ numSerieFactura: 'A2026-0001',
176
+ ejercicio: '2026',
177
+ periodo: '09', // 01–12, el mes real de la factura
178
+ });
179
+
180
+ const huella = res.consulta?.huella; // hash confirmado por AEAT
181
+ const pagSiguiente = res.consulta?.clavePaginacion; // pasar como clavePaginacion en la siguiente consulta
182
+ ```
183
+
184
+ > **Recomendación:** para encadenar la siguiente factura es más fiable **guardar la huella localmente** (`getLastRecord()`). `queryInvoice` es útil para auditoría o recuperación puntual. Consultar "por periodo" sin número devuelve registros de todos los sistemas y ejecuciones (poco fiable).
185
+
186
+ ### 6. Enviar un evento (`RegistroEvento`)
187
+
188
+ ```ts
189
+ import { computeRegistroEvento } from '@dwast/verifactu-lib/core';
190
+
191
+ const last = await repo.getLastRecord(); // último registro (para encadenar)
192
+ const fechaHora = nowIsoWithOffset();
193
+ const hash = computeRegistroEvento({
194
+ nifSistemaInformatico: 'B18579458',
195
+ id: '', // IDOtro del software (vacío si usa NIF)
196
+ idSistemaInformatico: '01',
197
+ version: '1.0',
198
+ numeroInstalacion: '1',
199
+ nifObligado: 'B18579458',
200
+ tipoEvento: '01', // 01–10, 90 (ver EventosSIF.xsd)
201
+ huellaAnterior: last?.hash ?? null,
202
+ fechaHoraHusoGenEvento: fechaHora,
203
+ });
204
+
205
+ await service.registerEvent({
206
+ sistemaInformatico: { /* igual que en la factura */ },
207
+ obligadoEmision: { nombreRazon: 'MOVVIENDO TOURISM GROUP SL', nif: 'B18579458' },
208
+ fechaHoraHusoGenEvento: fechaHora,
209
+ tipoEvento: '01',
210
+ huellaEvento: hash,
211
+ // eventoAnterior: { tipoEvento, fechaHoraHusoGenEvento, huellaEvento } // para el 2º evento en adelante
212
+ });
213
+ ```
214
+
215
+ > **⚠️ Envío de eventos.** La generación del XML (`generateEventoXml`) es correcta y está cubierta por unit tests, pero **el envío real necesita un endpoint/operación SOAP específica de eventos** que el cliente actual (`VerifactuSOAP`) todavía no cubre (AEAT devuelve error 4118). Úsalo cuando se añada ese endpoint.
216
+
217
+ ---
218
+
219
+ ## Núcleo (`core`)
220
+
221
+ ### Modelos (`core/models`)
222
+
223
+ ```ts
224
+ import type { Invoice } from '@dwast/verifactu-lib/core';
225
+
226
+ const invoice: Invoice = {
227
+ id: { serie: 'A', numero: '2025-001', fechaExpedicion: '07-09-2026' },
228
+ emisor: { nif: 'B18579458', nombreRazon: 'MOVVIENDO TOURISM GROUP SL' },
229
+ tipoFactura: 'F1', // F1–F9
230
+ tipoDesglose: 'S1', // S1–S6
231
+ descripcion: 'Servicios de consultoria',
232
+ destinatarios: [{ nombreRazon: 'CLIENTE SL', nif: 'B12345678' }],
233
+ desglose: {
234
+ detalles: [
235
+ {
236
+ calificacionOperacion: 'S1', // S1/S2/N1/N2...
237
+ claveRegimen: '01', // obligatorio si Impuesto es IVA/IPSI/IGIC
238
+ tipoImpositivo: 21,
239
+ baseImponible: 100,
240
+ cuotaRepercutida: 21,
241
+ },
242
+ ],
243
+ cuotaTotal: 21,
244
+ importeTotal: 121,
245
+ },
246
+ sistemaInformatico: {
247
+ nombreRazon: 'MOVVIENDO TOURISM GROUP SL',
248
+ nif: 'B18579458',
249
+ nombreSistemaInformatico: 'SIF-TEST',
250
+ idSistemaInformatico: '01', // TextMax2Type: máx 2 caracteres
251
+ version: '1.0',
252
+ numeroInstalacion: '1',
253
+ },
254
+ fechaHoraHusoGenRegistro: '2026-09-07T10:00:00+02:00',
255
+ huella: 'ABC123', // se rellena con computeRegistroAlta
256
+ };
257
+ ```
258
+
259
+ > **⚠️ 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).
260
+
261
+ > **⚠️ 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).
262
+
263
+ ### Hash (`core/hash`)
264
+
265
+ 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).
266
+
267
+ **Campos por tipo de registro:**
268
+
269
+ | Registro | Campos (en orden) |
270
+ |----------|-------------------|
271
+ | **Alta (8)** | `IDEmisorFactura`, `NumSerieFactura`, `FechaExpedicionFactura`, `TipoFactura`, `CuotaTotal`, `ImporteTotal`, `Huella`, `FechaHoraHusoGenRegistro` |
272
+ | **Anulación (5)** | `IDEmisorFacturaAnulada`, `NumSerieFacturaAnulada`, `FechaExpedicionFacturaAnulada`, `Huella`, `FechaHoraHusoGenRegistro` |
273
+ | **Evento (9)** | `NIF`, `ID`, `IdSistemaInformatico`, `Version`, `NumeroInstalacion`, `NIF`, `TipoEvento`, `HuellaEvento`, `FechaHoraHusoGenEvento` (el `NIF` aparece dos veces: sistema informático y obligado a emitir) |
274
+
275
+ ```ts
276
+ import {
277
+ computeRegistroAlta,
278
+ computeRegistroAnulacion,
279
+ computeRegistroEvento,
280
+ verifyRegistroAlta,
281
+ concatenateRegistroAlta,
282
+ HashChain,
283
+ } from '@dwast/verifactu-lib/core';
284
+
285
+ // Huella de un registro de alta
286
+ const hash = computeRegistroAlta({
287
+ idEmisorFactura: 'B18579458',
288
+ numSerieFactura: 'A2025-001',
289
+ fechaExpedicionFactura: '07-09-2026', // DD-MM-YYYY
290
+ tipoFactura: 'F1',
291
+ cuotaTotal: '21.00',
292
+ importeTotal: '121.00',
293
+ huellaAnterior: null, // hash del registro anterior (o null si es el primero)
294
+ fechaHoraHusoGenRegistro: '2026-09-07T10:00:00+02:00',
295
+ });
296
+
297
+ // Verificar que un hash coincide
298
+ const ok = verifyRegistroAlta({ ... }, hash);
299
+
300
+ // Cadena con estado (encadena automáticamente)
301
+ const chain = new HashChain();
302
+ const h1 = chain.pushAlta({ /* ... */ });
303
+ const h2 = chain.pushAlta({ /* ... */ }); // encadena con h1
304
+ ```
305
+
306
+ **Funciones exportadas:**
307
+
308
+ | Función | Descripción |
309
+ |---------|-------------|
310
+ | `sha256(data)` | SHA-256 en hex MAYÚSCULA |
311
+ | `concatenateFields(fields)` | Concatena `nombreCampo=valor&...` (trim, vacíos como `campo=`) |
312
+ | `concatenateRegistroAlta(input)` | Cadena canónica de un alta |
313
+ | `concatenateRegistroAnulacion(input)` | Cadena canónica de una anulación |
314
+ | `concatenateRegistroEvento(input)` | Cadena canónica de un evento |
315
+ | `computeRegistroAlta(input)` | Huella de un alta |
316
+ | `computeRegistroAnulacion(input)` | Huella de una anulación |
317
+ | `computeRegistroEvento(input)` | Huella de un evento |
318
+ | `verifyRegistroAlta(input, hash)` | Comprueba la huella de un alta |
319
+ | `verifyRegistroAnulacion(input, hash)` | Comprueba la huella de una anulación |
320
+ | `verifyRegistroEvento(input, hash)` | Comprueba la huella de un evento |
321
+ | `HashChain` | Cadena con estado (`pushAlta`, `pushAnulacion`, `pushEvento`, `reset`) |
322
+
323
+ ### XML (`core/xml`)
324
+
325
+ ```ts
326
+ import {
327
+ generateInvoiceXml, // alias de generateRegistroAltaXml
328
+ generateRegistroAltaXml,
329
+ generateAnulacionXml,
330
+ generateEventoXml,
331
+ generateSuministroXml,
332
+ buildCabecera,
333
+ buildRemisionEnvelope,
334
+ } from '@dwast/verifactu-lib/core';
335
+
336
+ // 1. XML del registro de alta (RegistroAlta)
337
+ const registroAltaXml = generateInvoiceXml(invoice, previousHash);
338
+
339
+ // 2. Cabecera (ObligadoEmision + Representante si el certificado es de representante)
340
+ const cabecera = buildCabecera(
341
+ { nombreRazon: invoice.emisor.nombreRazon, nif: invoice.emisor.nif },
342
+ certificatePem, // opcional
343
+ extractCertificateInfo, // opcional, para detectar representante
344
+ );
345
+
346
+ // 3. Envolver en el suministro (RegFactuSistemaFacturacion)
347
+ const xml = generateSuministroXml(registroAltaXml, cabecera);
348
+
349
+ // 4. Sobre SOAP para el envío a AEAT
350
+ const envelope = buildRemisionEnvelope(xml);
351
+ ```
352
+
353
+ **Funciones exportadas:**
354
+
355
+ | Función | Descripción |
356
+ |---------|-------------|
357
+ | `generateRegistroAltaXml(invoice, previousHash)` | XML `<sf:RegistroAlta>` de un alta |
358
+ | `generateInvoiceXml(invoice, previousHash)` | Alias de `generateRegistroAltaXml` |
359
+ | `generateAnulacionXml(anulacion, previousHash)` | XML `<sf:RegistroAnulacion>` |
360
+ | `generateEventoXml(evento)` | XML `<sf:RegistroEvento>` (estructura `EventosSIF.xsd`) |
361
+ | `generateSuministroXml(registroXml, cabecera)` | Envuelve en `<sfLR:RegFactuSistemaFacturacion>` con `Cabecera` |
362
+ | `buildCabecera(obligado, certPem?, extractInfo?)` | Construye la cabecera; añade `Representante` si el NIF del cert difiere del obligado |
363
+ | `buildRemisionEnvelope(operationXml)` | Sobre SOAP Envelope/Body |
364
+
365
+ **Namespaces (críticos):**
366
+
367
+ | Prefijo | Namespace |
368
+ |---------|-----------|
369
+ | `sfLR` | `.../tike/cont/ws/SuministroLR.xsd` — `RegFactuSistemaFacturacion`, `Cabecera`, `RegistroFactura` |
370
+ | `sf` | `.../tike/cont/ws/SuministroInformacion.xsd` — `ObligadoEmision`, `Representante`, `RegistroAlta` y todos sus hijos |
371
+ | `sf` (eventos) | `.../tike/cont/ws/EventosSIF.xsd` — `RegistroEvento` (suministro independiente, no va dentro de `RegFactuSistemaFacturacion`) |
372
+
373
+ **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`.
374
+
375
+ ### QR (`core/qr`)
376
+
377
+ ```ts
378
+ import { buildQrPayload } from '@dwast/verifactu-lib/core';
379
+
380
+ const qr = buildQrPayload(hash); // URL de verificación con el hash
381
+ ```
382
+
383
+ ### Fecha/hora (`core/utils`)
384
+
385
+ ```ts
386
+ import { nowIsoWithOffset } from '@dwast/verifactu-lib/core';
387
+
388
+ // Fecha/hora actual con el huso detectado automáticamente (no asume +02:00)
389
+ const ts = nowIsoWithOffset(); // → 2026-09-07T10:29:05+02:00
390
+ ```
391
+
392
+ AEAT exige que `FechaHoraHusoGenRegistro` sea la **fecha/hora actual** del sistema (margen de 240 segundos). Usa este helper en vez de hardcodear un huso.
393
+
394
+ ### Validación (`core/validation`)
395
+
396
+ ```ts
397
+ import { validateInvoice } from '@dwast/verifactu-lib/core';
398
+
399
+ const { valid, errors } = validateInvoice(invoice);
400
+ if (!valid) {
401
+ console.error(errors); // lista de campos obligatorios o con formato incorrecto
402
+ }
403
+ ```
404
+
405
+ 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.
406
+
407
+ ---
408
+
409
+ ## Cliente AEAT (`aeat`)
410
+
411
+ 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.
412
+
413
+ ```ts
414
+ import { VerifactuClient, AEAT_TEST_SOAP_URL } from '@dwast/verifactu-lib/aeat';
415
+
416
+ const client = new VerifactuClient({
417
+ testBaseUrl: AEAT_TEST_SOAP_URL, // https://prewww1.aeat.es/.../VerifactuSOAP
418
+ env: 'test', // 'production' | 'test' | 'development'
419
+ cert, // PEM (mTLS)
420
+ key, // PEM (mTLS)
421
+ timeoutMs: 15_000,
422
+ });
423
+ ```
424
+
425
+ **Config (`VerifactuClientConfig`):**
426
+
427
+ | Campo | Descripción |
428
+ |-------|-------------|
429
+ | `baseUrl` | URL del endpoint SOAP en producción |
430
+ | `testBaseUrl` | URL del endpoint SOAP en pruebas |
431
+ | `env` | `production` (default) \| `test` \| `development` |
432
+ | `authToken` | Token OAuth alternativo al certificado |
433
+ | `cert` / `key` | Certificado y clave PEM (mTLS) |
434
+ | `operation` | Operación SOAP (default `RegFactuSistemaFacturacion`) |
435
+ | `namespace` | Namespace del servicio |
436
+ | `timeoutMs` | Timeout de petición (default 10s) |
437
+
438
+ **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.
439
+
440
+ ### Métodos
441
+
442
+ | Método | Descripción |
443
+ |--------|-------------|
444
+ | `sendRecord(xml)` | Envía un registro (alta/anulación/evento) a AEAT |
445
+ | `queryInvoice({ obligado, numSerieFactura, ejercicio, periodo, clavePaginacion? })` | Consulta un registro por número de factura y devuelve su huella; acepta `clavePaginacion` para cursar las páginas siguientes |
446
+
447
+ ### `queryInvoice` — obtener la huella de un registro enviado
448
+
449
+ En vez de guardar la huella localmente, puedes consultar AEAT y obtener la huella confirmada de un registro enviado:
450
+
451
+ ```ts
452
+ const res = await client.queryInvoice({
453
+ obligado: { nombreRazon: 'MOVVIENDO TOURISM GROUP SL', nif: 'B18579458' },
454
+ numSerieFactura: 'A2026-...',
455
+ ejercicio: '2026',
456
+ periodo: '09', // 01–12 (meses); no existe '13'
59
457
  });
458
+
459
+ const huella = res.consulta?.huella; // hash confirmado por AEAT
460
+ ```
461
+
462
+ > **Nota:** `periodo` solo admite `01`–`12` (meses). Usa el mes real de la factura.
463
+
464
+ > **⚠️ ¿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.
465
+
466
+ ### Formato de respuesta (`SendResult`)
467
+
468
+ `sendRecord` y `queryInvoice` devuelven **todo lo recibido de AEAT**, sea OK o error:
469
+
470
+ ```ts
471
+ interface SendResult {
472
+ operationId: string; // ID de la operación devuelto por AEAT
473
+ status: 'OK' | 'ERROR'; // estado HTTP/registro
474
+ message?: string; // mensaje de AEAT
475
+ errorCode?: string; // código de error de AEAT
476
+ rawResponse?: string; // cuerpo crudo (para auditoría)
477
+ data?: Record<string, unknown>; // body completo parseado
478
+ response?: SoapResponse; // respuesta SOAP normalizada a JSON
479
+ registro?: RegistroResponse; // si la operación es de registro
480
+ consulta?: ConsultaResponse; // si la operación es de consulta
481
+ evento?: EventoResponse; // si la operación es de evento
482
+ }
60
483
  ```
61
484
 
485
+ **Interfaces tipadas:**
486
+
487
+ ```ts
488
+ interface RegistroResponse {
489
+ estadoRegistro?: string;
490
+ codigoErrorRegistro?: string;
491
+ descripcionErrorRegistro?: string;
492
+ csv?: string; // Código Seguro de Verificación
493
+ huella?: string; // hash confirmado por AEAT
494
+ fechaHoraHusoGenRegistro?: string;
495
+ idFactura?: string;
496
+ }
497
+
498
+ interface ConsultaResponse {
499
+ estado?: string;
500
+ codigoError?: string;
501
+ descripcionError?: string;
502
+ csv?: string;
503
+ huella?: string; // hash del primer registro devuelto
504
+ clavePaginacion?: string; // cursor para la página siguiente
505
+ registros?: RegistroResponse[];
506
+ }
507
+
508
+ interface EventoResponse {
509
+ estadoEvento?: string;
510
+ codigoErrorEvento?: string;
511
+ descripcionErrorEvento?: string;
512
+ csv?: string;
513
+ huella?: string;
514
+ }
515
+ ```
516
+
517
+ **Ejemplo de respuesta OK (registro):**
518
+
519
+ ```json
520
+ {
521
+ "operationId": "",
522
+ "status": "OK",
523
+ "response": {
524
+ "operation": "RespuestaRegFactuSistemaFacturacion",
525
+ "body": {
526
+ "CSV": "A-UAGWU4Y3GE47KR",
527
+ "EstadoEnvio": "Correcto",
528
+ "RespuestaLinea": {
529
+ "EstadoRegistro": "Correcto",
530
+ "IDFactura": { "NumSerieFactura": "A2026-..." }
531
+ }
532
+ },
533
+ "registro": { "estadoRegistro": "Correcto", "csv": "A-UAGWU4Y3GE47KR" }
534
+ }
535
+ }
536
+ ```
537
+
538
+ **Ejemplo de respuesta ERROR (validación de AEAT):**
539
+
540
+ ```json
541
+ {
542
+ "status": "OK",
543
+ "response": {
544
+ "operation": "RespuestaRegFactuSistemaFacturacion",
545
+ "body": {
546
+ "EstadoEnvio": "Incorrecto",
547
+ "RespuestaLinea": {
548
+ "EstadoRegistro": "Incorrecto",
549
+ "CodigoErrorRegistro": "1189",
550
+ "DescripcionErrorRegistro": "Si TipoFactura es F1 o F3 o R1 o R2 o R3 o R4 el bloque Destinatarios tiene que estar cumplimentado."
551
+ }
552
+ },
553
+ "registro": {
554
+ "estadoRegistro": "Incorrecto",
555
+ "codigoErrorRegistro": "1189",
556
+ "descripcionErrorRegistro": "Si TipoFactura es F1 o F3 o R1 o R2 o R3 o R4 el bloque Destinatarios tiene que estar cumplimentado."
557
+ }
558
+ }
559
+ }
560
+ ```
561
+
562
+ > **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.
563
+
564
+ ### Parser XML→JSON (`parseSoapResponse`)
565
+
566
+ ```ts
567
+ import { parseSoapResponse } from '@dwast/verifactu-lib/aeat';
568
+
569
+ const soap = parseSoapResponse(xmlResponse); // → SoapResponse | null
570
+ ```
571
+
572
+ Convierte el SOAP XML de AEAT a JSON normalizado, mapeando a las interfaces tipadas. Devuelve `null` si la entrada no es XML.
573
+
574
+ ### URLs oficiales
575
+
576
+ | Entorno | Endpoint SOAP (remisión voluntaria) |
577
+ |---------|-------------------------------------|
578
+ | Producción | `https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP` |
579
+ | Pruebas | `https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP` |
580
+
581
+ | Entorno | Endpoint SOAP (bajo requerimiento) |
582
+ |---------|-------------------------------------|
583
+ | Producción | `https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP` |
584
+ | Pruebas | `https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP` |
585
+
586
+ WSDL: `.../tikeV1.0/cont/ws/SistemaFacturacion.wsdl`. La operación de consulta (`ConsultaFactuSistemaFacturacion`) usa el **mismo endpoint** `VerifactuSOAP`.
587
+
588
+ ---
589
+
590
+ ## Integración (`integration`)
591
+
592
+ ### Requisitos del sistema consumidor
593
+
594
+ 1. **Persistencia.** El sistema DEBE implementar `VerifactuRepository` y disponer de las tablas de `src/integration/schema.sql` (`verifactu_records`, `verifactu_responses`). La AEAT exige conservar la respuesta de cada envío.
595
+ 2. **Credenciales.** Para el envío real, el sistema aporta el certificado (`.p12`) y el NIF.
596
+
597
+ ### Repositorio (`VerifactuRepository`)
598
+
599
+ ```ts
600
+ import type { VerifactuRepository, StoredRecord, StoredResponse, RecordStatus } from '@dwast/verifactu-lib/integration';
601
+
602
+ // El sistema consumidor implementa esta interfaz (Mongoose, Prisma, etc.)
603
+ const repo: VerifactuRepository = {
604
+ saveRecord(record) { /* ... */ },
605
+ updateRecordStatus(id, status) { /* ... */ },
606
+ getRecord(id) { /* ... */ },
607
+ getLastRecord() { /* ... */ },
608
+ saveResponse(response) { /* ... */ },
609
+ getResponses(recordId) { /* ... */ },
610
+ };
611
+ ```
612
+
613
+ **Estados (`RecordStatus`):** `PENDING` → `SENT` → `CONFIRMED` / `REJECTED` → `ANNULLED`.
614
+
615
+ ### Servicio de facturación (`VerifactuService`)
616
+
617
+ ```ts
618
+ import { VerifactuService } from '@dwast/verifactu-lib/integration';
619
+ import { VerifactuClient } from '@dwast/verifactu-lib/aeat';
620
+
621
+ const service = new VerifactuService(repo, client, certificatePem?);
622
+
623
+ await service.registerInvoice(invoice); // emitir (alta)
624
+ await service.annulInvoice(invoiceId, invoice?); // anular
625
+ await service.modifyInvoice(invoiceId, corrected); // modificar (anula + alta)
626
+ ```
627
+
628
+ | Método | Descripción |
629
+ |--------|-------------|
630
+ | `registerInvoice(invoice)` | Valida, genera XML + hash, guarda el registro, lo envía a AEAT y persiste la respuesta |
631
+ | `annulInvoice(invoiceId, invoice?)` | Genera un registro de anulación y marca la original como `ANNULLED` |
632
+ | `modifyInvoice(invoiceId, correctedInvoice)` | Verifactu no tiene "modificación": anula la original + emite la corregida |
633
+ | `registerEvent(evento)` | Genera el XML de un evento, calcula su huella encadenada, lo persiste y lo envía |
634
+
635
+ ### Lectura del `.p12` (`readP12`)
636
+
637
+ Lee el `.p12` e itera **todos** sus campos, devolviéndolos. No interpreta qué campo es cada cosa.
638
+
639
+ ```ts
640
+ import { readP12, extractCertificateInfo, extractNifFromCertificate } from '@dwast/verifactu-lib/integration';
641
+
642
+ const content = readP12(buffer, password);
643
+ // content.certificates[0] → cert PEM
644
+ // content.keys[0] → key PEM
645
+ // content.bags → todos los campos
646
+
647
+ // Utilidades explícitas (para detección de representante)
648
+ const info = extractCertificateInfo(certPem); // { nif, nombreRazon } | null
649
+ const nif = extractNifFromCertificate(certPem); // NIF del certificado | null
650
+ ```
651
+
652
+ ### Detección de representante
653
+
654
+ 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:
655
+
656
+ ```ts
657
+ import { buildCabecera } from '@dwast/verifactu-lib/core';
658
+ import { extractCertificateInfo } from '@dwast/verifactu-lib/integration';
659
+
660
+ const cabecera = buildCabecera(
661
+ { nombreRazon: 'MOVVIENDO TOURISM GROUP SL', nif: 'B18579458' },
662
+ certificatePem,
663
+ extractCertificateInfo,
664
+ );
665
+ // cabecera.representante = { nombreRazon: 'ANTONIO JOSE RECHE MARTÍNEZ', nif: '74654958V' }
666
+ ```
667
+
668
+ ### Accountability (contabilidad) (`AccountabilityService`)
669
+
670
+ Combina credenciales + cliente mTLS + persistencia. El NIF se aporta en el config (la librería no interpreta los campos del certificado).
671
+
672
+ ```ts
673
+ import { AccountabilityService } from '@dwast/verifactu-lib/integration';
674
+
675
+ const service = new AccountabilityService(repo, {
676
+ baseUrl: 'https://...aeat...',
677
+ nif: 'B18579458',
678
+ p12Buffer, // o cert/key PEM
679
+ p12Password: '...',
680
+ });
681
+
682
+ await service.registerInvoice(invoice);
683
+ ```
684
+
685
+ ### Credenciales por tenant
686
+
687
+ 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.
688
+
689
+ 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.
690
+
691
+ ---
692
+
693
+ ## Tests
694
+
695
+ La suite usa **vitest**. Ejecuta:
696
+
697
+ ```bash
698
+ npm test
699
+ ```
700
+
701
+ ### Tests unitarios (sin red)
702
+
703
+ | Archivo | Qué cubre |
704
+ |---------|-----------|
705
+ | `test/hash.test.ts` | Cálculo de huella (alta, anulación, evento), concatenación, verificación, cadena `HashChain` |
706
+ | `test/xml.test.ts` | Generación de XML (RegistroAlta, Suministro, Cabecera, Representante) |
707
+ | `test/qr.test.ts` | Generación del payload QR |
708
+ | `test/p12.test.ts` | Lectura del `.p12`, extracción de NIF e info del certificado, detección de representante |
709
+ | `test/client.test.ts` | Resolución de URL por entorno, parseo de respuesta |
710
+ | `test/response.test.ts` | Parser SOAP→JSON y mapeo a interfaces tipadas |
711
+ | `test/validation.test.ts` | Validación de facturas (campos obligatorios, formato de fechas) |
712
+ | `test/integration.test.ts` | Servicio de orquestación (con repositorio mock) |
713
+
714
+ ### Test de integración real contra AEAT (`test/aeat-integration.test.ts`)
715
+
716
+ 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).
717
+
718
+ **Casos cubiertos:**
719
+
720
+ | Caso | Descripción |
721
+ |------|-------------|
722
+ | Envía 1 factura | Alta con `PrimerRegistro` |
723
+ | Envía 2 facturas encadenadas | La 2ª usa la huella de la 1ª (`RegistroAnterior`) |
724
+ | Anula una factura y emite una nueva | Alta → Anulación → Nueva alta |
725
+ | Consulta la huella de un registro enviado | Usa `queryInvoice` en vez de guardar la huella localmente |
726
+
727
+ **Detalles del test de integración:**
728
+
729
+ - **Número de factura único** `YYYYMMDDHHMMSS` para evitar duplicados entre ejecuciones.
730
+ - **Espera de 1s** al inicio de cada test para que los números no colisionen.
731
+ - **Sistema informático único por test** (`numeroInstalacion`) para que `PrimerRegistro` sea válido aunque el entorno ya tenga facturas.
732
+ - **`FechaHoraHusoGenRegistro`** = hora actual con huso detectado (`nowIsoWithOffset`).
733
+ - **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.
734
+
735
+ > **Nota:** `IdSistemaInformatico` es `TextMax2Type` (máx 2 caracteres). La unicidad por test va en `NumeroInstalacion` (hasta 100 caracteres).
736
+
737
+ ---
738
+
739
+ ## Autenticación ante AEAT
740
+
741
+ La AEAT sabe de quién es cada factura por **tres mecanismos**:
742
+
743
+ | Mecanismo | Qué garantiza |
744
+ |-----------|---------------|
745
+ | **NIF en el XML** (`IDEmisorFactura`) | De quién es la factura (identidad en el contenido) |
746
+ | **Certificado digital en la conexión** (mTLS) | Quién envía (autenticación del canal) |
747
+ | **Huella/hash** | Que no se ha manipulado (integridad) |
748
+
749
+ 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.
750
+
751
+ ---
752
+
753
+ ## Seguridad de credenciales
754
+
755
+ | Enfoque | Seguridad |
756
+ |---------|-----------|
757
+ | `.p12` + contraseña en la BD | ❌ Mala (atacante lo tiene todo) |
758
+ | `.p12` en la BD, contraseña en env/secret manager | ✅ Buena |
759
+ | `.p12` en la BD, contraseña pedida en runtime y cacheada en memoria | ✅ Buena |
760
+ | Solo certificado en la BD, clave privada en secret manager | ✅✅ La más segura |
761
+
762
+ ---
763
+
62
764
  ## Scripts
63
765
 
64
766
  ```bash
65
- npm run build # compila a dist/
767
+ npm run build # compila ESM + CJS a dist/
66
768
  npm run typecheck # typecheck sin emitir
67
769
  npm test # ejecuta vitest
68
770
  ```
69
771
 
70
772
  ## Publicación
71
773
 
72
- Paquete privado (`"private": true`). Instálalo desde un registry privado (GitHub Packages, Verdaccio, GitLab Package Registry) o directamente desde el repo:
774
+ Paquete público en npm. Instálalo con:
73
775
 
74
776
  ```bash
75
- npm install git+https://github.com/tu-org/verifactu-lib.git
777
+ npm install @dwast/verifactu-lib
76
778
  ```
77
779
 
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
780
  ## Nota sobre el esquema oficial
88
781
 
89
- El XML generado sigue la estructura del RD 1007/2023 y la documentación de la AEAT, pero **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.
782
+ 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.