@dwast/verifactu-lib 0.1.0-beta.0

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 (83) hide show
  1. package/dist/aeat/client.d.ts +57 -0
  2. package/dist/aeat/client.d.ts.map +1 -0
  3. package/dist/aeat/client.js +87 -0
  4. package/dist/aeat/client.js.map +1 -0
  5. package/dist/aeat/index.d.ts +2 -0
  6. package/dist/aeat/index.d.ts.map +1 -0
  7. package/dist/aeat/index.js +2 -0
  8. package/dist/aeat/index.js.map +1 -0
  9. package/dist/core/hash/chain.d.ts +97 -0
  10. package/dist/core/hash/chain.d.ts.map +1 -0
  11. package/dist/core/hash/chain.js +115 -0
  12. package/dist/core/hash/chain.js.map +1 -0
  13. package/dist/core/hash/index.d.ts +2 -0
  14. package/dist/core/hash/index.d.ts.map +1 -0
  15. package/dist/core/hash/index.js +2 -0
  16. package/dist/core/hash/index.js.map +1 -0
  17. package/dist/core/index.d.ts +5 -0
  18. package/dist/core/index.d.ts.map +1 -0
  19. package/dist/core/index.js +5 -0
  20. package/dist/core/index.js.map +1 -0
  21. package/dist/core/models/event.d.ts +25 -0
  22. package/dist/core/models/event.d.ts.map +1 -0
  23. package/dist/core/models/event.js +2 -0
  24. package/dist/core/models/event.js.map +1 -0
  25. package/dist/core/models/index.d.ts +3 -0
  26. package/dist/core/models/index.d.ts.map +1 -0
  27. package/dist/core/models/index.js +3 -0
  28. package/dist/core/models/index.js.map +1 -0
  29. package/dist/core/models/invoice.d.ts +90 -0
  30. package/dist/core/models/invoice.d.ts.map +1 -0
  31. package/dist/core/models/invoice.js +2 -0
  32. package/dist/core/models/invoice.js.map +1 -0
  33. package/dist/core/qr/index.d.ts +2 -0
  34. package/dist/core/qr/index.d.ts.map +1 -0
  35. package/dist/core/qr/index.js +2 -0
  36. package/dist/core/qr/index.js.map +1 -0
  37. package/dist/core/qr/qr.d.ts +13 -0
  38. package/dist/core/qr/qr.d.ts.map +1 -0
  39. package/dist/core/qr/qr.js +16 -0
  40. package/dist/core/qr/qr.js.map +1 -0
  41. package/dist/core/xml/generator.d.ts +32 -0
  42. package/dist/core/xml/generator.d.ts.map +1 -0
  43. package/dist/core/xml/generator.js +103 -0
  44. package/dist/core/xml/generator.js.map +1 -0
  45. package/dist/core/xml/index.d.ts +2 -0
  46. package/dist/core/xml/index.d.ts.map +1 -0
  47. package/dist/core/xml/index.js +2 -0
  48. package/dist/core/xml/index.js.map +1 -0
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +4 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/integration/index.d.ts +3 -0
  54. package/dist/integration/index.d.ts.map +1 -0
  55. package/dist/integration/index.js +3 -0
  56. package/dist/integration/index.js.map +1 -0
  57. package/dist/integration/repository.d.ts +77 -0
  58. package/dist/integration/repository.d.ts.map +1 -0
  59. package/dist/integration/repository.js +2 -0
  60. package/dist/integration/repository.js.map +1 -0
  61. package/dist/integration/service.d.ts +36 -0
  62. package/dist/integration/service.d.ts.map +1 -0
  63. package/dist/integration/service.js +140 -0
  64. package/dist/integration/service.js.map +1 -0
  65. package/package.json +58 -0
  66. package/readme.md +89 -0
  67. package/src/aeat/client.ts +137 -0
  68. package/src/aeat/index.ts +1 -0
  69. package/src/core/hash/chain.ts +169 -0
  70. package/src/core/hash/index.ts +1 -0
  71. package/src/core/index.ts +4 -0
  72. package/src/core/models/event.ts +25 -0
  73. package/src/core/models/index.ts +2 -0
  74. package/src/core/models/invoice.ts +105 -0
  75. package/src/core/qr/index.ts +1 -0
  76. package/src/core/qr/qr.ts +19 -0
  77. package/src/core/xml/generator.ts +133 -0
  78. package/src/core/xml/index.ts +1 -0
  79. package/src/index.ts +3 -0
  80. package/src/integration/index.ts +2 -0
  81. package/src/integration/repository.ts +86 -0
  82. package/src/integration/schema.sql +51 -0
  83. package/src/integration/service.ts +158 -0
