@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.
Files changed (249) hide show
  1. package/dist/cjs/aeat/client.d.ts +162 -0
  2. package/dist/cjs/aeat/client.d.ts.map +1 -0
  3. package/dist/cjs/aeat/client.js +267 -0
  4. package/dist/cjs/aeat/client.js.map +1 -0
  5. package/dist/cjs/aeat/index.d.ts +3 -0
  6. package/dist/cjs/aeat/index.d.ts.map +1 -0
  7. package/dist/cjs/aeat/index.js +19 -0
  8. package/dist/cjs/aeat/index.js.map +1 -0
  9. package/dist/cjs/aeat/response.d.ts +78 -0
  10. package/dist/cjs/aeat/response.d.ts.map +1 -0
  11. package/dist/cjs/aeat/response.js +117 -0
  12. package/dist/cjs/aeat/response.js.map +1 -0
  13. package/dist/{core → cjs/core}/hash/chain.d.ts +42 -0
  14. package/dist/cjs/core/hash/chain.d.ts.map +1 -0
  15. package/dist/cjs/core/hash/chain.js +167 -0
  16. package/dist/cjs/core/hash/chain.js.map +1 -0
  17. package/dist/cjs/core/hash/index.d.ts.map +1 -0
  18. package/dist/cjs/core/hash/index.js +18 -0
  19. package/dist/cjs/core/hash/index.js.map +1 -0
  20. package/dist/{core → cjs/core}/index.d.ts +2 -0
  21. package/dist/cjs/core/index.d.ts.map +1 -0
  22. package/dist/cjs/core/index.js +23 -0
  23. package/dist/cjs/core/index.js.map +1 -0
  24. package/dist/cjs/core/models/event.d.ts.map +1 -0
  25. package/dist/cjs/core/models/event.js +3 -0
  26. package/dist/{core → cjs/core}/models/event.js.map +1 -1
  27. package/dist/cjs/core/models/index.d.ts.map +1 -0
  28. package/dist/cjs/core/models/index.js +19 -0
  29. package/dist/cjs/core/models/index.js.map +1 -0
  30. package/dist/{core → cjs/core}/models/invoice.d.ts +42 -17
  31. package/dist/cjs/core/models/invoice.d.ts.map +1 -0
  32. package/dist/cjs/core/models/invoice.js +3 -0
  33. package/dist/{core → cjs/core}/models/invoice.js.map +1 -1
  34. package/dist/cjs/core/qr/index.d.ts.map +1 -0
  35. package/dist/cjs/core/qr/index.js +18 -0
  36. package/dist/cjs/core/qr/index.js.map +1 -0
  37. package/dist/cjs/core/qr/qr.d.ts.map +1 -0
  38. package/dist/cjs/core/qr/qr.js +19 -0
  39. package/dist/cjs/core/qr/qr.js.map +1 -0
  40. package/dist/cjs/core/utils/date.d.ts +16 -0
  41. package/dist/cjs/core/utils/date.d.ts.map +1 -0
  42. package/dist/cjs/core/utils/date.js +27 -0
  43. package/dist/cjs/core/utils/date.js.map +1 -0
  44. package/dist/cjs/core/utils/index.d.ts +2 -0
  45. package/dist/cjs/core/utils/index.d.ts.map +1 -0
  46. package/dist/cjs/core/utils/index.js +18 -0
  47. package/dist/cjs/core/utils/index.js.map +1 -0
  48. package/dist/cjs/core/validation/index.d.ts +2 -0
  49. package/dist/cjs/core/validation/index.d.ts.map +1 -0
  50. package/dist/cjs/core/validation/index.js +18 -0
  51. package/dist/cjs/core/validation/index.js.map +1 -0
  52. package/dist/cjs/core/validation/validate.d.ts +18 -0
  53. package/dist/cjs/core/validation/validate.d.ts.map +1 -0
  54. package/dist/cjs/core/validation/validate.js +65 -0
  55. package/dist/cjs/core/validation/validate.js.map +1 -0
  56. package/dist/cjs/core/xml/generator.d.ts +140 -0
  57. package/dist/cjs/core/xml/generator.d.ts.map +1 -0
  58. package/dist/cjs/core/xml/generator.js +269 -0
  59. package/dist/cjs/core/xml/generator.js.map +1 -0
  60. package/dist/cjs/core/xml/index.d.ts.map +1 -0
  61. package/dist/cjs/core/xml/index.js +18 -0
  62. package/dist/cjs/core/xml/index.js.map +1 -0
  63. package/dist/cjs/index.d.ts.map +1 -0
  64. package/dist/cjs/index.js +20 -0
  65. package/dist/cjs/index.js.map +1 -0
  66. package/dist/cjs/integration/accountability.d.ts +41 -0
  67. package/dist/cjs/integration/accountability.d.ts.map +1 -0
  68. package/dist/cjs/integration/accountability.js +92 -0
  69. package/dist/cjs/integration/accountability.js.map +1 -0
  70. package/dist/cjs/integration/index.d.ts +5 -0
  71. package/dist/cjs/integration/index.d.ts.map +1 -0
  72. package/dist/cjs/integration/index.js +21 -0
  73. package/dist/cjs/integration/index.js.map +1 -0
  74. package/dist/cjs/integration/p12.d.ts +58 -0
  75. package/dist/cjs/integration/p12.d.ts.map +1 -0
  76. package/dist/cjs/integration/p12.js +127 -0
  77. package/dist/cjs/integration/p12.js.map +1 -0
  78. package/dist/{integration → cjs/integration}/repository.d.ts +2 -0
  79. package/dist/cjs/integration/repository.d.ts.map +1 -0
  80. package/dist/cjs/integration/repository.js +3 -0
  81. package/dist/cjs/integration/repository.js.map +1 -0
  82. package/dist/{integration → cjs/integration}/service.d.ts +11 -2
  83. package/dist/cjs/integration/service.d.ts.map +1 -0
  84. package/dist/cjs/integration/service.js +230 -0
  85. package/dist/cjs/integration/service.js.map +1 -0
  86. package/dist/cjs/integration/tenant-credentials.d.ts +44 -0
  87. package/dist/cjs/integration/tenant-credentials.d.ts.map +1 -0
  88. package/dist/cjs/integration/tenant-credentials.js +62 -0
  89. package/dist/cjs/integration/tenant-credentials.js.map +1 -0
  90. package/dist/cjs/package.json +3 -0
  91. package/dist/esm/aeat/client.d.ts +162 -0
  92. package/dist/esm/aeat/client.d.ts.map +1 -0
  93. package/dist/esm/aeat/client.js +259 -0
  94. package/dist/esm/aeat/client.js.map +1 -0
  95. package/dist/esm/aeat/index.d.ts +3 -0
  96. package/dist/esm/aeat/index.d.ts.map +1 -0
  97. package/dist/esm/aeat/index.js +3 -0
  98. package/dist/esm/aeat/index.js.map +1 -0
  99. package/dist/esm/aeat/response.d.ts +78 -0
  100. package/dist/esm/aeat/response.d.ts.map +1 -0
  101. package/dist/esm/aeat/response.js +114 -0
  102. package/dist/esm/aeat/response.js.map +1 -0
  103. package/dist/esm/core/hash/chain.d.ts +139 -0
  104. package/dist/esm/core/hash/chain.d.ts.map +1 -0
  105. package/dist/{core → esm/core}/hash/chain.js +37 -0
  106. package/dist/esm/core/hash/chain.js.map +1 -0
  107. package/dist/esm/core/hash/index.d.ts +2 -0
  108. package/dist/esm/core/hash/index.d.ts.map +1 -0
  109. package/dist/esm/core/hash/index.js.map +1 -0
  110. package/dist/esm/core/index.d.ts +7 -0
  111. package/dist/esm/core/index.d.ts.map +1 -0
  112. package/dist/{core → esm/core}/index.js +2 -0
  113. package/dist/esm/core/index.js.map +1 -0
  114. package/dist/esm/core/models/event.d.ts +25 -0
  115. package/dist/esm/core/models/event.d.ts.map +1 -0
  116. package/dist/esm/core/models/event.js.map +1 -0
  117. package/dist/esm/core/models/index.d.ts +3 -0
  118. package/dist/esm/core/models/index.d.ts.map +1 -0
  119. package/dist/esm/core/models/index.js.map +1 -0
  120. package/dist/esm/core/models/invoice.d.ts +115 -0
  121. package/dist/esm/core/models/invoice.d.ts.map +1 -0
  122. package/dist/esm/core/models/invoice.js.map +1 -0
  123. package/dist/esm/core/qr/index.d.ts +2 -0
  124. package/dist/esm/core/qr/index.d.ts.map +1 -0
  125. package/dist/esm/core/qr/index.js.map +1 -0
  126. package/dist/esm/core/qr/qr.d.ts +13 -0
  127. package/dist/esm/core/qr/qr.d.ts.map +1 -0
  128. package/dist/esm/core/qr/qr.js.map +1 -0
  129. package/dist/esm/core/utils/date.d.ts +16 -0
  130. package/dist/esm/core/utils/date.d.ts.map +1 -0
  131. package/dist/esm/core/utils/date.js +24 -0
  132. package/dist/esm/core/utils/date.js.map +1 -0
  133. package/dist/esm/core/utils/index.d.ts +2 -0
  134. package/dist/esm/core/utils/index.d.ts.map +1 -0
  135. package/dist/esm/core/utils/index.js +2 -0
  136. package/dist/esm/core/utils/index.js.map +1 -0
  137. package/dist/esm/core/validation/index.d.ts +2 -0
  138. package/dist/esm/core/validation/index.d.ts.map +1 -0
  139. package/dist/esm/core/validation/index.js +2 -0
  140. package/dist/esm/core/validation/index.js.map +1 -0
  141. package/dist/esm/core/validation/validate.d.ts +18 -0
  142. package/dist/esm/core/validation/validate.d.ts.map +1 -0
  143. package/dist/esm/core/validation/validate.js +62 -0
  144. package/dist/esm/core/validation/validate.js.map +1 -0
  145. package/dist/esm/core/xml/generator.d.ts +140 -0
  146. package/dist/esm/core/xml/generator.d.ts.map +1 -0
  147. package/dist/esm/core/xml/generator.js +259 -0
  148. package/dist/esm/core/xml/generator.js.map +1 -0
  149. package/dist/esm/core/xml/index.d.ts +2 -0
  150. package/dist/esm/core/xml/index.d.ts.map +1 -0
  151. package/dist/esm/core/xml/index.js.map +1 -0
  152. package/dist/esm/index.d.ts +4 -0
  153. package/dist/esm/index.d.ts.map +1 -0
  154. package/dist/esm/index.js.map +1 -0
  155. package/dist/esm/integration/accountability.d.ts +41 -0
  156. package/dist/esm/integration/accountability.d.ts.map +1 -0
  157. package/dist/esm/integration/accountability.js +88 -0
  158. package/dist/esm/integration/accountability.js.map +1 -0
  159. package/dist/esm/integration/index.d.ts +5 -0
  160. package/dist/esm/integration/index.d.ts.map +1 -0
  161. package/dist/esm/integration/index.js +5 -0
  162. package/dist/esm/integration/index.js.map +1 -0
  163. package/dist/esm/integration/p12.d.ts +58 -0
  164. package/dist/esm/integration/p12.d.ts.map +1 -0
  165. package/dist/esm/integration/p12.js +119 -0
  166. package/dist/esm/integration/p12.js.map +1 -0
  167. package/dist/esm/integration/repository.d.ts +79 -0
  168. package/dist/esm/integration/repository.d.ts.map +1 -0
  169. package/dist/esm/integration/repository.js.map +1 -0
  170. package/dist/esm/integration/service.d.ts +45 -0
  171. package/dist/esm/integration/service.d.ts.map +1 -0
  172. package/dist/{integration → esm/integration}/service.js +101 -15
  173. package/dist/esm/integration/service.js.map +1 -0
  174. package/dist/esm/integration/tenant-credentials.d.ts +44 -0
  175. package/dist/esm/integration/tenant-credentials.d.ts.map +1 -0
  176. package/dist/esm/integration/tenant-credentials.js +58 -0
  177. package/dist/esm/integration/tenant-credentials.js.map +1 -0
  178. package/package.json +45 -13
  179. package/readme.md +566 -42
  180. package/src/aeat/client.ts +296 -55
  181. package/src/aeat/index.ts +1 -0
  182. package/src/aeat/response.ts +195 -0
  183. package/src/core/hash/chain.ts +68 -0
  184. package/src/core/index.ts +2 -0
  185. package/src/core/models/invoice.ts +40 -18
  186. package/src/core/utils/date.ts +27 -0
  187. package/src/core/utils/index.ts +1 -0
  188. package/src/core/validation/index.ts +1 -0
  189. package/src/core/validation/validate.ts +71 -0
  190. package/src/core/xml/generator.ts +306 -73
  191. package/src/integration/accountability.ts +125 -0
  192. package/src/integration/index.ts +2 -0
  193. package/src/integration/p12.ts +155 -0
  194. package/src/integration/repository.ts +2 -0
  195. package/src/integration/service.ts +113 -14
  196. package/dist/aeat/client.d.ts +0 -57
  197. package/dist/aeat/client.d.ts.map +0 -1
  198. package/dist/aeat/client.js +0 -87
  199. package/dist/aeat/client.js.map +0 -1
  200. package/dist/aeat/index.d.ts +0 -2
  201. package/dist/aeat/index.d.ts.map +0 -1
  202. package/dist/aeat/index.js +0 -2
  203. package/dist/aeat/index.js.map +0 -1
  204. package/dist/core/hash/chain.d.ts.map +0 -1
  205. package/dist/core/hash/chain.js.map +0 -1
  206. package/dist/core/hash/index.d.ts.map +0 -1
  207. package/dist/core/hash/index.js.map +0 -1
  208. package/dist/core/index.d.ts.map +0 -1
  209. package/dist/core/index.js.map +0 -1
  210. package/dist/core/models/event.d.ts.map +0 -1
  211. package/dist/core/models/index.d.ts.map +0 -1
  212. package/dist/core/models/index.js.map +0 -1
  213. package/dist/core/models/invoice.d.ts.map +0 -1
  214. package/dist/core/qr/index.d.ts.map +0 -1
  215. package/dist/core/qr/index.js.map +0 -1
  216. package/dist/core/qr/qr.d.ts.map +0 -1
  217. package/dist/core/qr/qr.js.map +0 -1
  218. package/dist/core/xml/generator.d.ts +0 -32
  219. package/dist/core/xml/generator.d.ts.map +0 -1
  220. package/dist/core/xml/generator.js +0 -103
  221. package/dist/core/xml/generator.js.map +0 -1
  222. package/dist/core/xml/index.d.ts.map +0 -1
  223. package/dist/core/xml/index.js.map +0 -1
  224. package/dist/index.d.ts.map +0 -1
  225. package/dist/index.js.map +0 -1
  226. package/dist/integration/index.d.ts +0 -3
  227. package/dist/integration/index.d.ts.map +0 -1
  228. package/dist/integration/index.js +0 -3
  229. package/dist/integration/index.js.map +0 -1
  230. package/dist/integration/repository.d.ts.map +0 -1
  231. package/dist/integration/repository.js.map +0 -1
  232. package/dist/integration/service.d.ts.map +0 -1
  233. package/dist/integration/service.js.map +0 -1
  234. /package/dist/{core → cjs/core}/hash/index.d.ts +0 -0
  235. /package/dist/{core → cjs/core}/models/event.d.ts +0 -0
  236. /package/dist/{core → cjs/core}/models/index.d.ts +0 -0
  237. /package/dist/{core → cjs/core}/qr/index.d.ts +0 -0
  238. /package/dist/{core → cjs/core}/qr/qr.d.ts +0 -0
  239. /package/dist/{core → cjs/core}/xml/index.d.ts +0 -0
  240. /package/dist/{index.d.ts → cjs/index.d.ts} +0 -0
  241. /package/dist/{core → esm/core}/hash/index.js +0 -0
  242. /package/dist/{core → esm/core}/models/event.js +0 -0
  243. /package/dist/{core → esm/core}/models/index.js +0 -0
  244. /package/dist/{core → esm/core}/models/invoice.js +0 -0
  245. /package/dist/{core → esm/core}/qr/index.js +0 -0
  246. /package/dist/{core → esm/core}/qr/qr.js +0 -0
  247. /package/dist/{core → esm/core}/xml/index.js +0 -0
  248. /package/dist/{index.js → esm/index.js} +0 -0
  249. /package/dist/{integration → esm/integration}/repository.js +0 -0
