@dwast/verifactu-lib 0.1.0-beta.6 → 0.1.0-beta.8
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 +10 -17
- package/dist/cjs/aeat/client.d.ts.map +1 -1
- package/dist/cjs/aeat/client.js +9 -15
- package/dist/cjs/aeat/client.js.map +1 -1
- package/dist/cjs/integration/accountability.d.ts +3 -2
- package/dist/cjs/integration/accountability.d.ts.map +1 -1
- package/dist/cjs/integration/accountability.js +1 -1
- package/dist/cjs/integration/accountability.js.map +1 -1
- package/dist/cjs/integration/index.d.ts +1 -0
- package/dist/cjs/integration/index.d.ts.map +1 -1
- package/dist/cjs/integration/index.js +1 -0
- package/dist/cjs/integration/index.js.map +1 -1
- package/dist/cjs/integration/p12.d.ts +28 -0
- package/dist/cjs/integration/p12.d.ts.map +1 -1
- package/dist/cjs/integration/p12.js +29 -0
- package/dist/cjs/integration/p12.js.map +1 -1
- package/dist/cjs/integration/validate.d.ts +54 -0
- package/dist/cjs/integration/validate.d.ts.map +1 -0
- package/dist/cjs/integration/validate.js +124 -0
- package/dist/cjs/integration/validate.js.map +1 -0
- package/dist/esm/aeat/client.d.ts +10 -17
- package/dist/esm/aeat/client.d.ts.map +1 -1
- package/dist/esm/aeat/client.js +9 -15
- package/dist/esm/aeat/client.js.map +1 -1
- package/dist/esm/integration/accountability.d.ts +3 -2
- package/dist/esm/integration/accountability.d.ts.map +1 -1
- package/dist/esm/integration/accountability.js +1 -1
- package/dist/esm/integration/accountability.js.map +1 -1
- package/dist/esm/integration/index.d.ts +1 -0
- package/dist/esm/integration/index.d.ts.map +1 -1
- package/dist/esm/integration/index.js +1 -0
- package/dist/esm/integration/index.js.map +1 -1
- package/dist/esm/integration/p12.d.ts +28 -0
- package/dist/esm/integration/p12.d.ts.map +1 -1
- package/dist/esm/integration/p12.js +28 -0
- package/dist/esm/integration/p12.js.map +1 -1
- package/dist/esm/integration/validate.d.ts +54 -0
- package/dist/esm/integration/validate.d.ts.map +1 -0
- package/dist/esm/integration/validate.js +121 -0
- package/dist/esm/integration/validate.js.map +1 -0
- package/package.json +1 -1
- package/readme.md +23 -4
- package/src/aeat/client.ts +11 -23
- package/src/integration/accountability.ts +4 -4
- package/src/integration/index.ts +1 -0
- package/src/integration/p12.ts +52 -0
- package/src/integration/validate.ts +171 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { VerifactuClient, type VerifactuEnv } from '../aeat/client.js';
|
|
2
|
+
import { readP12, decodeCertificate, extractCertificateInfo, type DecodedCertificate } from './p12.js';
|
|
3
|
+
import { computeRegistroAlta } from '../core/hash/chain.js';
|
|
4
|
+
import { generateInvoiceXml, generateSuministroXml, buildCabecera } from '../core/xml/generator.js';
|
|
5
|
+
import { nowIsoWithOffset } from '../core/utils/date.js';
|
|
6
|
+
import type { Invoice } from '../core/models/invoice.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Respuesta de AEAT a la factura de prueba de validación.
|
|
10
|
+
*/
|
|
11
|
+
export interface AeatValidationResponse {
|
|
12
|
+
/** Estado devuelto por AEAT (`OK` | `ERROR`). */
|
|
13
|
+
status: string;
|
|
14
|
+
/** Mensaje de AEAT. */
|
|
15
|
+
message?: string;
|
|
16
|
+
/** CSV de la factura de prueba (si AEAT la aceptó). */
|
|
17
|
+
csv?: string;
|
|
18
|
+
/** Identificador de la operación. */
|
|
19
|
+
operationId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resultado de la validación de un `.p12` contra AEAT.
|
|
24
|
+
*/
|
|
25
|
+
export interface P12ValidationResult {
|
|
26
|
+
/** `true` si AEAT aceptó la factura de prueba con este certificado. */
|
|
27
|
+
valid: boolean;
|
|
28
|
+
/** Campos decodificados del certificado (para mostrar/validar la pass). */
|
|
29
|
+
certificate?: DecodedCertificate;
|
|
30
|
+
/** Respuesta de AEAT a la factura de prueba. */
|
|
31
|
+
aeat?: AeatValidationResponse;
|
|
32
|
+
/** Mensaje informativo. */
|
|
33
|
+
message?: string;
|
|
34
|
+
/** Descripción del error cuando la validación falla. */
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ValidateP12Options {
|
|
39
|
+
/** Entorno contra el que validar. Por defecto `test` (desarrollo). */
|
|
40
|
+
env?: VerifactuEnv;
|
|
41
|
+
/** Timeout de la petición en ms. */
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Convierte una fecha a `DD-MM-YYYY` (formato XSD `fecha` de AEAT). */
|
|
46
|
+
function toDdMmYyyy(d: Date): string {
|
|
47
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
48
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
49
|
+
return `${dd}-${mm}-${d.getFullYear()}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Valida un `.p12`/`.pfx` contra el entorno de AEAT indicado (por defecto el
|
|
54
|
+
* de **pruebas/desarrollo**).
|
|
55
|
+
*
|
|
56
|
+
* Pasos:
|
|
57
|
+
* 1. Lee el `.p12` y extrae certificado + clave privada (valida la contraseña).
|
|
58
|
+
* 2. Decodifica los campos del certificado (NIF, nombre, validez, emisor...).
|
|
59
|
+
* 3. Construye un `VerifactuClient` con mTLS y **emite una factura de prueba**
|
|
60
|
+
* en el entorno indicado. Si AEAT la acepta, el certificado es válido.
|
|
61
|
+
*
|
|
62
|
+
* Devuelve los campos decodificados (para mostrar) y la respuesta de AEAT.
|
|
63
|
+
*
|
|
64
|
+
* @param buffer Contenido del `.p12`/`.pfx`.
|
|
65
|
+
* @param password Contraseña del `.p12`.
|
|
66
|
+
* @param options Entorno y timeout.
|
|
67
|
+
*/
|
|
68
|
+
export async function validateP12AgainstAEAT(
|
|
69
|
+
buffer: Buffer,
|
|
70
|
+
password: string,
|
|
71
|
+
options: ValidateP12Options = {},
|
|
72
|
+
): Promise<P12ValidationResult> {
|
|
73
|
+
const env = options.env ?? 'test';
|
|
74
|
+
|
|
75
|
+
let content;
|
|
76
|
+
try {
|
|
77
|
+
content = readP12(buffer, password);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return { valid: false, error: `No se pudo leer el .p12: ${(e as Error).message}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cert = content.certificates[0];
|
|
83
|
+
const key = content.keys[0];
|
|
84
|
+
if (!cert || !key) {
|
|
85
|
+
return { valid: false, error: 'El .p12 no contiene certificado y clave privada.' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const certificate = decodeCertificate(cert);
|
|
89
|
+
// El emisor de la factura es el OBLIGADO (representado). Si el certificado es
|
|
90
|
+
// de un representante, el NIF del obligado está en `representedNif`.
|
|
91
|
+
const obligadoNif = certificate.representedNif ?? certificate.nif ?? '';
|
|
92
|
+
const nombreRazon = certificate.nombreRazon ?? '';
|
|
93
|
+
|
|
94
|
+
const client = new VerifactuClient({
|
|
95
|
+
env,
|
|
96
|
+
cert,
|
|
97
|
+
key,
|
|
98
|
+
timeoutMs: options.timeoutMs ?? 15_000,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Factura de prueba mínima para validar el certificado end-to-end.
|
|
102
|
+
const now = new Date();
|
|
103
|
+
const numeroInstalacion = `VAL${Date.now()}`;
|
|
104
|
+
const invoice: Invoice = {
|
|
105
|
+
id: { serie: 'T', numero: `VAL${Date.now()}`, fechaExpedicion: toDdMmYyyy(now) },
|
|
106
|
+
emisor: { nif: obligadoNif, nombreRazon },
|
|
107
|
+
tipoFactura: 'F1',
|
|
108
|
+
tipoDesglose: 'S1',
|
|
109
|
+
descripcion: 'Factura de prueba de validación de certificado',
|
|
110
|
+
destinatarios: [{ nombreRazon, nif: obligadoNif }],
|
|
111
|
+
desglose: {
|
|
112
|
+
detalles: [
|
|
113
|
+
{ calificacionOperacion: 'S1', claveRegimen: '01', tipoImpositivo: 21, baseImponible: 1, cuotaRepercutida: 0.21 },
|
|
114
|
+
],
|
|
115
|
+
cuotaTotal: 0.21,
|
|
116
|
+
importeTotal: 1.21,
|
|
117
|
+
},
|
|
118
|
+
sistemaInformatico: {
|
|
119
|
+
nombreRazon,
|
|
120
|
+
nif: obligadoNif,
|
|
121
|
+
nombreSistemaInformatico: 'VALIDACION',
|
|
122
|
+
idSistemaInformatico: '01',
|
|
123
|
+
version: '1.0',
|
|
124
|
+
numeroInstalacion,
|
|
125
|
+
},
|
|
126
|
+
fechaHoraHusoGenRegistro: nowIsoWithOffset(),
|
|
127
|
+
huella: '',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// El hash se calcula sobre el MISMO `fechaHoraHusoGenRegistro` que va en el
|
|
132
|
+
// XML (si no, la huella no coincide con el contenido y AEAT lo rechaza).
|
|
133
|
+
const hash = computeRegistroAlta({
|
|
134
|
+
idEmisorFactura: obligadoNif,
|
|
135
|
+
numSerieFactura: `${invoice.id.serie ?? ''}${invoice.id.numero}`,
|
|
136
|
+
fechaExpedicionFactura: invoice.id.fechaExpedicion,
|
|
137
|
+
tipoFactura: invoice.tipoFactura,
|
|
138
|
+
cuotaTotal: invoice.desglose.cuotaTotal.toFixed(2),
|
|
139
|
+
importeTotal: invoice.desglose.importeTotal.toFixed(2),
|
|
140
|
+
huellaAnterior: null,
|
|
141
|
+
fechaHoraHusoGenRegistro: invoice.fechaHoraHusoGenRegistro,
|
|
142
|
+
});
|
|
143
|
+
invoice.huella = hash;
|
|
144
|
+
|
|
145
|
+
const registroAltaXml = generateInvoiceXml(invoice, null);
|
|
146
|
+
const cabecera = buildCabecera({ nombreRazon, nif: obligadoNif }, cert, extractCertificateInfo);
|
|
147
|
+
const xml = generateSuministroXml(registroAltaXml, cabecera);
|
|
148
|
+
const result = await client.sendRecord(xml);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
valid: result.status === 'OK',
|
|
152
|
+
certificate,
|
|
153
|
+
aeat: {
|
|
154
|
+
status: result.status,
|
|
155
|
+
message: result.message,
|
|
156
|
+
csv: result.registro?.csv,
|
|
157
|
+
operationId: result.operationId,
|
|
158
|
+
},
|
|
159
|
+
message:
|
|
160
|
+
result.status === 'OK'
|
|
161
|
+
? `Certificado aceptado por AEAT (${env}). Factura de prueba enviada.`
|
|
162
|
+
: `AEAT respondió con error: ${result.message ?? result.errorCode ?? ''}`,
|
|
163
|
+
};
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
valid: false,
|
|
167
|
+
certificate,
|
|
168
|
+
error: `AEAT rechazó el certificado: ${(e as Error).message}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|