@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.
- package/dist/aeat/client.d.ts +57 -0
- package/dist/aeat/client.d.ts.map +1 -0
- package/dist/aeat/client.js +87 -0
- package/dist/aeat/client.js.map +1 -0
- package/dist/aeat/index.d.ts +2 -0
- package/dist/aeat/index.d.ts.map +1 -0
- package/dist/aeat/index.js +2 -0
- package/dist/aeat/index.js.map +1 -0
- package/dist/core/hash/chain.d.ts +97 -0
- package/dist/core/hash/chain.d.ts.map +1 -0
- package/dist/core/hash/chain.js +115 -0
- package/dist/core/hash/chain.js.map +1 -0
- package/dist/core/hash/index.d.ts +2 -0
- package/dist/core/hash/index.d.ts.map +1 -0
- package/dist/core/hash/index.js +2 -0
- package/dist/core/hash/index.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/models/event.d.ts +25 -0
- package/dist/core/models/event.d.ts.map +1 -0
- package/dist/core/models/event.js +2 -0
- package/dist/core/models/event.js.map +1 -0
- package/dist/core/models/index.d.ts +3 -0
- package/dist/core/models/index.d.ts.map +1 -0
- package/dist/core/models/index.js +3 -0
- package/dist/core/models/index.js.map +1 -0
- package/dist/core/models/invoice.d.ts +90 -0
- package/dist/core/models/invoice.d.ts.map +1 -0
- package/dist/core/models/invoice.js +2 -0
- package/dist/core/models/invoice.js.map +1 -0
- package/dist/core/qr/index.d.ts +2 -0
- package/dist/core/qr/index.d.ts.map +1 -0
- package/dist/core/qr/index.js +2 -0
- package/dist/core/qr/index.js.map +1 -0
- package/dist/core/qr/qr.d.ts +13 -0
- package/dist/core/qr/qr.d.ts.map +1 -0
- package/dist/core/qr/qr.js +16 -0
- package/dist/core/qr/qr.js.map +1 -0
- package/dist/core/xml/generator.d.ts +32 -0
- package/dist/core/xml/generator.d.ts.map +1 -0
- package/dist/core/xml/generator.js +103 -0
- package/dist/core/xml/generator.js.map +1 -0
- package/dist/core/xml/index.d.ts +2 -0
- package/dist/core/xml/index.d.ts.map +1 -0
- package/dist/core/xml/index.js +2 -0
- package/dist/core/xml/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/index.d.ts +3 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +3 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/repository.d.ts +77 -0
- package/dist/integration/repository.d.ts.map +1 -0
- package/dist/integration/repository.js +2 -0
- package/dist/integration/repository.js.map +1 -0
- package/dist/integration/service.d.ts +36 -0
- package/dist/integration/service.d.ts.map +1 -0
- package/dist/integration/service.js +140 -0
- package/dist/integration/service.js.map +1 -0
- package/package.json +58 -0
- package/readme.md +89 -0
- package/src/aeat/client.ts +137 -0
- package/src/aeat/index.ts +1 -0
- package/src/core/hash/chain.ts +169 -0
- package/src/core/hash/index.ts +1 -0
- package/src/core/index.ts +4 -0
- package/src/core/models/event.ts +25 -0
- package/src/core/models/index.ts +2 -0
- package/src/core/models/invoice.ts +105 -0
- package/src/core/qr/index.ts +1 -0
- package/src/core/qr/qr.ts +19 -0
- package/src/core/xml/generator.ts +133 -0
- package/src/core/xml/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/integration/index.ts +2 -0
- package/src/integration/repository.ts +86 -0
- package/src/integration/schema.sql +51 -0
- package/src/integration/service.ts +158 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cliente para los servicios web de AEAT (Verifactu).
|
|
3
|
+
*
|
|
4
|
+
* Capa de red, separada del núcleo puro. Depende de credenciales y entornos
|
|
5
|
+
* (producción vs. pruebas), por lo que no debe acoplarse a la lógica de
|
|
6
|
+
* negocio.
|
|
7
|
+
*
|
|
8
|
+
* NOTA: los endpoints y el formato de petición/respuesta deben ajustarse a
|
|
9
|
+
* la documentación oficial de la AEAT (remisión voluntaria / bajo
|
|
10
|
+
* requerimiento). Este cliente expone una implementación base con `fetch`
|
|
11
|
+
* y rutas configurables.
|
|
12
|
+
*/
|
|
13
|
+
export interface VerifactuClientConfig {
|
|
14
|
+
/** URL base del servicio AEAT. */
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
/** Token de autenticación (OAuth) o certificado. */
|
|
17
|
+
authToken?: string;
|
|
18
|
+
/** Ruta del endpoint de envío de registros. */
|
|
19
|
+
sendPath?: string;
|
|
20
|
+
/** Ruta del endpoint de consulta de estado. */
|
|
21
|
+
statusPath?: string;
|
|
22
|
+
/** Timeout de petición en ms. */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SendResult {
|
|
27
|
+
/** Identificador de la operación devuelto por AEAT. */
|
|
28
|
+
operationId: string;
|
|
29
|
+
/** Estado de la respuesta. */
|
|
30
|
+
status: 'OK' | 'ERROR';
|
|
31
|
+
/** Mensaje de la AEAT. */
|
|
32
|
+
message?: string;
|
|
33
|
+
/** Código de error de AEAT (si lo hay). */
|
|
34
|
+
errorCode?: string;
|
|
35
|
+
/** Cuerpo completo de la respuesta (para auditoría). */
|
|
36
|
+
rawResponse?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ResolvedConfig {
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
authToken?: string;
|
|
42
|
+
sendPath: string;
|
|
43
|
+
statusPath: string;
|
|
44
|
+
timeoutMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Cliente de servicios AEAT para Verifactu.
|
|
49
|
+
*/
|
|
50
|
+
export class VerifactuClient {
|
|
51
|
+
private readonly config: ResolvedConfig;
|
|
52
|
+
|
|
53
|
+
constructor(config: VerifactuClientConfig) {
|
|
54
|
+
this.config = {
|
|
55
|
+
sendPath: '/verifactu/registro',
|
|
56
|
+
statusPath: '/verifactu/estado',
|
|
57
|
+
timeoutMs: 10_000,
|
|
58
|
+
...config,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Envía un registro (factura o evento) a AEAT.
|
|
64
|
+
*/
|
|
65
|
+
async sendRecord(xml: string): Promise<SendResult> {
|
|
66
|
+
const url = `${this.config.baseUrl}${this.config.sendPath}`;
|
|
67
|
+
const headers: Record<string, string> = {
|
|
68
|
+
'Content-Type': 'application/xml',
|
|
69
|
+
Accept: 'application/json',
|
|
70
|
+
};
|
|
71
|
+
if (this.config.authToken) {
|
|
72
|
+
headers.Authorization = `Bearer ${this.config.authToken}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers,
|
|
81
|
+
body: xml,
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
const raw = await res.text();
|
|
85
|
+
return this.parseResponse(raw, res.status);
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Consulta el estado de una operación enviada.
|
|
93
|
+
*/
|
|
94
|
+
async getOperationStatus(operationId: string): Promise<SendResult> {
|
|
95
|
+
const url = `${this.config.baseUrl}${this.config.statusPath}/${encodeURIComponent(operationId)}`;
|
|
96
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
97
|
+
if (this.config.authToken) {
|
|
98
|
+
headers.Authorization = `Bearer ${this.config.authToken}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(url, { method: 'GET', headers, signal: controller.signal });
|
|
105
|
+
const raw = await res.text();
|
|
106
|
+
return this.parseResponse(raw, res.status);
|
|
107
|
+
} finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parsea la respuesta de AEAT. La estructura exacta depende del servicio;
|
|
114
|
+
* este es un mapeo base que el consumidor puede sobrescribir.
|
|
115
|
+
*/
|
|
116
|
+
protected parseResponse(raw: string, httpStatus: number): SendResult {
|
|
117
|
+
let operationId = '';
|
|
118
|
+
let status: 'OK' | 'ERROR' = httpStatus >= 200 && httpStatus < 300 ? 'OK' : 'ERROR';
|
|
119
|
+
let message: string | undefined;
|
|
120
|
+
let errorCode: string | undefined;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const json = JSON.parse(raw) as Record<string, unknown>;
|
|
124
|
+
operationId = String(json.operationId ?? json.id ?? '');
|
|
125
|
+
if (json.status === 'OK' || json.status === 'ERROR') {
|
|
126
|
+
status = json.status;
|
|
127
|
+
}
|
|
128
|
+
message = json.message as string | undefined;
|
|
129
|
+
errorCode = json.errorCode as string | undefined;
|
|
130
|
+
} catch {
|
|
131
|
+
// Respuesta no JSON: se conserva el cuerpo como mensaje.
|
|
132
|
+
message = raw;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { operationId, status, message, errorCode, rawResponse: raw };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './client.js';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calcula el hash SHA-256 de un dato en hexadecimal MAYÚSCULA.
|
|
5
|
+
* Verifactu exige el hash en hexadecimal en mayúsculas (64 caracteres).
|
|
6
|
+
*/
|
|
7
|
+
export function sha256(data: string | Buffer): string {
|
|
8
|
+
return createHash('sha256').update(data).digest('hex').toUpperCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Concatena los campos de un registro en el formato Verifactu:
|
|
13
|
+
* `nombreCampo1=valor1&nombreCampo2=valor2&...`
|
|
14
|
+
*
|
|
15
|
+
* Reglas de normalización (AEAT):
|
|
16
|
+
* - Se eliminan los espacios al inicio y final de cada valor (trim).
|
|
17
|
+
* - Los campos vacíos o ausentes se incluyen como `nombreCampo=` (sin valor).
|
|
18
|
+
* - No se aplica URL-encoding ni escaping a caracteres especiales.
|
|
19
|
+
*/
|
|
20
|
+
export function concatenateFields(fields: Array<[string, string | null | undefined]>): string {
|
|
21
|
+
return fields
|
|
22
|
+
.map(([name, value]) => {
|
|
23
|
+
const trimmed = value ? value.trim() : '';
|
|
24
|
+
return `${name}=${trimmed}`;
|
|
25
|
+
})
|
|
26
|
+
.join('&');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Campos de un registro de alta (RegistroAlta) para el cálculo de la huella.
|
|
31
|
+
*/
|
|
32
|
+
export interface RegistroAltaInput {
|
|
33
|
+
/** Identificador del emisor (NIF). */
|
|
34
|
+
idEmisorFactura: string;
|
|
35
|
+
/** Número y serie de la factura. */
|
|
36
|
+
numSerieFactura: string;
|
|
37
|
+
/** Fecha de expedición de la factura. */
|
|
38
|
+
fechaExpedicionFactura: string;
|
|
39
|
+
/** Tipo de factura (F1, F2, ...). */
|
|
40
|
+
tipoFactura: string;
|
|
41
|
+
/** Cuota total. */
|
|
42
|
+
cuotaTotal: string;
|
|
43
|
+
/** Importe total. */
|
|
44
|
+
importeTotal: string;
|
|
45
|
+
/** Huella (hash) del registro anterior, o null para el primero. */
|
|
46
|
+
huellaAnterior: string | null;
|
|
47
|
+
/** Fecha/hora de generación con huso horario. */
|
|
48
|
+
fechaHoraHusoGenRegistro: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Campos de un registro de anulación (RegistroAnulacion).
|
|
53
|
+
*/
|
|
54
|
+
export interface RegistroAnulacionInput {
|
|
55
|
+
/** Identificador del emisor de la factura anulada. */
|
|
56
|
+
idEmisorFacturaAnulada: string;
|
|
57
|
+
/** Número y serie de la factura anulada. */
|
|
58
|
+
numSerieFacturaAnulada: string;
|
|
59
|
+
/** Fecha de expedición de la factura anulada. */
|
|
60
|
+
fechaExpedicionFacturaAnulada: string;
|
|
61
|
+
/** Huella (hash) del registro anterior. */
|
|
62
|
+
huellaAnterior: string | null;
|
|
63
|
+
/** Fecha/hora de generación con huso horario. */
|
|
64
|
+
fechaHoraHusoGenRegistro: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Construye la cadena canónica a hashear de un registro de alta.
|
|
69
|
+
*/
|
|
70
|
+
export function concatenateRegistroAlta(input: RegistroAltaInput): string {
|
|
71
|
+
return concatenateFields([
|
|
72
|
+
['IDEmisorFactura', input.idEmisorFactura],
|
|
73
|
+
['NumSerieFactura', input.numSerieFactura],
|
|
74
|
+
['FechaExpedicionFactura', input.fechaExpedicionFactura],
|
|
75
|
+
['TipoFactura', input.tipoFactura],
|
|
76
|
+
['CuotaTotal', input.cuotaTotal],
|
|
77
|
+
['ImporteTotal', input.importeTotal],
|
|
78
|
+
['Huella', input.huellaAnterior],
|
|
79
|
+
['FechaHoraHusoGenRegistro', input.fechaHoraHusoGenRegistro],
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Construye la cadena canónica a hashear de un registro de anulación.
|
|
85
|
+
*/
|
|
86
|
+
export function concatenateRegistroAnulacion(input: RegistroAnulacionInput): string {
|
|
87
|
+
return concatenateFields([
|
|
88
|
+
['IDEmisorFacturaAnulada', input.idEmisorFacturaAnulada],
|
|
89
|
+
['NumSerieFacturaAnulada', input.numSerieFacturaAnulada],
|
|
90
|
+
['FechaExpedicionFacturaAnulada', input.fechaExpedicionFacturaAnulada],
|
|
91
|
+
['Huella', input.huellaAnterior],
|
|
92
|
+
['FechaHoraHusoGenRegistro', input.fechaHoraHusoGenRegistro],
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Calcula la huella (hash) de un registro de alta.
|
|
98
|
+
*/
|
|
99
|
+
export function computeRegistroAlta(input: RegistroAltaInput): string {
|
|
100
|
+
return sha256(concatenateRegistroAlta(input));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Calcula la huella (hash) de un registro de anulación.
|
|
105
|
+
*/
|
|
106
|
+
export function computeRegistroAnulacion(input: RegistroAnulacionInput): string {
|
|
107
|
+
return sha256(concatenateRegistroAnulacion(input));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Verifica que el hash de un registro de alta coincide con el esperado.
|
|
112
|
+
*/
|
|
113
|
+
export function verifyRegistroAlta(input: RegistroAltaInput, expectedHash: string): boolean {
|
|
114
|
+
return computeRegistroAlta(input).toLowerCase() === expectedHash.toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Verifica que el hash de un registro de anulación coincide con el esperado.
|
|
119
|
+
*/
|
|
120
|
+
export function verifyRegistroAnulacion(
|
|
121
|
+
input: RegistroAnulacionInput,
|
|
122
|
+
expectedHash: string,
|
|
123
|
+
): boolean {
|
|
124
|
+
return computeRegistroAnulacion(input).toLowerCase() === expectedHash.toLowerCase();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Cadena de hash con estado, para encadenar registros de forma secuencial.
|
|
129
|
+
*/
|
|
130
|
+
export class HashChain {
|
|
131
|
+
private _lastHash: string | null = null;
|
|
132
|
+
private _count = 0;
|
|
133
|
+
|
|
134
|
+
/** Hash del último registro encadenado, o `null` si aún no hay ninguno. */
|
|
135
|
+
get lastHash(): string | null {
|
|
136
|
+
return this._lastHash;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Número de registros encadenados hasta ahora. */
|
|
140
|
+
get count(): number {
|
|
141
|
+
return this._count;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Encadena un nuevo registro de alta y devuelve su huella.
|
|
146
|
+
*/
|
|
147
|
+
pushAlta(input: Omit<RegistroAltaInput, 'huellaAnterior'>): string {
|
|
148
|
+
const hash = computeRegistroAlta({ ...input, huellaAnterior: this._lastHash });
|
|
149
|
+
this._lastHash = hash;
|
|
150
|
+
this._count += 1;
|
|
151
|
+
return hash;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Encadena un nuevo registro de anulación y devuelve su huella.
|
|
156
|
+
*/
|
|
157
|
+
pushAnulacion(input: Omit<RegistroAnulacionInput, 'huellaAnterior'>): string {
|
|
158
|
+
const hash = computeRegistroAnulacion({ ...input, huellaAnterior: this._lastHash });
|
|
159
|
+
this._lastHash = hash;
|
|
160
|
+
this._count += 1;
|
|
161
|
+
return hash;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Reinicia la cadena. */
|
|
165
|
+
reset(): void {
|
|
166
|
+
this._lastHash = null;
|
|
167
|
+
this._count = 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './chain.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datos identificativos del emisor de la factura.
|
|
3
|
+
* Verifactu exige identificar al emisor (NIF, nombre/razón social).
|
|
4
|
+
*/
|
|
5
|
+
export interface Emisor {
|
|
6
|
+
/** NIF del emisor. */
|
|
7
|
+
nif: string;
|
|
8
|
+
/** Nombre o razón social. */
|
|
9
|
+
nombreRazon: string;
|
|
10
|
+
/** Código de país (ISO 3166-1 alpha-2). Opcional si es España. */
|
|
11
|
+
pais?: string;
|
|
12
|
+
/** Código postal. */
|
|
13
|
+
codigoPostal?: string;
|
|
14
|
+
/** Municipio. */
|
|
15
|
+
municipio?: string;
|
|
16
|
+
/** Dirección. */
|
|
17
|
+
direccion?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Identificación de la factura.
|
|
22
|
+
*/
|
|
23
|
+
export interface FacturaId {
|
|
24
|
+
/** Serie de la factura (opcional). */
|
|
25
|
+
serie?: string;
|
|
26
|
+
/** Número de la factura. */
|
|
27
|
+
numero: string;
|
|
28
|
+
/** Fecha de expedición en formato ISO (YYYY-MM-DD). */
|
|
29
|
+
fechaExpedicion: string;
|
|
30
|
+
/** Fecha de operación (opcional). */
|
|
31
|
+
fechaOperacion?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tipo de factura Verifactu (F1–F9).
|
|
36
|
+
*/
|
|
37
|
+
export type TipoFactura =
|
|
38
|
+
| 'F1' // factura completa
|
|
39
|
+
| 'F2' // factura simplificada
|
|
40
|
+
| 'F3' // factura rectificativa
|
|
41
|
+
| 'F4' // factura recapitulativa
|
|
42
|
+
| 'F5' // factura de importación
|
|
43
|
+
| 'F6' // operaciones de seguro
|
|
44
|
+
| 'F7' // operaciones de crédito
|
|
45
|
+
| 'F8' // arrendamiento de bienes inmuebles
|
|
46
|
+
| 'F9'; // transporte de viajeros
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tipo de desglose Verifactu (S1–S6).
|
|
50
|
+
*/
|
|
51
|
+
export type TipoDesglose = 'S1' | 'S2' | 'S3' | 'S4' | 'S5' | 'S6';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detalle de IVA de una línea de desglose.
|
|
55
|
+
*/
|
|
56
|
+
export interface DetalleIva {
|
|
57
|
+
/** Tipo impositivo (%). */
|
|
58
|
+
tipoImpositivo: number;
|
|
59
|
+
/** Base imponible. */
|
|
60
|
+
baseImponible: number;
|
|
61
|
+
/** Cuota repercutida. */
|
|
62
|
+
cuotaRepercutida: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Desglose de la operación por tipo de operación e IVA.
|
|
67
|
+
*/
|
|
68
|
+
export interface DesgloseTipoOperacion {
|
|
69
|
+
/** Clave de tipo de operación (p. ej. '01'). */
|
|
70
|
+
tipoOperacion: string;
|
|
71
|
+
/** Detalles de IVA. */
|
|
72
|
+
detallesIva: DetalleIva[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Desglose completo de la factura.
|
|
77
|
+
*/
|
|
78
|
+
export interface Desglose {
|
|
79
|
+
/** Desglose por tipo de operación. */
|
|
80
|
+
tipoOperaciones: DesgloseTipoOperacion[];
|
|
81
|
+
/** Cuota total de la factura. */
|
|
82
|
+
cuotaTotal: number;
|
|
83
|
+
/** Importe total de la factura. */
|
|
84
|
+
importeTotal: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Factura en el modelo de datos Verifactu.
|
|
89
|
+
*/
|
|
90
|
+
export interface Invoice {
|
|
91
|
+
/** Identificación de la factura. */
|
|
92
|
+
id: FacturaId;
|
|
93
|
+
/** Emisor de la factura. */
|
|
94
|
+
emisor: Emisor;
|
|
95
|
+
/** Desglose de la factura. */
|
|
96
|
+
desglose: Desglose;
|
|
97
|
+
/** Tipo de factura. */
|
|
98
|
+
tipoFactura: TipoFactura;
|
|
99
|
+
/** Tipo de desglose. */
|
|
100
|
+
tipoDesglose: TipoDesglose;
|
|
101
|
+
/** Descripción de la operación. */
|
|
102
|
+
descripcion?: string;
|
|
103
|
+
/** Metadatos adicionales libres. */
|
|
104
|
+
metadata?: Record<string, unknown>;
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './qr.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construye el payload del QR Verifactu.
|
|
3
|
+
*
|
|
4
|
+
* El QR Verifactu codifica una URL de verificación que incluye el hash del
|
|
5
|
+
* registro, de modo que cualquier persona pueda verificar la integridad de
|
|
6
|
+
* la factura contra la AEAT.
|
|
7
|
+
*
|
|
8
|
+
* @param hash Hash del registro (encadenado).
|
|
9
|
+
* @param baseUrl URL base de verificación de la AEAT.
|
|
10
|
+
* @param extraParams Parámetros adicionales a incluir en la URL.
|
|
11
|
+
*/
|
|
12
|
+
export function buildQrPayload(
|
|
13
|
+
hash: string,
|
|
14
|
+
baseUrl = 'https://www2.agenciatributaria.gob.es/verifactu/verificar',
|
|
15
|
+
extraParams: Record<string, string> = {},
|
|
16
|
+
): string {
|
|
17
|
+
const params = new URLSearchParams({ hash, ...extraParams });
|
|
18
|
+
return `${baseUrl}?${params.toString()}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Invoice } from '../models/invoice.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Datos de la factura que se anula.
|
|
5
|
+
*/
|
|
6
|
+
export interface AnulacionInput {
|
|
7
|
+
/** NIF del emisor de la factura anulada. */
|
|
8
|
+
idEmisorFacturaAnulada: string;
|
|
9
|
+
/** Número y serie de la factura anulada. */
|
|
10
|
+
numSerieFacturaAnulada: string;
|
|
11
|
+
/** Fecha de expedición de la factura anulada. */
|
|
12
|
+
fechaExpedicionFacturaAnulada: string;
|
|
13
|
+
/** Huella (hash) de la factura anulada. */
|
|
14
|
+
huellaFacturaAnulada: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escapa caracteres especiales para XML.
|
|
19
|
+
*/
|
|
20
|
+
function escapeXml(value: string): string {
|
|
21
|
+
return value
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Formatea un número con 2 decimales (formato Verifactu).
|
|
31
|
+
*/
|
|
32
|
+
function formatAmount(value: number): string {
|
|
33
|
+
return value.toFixed(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Genera el XML Verifactu de una factura (registro de alta).
|
|
38
|
+
*
|
|
39
|
+
* Estructura basada en el RD 1007/2023 y la documentación técnica de la
|
|
40
|
+
* AEAT. NOTA: el esquema XSD oficial debe validarse antes de producción;
|
|
41
|
+
* este esqueleto refleja los bloques principales (IDFactura, IDEmisorFactura,
|
|
42
|
+
* TipoFactura, TipoDesglose, Desglose, CuotaTotal, Encadenamiento, Huella).
|
|
43
|
+
*/
|
|
44
|
+
export function generateInvoiceXml(invoice: Invoice, previousHash: string | null): string {
|
|
45
|
+
const { id, emisor, desglose, tipoFactura, tipoDesglose } = invoice;
|
|
46
|
+
|
|
47
|
+
const detalles = desglose.tipoOperaciones
|
|
48
|
+
.map((op) => {
|
|
49
|
+
const iva = op.detallesIva
|
|
50
|
+
.map(
|
|
51
|
+
(d) =>
|
|
52
|
+
` <DetalleIVA>` +
|
|
53
|
+
`<TipoImpositivo>${formatAmount(d.tipoImpositivo)}</TipoImpositivo>` +
|
|
54
|
+
`<BaseImponible>${formatAmount(d.baseImponible)}</BaseImponible>` +
|
|
55
|
+
`<CuotaRepercutida>${formatAmount(d.cuotaRepercutida)}</CuotaRepercutida>` +
|
|
56
|
+
`</DetalleIVA>`,
|
|
57
|
+
)
|
|
58
|
+
.join('\n');
|
|
59
|
+
return (
|
|
60
|
+
` <DesgloseTipoOperacion>` +
|
|
61
|
+
`<TipoOperacion>${escapeXml(op.tipoOperacion)}</TipoOperacion>` +
|
|
62
|
+
`\n <DesgloseIVA>\n${iva}\n </DesgloseIVA>` +
|
|
63
|
+
`\n </DesgloseTipoOperacion>`
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
.join('\n');
|
|
67
|
+
|
|
68
|
+
const xml = [
|
|
69
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
70
|
+
'<RegistroFactura>',
|
|
71
|
+
' <IDFactura>',
|
|
72
|
+
' <IDEmisorFactura>',
|
|
73
|
+
` <NIF>${escapeXml(emisor.nif)}</NIF>`,
|
|
74
|
+
` <NombreRazon>${escapeXml(emisor.nombreRazon)}</NombreRazon>`,
|
|
75
|
+
emisor.pais ? ` <Pais>${escapeXml(emisor.pais)}</Pais>` : '',
|
|
76
|
+
emisor.codigoPostal ? ` <CodigoPostal>${escapeXml(emisor.codigoPostal)}</CodigoPostal>` : '',
|
|
77
|
+
emisor.municipio ? ` <Municipio>${escapeXml(emisor.municipio)}</Municipio>` : '',
|
|
78
|
+
emisor.direccion ? ` <Direccion>${escapeXml(emisor.direccion)}</Direccion>` : '',
|
|
79
|
+
' </IDEmisorFactura>',
|
|
80
|
+
id.serie ? ` <SerieFactura>${escapeXml(id.serie)}</SerieFactura>` : '',
|
|
81
|
+
` <NumeroFactura>${escapeXml(id.numero)}</NumeroFactura>`,
|
|
82
|
+
` <FechaExpedicionFactura>${id.fechaExpedicion}</FechaExpedicionFactura>`,
|
|
83
|
+
id.fechaOperacion ? ` <FechaOperacion>${id.fechaOperacion}</FechaOperacion>` : '',
|
|
84
|
+
' </IDFactura>',
|
|
85
|
+
` <TipoFactura>${tipoFactura}</TipoFactura>`,
|
|
86
|
+
` <TipoDesglose>${tipoDesglose}</TipoDesglose>`,
|
|
87
|
+
' <Desglose>',
|
|
88
|
+
detalles,
|
|
89
|
+
' </Desglose>',
|
|
90
|
+
` <CuotaTotal>${formatAmount(desglose.cuotaTotal)}</CuotaTotal>`,
|
|
91
|
+
' <Encadenamiento>',
|
|
92
|
+
` <HashAnterior>${previousHash ?? ''}</HashAnterior>`,
|
|
93
|
+
' </Encadenamiento>',
|
|
94
|
+
'</RegistroFactura>',
|
|
95
|
+
]
|
|
96
|
+
.filter((line) => line !== '')
|
|
97
|
+
.join('\n');
|
|
98
|
+
|
|
99
|
+
return xml;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Genera el XML Verifactu de un registro de anulación.
|
|
104
|
+
*
|
|
105
|
+
* Referencia la factura anulada (IDFactura + Huella) y encadena con el hash
|
|
106
|
+
* anterior. Estructura basada en el RD 1007/2023; validar contra el XSD
|
|
107
|
+
* oficial antes de producción.
|
|
108
|
+
*/
|
|
109
|
+
export function generateAnulacionXml(
|
|
110
|
+
anulacion: AnulacionInput,
|
|
111
|
+
previousHash: string | null,
|
|
112
|
+
): string {
|
|
113
|
+
const xml = [
|
|
114
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
115
|
+
'<RegistroFactura>',
|
|
116
|
+
' <RegistroAnulacion>',
|
|
117
|
+
' <IDFactura>',
|
|
118
|
+
' <IDEmisorFactura>',
|
|
119
|
+
` <NIF>${escapeXml(anulacion.idEmisorFacturaAnulada)}</NIF>`,
|
|
120
|
+
' </IDEmisorFactura>',
|
|
121
|
+
` <NumeroFactura>${escapeXml(anulacion.numSerieFacturaAnulada)}</NumeroFactura>`,
|
|
122
|
+
` <FechaExpedicionFactura>${anulacion.fechaExpedicionFacturaAnulada}</FechaExpedicionFactura>`,
|
|
123
|
+
' </IDFactura>',
|
|
124
|
+
` <Huella>${anulacion.huellaFacturaAnulada}</Huella>`,
|
|
125
|
+
' </RegistroAnulacion>',
|
|
126
|
+
' <Encadenamiento>',
|
|
127
|
+
` <HashAnterior>${previousHash ?? ''}</HashAnterior>`,
|
|
128
|
+
' </Encadenamiento>',
|
|
129
|
+
'</RegistroFactura>',
|
|
130
|
+
].join('\n');
|
|
131
|
+
|
|
132
|
+
return xml;
|
|
133
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './generator.js';
|
package/src/index.ts
ADDED