@@ -1,3 +1,72 @@
1
+ import https from 'node:https';
2
+ import { buildRemisionEnvelope, AEAT_INFORMACION_NAMESPACE } from '../core/xml/generator.js';
3
+ import { parseSoapResponse, type SoapResponse, type RegistroResponse, type ConsultaResponse, type EventoResponse } from './response.js';
4
+
5
+ /**
6
+ * URL base del servicio AEAT en PRODUCCIÓN (Verifactu v1.0).
7
+ * Fuente: Veri-Factu_Descripcion_SWeb.pdf (Anexo I, entorno de producción).
8
+ */
9
+ export const AEAT_PRODUCTION_BASE_URL = 'https://www2.agenciatributaria.gob.es';
10
+
11
+ /**
12
+ * URL base del servicio AEAT en PRUEBAS/desarrollo (Verifactu v1.0).
13
+ * Fuente: Veri-Factu_Descripcion_SWeb.pdf (Anexo I, entorno de pruebas).
14
+ */
15
+ export const AEAT_TEST_BASE_URL = 'https://prewww2.aeat.es';
16
+
17
+ /**
18
+ * Ruta del WSDL del servicio SistemaFacturacion (Verifactu v1.0).
19
+ */
20
+ export const AEAT_WSDL_PATH =
21
+ '/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl';
22
+
23
+ /**
24
+ * Endpoint SOAP de remisión voluntaria (VerifactuSOAP).
25
+ * - Producción: `https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP`
26
+ * - Pruebas: `https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP`
27
+ */
28
+ export const AEAT_PRODUCTION_SOAP_URL =
29
+ 'https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
30
+ export const AEAT_TEST_SOAP_URL =
31
+ 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
32
+
33
+ /**
34
+ * Endpoint SOAP de remisión bajo requerimiento (RequerimientoSOAP).
35
+ */
36
+ export const AEAT_PRODUCTION_REQUERIMIENTO_URL =
37
+ 'https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP';
38
+ export const AEAT_TEST_REQUERIMIENTO_URL =
39
+ 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP';
40
+
41
+ /**
42
+ * Operación SOAP de alta/anulación de registros.
43
+ */
44
+ export const AEAT_OPERATION_REGISTRO = 'RegFactuSistemaFacturacion';
45
+
46
+ /**
47
+ * Operación SOAP de consulta.
48
+ */
49
+ export const AEAT_OPERATION_CONSULTA = 'ConsultaFactuSistemaFacturacion';
50
+
51
+ /**
52
+ * Namespace del servicio (targetNamespace del WSDL).
53
+ */
54
+ export const AEAT_SOAP_NAMESPACE =
55
+ 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.wsdl';
56
+
57
+ /**
58
+ * Namespace del elemento de consulta (`ConsultaLR.xsd`).
59
+ */
60
+ export const AEAT_CONSULTA_NAMESPACE =
61
+ 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd';
62
+
63
+ /**
64
+ * Entorno de ejecución del cliente AEAT.
65
+ * - `production`: usa la URL de producción.
66
+ * - `test` / `development`: usa la URL de pruebas (si se aporta).
67
+ */
68
+ export type VerifactuEnv = 'production' | 'test' | 'development';
69
+
1
70
  /**
2
71
  * Cliente para los servicios web de AEAT (Verifactu).
3
72
  *
@@ -5,24 +74,54 @@
5
74
  * (producción vs. pruebas), por lo que no debe acoplarse a la lógica de
6
75
  * negocio.
7
76
  *
8
- * NOTA: los endpoints y el formato de petición/respuesta deben ajustarse a
9
- * la documentación oficial de la AEAT (remisión voluntaria / bajo
10
- * requerimiento). Este cliente expone una implementación base con `fetch`
11
- * y rutas configurables.
77
+ * Soporta autenticación por certificado digital (mTLS): se pasan `cert` y
78
+ * `key` (PEM) extraídos de un `.p12` (ver `readP12` en `integration/p12.ts`).
12
79
  */
