@devlas/dte-sii 2.5.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/BoletaService.js +109 -0
- package/CAF.js +173 -0
- package/CafSolicitor.js +380 -0
- package/Certificado.js +123 -0
- package/ConsumoFolio.js +376 -0
- package/DTE.js +399 -0
- package/EnviadorSII.js +1304 -0
- package/Envio.js +196 -0
- package/FolioRegistry.js +553 -0
- package/FolioService.js +703 -0
- package/LICENSE +27 -0
- package/LibroBase.js +134 -0
- package/LibroCompraVenta.js +205 -0
- package/LibroGuia.js +225 -0
- package/README.md +239 -0
- package/Signer.js +94 -0
- package/SiiCertificacion.js +1189 -0
- package/SiiPortalAuth.js +460 -0
- package/SiiSession.js +499 -0
- package/cert/BoletaCert.js +731 -0
- package/cert/CertFolioHelper.js +185 -0
- package/cert/CertRunner.js +2658 -0
- package/cert/ConfigLoader.js +133 -0
- package/cert/IntercambioCert.js +429 -0
- package/cert/LibroCompras.js +359 -0
- package/cert/LibroGuias.js +171 -0
- package/cert/LibroVentas.js +153 -0
- package/cert/MuestrasImpresas.js +676 -0
- package/cert/SetBase.js +321 -0
- package/cert/SetBasico.js +413 -0
- package/cert/SetCompra.js +472 -0
- package/cert/SetExenta.js +490 -0
- package/cert/SetGuia.js +283 -0
- package/cert/SetParser.js +1184 -0
- package/cert/SetsProvider.js +499 -0
- package/cert/Simulacion.js +521 -0
- package/cert/comunaOficina.js +460 -0
- package/cert/index.js +124 -0
- package/cert/types.js +330 -0
- package/dte-sii.d.ts +458 -0
- package/index.js +428 -0
- package/package.json +48 -0
- package/utils/c14n.js +275 -0
- package/utils/calculo.js +396 -0
- package/utils/config.js +276 -0
- package/utils/constants.js +302 -0
- package/utils/emisor.js +174 -0
- package/utils/endpoints.js +225 -0
- package/utils/error.js +235 -0
- package/utils/index.js +339 -0
- package/utils/logger.js +239 -0
- package/utils/pfx.js +203 -0
- package/utils/receptor.js +218 -0
- package/utils/referencia.js +169 -0
- package/utils/resolucion.js +119 -0
- package/utils/rut.js +169 -0
- package/utils/sanitize.js +124 -0
- package/utils/tokenCache.js +214 -0
- package/utils/xml.js +358 -0
- package/utils.js +4 -0
package/FolioRegistry.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* FolioRegistry.js - Registro local de folios
|
|
5
|
+
*
|
|
6
|
+
* Maneja el control de folios usados, reservados y enviados al SII.
|
|
7
|
+
* Proporciona persistencia en archivo JSON y control de conflictos.
|
|
8
|
+
*
|
|
9
|
+
* @module FolioRegistry
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clase para manejar el registro de folios
|
|
18
|
+
*/
|
|
19
|
+
class FolioRegistry {
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} options - Opciones de configuración
|
|
22
|
+
* @param {string} [options.registryPath] - Ruta al archivo de registro
|
|
23
|
+
* @param {string} [options.baseDir] - Directorio base para el registro
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
if (options.registryPath) {
|
|
27
|
+
this.registryPath = options.registryPath;
|
|
28
|
+
} else if (options.baseDir) {
|
|
29
|
+
this.registryPath = path.join(options.baseDir, 'debug', 'folios.json');
|
|
30
|
+
} else {
|
|
31
|
+
this.registryPath = path.resolve(__dirname, '..', '..', 'debug', 'folios.json');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Asegura que existe el directorio del registro
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
_ensureDir() {
|
|
40
|
+
const dir = path.dirname(this.registryPath);
|
|
41
|
+
if (!fs.existsSync(dir)) {
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Carga el registro desde disco
|
|
48
|
+
* @returns {Object}
|
|
49
|
+
*/
|
|
50
|
+
load() {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(this.registryPath)) {
|
|
53
|
+
return { version: 1, entries: {} };
|
|
54
|
+
}
|
|
55
|
+
const raw = fs.readFileSync(this.registryPath, 'utf8');
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.entries) {
|
|
58
|
+
return { version: 1, entries: {} };
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { version: 1, entries: {} };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Guarda el registro a disco
|
|
68
|
+
* @param {Object} registry - Datos del registro
|
|
69
|
+
*/
|
|
70
|
+
save(registry) {
|
|
71
|
+
this._ensureDir();
|
|
72
|
+
fs.writeFileSync(this.registryPath, JSON.stringify(registry, null, 2), 'utf8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Crea un fingerprint único para un CAF
|
|
77
|
+
* @param {string} cafXml - XML del CAF
|
|
78
|
+
* @returns {string|undefined}
|
|
79
|
+
*/
|
|
80
|
+
static createCafFingerprint(cafXml) {
|
|
81
|
+
if (!cafXml) return undefined;
|
|
82
|
+
return crypto.createHash('sha256').update(cafXml).digest('hex').slice(0, 12);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Construye la clave única para una entrada
|
|
87
|
+
* @param {Object} params - Parámetros
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
static buildKey({ rutEmisor, tipoDte, folioDesde, folioHasta, ambiente, cafFingerprint }) {
|
|
91
|
+
if (!rutEmisor || !tipoDte || !folioDesde || !folioHasta) {
|
|
92
|
+
throw new Error('Faltan datos para construir la clave del registro de folios');
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
String(rutEmisor),
|
|
96
|
+
String(tipoDte),
|
|
97
|
+
String(folioDesde),
|
|
98
|
+
String(folioHasta),
|
|
99
|
+
String(ambiente || ''),
|
|
100
|
+
String(cafFingerprint || ''),
|
|
101
|
+
].join('|');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Recolecta folios bloqueados (anulados)
|
|
106
|
+
* @param {Object} params - Parámetros de búsqueda
|
|
107
|
+
* @returns {Set<number>}
|
|
108
|
+
*/
|
|
109
|
+
collectBlockedFolios({ registry, rutEmisor, tipoDte, ambiente }) {
|
|
110
|
+
const blocked = new Set();
|
|
111
|
+
if (!registry || !registry.entries) return blocked;
|
|
112
|
+
|
|
113
|
+
Object.values(registry.entries).forEach((entry) => {
|
|
114
|
+
const meta = entry && entry.meta ? entry.meta : {};
|
|
115
|
+
if (String(meta.rutEmisor) !== String(rutEmisor)) return;
|
|
116
|
+
if (String(meta.tipoDte) !== String(tipoDte)) return;
|
|
117
|
+
if (String(meta.ambiente || '') !== String(ambiente || '')) return;
|
|
118
|
+
|
|
119
|
+
const nota = String(meta.nota || '').toLowerCase();
|
|
120
|
+
if (nota.includes('anulad')) {
|
|
121
|
+
(entry.usedFolios || []).forEach((folio) => blocked.add(Number(folio)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const sent = entry.sentFolios || {};
|
|
125
|
+
Object.keys(sent).forEach((folio) => {
|
|
126
|
+
if (sent[folio] && sent[folio].anulado) {
|
|
127
|
+
blocked.add(Number(folio));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return blocked;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reserva el siguiente folio disponible
|
|
137
|
+
* @param {Object} params - Parámetros
|
|
138
|
+
* @returns {number} - Folio reservado
|
|
139
|
+
*/
|
|
140
|
+
reserveNextFolio({
|
|
141
|
+
rutEmisor,
|
|
142
|
+
tipoDte,
|
|
143
|
+
folioDesde,
|
|
144
|
+
folioHasta,
|
|
145
|
+
ambiente,
|
|
146
|
+
cafFingerprint,
|
|
147
|
+
}) {
|
|
148
|
+
const desde = Number(folioDesde);
|
|
149
|
+
const hasta = Number(folioHasta);
|
|
150
|
+
|
|
151
|
+
if (!rutEmisor || !tipoDte || Number.isNaN(desde) || Number.isNaN(hasta)) {
|
|
152
|
+
throw new Error('Parámetros inválidos para reservar folio');
|
|
153
|
+
}
|
|
154
|
+
if (desde > hasta) {
|
|
155
|
+
throw new Error(`Rango de folios inválido (${desde}-${hasta})`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const registry = this.load();
|
|
159
|
+
const blocked = this.collectBlockedFolios({ registry, rutEmisor, tipoDte, ambiente });
|
|
160
|
+
const key = FolioRegistry.buildKey({ rutEmisor, tipoDte, folioDesde: desde, folioHasta: hasta, ambiente, cafFingerprint });
|
|
161
|
+
|
|
162
|
+
const entry = registry.entries[key] || {
|
|
163
|
+
usedFolios: [],
|
|
164
|
+
lastReserved: null,
|
|
165
|
+
meta: {
|
|
166
|
+
rutEmisor,
|
|
167
|
+
tipoDte,
|
|
168
|
+
folioDesde: desde,
|
|
169
|
+
folioHasta: hasta,
|
|
170
|
+
ambiente: ambiente || null,
|
|
171
|
+
cafFingerprint: cafFingerprint || null,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const inRange = (folio) => Number(folio) >= desde && Number(folio) <= hasta;
|
|
176
|
+
const baseUsed = (entry.usedFolios || []).filter(inRange).map((f) => Number(f));
|
|
177
|
+
const usedSet = new Set(baseUsed);
|
|
178
|
+
const sentFolios = entry.sentFolios || {};
|
|
179
|
+
|
|
180
|
+
// Folios reutilizables: reservados pero no enviados ni bloqueados
|
|
181
|
+
const reusable = baseUsed
|
|
182
|
+
.filter((folio) => !sentFolios[String(folio)] && !blocked.has(Number(folio)))
|
|
183
|
+
.sort((a, b) => a - b);
|
|
184
|
+
|
|
185
|
+
let next = reusable.length
|
|
186
|
+
? reusable[0]
|
|
187
|
+
: (entry.lastReserved ? Math.max(desde, Number(entry.lastReserved) + 1) : desde);
|
|
188
|
+
|
|
189
|
+
while (usedSet.has(next) || blocked.has(next)) {
|
|
190
|
+
next += 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (next > hasta) {
|
|
194
|
+
throw new Error(`No hay más folios disponibles (${desde}-${hasta})`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
usedSet.add(next);
|
|
198
|
+
entry.usedFolios = Array.from(usedSet).filter(inRange).sort((a, b) => a - b);
|
|
199
|
+
entry.lastReserved = entry.usedFolios.length ? Math.max(...entry.usedFolios) : null;
|
|
200
|
+
entry.updatedAt = new Date().toISOString();
|
|
201
|
+
|
|
202
|
+
registry.entries[key] = entry;
|
|
203
|
+
this.save(registry);
|
|
204
|
+
|
|
205
|
+
return next;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Marca un folio como enviado al SII
|
|
210
|
+
* @param {Object} params - Parámetros
|
|
211
|
+
*/
|
|
212
|
+
markFolioSent({
|
|
213
|
+
rutEmisor,
|
|
214
|
+
tipoDte,
|
|
215
|
+
folio,
|
|
216
|
+
folioDesde,
|
|
217
|
+
folioHasta,
|
|
218
|
+
ambiente,
|
|
219
|
+
cafFingerprint,
|
|
220
|
+
trackId,
|
|
221
|
+
sentAt,
|
|
222
|
+
extra,
|
|
223
|
+
}) {
|
|
224
|
+
const desde = Number(folioDesde);
|
|
225
|
+
const hasta = Number(folioHasta);
|
|
226
|
+
const folioNum = Number(folio);
|
|
227
|
+
|
|
228
|
+
if (!rutEmisor || !tipoDte || Number.isNaN(desde) || Number.isNaN(hasta) || Number.isNaN(folioNum)) {
|
|
229
|
+
throw new Error('Parámetros inválidos para marcar folio como enviado');
|
|
230
|
+
}
|
|
231
|
+
if (folioNum < desde || folioNum > hasta) {
|
|
232
|
+
throw new Error(`Folio ${folioNum} fuera de rango CAF (${desde}-${hasta})`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const registry = this.load();
|
|
236
|
+
const key = FolioRegistry.buildKey({ rutEmisor, tipoDte, folioDesde: desde, folioHasta: hasta, ambiente, cafFingerprint });
|
|
237
|
+
|
|
238
|
+
const entry = registry.entries[key] || {
|
|
239
|
+
usedFolios: [],
|
|
240
|
+
lastReserved: null,
|
|
241
|
+
sentFolios: {},
|
|
242
|
+
meta: {
|
|
243
|
+
rutEmisor,
|
|
244
|
+
tipoDte,
|
|
245
|
+
folioDesde: desde,
|
|
246
|
+
folioHasta: hasta,
|
|
247
|
+
ambiente: ambiente || null,
|
|
248
|
+
cafFingerprint: cafFingerprint || null,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (!entry.usedFolios) entry.usedFolios = [];
|
|
253
|
+
if (!entry.sentFolios) entry.sentFolios = {};
|
|
254
|
+
|
|
255
|
+
if (!entry.usedFolios.includes(folioNum)) {
|
|
256
|
+
entry.usedFolios.push(folioNum);
|
|
257
|
+
entry.usedFolios.sort((a, b) => a - b);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
entry.sentFolios[String(folioNum)] = {
|
|
261
|
+
trackId: trackId || null,
|
|
262
|
+
sentAt: sentAt || new Date().toISOString(),
|
|
263
|
+
...(extra ? { extra } : {}),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
entry.updatedAt = new Date().toISOString();
|
|
267
|
+
registry.entries[key] = entry;
|
|
268
|
+
this.save(registry);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Libera folios del registro
|
|
273
|
+
* @param {Object} params - Parámetros
|
|
274
|
+
*/
|
|
275
|
+
releaseFolios({
|
|
276
|
+
rutEmisor,
|
|
277
|
+
tipoDte,
|
|
278
|
+
folios,
|
|
279
|
+
folioDesde,
|
|
280
|
+
folioHasta,
|
|
281
|
+
ambiente,
|
|
282
|
+
cafFingerprint,
|
|
283
|
+
}) {
|
|
284
|
+
const desde = Number(folioDesde);
|
|
285
|
+
const hasta = Number(folioHasta);
|
|
286
|
+
|
|
287
|
+
if (!rutEmisor || !tipoDte || Number.isNaN(desde) || Number.isNaN(hasta)) {
|
|
288
|
+
throw new Error('Parámetros inválidos para liberar folios');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const foliosNum = (Array.isArray(folios) ? folios : [folios])
|
|
292
|
+
.map((f) => Number(f))
|
|
293
|
+
.filter((f) => !Number.isNaN(f));
|
|
294
|
+
|
|
295
|
+
if (!foliosNum.length) return;
|
|
296
|
+
|
|
297
|
+
const registry = this.load();
|
|
298
|
+
const key = FolioRegistry.buildKey({ rutEmisor, tipoDte, folioDesde: desde, folioHasta: hasta, ambiente, cafFingerprint });
|
|
299
|
+
const entry = registry.entries[key];
|
|
300
|
+
|
|
301
|
+
if (!entry) return;
|
|
302
|
+
|
|
303
|
+
const toRemove = new Set(foliosNum);
|
|
304
|
+
const inRange = (f) => Number(f) >= desde && Number(f) <= hasta;
|
|
305
|
+
|
|
306
|
+
entry.usedFolios = (entry.usedFolios || [])
|
|
307
|
+
.filter((f) => !toRemove.has(Number(f)))
|
|
308
|
+
.filter(inRange)
|
|
309
|
+
.map((f) => Number(f));
|
|
310
|
+
|
|
311
|
+
if (entry.sentFolios) {
|
|
312
|
+
Object.keys(entry.sentFolios).forEach((folioKey) => {
|
|
313
|
+
if (toRemove.has(Number(folioKey))) {
|
|
314
|
+
delete entry.sentFolios[folioKey];
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
entry.lastReserved = entry.usedFolios.length ? Math.max(...entry.usedFolios) : null;
|
|
320
|
+
entry.updatedAt = new Date().toISOString();
|
|
321
|
+
registry.entries[key] = entry;
|
|
322
|
+
this.save(registry);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Obtiene folios restantes disponibles
|
|
327
|
+
* @param {Object} params - Parámetros
|
|
328
|
+
* @returns {number}
|
|
329
|
+
*/
|
|
330
|
+
getRemainingFolios({
|
|
331
|
+
rutEmisor,
|
|
332
|
+
tipoDte,
|
|
333
|
+
folioDesde,
|
|
334
|
+
folioHasta,
|
|
335
|
+
ambiente,
|
|
336
|
+
cafFingerprint,
|
|
337
|
+
}) {
|
|
338
|
+
const desde = Number(folioDesde);
|
|
339
|
+
const hasta = Number(folioHasta);
|
|
340
|
+
const total = hasta - desde + 1;
|
|
341
|
+
|
|
342
|
+
if (!fs.existsSync(this.registryPath)) {
|
|
343
|
+
return total;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const registry = this.load();
|
|
348
|
+
const key = FolioRegistry.buildKey({
|
|
349
|
+
rutEmisor,
|
|
350
|
+
tipoDte,
|
|
351
|
+
folioDesde: desde,
|
|
352
|
+
folioHasta: hasta,
|
|
353
|
+
ambiente,
|
|
354
|
+
cafFingerprint
|
|
355
|
+
});
|
|
356
|
+
const entry = registry.entries[key];
|
|
357
|
+
const used = entry && Array.isArray(entry.usedFolios) ? entry.usedFolios.length : 0;
|
|
358
|
+
return Math.max(0, total - used);
|
|
359
|
+
} catch (_) {
|
|
360
|
+
return total;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Obtiene estadísticas del registro para un tipo de DTE
|
|
366
|
+
* @param {Object} params - Parámetros
|
|
367
|
+
* @returns {Object}
|
|
368
|
+
*/
|
|
369
|
+
getStats({ rutEmisor, tipoDte, ambiente }) {
|
|
370
|
+
const registry = this.load();
|
|
371
|
+
const stats = {
|
|
372
|
+
totalEntries: 0,
|
|
373
|
+
totalUsed: 0,
|
|
374
|
+
totalSent: 0,
|
|
375
|
+
ranges: [],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
Object.entries(registry.entries).forEach(([key, entry]) => {
|
|
379
|
+
const meta = entry.meta || {};
|
|
380
|
+
if (String(meta.rutEmisor) !== String(rutEmisor)) return;
|
|
381
|
+
if (String(meta.tipoDte) !== String(tipoDte)) return;
|
|
382
|
+
if (ambiente && String(meta.ambiente || '') !== String(ambiente)) return;
|
|
383
|
+
|
|
384
|
+
stats.totalEntries += 1;
|
|
385
|
+
stats.totalUsed += (entry.usedFolios || []).length;
|
|
386
|
+
stats.totalSent += Object.keys(entry.sentFolios || {}).length;
|
|
387
|
+
stats.ranges.push({
|
|
388
|
+
folioDesde: meta.folioDesde,
|
|
389
|
+
folioHasta: meta.folioHasta,
|
|
390
|
+
used: (entry.usedFolios || []).length,
|
|
391
|
+
sent: Object.keys(entry.sentFolios || {}).length,
|
|
392
|
+
remaining: (meta.folioHasta - meta.folioDesde + 1) - (entry.usedFolios || []).length,
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return stats;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Busca el CAF más reciente para un tipo de DTE en un directorio
|
|
401
|
+
* @param {number} tipoDte - Tipo de DTE (OBLIGATORIO)
|
|
402
|
+
* @param {string} [cafDir] - Directorio base de CAFs (legacy)
|
|
403
|
+
* @param {string} rutEmisor - RUT del emisor (OBLIGATORIO)
|
|
404
|
+
* @param {string} ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
|
|
405
|
+
* @param {string} [baseDir] - Directorio base del proyecto (para multi-tenant)
|
|
406
|
+
* @returns {string|null} - Ruta al CAF o null
|
|
407
|
+
*/
|
|
408
|
+
static findLatestCaf(tipoDte, cafDir, rutEmisor, ambiente, baseDir = null) {
|
|
409
|
+
// Validar parámetros obligatorios
|
|
410
|
+
if (!tipoDte) throw new Error('findLatestCaf: tipoDte es obligatorio');
|
|
411
|
+
if (!rutEmisor) throw new Error('findLatestCaf: rutEmisor es obligatorio');
|
|
412
|
+
if (!ambiente) throw new Error('findLatestCaf: ambiente es obligatorio');
|
|
413
|
+
if (!['certificacion', 'produccion'].includes(ambiente)) {
|
|
414
|
+
throw new Error(`findLatestCaf: ambiente inválido "${ambiente}"`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const base = baseDir || process.cwd();
|
|
418
|
+
|
|
419
|
+
// Buscar en estructura organizada: debug/caf/<ambiente>/<rut>/<tipoDte>/<fecha>/
|
|
420
|
+
const rutClean = rutEmisor.replace(/\./g, '').toUpperCase();
|
|
421
|
+
const organizedDir = path.resolve(base, 'debug', 'caf', ambiente, rutClean, String(tipoDte));
|
|
422
|
+
|
|
423
|
+
if (fs.existsSync(organizedDir)) {
|
|
424
|
+
// Obtener todas las subcarpetas de fecha, ordenadas de más reciente a más antigua
|
|
425
|
+
const dateDirs = fs.readdirSync(organizedDir)
|
|
426
|
+
.filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d))
|
|
427
|
+
.map((d) => path.join(organizedDir, d))
|
|
428
|
+
.filter((d) => fs.statSync(d).isDirectory())
|
|
429
|
+
.sort((a, b) => b.localeCompare(a)); // Ordenar fechas descendente
|
|
430
|
+
|
|
431
|
+
const matches = [];
|
|
432
|
+
|
|
433
|
+
// Buscar CAFs en cada carpeta de fecha
|
|
434
|
+
for (const dateDir of dateDirs) {
|
|
435
|
+
const files = fs.readdirSync(dateDir)
|
|
436
|
+
.filter((f) => f.endsWith('.xml') && f.startsWith(`caf-${tipoDte}-`))
|
|
437
|
+
.map((f) => path.join(dateDir, f));
|
|
438
|
+
|
|
439
|
+
files.forEach((filePath) => {
|
|
440
|
+
try {
|
|
441
|
+
const xml = fs.readFileSync(filePath, 'utf8');
|
|
442
|
+
if (!xml.includes('<AUTORIZACION')) return;
|
|
443
|
+
const stat = fs.statSync(filePath);
|
|
444
|
+
matches.push({ filePath, mtime: stat.mtimeMs });
|
|
445
|
+
} catch (_) {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (matches.length) {
|
|
452
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
453
|
+
return matches[0].filePath;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Fallback: buscar en debug/auto-caf (legacy)
|
|
458
|
+
const dir = cafDir || path.resolve(base, 'debug', 'auto-caf');
|
|
459
|
+
if (!fs.existsSync(dir)) return null;
|
|
460
|
+
|
|
461
|
+
const files = fs.readdirSync(dir)
|
|
462
|
+
.filter((f) => f.endsWith('.xml') && f.startsWith('caf-solicitar-'))
|
|
463
|
+
.map((f) => path.join(dir, f));
|
|
464
|
+
|
|
465
|
+
const matches = [];
|
|
466
|
+
files.forEach((filePath) => {
|
|
467
|
+
try {
|
|
468
|
+
const xml = fs.readFileSync(filePath, 'utf8');
|
|
469
|
+
const tdMatch = xml.match(/<TD>(\d+)<\/TD>/i);
|
|
470
|
+
if (tdMatch && Number(tdMatch[1]) === Number(tipoDte)) {
|
|
471
|
+
const stat = fs.statSync(filePath);
|
|
472
|
+
matches.push({ filePath, mtime: stat.mtimeMs });
|
|
473
|
+
}
|
|
474
|
+
} catch (_) {
|
|
475
|
+
// ignore
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (!matches.length) return null;
|
|
480
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
481
|
+
return matches[0].filePath;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Resuelve la ruta de un CAF, buscando uno disponible si es necesario
|
|
486
|
+
* @param {Object} params - Parámetros
|
|
487
|
+
* @param {number} params.tipoDte - Tipo de DTE (OBLIGATORIO)
|
|
488
|
+
* @param {string} params.rutEmisor - RUT del emisor (OBLIGATORIO)
|
|
489
|
+
* @param {string} params.ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
|
|
490
|
+
* @param {string} [params.cafPath] - Ruta explícita al CAF
|
|
491
|
+
* @param {number} [params.requiredCount=1] - Folios mínimos requeridos
|
|
492
|
+
* @param {string} [params.cafDir] - Directorio de CAFs (legacy)
|
|
493
|
+
* @param {string} [params.baseDir] - Directorio base del proyecto (para multi-tenant)
|
|
494
|
+
* @param {string} [params.registryPath] - Ruta al registro de folios
|
|
495
|
+
* @returns {string|null} - Ruta al CAF o null
|
|
496
|
+
*/
|
|
497
|
+
static resolveCafPath({
|
|
498
|
+
tipoDte,
|
|
499
|
+
rutEmisor,
|
|
500
|
+
ambiente,
|
|
501
|
+
cafPath = null,
|
|
502
|
+
requiredCount = 1,
|
|
503
|
+
cafDir,
|
|
504
|
+
baseDir = null,
|
|
505
|
+
registryPath = null,
|
|
506
|
+
}) {
|
|
507
|
+
// Validar parámetros obligatorios
|
|
508
|
+
if (!tipoDte) throw new Error('resolveCafPath: tipoDte es obligatorio');
|
|
509
|
+
if (!rutEmisor) throw new Error('resolveCafPath: rutEmisor es obligatorio');
|
|
510
|
+
if (!ambiente) throw new Error('resolveCafPath: ambiente es obligatorio');
|
|
511
|
+
if (!['certificacion', 'produccion'].includes(ambiente)) {
|
|
512
|
+
throw new Error(`resolveCafPath: ambiente inválido "${ambiente}"`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const CAF = require('./CAF');
|
|
516
|
+
const registry = new FolioRegistry({ baseDir, registryPath });
|
|
517
|
+
|
|
518
|
+
let resolvedPath = cafPath;
|
|
519
|
+
|
|
520
|
+
// Si no hay CAF o no existe, buscar el más reciente (estructura organizada primero)
|
|
521
|
+
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
522
|
+
resolvedPath = FolioRegistry.findLatestCaf(tipoDte, cafDir, rutEmisor, ambiente, baseDir);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Validar si hay folios suficientes
|
|
526
|
+
if (resolvedPath && fs.existsSync(resolvedPath)) {
|
|
527
|
+
const cafXml = fs.readFileSync(resolvedPath, 'utf8');
|
|
528
|
+
const caf = new CAF(cafXml);
|
|
529
|
+
const cafFingerprint = FolioRegistry.createCafFingerprint(cafXml);
|
|
530
|
+
|
|
531
|
+
const remaining = registry.getRemainingFolios({
|
|
532
|
+
rutEmisor,
|
|
533
|
+
tipoDte: caf.getTipoDTE(),
|
|
534
|
+
folioDesde: caf.getFolioDesde(),
|
|
535
|
+
folioHasta: caf.getFolioHasta(),
|
|
536
|
+
ambiente,
|
|
537
|
+
cafFingerprint,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (remaining < requiredCount) {
|
|
541
|
+
// Intentar buscar otro CAF más nuevo
|
|
542
|
+
const newerCaf = FolioRegistry.findLatestCaf(tipoDte, cafDir, rutEmisor, ambiente, baseDir);
|
|
543
|
+
if (newerCaf && newerCaf !== resolvedPath) {
|
|
544
|
+
resolvedPath = newerCaf;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return resolvedPath;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
module.exports = FolioRegistry;
|