@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.
- package/dist/cjs/aeat/client.d.ts +115 -14
- package/dist/cjs/aeat/client.d.ts.map +1 -1
- package/dist/cjs/aeat/client.js +223 -47
- package/dist/cjs/aeat/client.js.map +1 -1
- package/dist/cjs/aeat/index.d.ts +1 -0
- package/dist/cjs/aeat/index.d.ts.map +1 -1
- package/dist/cjs/aeat/index.js +1 -0
- package/dist/cjs/aeat/index.js.map +1 -1
- package/dist/cjs/aeat/response.d.ts +80 -0
- package/dist/cjs/aeat/response.d.ts.map +1 -0
- package/dist/cjs/aeat/response.js +118 -0
- package/dist/cjs/aeat/response.js.map +1 -0
- package/dist/cjs/core/hash/chain.d.ts +42 -0
- package/dist/cjs/core/hash/chain.d.ts.map +1 -1
- package/dist/cjs/core/hash/chain.js +40 -0
- package/dist/cjs/core/hash/chain.js.map +1 -1
- package/dist/cjs/core/index.d.ts +2 -0
- package/dist/cjs/core/index.d.ts.map +1 -1
- package/dist/cjs/core/index.js +2 -0
- package/dist/cjs/core/index.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/models/invoice.d.ts +42 -17
- package/dist/cjs/core/models/invoice.d.ts.map +1 -1
- package/dist/cjs/core/utils/date.d.ts +16 -0
- package/dist/cjs/core/utils/date.d.ts.map +1 -0
- package/dist/cjs/core/utils/date.js +27 -0
- package/dist/cjs/core/utils/date.js.map +1 -0
- package/dist/cjs/core/utils/index.d.ts +2 -0
- package/dist/cjs/core/utils/index.d.ts.map +1 -0
- package/dist/cjs/core/utils/index.js +18 -0
- package/dist/cjs/core/utils/index.js.map +1 -0
- package/dist/cjs/core/validation/index.d.ts +2 -0
- package/dist/cjs/core/validation/index.d.ts.map +1 -0
- package/dist/cjs/core/validation/index.js +18 -0
- package/dist/cjs/core/validation/index.js.map +1 -0
- package/dist/cjs/core/validation/validate.d.ts +18 -0
- package/dist/cjs/core/validation/validate.d.ts.map +1 -0
- package/dist/cjs/core/validation/validate.js +65 -0
- package/dist/cjs/core/validation/validate.js.map +1 -0
- package/dist/cjs/core/xml/generator.d.ts +117 -11
- package/dist/cjs/core/xml/generator.d.ts.map +1 -1
- package/dist/cjs/core/xml/generator.js +239 -64
- package/dist/cjs/core/xml/generator.js.map +1 -1
- package/dist/cjs/integration/accountability.d.ts +41 -0
- package/dist/cjs/integration/accountability.d.ts.map +1 -0
- package/dist/cjs/integration/accountability.js +92 -0
- package/dist/cjs/integration/accountability.js.map +1 -0
- package/dist/cjs/integration/index.d.ts +2 -0
- package/dist/cjs/integration/index.d.ts.map +1 -1
- package/dist/cjs/integration/index.js +2 -0
- package/dist/cjs/integration/index.js.map +1 -1
- package/dist/cjs/integration/p12.d.ts +58 -0
- package/dist/cjs/integration/p12.d.ts.map +1 -0
- package/dist/cjs/integration/p12.js +127 -0
- package/dist/cjs/integration/p12.js.map +1 -0
- package/dist/cjs/integration/repository.d.ts +4 -3
- package/dist/cjs/integration/repository.d.ts.map +1 -1
- package/dist/cjs/integration/service.d.ts +11 -2
- package/dist/cjs/integration/service.d.ts.map +1 -1
- package/dist/cjs/integration/service.js +115 -15
- package/dist/cjs/integration/service.js.map +1 -1
- package/dist/cjs/integration/tenant-credentials.d.ts +44 -0
- package/dist/cjs/integration/tenant-credentials.d.ts.map +1 -0
- package/dist/cjs/integration/tenant-credentials.js +62 -0
- package/dist/cjs/integration/tenant-credentials.js.map +1 -0
- package/dist/esm/aeat/client.d.ts +115 -14
- package/dist/esm/aeat/client.d.ts.map +1 -1
- package/dist/esm/aeat/client.js +218 -46
- package/dist/esm/aeat/client.js.map +1 -1
- package/dist/esm/aeat/index.d.ts +1 -0
- package/dist/esm/aeat/index.d.ts.map +1 -1
- package/dist/esm/aeat/index.js +1 -0
- package/dist/esm/aeat/index.js.map +1 -1
- package/dist/esm/aeat/response.d.ts +80 -0
- package/dist/esm/aeat/response.d.ts.map +1 -0
- package/dist/esm/aeat/response.js +115 -0
- package/dist/esm/aeat/response.js.map +1 -0
- package/dist/esm/core/hash/chain.d.ts +42 -0
- package/dist/esm/core/hash/chain.d.ts.map +1 -1
- package/dist/esm/core/hash/chain.js +37 -0
- package/dist/esm/core/hash/chain.js.map +1 -1
- package/dist/esm/core/index.d.ts +2 -0
- package/dist/esm/core/index.d.ts.map +1 -1
- package/dist/esm/core/index.js +2 -0
- package/dist/esm/core/index.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/models/invoice.d.ts +42 -17
- package/dist/esm/core/models/invoice.d.ts.map +1 -1
- package/dist/esm/core/utils/date.d.ts +16 -0
- package/dist/esm/core/utils/date.d.ts.map +1 -0
- package/dist/esm/core/utils/date.js +24 -0
- package/dist/esm/core/utils/date.js.map +1 -0
- package/dist/esm/core/utils/index.d.ts +2 -0
- package/dist/esm/core/utils/index.d.ts.map +1 -0
- package/dist/esm/core/utils/index.js +2 -0
- package/dist/esm/core/utils/index.js.map +1 -0
- package/dist/esm/core/validation/index.d.ts +2 -0
- package/dist/esm/core/validation/index.d.ts.map +1 -0
- package/dist/esm/core/validation/index.js +2 -0
- package/dist/esm/core/validation/index.js.map +1 -0
- package/dist/esm/core/validation/validate.d.ts +18 -0
- package/dist/esm/core/validation/validate.d.ts.map +1 -0
- package/dist/esm/core/validation/validate.js +62 -0
- package/dist/esm/core/validation/validate.js.map +1 -0
- package/dist/esm/core/xml/generator.d.ts +117 -11
- package/dist/esm/core/xml/generator.d.ts.map +1 -1
- package/dist/esm/core/xml/generator.js +233 -64
- package/dist/esm/core/xml/generator.js.map +1 -1
- package/dist/esm/integration/accountability.d.ts +41 -0
- package/dist/esm/integration/accountability.d.ts.map +1 -0
- package/dist/esm/integration/accountability.js +88 -0
- package/dist/esm/integration/accountability.js.map +1 -0
- package/dist/esm/integration/index.d.ts +2 -0
- package/dist/esm/integration/index.d.ts.map +1 -1
- package/dist/esm/integration/index.js +2 -0
- package/dist/esm/integration/index.js.map +1 -1
- package/dist/esm/integration/p12.d.ts +58 -0
- package/dist/esm/integration/p12.d.ts.map +1 -0
- package/dist/esm/integration/p12.js +119 -0
- package/dist/esm/integration/p12.js.map +1 -0
- package/dist/esm/integration/repository.d.ts +4 -3
- package/dist/esm/integration/repository.d.ts.map +1 -1
- package/dist/esm/integration/service.d.ts +11 -2
- package/dist/esm/integration/service.d.ts.map +1 -1
- package/dist/esm/integration/service.js +117 -17
- package/dist/esm/integration/service.js.map +1 -1
- package/dist/esm/integration/tenant-credentials.d.ts +44 -0
- package/dist/esm/integration/tenant-credentials.d.ts.map +1 -0
- package/dist/esm/integration/tenant-credentials.js +58 -0
- package/dist/esm/integration/tenant-credentials.js.map +1 -0
- package/package.json +3 -1
- package/readme.md +731 -38
- package/src/aeat/client.ts +298 -57
- package/src/aeat/index.ts +1 -0
- package/src/aeat/response.ts +198 -0
- package/src/core/hash/chain.ts +68 -0
- package/src/core/index.ts +2 -0
- package/src/core/models/index.ts +0 -1
- package/src/core/models/invoice.ts +40 -18
- package/src/core/utils/date.ts +27 -0
- package/src/core/utils/index.ts +1 -0
- package/src/core/validation/index.ts +1 -0
- package/src/core/validation/validate.ts +71 -0
- package/src/core/xml/generator.ts +312 -73
- package/src/integration/accountability.ts +125 -0
- package/src/integration/index.ts +2 -0
- package/src/integration/p12.ts +155 -0
- package/src/integration/repository.ts +4 -3
- package/src/integration/schema.sql +0 -14
- package/src/integration/service.ts +134 -16
- package/src/core/models/event.ts +0 -25
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import forge from 'node-forge';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Un campo (bag) del `.p12`.
|
|
5
|
+
*/
|
|
6
|
+
export interface P12Bag {
|
|
7
|
+
/** Tipo del bag (certBag, keyBag, pkcs8ShroudedKeyBag, ...). */
|
|
8
|
+
type: string;
|
|
9
|
+
/** Contenido en formato PEM (si es un certificado o clave). */
|
|
10
|
+
pem?: string;
|
|
11
|
+
/** Valor en bruto (si no es PEM). */
|
|
12
|
+
value?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Contenido completo de un `.p12`, con todos sus campos.
|
|
17
|
+
*/
|
|
18
|
+
export interface P12Content {
|
|
19
|
+
/** Todos los bags encontrados, en orden. */
|
|
20
|
+
bags: P12Bag[];
|
|
21
|
+
/** Certificados en PEM (conveniencia). */
|
|
22
|
+
certificates: string[];
|
|
23
|
+
/** Claves privadas en PEM (conveniencia). */
|
|
24
|
+
keys: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lee un certificado `.p12`/`.pfx` e itera todos sus campos (bags),
|
|
29
|
+
* devolviéndolos todos. La librería no interpreta qué campo es cada cosa:
|
|
30
|
+
* simplemente expone el contenido para que la aplicación lo use.
|
|
31
|
+
*
|
|
32
|
+
* @param buffer Contenido del archivo `.p12`/`.pfx`.
|
|
33
|
+
* @param password Contraseña del `.p12`.
|
|
34
|
+
*/
|
|
35
|
+
export function readP12(buffer: Buffer, password: string): P12Content {
|
|
36
|
+
const asn1 = forge.asn1.fromDer(forge.util.createBuffer(new Uint8Array(buffer)));
|
|
37
|
+
const p12 = forge.pkcs12.pkcs12FromAsn1(asn1, password);
|
|
38
|
+
|
|
39
|
+
const bags: P12Bag[] = [];
|
|
40
|
+
const certificates: string[] = [];
|
|
41
|
+
const keys: string[] = [];
|
|
42
|
+
|
|
43
|
+
p12.safeContents.forEach((safe) => {
|
|
44
|
+
safe.safeBags.forEach((bag) => {
|
|
45
|
+
const type = bag.type ?? 'unknown';
|
|
46
|
+
let pem: string | undefined;
|
|
47
|
+
let value: unknown;
|
|
48
|
+
|
|
49
|
+
if (bag.cert) {
|
|
50
|
+
pem = forge.pki.certificateToPem(bag.cert);
|
|
51
|
+
certificates.push(pem);
|
|
52
|
+
} else if (bag.key) {
|
|
53
|
+
pem = forge.pki.privateKeyToPem(bag.key);
|
|
54
|
+
keys.push(pem);
|
|
55
|
+
} else {
|
|
56
|
+
value = bag.attributes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
bags.push({ type, pem, value });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { bags, certificates, keys };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extrae el NIF de un certificado en PEM.
|
|
68
|
+
*
|
|
69
|
+
* El NIF suele aparecer en el `serialNumber` (p. ej. `IDCES-74654958V`) o como
|
|
70
|
+
* primer token del `commonName` (p. ej. `74654958V ANTONIO JOSE RECHE`).
|
|
71
|
+
* Devuelve `null` si no se puede determinar.
|
|
72
|
+
*/
|
|
73
|
+
export function extractNifFromCertificate(pem: string): string | null {
|
|
74
|
+
try {
|
|
75
|
+
const cert = forge.pki.certificateFromPem(pem);
|
|
76
|
+
const attrs = cert.subject.attributes;
|
|
77
|
+
|
|
78
|
+
// serialNumber: "IDCES-74654958V" o directamente "B12345678"
|
|
79
|
+
const serial = attrs.find((a) => a.name === 'serialNumber')?.value;
|
|
80
|
+
if (serial) {
|
|
81
|
+
const m = String(serial).match(/IDCES-(\S+)/i) ?? String(serial).match(/^([A-Z0-9]{8,9})/i);
|
|
82
|
+
if (m && m[1]) return m[1];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// commonName: "74654958V ANTONIO JOSE RECHE (R: B18579458)"
|
|
86
|
+
const cn = attrs.find((a) => a.name === 'commonName')?.value;
|
|
87
|
+
if (cn) {
|
|
88
|
+
const m = String(cn).match(/^([A-Z0-9]{8,9})/i);
|
|
89
|
+
if (m && m[1]) return m[1];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Información de un certificado en PEM: NIF y nombre/razón del titular.
|
|
100
|
+
*/
|
|
101
|
+
export interface CertificateInfo {
|
|
102
|
+
/** NIF del titular del certificado. */
|
|
103
|
+
nif: string;
|
|
104
|
+
/** Nombre o razón social del titular. */
|
|
105
|
+
nombreRazon: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extrae el NIF y el nombre/razón del titular de un certificado en PEM.
|
|
110
|
+
*
|
|
111
|
+
* El NIF se obtiene del `serialNumber` (p. ej. `IDCES-74654958V`) o del primer
|
|
112
|
+
* token del `commonName`. El nombre se compone de `givenName` + `surname`, o
|
|
113
|
+
* del `commonName` sin el prefijo de NIF. Devuelve `null` si no se puede
|
|
114
|
+
* determinar el NIF.
|
|
115
|
+
*/
|
|
116
|
+
export function extractCertificateInfo(pem: string): CertificateInfo | null {
|
|
117
|
+
try {
|
|
118
|
+
const cert = forge.pki.certificateFromPem(pem);
|
|
119
|
+
const attrs = cert.subject.attributes;
|
|
120
|
+
|
|
121
|
+
// NIF
|
|
122
|
+
const serial = attrs.find((a) => a.name === 'serialNumber')?.value;
|
|
123
|
+
let nif: string | null = null;
|
|
124
|
+
if (serial) {
|
|
125
|
+
const m = String(serial).match(/IDCES-(\S+)/i) ?? String(serial).match(/^([A-Z0-9]{8,9})/i);
|
|
126
|
+
if (m && m[1]) nif = m[1];
|
|
127
|
+
}
|
|
128
|
+
if (!nif) {
|
|
129
|
+
const cn = attrs.find((a) => a.name === 'commonName')?.value;
|
|
130
|
+
if (cn) {
|
|
131
|
+
const m = String(cn).match(/^([A-Z0-9]{8,9})/i);
|
|
132
|
+
if (m && m[1]) nif = m[1];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!nif) return null;
|
|
136
|
+
|
|
137
|
+
// Nombre: givenName + surname, o commonName sin el NIF.
|
|
138
|
+
const given = attrs.find((a) => a.name === 'givenName')?.value;
|
|
139
|
+
const surname = attrs.find((a) => a.name === 'surname')?.value;
|
|
140
|
+
let nombreRazon = '';
|
|
141
|
+
if (given) nombreRazon += given;
|
|
142
|
+
if (surname) nombreRazon += (nombreRazon ? ' ' : '') + surname;
|
|
143
|
+
nombreRazon = nombreRazon.trim();
|
|
144
|
+
if (!nombreRazon) {
|
|
145
|
+
const cn = attrs.find((a) => a.name === 'commonName')?.value;
|
|
146
|
+
if (cn) {
|
|
147
|
+
nombreRazon = String(cn!).replace(/^[A-Z0-9]{8,9}\s*/, '').split(' (R:')[0]?.trim() ?? '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { nif, nombreRazon };
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -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.
|
|
@@ -19,6 +18,8 @@ export interface StoredRecord {
|
|
|
19
18
|
id: string;
|
|
20
19
|
/** Referencia a la factura (serie + número). */
|
|
21
20
|
invoiceId: string;
|
|
21
|
+
/** NIF del emisor de la factura. */
|
|
22
|
+
emisorNif: string;
|
|
22
23
|
/** XML Verifactu generado. */
|
|
23
24
|
xml: string;
|
|
24
25
|
/** Hash del propio registro (huella). */
|
|
@@ -27,6 +28,8 @@ export interface StoredRecord {
|
|
|
27
28
|
previousHash: string | null;
|
|
28
29
|
/** Payload del QR Verifactu (URL de verificación con el hash). */
|
|
29
30
|
qrPayload?: string;
|
|
31
|
+
/** `true` si la huella devuelta por AEAT coincide con la calculada. */
|
|
32
|
+
hashVerified?: boolean;
|
|
30
33
|
/** Estado del registro. */
|
|
31
34
|
status: RecordStatus;
|
|
32
35
|
/** Fecha/hora de creación. */
|
|
@@ -81,6 +84,4 @@ export interface VerifactuRepository {
|
|
|
81
84
|
saveResponse(response: StoredResponse): Promise<void>;
|
|
82
85
|
/** Obtiene las respuestas de un registro. */
|
|
83
86
|
getResponses(recordId: string): Promise<StoredResponse[]>;
|
|
84
|
-
/** Guarda un evento. */
|
|
85
|
-
saveEvent(event: Event): Promise<void>;
|
|
86
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,10 +1,19 @@
|
|
|
1
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';
|
|
2
|
+
import { generateInvoiceXml, generateSuministroXml, buildCabecera, generateAnulacionXml, generateEventoXml, type EventoInput } from '../core/xml/generator.js';
|
|
3
|
+
import { computeRegistroAlta, computeRegistroAnulacion, computeRegistroEvento, verifyRegistroAlta } from '../core/hash/chain.js';
|
|
4
4
|
import { buildQrPayload } from '../core/qr/qr.js';
|
|
5
|
+
import { nowIsoWithOffset } from '../core/utils/date.js';
|
|
6
|
+
import { validateInvoice } from '../core/validation/validate.js';
|
|
7
|
+
import { extractCertificateInfo } from './p12.js';
|
|
5
8
|
import type { VerifactuClient } from '../aeat/client.js';
|
|
6
9
|
import type { VerifactuRepository, StoredRecord } from './repository.js';
|
|
7
10
|
|
|
11
|
+
/** Convierte `YYYY-MM-DD` a `DD-MM-YYYY` (formato XSD `fecha` de AEAT). */
|
|
12
|
+
function toDdMmYyyy(iso: string): string {
|
|
13
|
+
const [y, m, d] = iso.split('-');
|
|
14
|
+
return `${d}-${m}-${y}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
/**
|
|
9
18
|
* Servicio de orquestación de Verifactu.
|
|
10
19
|
*
|
|
@@ -16,6 +25,7 @@ export class VerifactuService {
|
|
|
16
25
|
constructor(
|
|
17
26
|
private readonly repository: VerifactuRepository,
|
|
18
27
|
private readonly client: VerifactuClient,
|
|
28
|
+
private readonly certificatePem?: string,
|
|
19
29
|
) {}
|
|
20
30
|
|
|
21
31
|
/**
|
|
@@ -23,12 +33,25 @@ export class VerifactuService {
|
|
|
23
33
|
* el registro y lo envía a AEAT, persistiendo la respuesta.
|
|
24
34
|
*/
|
|
25
35
|
async registerInvoice(invoice: Invoice): Promise<StoredRecord> {
|
|
36
|
+
// 0. Validar la factura antes de generar el XML.
|
|
37
|
+
const validation = validateInvoice(invoice);
|
|
38
|
+
if (!validation.valid) {
|
|
39
|
+
throw new Error(`Factura inválida: ${validation.errors.join('; ')}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
// 1. Obtener el hash del último registro para encadenar.
|
|
27
43
|
const last = await this.repository.getLastRecord();
|
|
28
44
|
const previousHash = last?.hash ?? null;
|
|
29
45
|
|
|
30
46
|
// 2. Generar XML y calcular la huella (hash) del registro.
|
|
31
|
-
const
|
|
47
|
+
const registroAltaXml = generateInvoiceXml(invoice, previousHash);
|
|
48
|
+
const cabecera = buildCabecera(
|
|
49
|
+
{ nombreRazon: invoice.emisor.nombreRazon, nif: invoice.emisor.nif },
|
|
50
|
+
this.certificatePem,
|
|
51
|
+
extractCertificateInfo,
|
|
52
|
+
);
|
|
53
|
+
const xml = generateSuministroXml(registroAltaXml, cabecera);
|
|
54
|
+
const fechaHoraHusoGenRegistro = nowIsoWithOffset();
|
|
32
55
|
const hash = computeRegistroAlta({
|
|
33
56
|
idEmisorFactura: invoice.emisor.nif,
|
|
34
57
|
numSerieFactura: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
|
|
@@ -37,7 +60,7 @@ export class VerifactuService {
|
|
|
37
60
|
cuotaTotal: invoice.desglose.cuotaTotal.toFixed(2),
|
|
38
61
|
importeTotal: invoice.desglose.importeTotal.toFixed(2),
|
|
39
62
|
huellaAnterior: previousHash,
|
|
40
|
-
fechaHoraHusoGenRegistro
|
|
63
|
+
fechaHoraHusoGenRegistro,
|
|
41
64
|
});
|
|
42
65
|
|
|
43
66
|
// 3. Persistir el registro como PENDING.
|
|
@@ -45,6 +68,7 @@ export class VerifactuService {
|
|
|
45
68
|
const record: StoredRecord = {
|
|
46
69
|
id: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
|
|
47
70
|
invoiceId: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
|
|
71
|
+
emisorNif: invoice.emisor.nif,
|
|
48
72
|
xml,
|
|
49
73
|
hash,
|
|
50
74
|
previousHash,
|
|
@@ -66,11 +90,30 @@ export class VerifactuService {
|
|
|
66
90
|
receivedAt: new Date().toISOString(),
|
|
67
91
|
});
|
|
68
92
|
|
|
69
|
-
// 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.
|
|
70
113
|
const status = result.status === 'OK' ? 'CONFIRMED' : 'REJECTED';
|
|
71
114
|
await this.repository.updateRecordStatus(record.id, status);
|
|
72
115
|
|
|
73
|
-
return { ...record, status };
|
|
116
|
+
return { ...record, status, hashVerified };
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
/**
|
|
@@ -79,7 +122,7 @@ export class VerifactuService {
|
|
|
79
122
|
* Genera un registro de anulación que referencia la factura anulada,
|
|
80
123
|
* lo envía a AEAT y marca la factura original como ANNULLED.
|
|
81
124
|
*/
|
|
82
|
-
async annulInvoice(invoiceId: string): Promise<StoredRecord> {
|
|
125
|
+
async annulInvoice(invoiceId: string, invoice?: Invoice): Promise<StoredRecord> {
|
|
83
126
|
// 1. Obtener la factura original (para su huella) y el último registro.
|
|
84
127
|
const original = await this.repository.getRecord(invoiceId);
|
|
85
128
|
if (!original) {
|
|
@@ -87,30 +130,46 @@ export class VerifactuService {
|
|
|
87
130
|
}
|
|
88
131
|
const last = await this.repository.getLastRecord();
|
|
89
132
|
const previousHash = last?.hash ?? null;
|
|
133
|
+
const fechaHoraHusoGenRegistro = nowIsoWithOffset();
|
|
134
|
+
const fechaExpedicionFacturaAnulada = toDdMmYyyy(
|
|
135
|
+
invoice?.id.fechaExpedicion ?? new Date().toISOString().slice(0, 10),
|
|
136
|
+
);
|
|
137
|
+
const sistemaInformatico = invoice?.sistemaInformatico ?? {
|
|
138
|
+
nombreRazon: 'Sistema',
|
|
139
|
+
nif: original.emisorNif,
|
|
140
|
+
nombreSistemaInformatico: 'SIF',
|
|
141
|
+
idSistemaInformatico: '01',
|
|
142
|
+
version: '1.0',
|
|
143
|
+
numeroInstalacion: '1',
|
|
144
|
+
};
|
|
90
145
|
|
|
91
146
|
// 2. Generar XML de anulación y calcular la huella.
|
|
147
|
+
const hash = computeRegistroAnulacion({
|
|
148
|
+
idEmisorFacturaAnulada: original.emisorNif,
|
|
149
|
+
numSerieFacturaAnulada: original.invoiceId,
|
|
150
|
+
fechaExpedicionFacturaAnulada,
|
|
151
|
+
huellaAnterior: previousHash,
|
|
152
|
+
fechaHoraHusoGenRegistro,
|
|
153
|
+
});
|
|
92
154
|
const xml = generateAnulacionXml(
|
|
93
155
|
{
|
|
94
|
-
idEmisorFacturaAnulada: original.
|
|
156
|
+
idEmisorFacturaAnulada: original.emisorNif,
|
|
95
157
|
numSerieFacturaAnulada: original.invoiceId,
|
|
96
|
-
fechaExpedicionFacturaAnulada
|
|
158
|
+
fechaExpedicionFacturaAnulada,
|
|
97
159
|
huellaFacturaAnulada: original.hash,
|
|
160
|
+
sistemaInformatico,
|
|
161
|
+
fechaHoraHusoGenRegistro,
|
|
162
|
+
huella: hash,
|
|
98
163
|
},
|
|
99
164
|
previousHash,
|
|
100
165
|
);
|
|
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
166
|
|
|
109
167
|
// 3. Persistir el registro de anulación como PENDING.
|
|
110
168
|
const now = new Date().toISOString();
|
|
111
169
|
const record: StoredRecord = {
|
|
112
170
|
id: `${invoiceId}-ANUL`,
|
|
113
171
|
invoiceId,
|
|
172
|
+
emisorNif: original.emisorNif,
|
|
114
173
|
xml,
|
|
115
174
|
hash,
|
|
116
175
|
previousHash,
|
|
@@ -155,4 +214,63 @@ export class VerifactuService {
|
|
|
155
214
|
// 2. Emitir la factura corregida como un nuevo alta.
|
|
156
215
|
return this.registerInvoice(correctedInvoice);
|
|
157
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Registra un evento (RegistroEvento) ante AEAT.
|
|
220
|
+
*
|
|
221
|
+
* Genera el XML del evento, calcula su huella encadenada con el último
|
|
222
|
+
* registro, persiste el evento y lo envía a AEAT.
|
|
223
|
+
*/
|
|
224
|
+
async registerEvent(evento: EventoInput): Promise<StoredRecord> {
|
|
225
|
+
// 1. Obtener el hash del último registro para encadenar.
|
|
226
|
+
const last = await this.repository.getLastRecord();
|
|
227
|
+
const previousHash = last?.hash ?? null;
|
|
228
|
+
|
|
229
|
+
// 2. Generar XML y calcular la huella del evento.
|
|
230
|
+
const si = evento.sistemaInformatico;
|
|
231
|
+
const hash = computeRegistroEvento({
|
|
232
|
+
nifSistemaInformatico: si.nif,
|
|
233
|
+
id: '',
|
|
234
|
+
idSistemaInformatico: si.idSistemaInformatico,
|
|
235
|
+
version: si.version,
|
|
236
|
+
numeroInstalacion: si.numeroInstalacion,
|
|
237
|
+
nifObligado: evento.obligadoEmision.nif,
|
|
238
|
+
tipoEvento: evento.tipoEvento,
|
|
239
|
+
huellaAnterior: previousHash,
|
|
240
|
+
fechaHoraHusoGenEvento: evento.fechaHoraHusoGenEvento,
|
|
241
|
+
});
|
|
242
|
+
const xml = generateEventoXml({ ...evento, huellaEvento: hash });
|
|
243
|
+
|
|
244
|
+
// 3. Persistir el evento.
|
|
245
|
+
const now = new Date().toISOString();
|
|
246
|
+
const record: StoredRecord = {
|
|
247
|
+
id: `EV-${evento.tipoEvento}-${Date.now()}`,
|
|
248
|
+
invoiceId: evento.obligadoEmision.nif,
|
|
249
|
+
emisorNif: evento.obligadoEmision.nif,
|
|
250
|
+
xml,
|
|
251
|
+
hash,
|
|
252
|
+
previousHash,
|
|
253
|
+
status: 'PENDING',
|
|
254
|
+
createdAt: now,
|
|
255
|
+
updatedAt: now,
|
|
256
|
+
};
|
|
257
|
+
await this.repository.saveRecord(record);
|
|
258
|
+
|
|
259
|
+
// 4. Enviar a AEAT y persistir la respuesta.
|
|
260
|
+
const result = await this.client.sendRecord(xml);
|
|
261
|
+
await this.repository.saveResponse({
|
|
262
|
+
id: `${record.id}-${Date.now()}`,
|
|
263
|
+
recordId: record.id,
|
|
264
|
+
operationId: result.operationId,
|
|
265
|
+
status: result.status,
|
|
266
|
+
message: result.message,
|
|
267
|
+
receivedAt: new Date().toISOString(),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// 5. Actualizar el estado.
|
|
271
|
+
const status = result.status === 'OK' ? 'CONFIRMED' : 'REJECTED';
|
|
272
|
+
await this.repository.updateRecordStatus(record.id, status);
|
|
273
|
+
|
|
274
|
+
return { ...record, status };
|
|
275
|
+
}
|
|
158
276
|
}
|
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
|
-
}
|