@dwast/verifactu-lib 0.1.0-beta.2 → 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 (52) hide show
  1. package/dist/cjs/aeat/client.d.ts +2 -6
  2. package/dist/cjs/aeat/client.d.ts.map +1 -1
  3. package/dist/cjs/aeat/client.js +19 -19
  4. package/dist/cjs/aeat/client.js.map +1 -1
  5. package/dist/cjs/aeat/response.d.ts +2 -0
  6. package/dist/cjs/aeat/response.d.ts.map +1 -1
  7. package/dist/cjs/aeat/response.js +1 -0
  8. package/dist/cjs/aeat/response.js.map +1 -1
  9. package/dist/cjs/core/models/index.d.ts +0 -1
  10. package/dist/cjs/core/models/index.d.ts.map +1 -1
  11. package/dist/cjs/core/models/index.js +0 -1
  12. package/dist/cjs/core/models/index.js.map +1 -1
  13. package/dist/cjs/core/xml/generator.d.ts +13 -15
  14. package/dist/cjs/core/xml/generator.d.ts.map +1 -1
  15. package/dist/cjs/core/xml/generator.js +16 -3
  16. package/dist/cjs/core/xml/generator.js.map +1 -1
  17. package/dist/cjs/integration/repository.d.ts +2 -3
  18. package/dist/cjs/integration/repository.d.ts.map +1 -1
  19. package/dist/cjs/integration/service.d.ts.map +1 -1
  20. package/dist/cjs/integration/service.js +16 -2
  21. package/dist/cjs/integration/service.js.map +1 -1
  22. package/dist/esm/aeat/client.d.ts +2 -6
  23. package/dist/esm/aeat/client.d.ts.map +1 -1
  24. package/dist/esm/aeat/client.js +19 -19
  25. package/dist/esm/aeat/client.js.map +1 -1
  26. package/dist/esm/aeat/response.d.ts +2 -0
  27. package/dist/esm/aeat/response.d.ts.map +1 -1
  28. package/dist/esm/aeat/response.js +1 -0
  29. package/dist/esm/aeat/response.js.map +1 -1
  30. package/dist/esm/core/models/index.d.ts +0 -1
  31. package/dist/esm/core/models/index.d.ts.map +1 -1
  32. package/dist/esm/core/models/index.js +0 -1
  33. package/dist/esm/core/models/index.js.map +1 -1
  34. package/dist/esm/core/xml/generator.d.ts +13 -15
  35. package/dist/esm/core/xml/generator.d.ts.map +1 -1
  36. package/dist/esm/core/xml/generator.js +16 -3
  37. package/dist/esm/core/xml/generator.js.map +1 -1
  38. package/dist/esm/integration/repository.d.ts +2 -3
  39. package/dist/esm/integration/repository.d.ts.map +1 -1
  40. package/dist/esm/integration/service.d.ts.map +1 -1
  41. package/dist/esm/integration/service.js +17 -3
  42. package/dist/esm/integration/service.js.map +1 -1
  43. package/package.json +1 -1
  44. package/readme.md +175 -6
  45. package/src/aeat/client.ts +21 -21
  46. package/src/aeat/response.ts +3 -0
  47. package/src/core/models/index.ts +0 -1
  48. package/src/core/xml/generator.ts +25 -19
  49. package/src/integration/repository.ts +2 -3
  50. package/src/integration/schema.sql +0 -14
  51. package/src/integration/service.ts +22 -3
  52. package/src/core/models/event.ts +0 -25
@@ -128,16 +128,6 @@ export function generateRegistroAltaXml(invoice: Invoice, previousHash: string |
128
128
  return xml;
129
129
  }
130
130
 
131
- /**
132
- * Cabecera del suministro (CabeceraType del XSD).
133
- */
134
- export interface Cabecera {
135
- /** Nombre o razón social del obligado a emitir. */
136
- nombreRazon: string;
137
- /** NIF del obligado a emitir. */
138
- nif: string;
139
- }
140
-
141
131
  /**
142
132
  * Namespace del esquema `SuministroLR.xsd` de la AEAT.
143
133
  */
@@ -282,12 +272,6 @@ export function generateAnulacionXml(
282
272
  ].join('\n');
283
273
  }
284
274
 
