@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.
- package/dist/cjs/aeat/client.d.ts +2 -6
- package/dist/cjs/aeat/client.d.ts.map +1 -1
- package/dist/cjs/aeat/client.js +19 -19
- package/dist/cjs/aeat/client.js.map +1 -1
- package/dist/cjs/aeat/response.d.ts +2 -0
- package/dist/cjs/aeat/response.d.ts.map +1 -1
- package/dist/cjs/aeat/response.js +1 -0
- package/dist/cjs/aeat/response.js.map +1 -1
- package/dist/cjs/core/models/index.d.ts +0 -1
- package/dist/cjs/core/models/index.d.ts.map +1 -1
- package/dist/cjs/core/models/index.js +0 -1
- package/dist/cjs/core/models/index.js.map +1 -1
- package/dist/cjs/core/xml/generator.d.ts +13 -15
- package/dist/cjs/core/xml/generator.d.ts.map +1 -1
- package/dist/cjs/core/xml/generator.js +16 -3
- package/dist/cjs/core/xml/generator.js.map +1 -1
- package/dist/cjs/integration/repository.d.ts +2 -3
- package/dist/cjs/integration/repository.d.ts.map +1 -1
- package/dist/cjs/integration/service.d.ts.map +1 -1
- package/dist/cjs/integration/service.js +16 -2
- package/dist/cjs/integration/service.js.map +1 -1
- package/dist/esm/aeat/client.d.ts +2 -6
- package/dist/esm/aeat/client.d.ts.map +1 -1
- package/dist/esm/aeat/client.js +19 -19
- package/dist/esm/aeat/client.js.map +1 -1
- package/dist/esm/aeat/response.d.ts +2 -0
- package/dist/esm/aeat/response.d.ts.map +1 -1
- package/dist/esm/aeat/response.js +1 -0
- package/dist/esm/aeat/response.js.map +1 -1
- package/dist/esm/core/models/index.d.ts +0 -1
- package/dist/esm/core/models/index.d.ts.map +1 -1
- package/dist/esm/core/models/index.js +0 -1
- package/dist/esm/core/models/index.js.map +1 -1
- package/dist/esm/core/xml/generator.d.ts +13 -15
- package/dist/esm/core/xml/generator.d.ts.map +1 -1
- package/dist/esm/core/xml/generator.js +16 -3
- package/dist/esm/core/xml/generator.js.map +1 -1
- package/dist/esm/integration/repository.d.ts +2 -3
- package/dist/esm/integration/repository.d.ts.map +1 -1
- package/dist/esm/integration/service.d.ts.map +1 -1
- package/dist/esm/integration/service.js +17 -3
- package/dist/esm/integration/service.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +175 -6
- package/src/aeat/client.ts +21 -21
- package/src/aeat/response.ts +3 -0
- package/src/core/models/index.ts +0 -1
- package/src/core/xml/generator.ts +25 -19
- package/src/integration/repository.ts +2 -3
- package/src/integration/schema.sql +0 -14
- package/src/integration/service.ts +22 -3
- 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
|
-
|
|
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.
|
|
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
|
/**
|
package/src/core/models/event.ts
DELETED
|
@@ -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
|
-
}
|