13
80
  export interface VerifactuClientConfig {
14
- /** URL base del servicio AEAT. */
15
- baseUrl: string;
16
- /** Token de autenticación (OAuth) o certificado. */
81
+ /** URL del endpoint SOAP del servicio AEAT en PRODUCCIÓN. */
82
+ baseUrl?: string;
83
+ /** URL del endpoint SOAP del servicio AEAT en PRUEBAS/desarrollo. */
84
+ testBaseUrl?: string;
85
+ /** Entorno de ejecución. Si no es `production`, se usa `testBaseUrl`. */
86
+ env?: VerifactuEnv;
87
+ /** Token de autenticación (OAuth) alternativo al certificado. */
17
88
  authToken?: string;
18
- /** Ruta del endpoint de envío de registros. */
19
- sendPath?: string;
20
- /** Ruta del endpoint de consulta de estado. */
21
- statusPath?: string;
89
+ /** Certificado de cliente en PEM (mTLS). */
90
+ cert?: string;
91
+ /** Clave privada del cliente en PEM (mTLS). */
92
+ key?: string;
93
+ /** Operación SOAP (por defecto `RegFactuSistemaFacturacion`). */
94
+ operation?: string;
95
+ /** Namespace del servicio SOAP. */
96
+ namespace?: string;
22
97
  /** Timeout de petición en ms. */
23
98
  timeoutMs?: number;
24
99
  }