@@ -0,0 +1,86 @@
1
+ import type { Invoice } from '../core/models/invoice.js';
2
+ import type { Event } from '../core/models/event.js';
3
+
4
+ /**
5
+ * Estado de un registro de facturación en el sistema consumidor.
6
+ */
7
+ export type RecordStatus =
8
+ | 'PENDING' // generado pero no enviado a AEAT
9
+ | 'SENT' // enviado a AEAT, pendiente de confirmación
10
+ | 'CONFIRMED' // confirmado por AEAT
11
+ | 'REJECTED' // rechazado por AEAT
12
+ | 'ANNULLED'; // anulado
13
+
14
+ /**
15
+ * Registro de facturación persistido en el sistema consumidor.
16
+ */
17
+ export interface StoredRecord {
18
+ /** Identificador interno del registro. */
19
+ id: string;
20
+ /** Referencia a la factura (serie + número). */
21
+ invoiceId: string;
22
+ /** XML Verifactu generado. */
23
+ xml: string;
24
+ /** Hash del propio registro (huella). */
25
+ hash: string;
26
+ /** Hash del registro anterior en la cadena. */
27
+ previousHash: string | null;
28
+ /** Payload del QR Verifactu (URL de verificación con el hash). */
29
+ qrPayload?: string;
30
+ /** Estado del registro. */
31
+ status: RecordStatus;
32
+ /** Fecha/hora de creación. */
33
+ createdAt: string;
34
+ /** Fecha/hora de última actualización. */
35
+ updatedAt: string;
36
+ }
37
+
38
+ /**
39
+ * Respuesta de AEAT persistida en el sistema consumidor.
40
+ *
41
+ * La AEAT exige conservar la respuesta de cada envío para poder acreditar
42
+ * la remisión. El sistema consumidor DEBE guardar estas respuestas en una
43
+ * tabla (ver `schema.sql`).
44
+ */
45
+ export interface StoredResponse {
46
+ /** Identificador interno de la respuesta. */
47
+ id: string;
48
+ /** Referencia al registro enviado. */
49
+ recordId: string;
50
+ /** Identificador de la operación devuelto por AEAT. */
51
+ operationId: string;
52
+ /** Estado devuelto por AEAT. */
53
+ status: 'OK' | 'ERROR';
54
+ /** Mensaje de AEAT. */
55
+ message?: string;
56
+ /** Código de error de AEAT (si lo hay). */
57
+ errorCode?: string;
58
+ /** Cuerpo completo de la respuesta (para auditoría). */
59
+ rawResponse?: string;
60
+ /** Fecha/hora de la respuesta. */
61
+ receivedAt: string;
62
+ }
63
+
64
+ /**
65
+ * Repositorio que el sistema consumidor DEBE implementar para persistir
66
+ * los registros y las respuestas de Verifactu.
67
+ *
68
+ * La librería no impone un ORM concreto (Mongoose, Prisma, etc.): define
69
+ * esta interfaz y el sistema consumidor la adapta a su stack MERN.
70
+ */
71
+ export interface VerifactuRepository {
72
+ /** Guarda un registro de facturación. */
73
+ saveRecord(record: StoredRecord): Promise<void>;
74
+ /** Actualiza el estado de un registro. */
75
+ updateRecordStatus(id: string, status: RecordStatus): Promise<void>;
76
+ /** Obtiene un registro por su id. */
77
+ getRecord(id: string): Promise<StoredRecord | null>;
78
+ /** Obtiene el último registro (para conocer el hash anterior). */
79
+ getLastRecord(): Promise<StoredRecord | null>;
80
+ /** Guarda la respuesta de AEAT. */
81
+ saveResponse(response: StoredResponse): Promise<void>;
82
+ /** Obtiene las respuestas de un registro. */
83
+ getResponses(recordId: string): Promise<StoredResponse[]>;
84
+ /** Guarda un evento. */
85
+ saveEvent(event: Event): Promise<void>;
86
+ }
@@ -0,0 +1,51 @@
1
+ -- ============================================================================
2
+ -- Esquema de persistencia para Verifactu
3
+ -- ----------------------------------------------------------------------------
4
+ -- El sistema consumidor DEBE disponer de estas tablas para cumplir con la
5
+ -- obligación de conservar los registros y las respuestas de AEAT.
6
+ --
7
+ -- Esquema relacional de referencia (PostgreSQL/MySQL). Si el sistema usa
8
+ -- MongoDB, adaptar a colecciones equivalentes (ver repository.ts).
9
+ -- ============================================================================
10
+
11
+ -- Registros de facturación (cada factura/evento generado en formato Verifactu)
12
+ CREATE TABLE IF NOT EXISTS verifactu_records (
13
+ id VARCHAR(64) PRIMARY KEY,
14
+ invoice_id VARCHAR(128) NOT NULL, -- serie + número de factura
15
+ xml TEXT NOT NULL, -- XML Verifactu generado
16
+ hash VARCHAR(64) NOT NULL, -- huella (SHA-256) del registro
17
+ previous_hash VARCHAR(64), -- hash del registro anterior
18
+ status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
19
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
21
+ );
22
+
23
+ -- Respuestas de AEAT (obligatorio conservarlas para acreditar la remisión)
24
+ CREATE TABLE IF NOT EXISTS verifactu_responses (
25
+ id VARCHAR(64) PRIMARY KEY,
26
+ record_id VARCHAR(64) NOT NULL REFERENCES verifactu_records(id),
27
+ operation_id VARCHAR(128) NOT NULL, -- id de operación devuelto por AEAT
28
+ status VARCHAR(8) NOT NULL, -- OK | ERROR
29
+ message TEXT,
30
+ error_code VARCHAR(16),
31
+ raw_response TEXT, -- cuerpo completo (auditoría)
32
+ received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
33
+ );
34
+
35
+ -- Eventos (alta, anulación, modificación, eventos del sistema)
36
+ CREATE TABLE IF NOT EXISTS verifactu_events (
37
+ id VARCHAR(64) PRIMARY KEY,
38
+ type VARCHAR(16) NOT NULL, -- ALTA | ANULACION | MODIFICACION
39
+ fecha_hora TIMESTAMP NOT NULL,
40
+ invoice_id VARCHAR(128),
41
+ motivo TEXT,
42
+ factura_anulada VARCHAR(128),
43
+ hash VARCHAR(64),
44
+ previous_hash VARCHAR(64),
45
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
46
+ );
47
+
48
+ -- Índices recomendados
49
+ CREATE INDEX IF NOT EXISTS idx_records_invoice ON verifactu_records (invoice_id);
50
+ CREATE INDEX IF NOT EXISTS idx_responses_record ON verifactu_responses (record_id);
51
+ CREATE INDEX IF NOT EXISTS idx_events_invoice ON verifactu_events (invoice_id);
@@ -0,0 +1,158 @@
1
+ import type { Invoice } from '../core/models/invoice.js';
2
+ import { generateInvoiceXml, generateAnulacionXml } from '../core/xml/generator.js';
3
+ import { computeRegistroAlta, computeRegistroAnulacion } from '../core/hash/chain.js';
4
+ import { buildQrPayload } from '../core/qr/qr.js';
5
+ import type { VerifactuClient } from '../aeat/client.js';
6
+ import type { VerifactuRepository, StoredRecord } from './repository.js';
7
+
8
+ /**
9
+ * Servicio de orquestación de Verifactu.
10
+ *
11
+ * Une el núcleo puro (generación XML + hash) con el cliente AEAT y la
12
+ * persistencia. El sistema consumidor DEBE proporcionar un repositorio que
13
+ * implemente `VerifactuRepository` (ver `repository.ts` y `schema.sql`).
14
+ */
15
+ export class VerifactuService {
16
+ constructor(
17
+ private readonly repository: VerifactuRepository,
18
+ private readonly client: VerifactuClient,
19
+ ) {}
20
+
21
+ /**
22
+ * Registra una factura: genera el XML, calcula el hash encadenado, guarda
23
+ * el registro y lo envía a AEAT, persistiendo la respuesta.
24
+ */
25
+ async registerInvoice(invoice: Invoice): Promise<StoredRecord> {
26
+ // 1. Obtener el hash del último registro para encadenar.
27
+ const last = await this.repository.getLastRecord();
28
+ const previousHash = last?.hash ?? null;
29
+
30
+ // 2. Generar XML y calcular la huella (hash) del registro.
31
+ const xml = generateInvoiceXml(invoice, previousHash);
32
+ const hash = computeRegistroAlta({
33
+ idEmisorFactura: invoice.emisor.nif,
34
+ numSerieFactura: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
35
+ fechaExpedicionFactura: invoice.id.fechaExpedicion,
36
+ tipoFactura: invoice.tipoFactura,
37
+ cuotaTotal: invoice.desglose.cuotaTotal.toFixed(2),
38
+ importeTotal: invoice.desglose.importeTotal.toFixed(2),
39
+ huellaAnterior: previousHash,
40
+ fechaHoraHusoGenRegistro: new Date().toISOString(),
41
+ });
42
+
43
+ // 3. Persistir el registro como PENDING.
44
+ const now = new Date().toISOString();
45
+ const record: StoredRecord = {
46
+ id: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
47
+ invoiceId: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
48
+ xml,
49
+ hash,
50
+ previousHash,
51
+ qrPayload: buildQrPayload(hash),
52
+ status: 'PENDING',
53
+ createdAt: now,
54
+ updatedAt: now,
55
+ };
56
+ await this.repository.saveRecord(record);
57
+
58
+ // 4. Enviar a AEAT y persistir la respuesta.
59
+ const result = await this.client.sendRecord(xml);
60
+ await this.repository.saveResponse({
61
+ id: `${record.id}-${Date.now()}`,
62
+ recordId: record.id,
63
+ operationId: result.operationId,
64
+ status: result.status,
65
+ message: result.message,
66
+ receivedAt: new Date().toISOString(),
67
+ });
68
+
69
+ // 5. Actualizar el estado del registro según la respuesta.
70
+ const status = result.status === 'OK' ? 'CONFIRMED' : 'REJECTED';
71
+ await this.repository.updateRecordStatus(record.id, status);
72
+
73
+ return { ...record, status };
74
+ }
75
+
76
+ /**
77
+ * Anula una factura previamente registrada.
78
+ *
79
+ * Genera un registro de anulación que referencia la factura anulada,
80
+ * lo envía a AEAT y marca la factura original como ANNULLED.
81
+ */
82
+ async annulInvoice(invoiceId: string): Promise<StoredRecord> {
83
+ // 1. Obtener la factura original (para su huella) y el último registro.
84
+ const original = await this.repository.getRecord(invoiceId);
85
+ if (!original) {
86
+ throw new Error(`No existe el registro a anular: ${invoiceId}`);
87
+ }
88
+ const last = await this.repository.getLastRecord();
89
+ const previousHash = last?.hash ?? null;
90
+
91
+ // 2. Generar XML de anulación y calcular la huella.
92
+ const xml = generateAnulacionXml(
93
+ {
94
+ idEmisorFacturaAnulada: original.invoiceId,
95
+ numSerieFacturaAnulada: original.invoiceId,
96
+ fechaExpedicionFacturaAnulada: new Date().toISOString().slice(0, 10),
97
+ huellaFacturaAnulada: original.hash,
98
+ },
99
+ previousHash,
100
+ );
101
+ const hash = computeRegistroAnulacion({
102
+ idEmisorFacturaAnulada: original.invoiceId,
103
+ numSerieFacturaAnulada: original.invoiceId,
104
+ fechaExpedicionFacturaAnulada: new Date().toISOString().slice(0, 10),
105
+ huellaAnterior: previousHash,
106
+ fechaHoraHusoGenRegistro: new Date().toISOString(),
107
+ });
108
+
109
+ // 3. Persistir el registro de anulación como PENDING.
110
+ const now = new Date().toISOString();
111
+ const record: StoredRecord = {
112
+ id: `${invoiceId}-ANUL`,
113
+ invoiceId,
114
+ xml,
115
+ hash,
116
+ previousHash,
117
+ status: 'PENDING',
118
+ createdAt: now,
119
+ updatedAt: now,
120
+ };
121
+ await this.repository.saveRecord(record);
122
+
123
+ // 4. Enviar a AEAT y persistir la respuesta.
124
+ const result = await this.client.sendRecord(xml);
125
+ await this.repository.saveResponse({
126
+ id: `${record.id}-${Date.now()}`,
127
+ recordId: record.id,
128
+ operationId: result.operationId,
129
+ status: result.status,
130
+ message: result.message,
131
+ receivedAt: new Date().toISOString(),
132
+ });
133
+
134
+ // 5. Actualizar estados.
135
+ const status = result.status === 'OK' ? 'CONFIRMED' : 'REJECTED';
136
+ await this.repository.updateRecordStatus(record.id, status);
137
+ if (result.status === 'OK') {
138
+ await this.repository.updateRecordStatus(invoiceId, 'ANNULLED');
139
+ }
140
+
141
+ return { ...record, status };
142
+ }
143
+
144
+ /**
145
+ * Modifica una factura previamente registrada.
146
+ *
147
+ * Verifactu no tiene un tipo "modificación": se implementa como anulación
148
+ * del registro original + alta con los datos corregidos. El usuario solo
149
+ * envía la referencia a la factura original y la factura corregida.
150
+ */
151
+ async modifyInvoice(invoiceId: string, correctedInvoice: Invoice): Promise<StoredRecord> {
152
+ // 1. Anular la factura original.
153
+ await this.annulInvoice(invoiceId);
154
+
155
+ // 2. Emitir la factura corregida como un nuevo alta.
156
+ return this.registerInvoice(correctedInvoice);
157
+ }
158
+ }