285
- /**
286
- * Datos de un registro de evento (RegistroEvento).
287
- *
288
- * Nota: el `NIF` aparece dos veces: `nifSistemaInformatico` (del sistema
289
- * informático) y `nifObligado` (del obligado a emitir).
290
- */
291
275
  /**
292
276
  * Datos de un registro de evento (RegistroEvento, EventosSIF.xsd).
293
277
  */
@@ -302,6 +286,15 @@ export interface EventoInput {
302
286
  tipoEvento: string;
303
287
  /** Huella (hash) del evento. */
304
288
  huellaEvento: string;
289
+ /** Evento anterior de la cadena (obligatorio para el 2º evento en adelante). */
290
+ eventoAnterior?: {
291
+ /** Tipo de evento del evento anterior. */
292
+ tipoEvento: string;
293
+ /** Fecha/hora de generación del evento anterior. */
294
+ fechaHoraHusoGenEvento: string;
295
+ /** Huella (hash) del evento anterior. */
296
+ huellaEvento: string;
297
+ };
305
298
  /** Versión del esquema (por defecto '1.0'). */
306
299
  idVersion?: string;
307
300
  }
@@ -311,9 +304,24 @@ export interface EventoInput {
311
304
  *
312
305
  * Estructura basada en `EventosSIF.xsd` (namespace `sf`). El evento se envía
313
306
  * como un suministro independiente (no dentro de `RegFactuSistemaFacturacion`).
307
+ *
308
+ * El encadenado emite `<sf:PrimerEvento>S</sf:PrimerEvento>` si es el primer
309
+ * evento, o `<sf:EventoAnterior>` con los datos del evento previo si se
310
+ * proporciona `eventoAnterior`.
314
311
  */
315
312
  export function generateEventoXml(evento: EventoInput): string {
316
313
  const si = evento.sistemaInformatico;
314
+ const encadenamiento = evento.eventoAnterior
315
+ ? [
316
+ ' <sf:Encadenamiento>',
317
+ ' <sf:EventoAnterior>',
318
+ ` <sf:TipoEvento>${escapeXml(evento.eventoAnterior.tipoEvento)}</sf:TipoEvento>`,
319
+ ` <sf:FechaHoraHusoGenEvento>${evento.eventoAnterior.fechaHoraHusoGenEvento}</sf:FechaHoraHusoGenEvento>`,
320
+ ` <sf:HuellaEvento>${evento.eventoAnterior.huellaEvento}</sf:HuellaEvento>`,
321
+ ' </sf:EventoAnterior>',
322
+ ' </sf:Encadenamiento>',
323
+ ]
324
+ : [' <sf:Encadenamiento>', ' <sf:PrimerEvento>S</sf:PrimerEvento>', ' </sf:Encadenamiento>'];
317
325
  const xml = [
318
326
  '<?xml version="1.0" encoding="UTF-8"?>',
319
327
  `<sf:RegistroEvento xmlns:sf="${AEAT_EVENTO_NAMESPACE}">`,
@@ -336,9 +344,7 @@ export function generateEventoXml(evento: EventoInput): string {
336
344
  ' </sf:ObligadoEmision>',
337
345
  ` <sf:FechaHoraHusoGenEvento>${evento.fechaHoraHusoGenEvento}</sf:FechaHoraHusoGenEvento>`,
338
346
  ` <sf:TipoEvento>${escapeXml(evento.tipoEvento)}</sf:TipoEvento>`,
339
- ' <sf:Encadenamiento>',
340
- ' <sf:PrimerEvento>S</sf:PrimerEvento>',
341
- ' </sf:Encadenamiento>',
347
+ ...encadenamiento,
342
348
  ' <sf:TipoHuella>01</sf:TipoHuella>',
343
349
  ` <sf:HuellaEvento>${evento.huellaEvento}</sf:HuellaEvento>`,
344
350
  ' </sf:Evento>',
@@ -1,5 +1,4 @@
1
1
  import type { Invoice } from '../core/models/invoice.js';
2
- import type { Event } from '../core/models/event.js';
3
2
 
4
3
  /**
5
4
  * Estado de un registro de facturación en el sistema consumidor.
@@ -29,6 +28,8 @@ export interface StoredRecord {
29
28
  previousHash: string | null;
30
29
  /** Payload del QR Verifactu (URL de verificación con el hash). */
31
30
  qrPayload?: string;
31
+ /** `true` si la huella devuelta por AEAT coincide con la calculada. */
32
+ hashVerified?: boolean;
32
33
  /** Estado del registro. */
33
34
  status: RecordStatus;
34
35
  /** Fecha/hora de creación. */
@@ -83,6 +84,4 @@ export interface VerifactuRepository {
83
84
  saveResponse(response: StoredResponse): Promise<void>;
84
85
  /** Obtiene las respuestas de un registro. */
85
86
  getResponses(recordId: string): Promise<StoredResponse[]>;
86
- /** Guarda un evento. */
87
- saveEvent(event: Event): Promise<void>;
88
87
  }
@@ -32,20 +32,6 @@ CREATE TABLE IF NOT EXISTS verifactu_responses (
32
32
  received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
33
33
  );
34
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
35
  -- Índices recomendados
49
36
  CREATE INDEX IF NOT EXISTS idx_records_invoice ON verifactu_records (invoice_id);
50
37
  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);
@@ -1,6 +1,6 @@
1
1
  import type { Invoice } from '../core/models/invoice.js';
2
2
  import { generateInvoiceXml, generateSuministroXml, buildCabecera, generateAnulacionXml, generateEventoXml, type EventoInput } from '../core/xml/generator.js';
3
- import { computeRegistroAlta, computeRegistroAnulacion, computeRegistroEvento } from '../core/hash/chain.js';
3
+ import { computeRegistroAlta, computeRegistroAnulacion, computeRegistroEvento, verifyRegistroAlta } from '../core/hash/chain.js';
4
4
  import { buildQrPayload } from '../core/qr/qr.js';
5
5
  import { nowIsoWithOffset } from '../core/utils/date.js';
6
6
  import { validateInvoice } from '../core/validation/validate.js';
@@ -90,11 +90,30 @@ export class VerifactuService {
90
90
  receivedAt: new Date().toISOString(),
91
91
  });
92
92
 
93
- // 5. Actualizar el estado del registro según la respuesta.
93
+ // 5. Verificar la huella devuelta por AEAT (integridad del registro).
94
+ const aeatHash = result.registro?.huella;
95
+ const hashVerified =
96
+ result.status === 'OK' && aeatHash !== undefined
97
+ ? verifyRegistroAlta(
98
+ {
99
+ idEmisorFactura: invoice.emisor.nif,
100
+ numSerieFactura: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
101
+ fechaExpedicionFactura: invoice.id.fechaExpedicion,
102
+ tipoFactura: invoice.tipoFactura,
103
+ cuotaTotal: invoice.desglose.cuotaTotal.toFixed(2),
104
+ importeTotal: invoice.desglose.importeTotal.toFixed(2),
105
+ huellaAnterior: previousHash,
106
+ fechaHoraHusoGenRegistro,
107
+ },
108
+ aeatHash,
109
+ )
110
+ : undefined;
111
+
112
+ // 6. Actualizar el estado del registro según la respuesta.
94
113
  const status = result.status === 'OK' ? 'CONFIRMED' : 'REJECTED';
95
114
  await this.repository.updateRecordStatus(record.id, status);
96
115
 
97
- return { ...record, status };
116
+ return { ...record, status, hashVerified };
98
117
  }
99
118
 
100
119
  /**
@@ -1,25 +0,0 @@
1
- /**
2
- * Tipos de evento de registro Verifactu.
3
- * - ALTA: alta de una factura.
4
- * - ANULACION: anulación de una factura previamente registrada.
5
- * - MODIFICACION: modificación de una factura.
6
- */
7
- export type EventType = 'ALTA' | 'ANULACION' | 'MODIFICACION';
8
-
9
- /**
10
- * Evento de registro Verifactu.
11
- */
12
- export interface Event {
13
- /** Tipo de evento. */
14
- type: EventType;
15
- /** Fecha/hora del evento en formato ISO 8601. */
16
- fechaHora: string;
17
- /** Referencia a la factura asociada. */
18
- invoiceId: string;
19
- /** Motivo de anulación (obligatorio si type === 'ANULACION'). */
20
- motivoAnulacion?: string;
21
- /** Referencia a la factura que se anula (para ANULACION). */
22
- facturaAnulada?: string;
23
- /** Metadatos adicionales. */
24
- metadata?: Record<string, unknown>;
25
- }