25
100
 
101
+ /**
102
+ * Resuelve la URL base efectiva según el entorno.
103
+ *
104
+ * Regla: **por defecto se usa producción** (la app real factura en prod).
105
+ * - Si `env` es `production` (o no se indica), usa `baseUrl`.
106
+ * - Si `env` es `test`/`development`, usa `testBaseUrl`.
107
+ *
108
+ * Los tests se ejecutan aparte y fijan `env: 'test'` + `testBaseUrl` para
109
+ * usar desarrollo (sin necesidad de indicar la URL de producción).
110
+ */
111
+ export function resolveBaseUrl(config: Pick<VerifactuClientConfig, 'baseUrl' | 'testBaseUrl' | 'env'>): string {
112
+ const env = config.env ?? 'production';
113
+ if (env !== 'production') {
114
+ if (config.testBaseUrl) {
115
+ return config.testBaseUrl;
116
+ }
117
+ throw new Error('env no es "production" y no se ha proporcionado testBaseUrl.');
118
+ }
119
+ if (!config.baseUrl) {
120
+ throw new Error('env es "production" y no se ha proporcionado baseUrl.');
121
+ }
122
+ return config.baseUrl;
123
+ }
124
+
26
125
  export interface SendResult {
27
126
  /** Identificador de la operación devuelto por AEAT. */
28
127
  operationId: string;
@@ -34,13 +133,25 @@ export interface SendResult {
34
133
  errorCode?: string;
35
134
  /** Cuerpo completo de la respuesta (para auditoría). */
36
135
  rawResponse?: string;
136
+ /** Respuesta completa parseada de AEAT (todo lo recibido, OK o error). */
137
+ data?: Record<string, unknown>;
138
+ /** Respuesta SOAP normalizada a JSON (XML → JSON). */
139
+ response?: SoapResponse;
140
+ /** Respuesta tipada de registro (si la operación es de registro). */
141
+ registro?: RegistroResponse;
142
+ /** Respuesta tipada de consulta (si la operación es de consulta). */
143
+ consulta?: ConsultaResponse;
144
+ /** Respuesta tipada de evento (si la operación es de evento). */
145
+ evento?: EventoResponse;
37
146
  }
38
147
 
39
148
  interface ResolvedConfig {
40
149
  baseUrl: string;
41
150
  authToken?: string;
42
- sendPath: string;
43
- statusPath: string;
151
+ cert?: string;
152
+ key?: string;
153
+ operation: string;
154
+ namespace: string;
44
155
  timeoutMs: number;
45
156
  }
46
157
 
@@ -49,89 +160,219 @@ interface ResolvedConfig {
49
160
  */
50
161
  export class VerifactuClient {
51
162
  private readonly config: ResolvedConfig;
163
+ private readonly agent: https.Agent;
52
164
 
53
165
  constructor(config: VerifactuClientConfig) {
54
166
  this.config = {
55
- sendPath: '/verifactu/registro',
56
- statusPath: '/verifactu/estado',
167
+ operation: AEAT_OPERATION_REGISTRO,
168
+ namespace: AEAT_SOAP_NAMESPACE,
57
169
  timeoutMs: 10_000,
58
170
  ...config,
171
+ baseUrl: resolveBaseUrl(config),
59
172
  };
173
+
174
+ // Si hay certificado y clave, se usan en el handshake TLS (mTLS).
175
+ this.agent = new https.Agent(
176
+ this.config.cert && this.config.key
177
+ ? { cert: this.config.cert, key: this.config.key }
178
+ : undefined,
179
+ );
60
180
  }
61
181
 
62
182
  /**
63
- * Envía un registro (factura o evento) a AEAT.
183
+ * Envía un registro (factura o evento) a AEAT mediante SOAP.
64
184
  */
65
185
  async sendRecord(xml: string): Promise<SendResult> {
66
- const url = `${this.config.baseUrl}${this.config.sendPath}`;
186
+ const envelope = buildRemisionEnvelope(xml);
187
+ const url = new URL(this.config.baseUrl);
67
188
  const headers: Record<string, string> = {
68
- 'Content-Type': 'application/xml',
69
- Accept: 'application/json',
189
+ 'Content-Type': 'text/xml; charset=utf-8',
190
+ SOAPAction: '',
191
+ Accept: 'application/xml',
70
192
  };
71
193
  if (this.config.authToken) {
72
194
  headers.Authorization = `Bearer ${this.config.authToken}`;
73
195
  }
74
196
 
75
- const controller = new AbortController();
76
- const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
77
- try {
78
- const res = await fetch(url, {
79
- method: 'POST',
80
- headers,
81
- body: xml,
82
- signal: controller.signal,
83
- });
84
- const raw = await res.text();
85
- return this.parseResponse(raw, res.status);
86
- } finally {
87
- clearTimeout(timer);
88
- }
197
+ return this.request(url, 'POST', headers, envelope);
89
198
  }
90
199
 
91
200
  /**
92
- * Consulta el estado de una operación enviada.
201
+ * @deprecated La consulta por ID de operación no está soportada por el
202
+ * esquema `ConsultaLR.xsd` de AEAT (devuelve error 4118). Usa
203
+ * {@link queryInvoice} para consultar registros por número de factura.
93
204
  */
94
205
  async getOperationStatus(operationId: string): Promise<SendResult> {
95
- const url = `${this.config.baseUrl}${this.config.statusPath}/${encodeURIComponent(operationId)}`;
96
- const headers: Record<string, string> = { Accept: 'application/json' };
206
+ const consultaXml = `<ConsultaFactuSistemaFacturacion xmlns="${this.config.namespace}"><ID>${operationId}</ID></ConsultaFactuSistemaFacturacion>`;
207
+ const envelope = buildRemisionEnvelope(consultaXml);
208
+ const url = new URL(this.config.baseUrl);
209
+ const headers: Record<string, string> = {
210
+ 'Content-Type': 'text/xml; charset=utf-8',
211
+ SOAPAction: '',
212
+ Accept: 'application/xml',
213
+ };
97
214
  if (this.config.authToken) {
98
215
  headers.Authorization = `Bearer ${this.config.authToken}`;
99
216
  }
100
217
 
101
- const controller = new AbortController();
102
- const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
103
- try {
104
- const res = await fetch(url, { method: 'GET', headers, signal: controller.signal });
105
- const raw = await res.text();
106
- return this.parseResponse(raw, res.status);
107
- } finally {
108
- clearTimeout(timer);
218
+ return this.request(url, 'POST', headers, envelope);
219
+ }
220
+
221
+ /**
222
+ * Consulta un registro de facturación por número de factura y devuelve la
223
+ * respuesta completa (incluida la huella confirmada por AEAT).
224
+ *
225
+ * Usa la operación `ConsultaFactuSistemaFacturacion` (mismo endpoint SOAP).
226
+ */
227
+ async queryInvoice(params: {
228
+ /** Obligado a la emisión (quien consulta). */
229
+ obligado: { nombreRazon: string; nif: string };
230
+ /** Nº serie + nº factura a consultar (opcional: consulta por periodo). */
231
+ numSerieFactura?: string;
232
+ /** Ejercicio (año) del periodo de imputación, p. ej. `2026`. */
233
+ ejercicio: string;
234
+ /** Periodo de imputación, p. ej. `01` (enero) o `12` (diciembre). */
235
+ periodo: string;
236
+ }): Promise<SendResult> {
237
+ const consultaXml = [
238
+ `<sfLRC:ConsultaFactuSistemaFacturacion xmlns:sfLRC="${AEAT_CONSULTA_NAMESPACE}" xmlns:sf="${AEAT_INFORMACION_NAMESPACE}">`,
239
+ ' <sfLRC:Cabecera>',
240
+ ' <sf:IDVersion>1.0</sf:IDVersion>',
241
+ ' <sf:ObligadoEmision>',
242
+ ` <sf:NombreRazon>${params.obligado.nombreRazon}</sf:NombreRazon>`,
243
+ ` <sf:NIF>${params.obligado.nif}</sf:NIF>`,
244
+ ' </sf:ObligadoEmision>',
245
+ ' </sfLRC:Cabecera>',
246
+ ' <sfLRC:FiltroConsulta>',
247
+ ' <sfLRC:PeriodoImputacion>',
248
+ ` <sf:Ejercicio>${params.ejercicio}</sf:Ejercicio>`,
249
+ ` <sf:Periodo>${params.periodo}</sf:Periodo>`,
250
+ ' </sfLRC:PeriodoImputacion>',
251
+ ...(params.numSerieFactura
252
+ ? [` <sfLRC:NumSerieFactura>${params.numSerieFactura}</sfLRC:NumSerieFactura>`]
253
+ : []),
254
+ ' </sfLRC:FiltroConsulta>',
255
+ '</sfLRC:ConsultaFactuSistemaFacturacion>',
256
+ ].join('\n');
257
+ const envelope = buildRemisionEnvelope(consultaXml);
258
+ const url = new URL(this.config.baseUrl);
259
+ const headers: Record<string, string> = {
260
+ 'Content-Type': 'text/xml; charset=utf-8',
261
+ SOAPAction: '',
262
+ Accept: 'application/xml',
263
+ };
264
+ if (this.config.authToken) {
265
+ headers.Authorization = `Bearer ${this.config.authToken}`;
109
266
  }
267
+
268
+ return this.request(url, 'POST', headers, envelope);
269
+ }
270
+
271
+ /**
272
+ * Realiza la petición HTTPS usando el agente (con mTLS si hay certificado).
273
+ */
274
+ private request(
275
+ url: URL,
276
+ method: 'GET' | 'POST',
277
+ headers: Record<string, string>,
278
+ body?: string,
279
+ ): Promise<SendResult> {
280
+ return new Promise((resolve, reject) => {
281
+ const req = https.request(
282
+ {
283
+ hostname: url.hostname,
284
+ port: url.port || 443,
285
+ path: `${url.pathname}${url.search}`,
286
+ method,
287
+ headers,
288
+ agent: this.agent,
289
+ },
290
+ (res) => {
291
+ let raw = '';
292
+ res.setEncoding('utf8');
293
+ res.on('data', (chunk) => {
294
+ raw += chunk;
295
+ });
296
+ res.on('end', () => {
297
+ resolve(this.parseResponse(raw, res.statusCode ?? 0));
298
+ });
299
+ },
300
+ );
301
+
302
+ req.on('error', reject);
303
+ req.setTimeout(this.config.timeoutMs, () => {
304
+ req.destroy(new Error(`Timeout tras ${this.config.timeoutMs}ms`));
305
+ });
306
+
307
+ if (body) {
308
+ req.write(body);
309
+ }
310
+ req.end();
311
+ });
110
312
  }
111
313
 
112
314
  /**
113
315
  * Parsea la respuesta de AEAT. La estructura exacta depende del servicio;
114
316
  * este es un mapeo base que el consumidor puede sobrescribir.
317
+ *
318
+ * Devuelve SIEMPRE todo lo recibido: la respuesta completa parseada en
319
+ * `data` y el cuerpo crudo en `rawResponse`, tanto si es OK como error.
115
320
  */
116
321
  protected parseResponse(raw: string, httpStatus: number): SendResult {
117
322
  let operationId = '';
118
323
  let status: 'OK' | 'ERROR' = httpStatus >= 200 && httpStatus < 300 ? 'OK' : 'ERROR';
119
324
  let message: string | undefined;
120
325
  let errorCode: string | undefined;
326
+ let data: Record<string, unknown> | undefined;
327
+ let response: SoapResponse | undefined;
121
328
 
122
- try {
123
- const json = JSON.parse(raw) as Record<string, unknown>;
124
- operationId = String(json.operationId ?? json.id ?? '');
125
- if (json.status === 'OK' || json.status === 'ERROR') {
126
- status = json.status;
329
+ // AEAT responde en SOAP/XML: se normaliza a JSON.
330
+ const soap = parseSoapResponse(raw);
331
+ if (soap) {
332
+ response = soap;
333
+ data = soap.body;
334
+ operationId = String(soap.body['ID'] ?? soap.body['IdPeticion'] ?? '');
335
+ // Estado según el registro/consulta/evento mapeado.
336
+ const reg = soap.registro;
337
+ const con = soap.consulta;
338
+ const ev = soap.evento;
339
+ const estado = reg?.estadoRegistro ?? con?.estado ?? ev?.estadoEvento;
340
+ if (estado) {
341
+ status = /OK|ACEPTAD|CONFIRMAD|CORRECT/i.test(estado) ? 'OK' : 'ERROR';
342
+ }
343
+ errorCode = reg?.codigoErrorRegistro ?? con?.codigoError ?? ev?.codigoErrorEvento;
344
+ message =
345
+ reg?.descripcionErrorRegistro ??
346
+ con?.descripcionError ??
347
+ ev?.descripcionErrorEvento ??
348
+ estado;
349
+ } else {
350
+ // Respuesta no XML/JSON: se conserva el cuerpo como mensaje.
351
+ try {
352
+ const json = JSON.parse(raw) as Record<string, unknown>;
353
+ data = json;
354
+ operationId = String(json.operationId ?? json.id ?? '');
355
+ if (json.status === 'OK' || json.status === 'ERROR') {
356
+ status = json.status;
357
+ }
358
+ message = json.message as string | undefined;
359
+ errorCode = json.errorCode as string | undefined;
360
+ } catch {
361
+ message = raw;
127
362
  }
128
- message = json.message as string | undefined;
129
- errorCode = json.errorCode as string | undefined;
130
- } catch {
131
- // Respuesta no JSON: se conserva el cuerpo como mensaje.
132
- message = raw;
133
363
  }
134
364
 
135
- return { operationId, status, message, errorCode, rawResponse: raw };
365
+ return {
366
+ operationId,
367
+ status,
368
+ message,
369
+ errorCode,
370
+ rawResponse: raw,
371
+ data,
372
+ response,
373
+ registro: response?.registro,
374
+ consulta: response?.consulta,
375
+ evento: response?.evento,
376
+ };
136
377
  }
137
378
  }
package/src/aeat/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './client.js';
2
+ export * from './response.js';
@@ -0,0 +1,195 @@
1
+ import { XMLParser } from 'fast-xml-parser';
2
+
3
+ /**
4
+ * Respuesta de AEAT para un registro de facturación (alta/anulación).
5
+ *
6
+ * AEAT devuelve un SOAP XML; este tipo es el resultado normalizado a JSON
7
+ * para que el sistema consumidor lo persista y lo maneje con facilidad.
8
+ */
9
+ export interface RegistroResponse {
10
+ /** Estado del registro devuelto por AEAT. */
11
+ estadoRegistro?: string;
12
+ /** Código de error del registro (si lo hay). */
13
+ codigoErrorRegistro?: string;
14
+ /** Descripción del error del registro (si lo hay). */
15
+ descripcionErrorRegistro?: string;
16
+ /** CSV (Código Seguro de Verificación) si AEAT lo devuelve. */
17
+ csv?: string;
18
+ /** Huella (hash) del registro confirmado por AEAT. */
19
+ huella?: string;
20
+ /** Fecha/hora de generación del registro en AEAT. */
21
+ fechaHoraHusoGenRegistro?: string;
22
+ /** Identificador de la factura (si AEAT lo devuelve). */
23
+ idFactura?: string;
24
+ }
25
+
26
+ /**
27
+ * Respuesta de AEAT para una consulta de estado de operación.
28
+ */
29
+ export interface ConsultaResponse {
30
+ /** Estado de la operación consultada. */
31
+ estado?: string;
32
+ /** Código de error (si lo hay). */
33
+ codigoError?: string;
34
+ /** Descripción del error (si lo hay). */
35
+ descripcionError?: string;
36
+ /** CSV de la operación consultada. */
37
+ csv?: string;
38
+ /** Huella (hash) de la operación consultada. */
39
+ huella?: string;
40
+ /** Registros devueltos por la consulta. */
41
+ registros?: RegistroResponse[];
42
+ }
43
+
44
+ /**
45
+ * Respuesta de AEAT para un registro de evento.
46
+ */
47
+ export interface EventoResponse {
48
+ /** Estado del evento devuelto por AEAT. */
49
+ estadoEvento?: string;
50
+ /** Código de error del evento (si lo hay). */
51
+ codigoErrorEvento?: string;
52
+ /** Descripción del error del evento (si lo hay). */
53
+ descripcionErrorEvento?: string;
54
+ /** CSV del evento si AEAT lo devuelve. */
55
+ csv?: string;
56
+ /** Huella (hash) del evento confirmado por AEAT. */
57
+ huella?: string;
58
+ }
59
+
60
+ /**
61
+ * Resultado normalizado de una respuesta SOAP de AEAT.
62
+ */
63
+ export interface SoapResponse {
64
+ /** Nombre del elemento raíz del body SOAP (p. ej. `RespuestaRegFactuSistemaFacturacion`). */
65
+ operation: string;
66
+ /** Objeto JSON completo parseado del body SOAP. */
67
+ body: Record<string, unknown>;
68
+ /** Respuesta tipada de registro (si la operación es de registro). */
69
+ registro?: RegistroResponse;
70
+ /** Respuesta tipada de consulta (si la operación es de consulta). */
71
+ consulta?: ConsultaResponse;
72
+ /** Respuesta tipada de evento (si la operación es de evento). */
73
+ evento?: EventoResponse;
74
+ }
75
+
76
+ const parser = new XMLParser({
77
+ ignoreAttributes: false,
78
+ attributeNamePrefix: '@_',
79
+ removeNSPrefix: true,
80
+ parseTagValue: false,
81
+ trimValues: true,
82
+ });
83
+
84
+ /**
85
+ * Convierte un XML SOAP de AEAT a un objeto JSON normalizado.
86
+ *
87
+ * Extrae el body del sobre SOAP y mapea los campos a las interfaces tipadas
88
+ * (`RegistroResponse`, `ConsultaResponse`, `EventoResponse`). Si el XML no
89
+ * es parseable, devuelve `null`.
90
+ */
91
+ export function parseSoapResponse(xml: string): SoapResponse | null {
92
+ // Solo procesar si parece XML (evita parsear JSON u otros textos).
93
+ if (typeof xml !== 'string' || !xml.trim().startsWith('<')) {
94
+ return null;
95
+ }
96
+
97
+ let parsed: Record<string, unknown>;
98
+ try {
99
+ parsed = parser.parse(xml) as Record<string, unknown>;
100
+ } catch {
101
+ return null;
102
+ }
103
+
104
+ // Navegar hasta el body del sobre SOAP (removeNSPrefix elimina el prefijo).
105
+ const envelope = (parsed['Envelope'] ?? parsed) as Record<string, unknown>;
106
+ const body = (envelope['Body'] ?? envelope) as Record<string, unknown>;
107
+
108
+ // El body contiene un único elemento de operación.
109
+ const keys = Object.keys(body).filter((k) => k !== 'Body');
110
+ const operation = keys[0] ?? '';
111
+ const opBody = body[operation] as Record<string, unknown> | undefined;
112
+
113
+ const result: SoapResponse = {
114
+ operation,
115
+ body: (opBody ?? body) as Record<string, unknown>,
116
+ };
117
+
118
+ // Mapear según el tipo de operación.
119
+ if (/RegFactu|Registro/i.test(operation)) {
120
+ result.registro = mapRegistro(opBody);
121
+ } else if (/Consulta/i.test(operation)) {
122
+ result.consulta = mapConsulta(opBody);
123
+ } else if (/Evento/i.test(operation)) {
124
+ result.evento = mapEvento(opBody);
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ function mapRegistro(opBody: Record<string, unknown> | undefined): RegistroResponse | undefined {
131
+ if (!opBody) return undefined;
132
+ const rf = (opBody['RegistroFactura'] ?? opBody) as Record<string, unknown>;
133
+ return {
134
+ estadoRegistro: str(rf['EstadoRegistro']),
135
+ codigoErrorRegistro: str(rf['CodigoErrorRegistro']),
136
+ descripcionErrorRegistro: str(rf['DescripcionErrorRegistro']),
137
+ csv: str(rf['CSV']),
138
+ huella: str(rf['Huella']),
139
+ fechaHoraHusoGenRegistro: str(rf['FechaHoraHusoGenRegistro']),
140
+ idFactura: str(rf['IDFactura']),
141
+ };
142
+ }
143
+
144
+ function mapConsulta(opBody: Record<string, unknown> | undefined): ConsultaResponse | undefined {
145
+ if (!opBody) return undefined;
146
+ // La consulta devuelve los registros en `RegistroRespuestaConsultaFactuSistemaFacturacion`
147
+ // (o `RegistroFactura`/`Registros` en otros formatos).
148
+ const registrosRaw =
149
+ opBody['RegistroRespuestaConsultaFactuSistemaFacturacion'] ??
150
+ opBody['RegistroFactura'] ??
151
+ opBody['Registros'];
152
+ const registros = Array.isArray(registrosRaw)
153
+ ? registrosRaw.map((r) => mapRegistroConsulta(r as Record<string, unknown>)).filter(Boolean)
154
+ : registrosRaw
155
+ ? [mapRegistroConsulta(registrosRaw as Record<string, unknown>)]
156
+ : undefined;
157
+ return {
158
+ estado: str(opBody['Estado']),
159
+ codigoError: str(opBody['CodigoError']),
160
+ descripcionError: str(opBody['DescripcionError']),
161
+ csv: str(opBody['CSV']),
162
+ huella: registros?.[0]?.huella,
163
+ registros: registros as RegistroResponse[] | undefined,
164
+ };
165
+ }
166
+
167
+ /** Mapea un registro de la respuesta de consulta (con `DatosRegistroFacturacion`). */
168
+ function mapRegistroConsulta(
169
+ raw: Record<string, unknown>,
170
+ ): RegistroResponse | undefined {
171
+ const datos = (raw['DatosRegistroFacturacion'] ?? raw) as Record<string, unknown>;
172
+ const estado = (raw['EstadoRegistro'] ?? {}) as Record<string, unknown>;
173
+ return {
174
+ estadoRegistro: str(estado['EstadoRegistro']),
175
+ huella: str(datos['Huella']),
176
+ fechaHoraHusoGenRegistro: str(datos['FechaHoraHusoGenRegistro']),
177
+ idFactura: str(raw['IDFactura']),
178
+ };
179
+ }
180
+
181
+ function mapEvento(opBody: Record<string, unknown> | undefined): EventoResponse | undefined {
182
+ if (!opBody) return undefined;
183
+ return {
184
+ estadoEvento: str(opBody['EstadoEvento']),
185
+ codigoErrorEvento: str(opBody['CodigoErrorEvento']),
186
+ descripcionErrorEvento: str(opBody['DescripcionErrorEvento']),
187
+ csv: str(opBody['CSV']),
188
+ huella: str(opBody['Huella']),
189
+ };
190
+ }
191
+
192
+ function str(value: unknown): string | undefined {
193
+ if (value === undefined || value === null) return undefined;
194
+ return String(value);
195
+ }