@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/FolioService.js
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* FolioService.js - Servicio de gestión de folios
|
|
5
|
+
*
|
|
6
|
+
* Servicio principal para obtener, consultar y anular folios del SII.
|
|
7
|
+
* Integra SiiSession para la comunicación y FolioRegistry para el control local.
|
|
8
|
+
*
|
|
9
|
+
* @module FolioService
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const SiiSession = require('./SiiSession');
|
|
15
|
+
const FolioRegistry = require('./FolioRegistry');
|
|
16
|
+
const CAF = require('./CAF');
|
|
17
|
+
const CafSolicitor = require('./CafSolicitor');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Clase para gestión integral de folios
|
|
21
|
+
*/
|
|
22
|
+
class FolioService {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} options - Opciones de configuración
|
|
25
|
+
* @param {string} options.ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
|
|
26
|
+
* @param {string} options.rutEmisor - RUT del emisor (OBLIGATORIO)
|
|
27
|
+
* @param {Object} [options.certificado] - Instancia de Certificado
|
|
28
|
+
* @param {string} [options.pfxPath] - Ruta al archivo PFX
|
|
29
|
+
* @param {string} [options.pfxPassword] - Contraseña del PFX
|
|
30
|
+
* @param {string} [options.baseDir] - Directorio base del proyecto
|
|
31
|
+
* @param {string} [options.cafDir] - Directorio de CAFs
|
|
32
|
+
* @param {string} [options.debugDir] - Directorio de debug
|
|
33
|
+
* @param {string} [options.registryPath] - Ruta al registro de folios
|
|
34
|
+
* @param {string} [options.solicitarScript] - Script para solicitar CAFs
|
|
35
|
+
* @param {number} [options.retries=2] - Número de reintentos para solicitar CAF
|
|
36
|
+
* @param {number} [options.retryDelayMs=1500] - Delay entre reintentos en ms
|
|
37
|
+
* @param {boolean} [options.useRegistry=true] - Usar registro de folios
|
|
38
|
+
* @param {boolean} [options.singleRequest=false] - Solicitar CAFs uno a uno
|
|
39
|
+
*/
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
// Validar parámetros obligatorios (multi-tenant: nunca usar defaults de .env)
|
|
42
|
+
if (!options.ambiente) {
|
|
43
|
+
throw new Error('FolioService: options.ambiente es obligatorio');
|
|
44
|
+
}
|
|
45
|
+
if (!options.rutEmisor) {
|
|
46
|
+
throw new Error('FolioService: options.rutEmisor es obligatorio');
|
|
47
|
+
}
|
|
48
|
+
if (!['certificacion', 'produccion'].includes(options.ambiente)) {
|
|
49
|
+
throw new Error(`FolioService: ambiente inválido "${options.ambiente}", debe ser 'certificacion' o 'produccion'`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.ambiente = options.ambiente;
|
|
53
|
+
this.rutEmisor = options.rutEmisor;
|
|
54
|
+
this.baseDir = options.baseDir || path.resolve(__dirname, '..', '..');
|
|
55
|
+
|
|
56
|
+
// Directorios
|
|
57
|
+
this.cafDir = options.cafDir || path.join(this.baseDir, 'debug', 'auto-caf');
|
|
58
|
+
this.debugDir = options.debugDir || path.join(this.baseDir, 'debug');
|
|
59
|
+
|
|
60
|
+
// Sesión SII
|
|
61
|
+
this.session = new SiiSession({
|
|
62
|
+
ambiente: this.ambiente,
|
|
63
|
+
certificado: options.certificado,
|
|
64
|
+
pfxPath: options.pfxPath,
|
|
65
|
+
pfxPassword: options.pfxPassword,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Cargar sesión compartida si está disponible
|
|
69
|
+
this.sessionPath = options.sessionPath || process.env.SII_SESSION_PATH;
|
|
70
|
+
if (this.sessionPath) {
|
|
71
|
+
const loaded = this.session.loadSession(this.sessionPath);
|
|
72
|
+
if (loaded) {
|
|
73
|
+
console.log('[FolioService] ✓ Usando sesión compartida');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Registro de folios
|
|
78
|
+
this.registry = new FolioRegistry({
|
|
79
|
+
registryPath: options.registryPath,
|
|
80
|
+
baseDir: this.baseDir,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Solicitador de CAF interno (migrado de test-caf-solicitar.js)
|
|
84
|
+
this.cafSolicitor = null;
|
|
85
|
+
if (options.pfxPath && options.pfxPassword) {
|
|
86
|
+
this.cafSolicitor = new CafSolicitor({
|
|
87
|
+
ambiente: this.ambiente,
|
|
88
|
+
rutEmisor: this.rutEmisor,
|
|
89
|
+
pfxPath: options.pfxPath,
|
|
90
|
+
pfxPassword: options.pfxPassword,
|
|
91
|
+
baseDir: this.baseDir,
|
|
92
|
+
sessionPath: this.sessionPath,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Configuración (parámetros explícitos, sin process.env para multi-tenant)
|
|
97
|
+
this.config = {
|
|
98
|
+
retries: Number(options.retries ?? 2),
|
|
99
|
+
retryDelayMs: Number(options.retryDelayMs ?? 1500),
|
|
100
|
+
useRegistry: options.useRegistry !== false,
|
|
101
|
+
singleRequest: options.singleRequest === true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Busca el CAF más reciente para un tipo de DTE
|
|
107
|
+
* Busca recursivamente en cafDir y también en debug/caf/{ambiente}/{rut}/{tipo}/
|
|
108
|
+
* @param {number} tipoDte - Tipo de DTE
|
|
109
|
+
* @returns {string|null} - Ruta al CAF o null
|
|
110
|
+
*/
|
|
111
|
+
findLatestCaf(tipoDte) {
|
|
112
|
+
const matches = [];
|
|
113
|
+
|
|
114
|
+
// Helper para buscar recursivamente
|
|
115
|
+
const searchRecursive = (dir, depth = 0) => {
|
|
116
|
+
if (!fs.existsSync(dir) || depth > 5) return;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const fullPath = path.join(dir, entry.name);
|
|
122
|
+
if (entry.isDirectory()) {
|
|
123
|
+
searchRecursive(fullPath, depth + 1);
|
|
124
|
+
} else if (entry.isFile() && entry.name.endsWith('.xml')) {
|
|
125
|
+
try {
|
|
126
|
+
const xml = fs.readFileSync(fullPath, 'utf8');
|
|
127
|
+
const tdMatch = xml.match(/<TD>(\d+)<\/TD>/i);
|
|
128
|
+
if (tdMatch && Number(tdMatch[1]) === Number(tipoDte)) {
|
|
129
|
+
const stat = fs.statSync(fullPath);
|
|
130
|
+
matches.push({ filePath: fullPath, mtime: stat.mtimeMs });
|
|
131
|
+
}
|
|
132
|
+
} catch (_) {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Buscar en cafDir (auto-caf)
|
|
143
|
+
searchRecursive(this.cafDir);
|
|
144
|
+
|
|
145
|
+
// También buscar en el directorio de CAFs canónicos
|
|
146
|
+
const canonicalDir = path.join(this.debugDir, 'caf', this.ambiente, this.rutEmisor, String(tipoDte));
|
|
147
|
+
searchRecursive(canonicalDir);
|
|
148
|
+
|
|
149
|
+
if (!matches.length) return null;
|
|
150
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
151
|
+
return matches[0].filePath;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Solicita un nuevo CAF al SII
|
|
156
|
+
* @param {Object} params - Parámetros
|
|
157
|
+
* @returns {Promise<boolean>} - true si fue exitoso
|
|
158
|
+
*/
|
|
159
|
+
async solicitarCaf({ tipoDte, cantidad = 1 }) {
|
|
160
|
+
if (!this.cafSolicitor) {
|
|
161
|
+
throw new Error('FolioService: CafSolicitor no inicializado (se requiere pfxPath y pfxPassword)');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let attempt = 0;
|
|
165
|
+
|
|
166
|
+
while (attempt <= this.config.retries) {
|
|
167
|
+
try {
|
|
168
|
+
const result = await this.cafSolicitor.solicitar({ tipoDte, cantidad });
|
|
169
|
+
|
|
170
|
+
if (result.success) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(`[FolioService] Intento ${attempt + 1} fallido: ${result.error}`);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.log(`[FolioService] Error en intento ${attempt + 1}: ${err.message}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (attempt >= this.config.retries) {
|
|
180
|
+
throw new Error(`Fallo auto-CAF para tipo ${tipoDte}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await this._sleep(this.config.retryDelayMs);
|
|
184
|
+
attempt += 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sleep asíncrono
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
_sleep(ms) {
|
|
195
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Solicita CAF con fallback a cantidad menor
|
|
200
|
+
* @param {Object} params - Parámetros
|
|
201
|
+
* @returns {Promise<string|null>} - Ruta al CAF obtenido
|
|
202
|
+
*/
|
|
203
|
+
async solicitarCafConFallback({ tipoDte, cantidad, previousPath = null }) {
|
|
204
|
+
try {
|
|
205
|
+
await this.solicitarCaf({ tipoDte, cantidad });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Continuar con fallback
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let resolvedPath = this.findLatestCaf(tipoDte);
|
|
211
|
+
|
|
212
|
+
if (this.config.singleRequest) {
|
|
213
|
+
return resolvedPath;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Si no hay CAF nuevo, intentar anular folios pendientes y pedir de nuevo
|
|
217
|
+
if (!resolvedPath || resolvedPath === previousPath) {
|
|
218
|
+
try {
|
|
219
|
+
await this.anularFolios({ tipoDte });
|
|
220
|
+
await this.solicitarCaf({ tipoDte, cantidad: 1 });
|
|
221
|
+
resolvedPath = this.findLatestCaf(tipoDte);
|
|
222
|
+
} catch (_) {
|
|
223
|
+
// ignore
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Fallback a cantidad 1
|
|
228
|
+
const shouldFallback = (!resolvedPath || resolvedPath === previousPath) && Number(cantidad) > 1;
|
|
229
|
+
if (shouldFallback) {
|
|
230
|
+
try {
|
|
231
|
+
await this.solicitarCaf({ tipoDte, cantidad: 1 });
|
|
232
|
+
resolvedPath = this.findLatestCaf(tipoDte);
|
|
233
|
+
} catch (_) {
|
|
234
|
+
// ignore
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return resolvedPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Consulta el estado de folios en el SII
|
|
243
|
+
* @param {Object} params - Parámetros
|
|
244
|
+
* @returns {Promise<Object>}
|
|
245
|
+
*/
|
|
246
|
+
async consultarFolios({ tipoDte }) {
|
|
247
|
+
const { rut, dv } = SiiSession.parseRut(this.rutEmisor);
|
|
248
|
+
const debugStamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
249
|
+
const debugDir = path.join(this.debugDir, 'auto-caf', 'anulacion', debugStamp);
|
|
250
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
251
|
+
|
|
252
|
+
// Asegurar sesión
|
|
253
|
+
this.session.reset();
|
|
254
|
+
const page = await this.session.ensureSession('/cvc_cgi/dte/af_anular1');
|
|
255
|
+
|
|
256
|
+
if (!page.body || !page.body.includes('ANULACION DE FOLIOS')) {
|
|
257
|
+
fs.writeFileSync(path.join(debugDir, 'error.html'), page.body || '', 'utf8');
|
|
258
|
+
throw new Error('No se pudo acceder a la página de anulación.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Consultar folios
|
|
262
|
+
const hiddenInputs = SiiSession.extractInputValues(page.body);
|
|
263
|
+
const fields = {
|
|
264
|
+
...hiddenInputs,
|
|
265
|
+
RUT_EMP: rut,
|
|
266
|
+
DV_EMP: dv,
|
|
267
|
+
COD_DOCTO: String(tipoDte),
|
|
268
|
+
ACEPTAR: 'Consultar',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const consulta = await this.session.submitForm(
|
|
272
|
+
'/cvc_cgi/dte/af_anular2',
|
|
273
|
+
fields,
|
|
274
|
+
`https://${this.session.getBaseHost()}/cvc_cgi/dte/af_anular1`
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
let currentHtml = consulta.body || '';
|
|
278
|
+
let info = this._parseAnulacionTable(currentHtml);
|
|
279
|
+
let action = SiiSession.extractFormActionByName(currentHtml, 'frm') || '/cvc_cgi/dte/af_anular2';
|
|
280
|
+
let hiddenInputsConsulta = SiiSession.extractInputValues(currentHtml);
|
|
281
|
+
|
|
282
|
+
// Paginar resultados
|
|
283
|
+
let nextButton = this._findNextButton(currentHtml);
|
|
284
|
+
let safety = 0;
|
|
285
|
+
|
|
286
|
+
while (nextButton && safety < 20) {
|
|
287
|
+
const formInputs = SiiSession.extractFormInputsByName(currentHtml, 'frm');
|
|
288
|
+
const nextFields = { ...formInputs };
|
|
289
|
+
|
|
290
|
+
if (Number.isFinite(nextButton.page)) {
|
|
291
|
+
nextFields.PAGINA = String(nextButton.page);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const nextRes = await this.session.submitForm(
|
|
295
|
+
action,
|
|
296
|
+
nextFields,
|
|
297
|
+
`https://${this.session.getBaseHost()}/cvc_cgi/dte/af_anular2`
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
currentHtml = nextRes.body || '';
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const pageStamp = nextButton.page ? `page-${nextButton.page}` : `page-${safety + 2}`;
|
|
304
|
+
fs.writeFileSync(path.join(debugDir, `${pageStamp}.html`), currentHtml, 'utf8');
|
|
305
|
+
} catch (_) {
|
|
306
|
+
// ignore
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const nextInfo = this._parseAnulacionTable(currentHtml);
|
|
310
|
+
info = {
|
|
311
|
+
ranges: this._mergeRanges(info.ranges, nextInfo.ranges),
|
|
312
|
+
ultimoFolioFinal: Math.max(info.ultimoFolioFinal || 0, nextInfo.ultimoFolioFinal || 0) || null,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
action = SiiSession.extractFormActionByName(currentHtml, 'frm') || action;
|
|
316
|
+
hiddenInputsConsulta = SiiSession.extractInputValues(currentHtml);
|
|
317
|
+
nextButton = this._findNextButton(currentHtml);
|
|
318
|
+
safety += 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
tipoDte,
|
|
324
|
+
baseHost: this.session.getBaseHost(),
|
|
325
|
+
html: consulta.body || '',
|
|
326
|
+
action,
|
|
327
|
+
hiddenInputs: hiddenInputsConsulta,
|
|
328
|
+
...info,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Anula folios en el SII
|
|
334
|
+
* @param {Object} params - Parámetros
|
|
335
|
+
* @returns {Promise<Object>}
|
|
336
|
+
*/
|
|
337
|
+
async anularFolios({ tipoDte, folioDesde = null, folioHasta = null, motivo = 'Folios no utilizados' }) {
|
|
338
|
+
const consulta = await this.consultarFolios({ tipoDte });
|
|
339
|
+
const anulados = [];
|
|
340
|
+
const rechazados = [];
|
|
341
|
+
|
|
342
|
+
// Calcular total de folios a anular para mostrar progreso
|
|
343
|
+
let totalFolios = 0;
|
|
344
|
+
if (Number.isFinite(folioDesde) && Number.isFinite(folioHasta)) {
|
|
345
|
+
totalFolios = folioHasta - folioDesde + 1;
|
|
346
|
+
} else {
|
|
347
|
+
for (const range of consulta.ranges) {
|
|
348
|
+
totalFolios += (range.folioHasta - range.folioDesde + 1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
let foliosAnulados = 0;
|
|
352
|
+
|
|
353
|
+
const anularFolioIndividual = async (folio) => {
|
|
354
|
+
let currentHtml = consulta.html;
|
|
355
|
+
|
|
356
|
+
const range = consulta.ranges.find((r) => folio >= r.folioDesde && folio <= r.folioHasta);
|
|
357
|
+
|
|
358
|
+
if (range && range.formFields && Object.keys(range.formFields).length) {
|
|
359
|
+
const selectRes = await this.session.submitForm(
|
|
360
|
+
range.formAction || consulta.action,
|
|
361
|
+
range.formFields,
|
|
362
|
+
`https://${this.session.getBaseHost()}/cvc_cgi/dte/af_anular1`
|
|
363
|
+
);
|
|
364
|
+
currentHtml = selectRes.body || currentHtml;
|
|
365
|
+
} else if (range && range.selection) {
|
|
366
|
+
const selectFields = {
|
|
367
|
+
...consulta.hiddenInputs,
|
|
368
|
+
[range.selection.name]: range.selection.value || '',
|
|
369
|
+
};
|
|
370
|
+
const selectRes = await this.session.submitForm(
|
|
371
|
+
consulta.action,
|
|
372
|
+
selectFields,
|
|
373
|
+
`https://${this.session.getBaseHost()}/cvc_cgi/dte/af_anular1`
|
|
374
|
+
);
|
|
375
|
+
currentHtml = selectRes.body || currentHtml;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const action = SiiSession.extractFormAction(currentHtml) || consulta.action;
|
|
379
|
+
const fields = SiiSession.extractInputValues(currentHtml);
|
|
380
|
+
this._setFolioFields(fields, folio, folio);
|
|
381
|
+
this._setMotivoField(fields, motivo);
|
|
382
|
+
|
|
383
|
+
const resultRes = await this.session.submitForm(
|
|
384
|
+
action,
|
|
385
|
+
fields,
|
|
386
|
+
`https://${this.session.getBaseHost()}/cvc_cgi/dte/af_anular2`
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Guardar resultado para debug
|
|
390
|
+
try {
|
|
391
|
+
const debugDir = path.join(this.debugDir, 'auto-caf', 'anulacion', 'resultados');
|
|
392
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
393
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
394
|
+
fs.writeFileSync(path.join(debugDir, `sii-anulacion-result-${folio}-${stamp}.html`), resultRes.body || '', 'utf8');
|
|
395
|
+
} catch (_) {
|
|
396
|
+
// ignore
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Log de progreso
|
|
400
|
+
foliosAnulados += 1;
|
|
401
|
+
const pct = Math.round((foliosAnulados / totalFolios) * 100);
|
|
402
|
+
process.stdout.write(`\r Anulando folio ${folio} (${foliosAnulados}/${totalFolios} - ${pct}%)`);
|
|
403
|
+
|
|
404
|
+
return { folio, ...this._parseAnulacionResult(resultRes.body || '') };
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Anular rango específico o todos
|
|
408
|
+
if (Number.isFinite(folioDesde) && Number.isFinite(folioHasta)) {
|
|
409
|
+
for (let folio = folioDesde; folio <= folioHasta; folio += 1) {
|
|
410
|
+
const result = await anularFolioIndividual(folio);
|
|
411
|
+
if (result.ok) {
|
|
412
|
+
anulados.push(result);
|
|
413
|
+
} else if (result.ok === false) {
|
|
414
|
+
rechazados.push(result);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
// Anular todos los rangos
|
|
419
|
+
for (const range of consulta.ranges) {
|
|
420
|
+
for (let folio = range.folioDesde; folio <= range.folioHasta; folio += 1) {
|
|
421
|
+
const result = await anularFolioIndividual(folio);
|
|
422
|
+
if (result.ok) {
|
|
423
|
+
anulados.push(result);
|
|
424
|
+
} else if (result.ok === false) {
|
|
425
|
+
rechazados.push(result);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Nueva línea después del progreso
|
|
432
|
+
if (totalFolios > 0) {
|
|
433
|
+
console.log('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { ok: true, anulados, rechazados };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Resuelve la ruta de un CAF, solicitando uno nuevo si es necesario
|
|
441
|
+
* @param {Object} params - Parámetros
|
|
442
|
+
* @returns {Promise<string|null>}
|
|
443
|
+
*/
|
|
444
|
+
async resolveCafPath({ tipoDte, cafPath = null, autoCaf = true, requiredCount = 1 }) {
|
|
445
|
+
let resolvedPath = cafPath;
|
|
446
|
+
|
|
447
|
+
// Si no existe, buscar o solicitar
|
|
448
|
+
if (autoCaf && (!resolvedPath || !fs.existsSync(resolvedPath))) {
|
|
449
|
+
resolvedPath = await this.solicitarCafConFallback({
|
|
450
|
+
tipoDte,
|
|
451
|
+
cantidad: requiredCount,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Validar si hay folios suficientes
|
|
456
|
+
if (autoCaf && resolvedPath && fs.existsSync(resolvedPath)) {
|
|
457
|
+
const cafXml = fs.readFileSync(resolvedPath, 'utf8');
|
|
458
|
+
const caf = new CAF(cafXml);
|
|
459
|
+
const cafFingerprint = FolioRegistry.createCafFingerprint(cafXml);
|
|
460
|
+
|
|
461
|
+
// Verificar con consulta al SII si está disponible
|
|
462
|
+
if (this.config.useRegistry) {
|
|
463
|
+
try {
|
|
464
|
+
const siiInfo = await this.consultarFolios({ tipoDte: caf.getTipoDTE() });
|
|
465
|
+
if (siiInfo && Number.isFinite(Number(siiInfo.ultimoFolioFinal))) {
|
|
466
|
+
const last = Number(siiInfo.ultimoFolioFinal);
|
|
467
|
+
if (Number(caf.getFolioHasta()) <= last) {
|
|
468
|
+
resolvedPath = await this.solicitarCafConFallback({
|
|
469
|
+
tipoDte,
|
|
470
|
+
cantidad: requiredCount,
|
|
471
|
+
previousPath: resolvedPath,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch (_) {
|
|
476
|
+
// Continuar sin validación SII
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Verificar folios restantes en registro local
|
|
481
|
+
const remaining = this.registry.getRemainingFolios({
|
|
482
|
+
rutEmisor: this.rutEmisor,
|
|
483
|
+
tipoDte: caf.getTipoDTE(),
|
|
484
|
+
folioDesde: caf.getFolioDesde(),
|
|
485
|
+
folioHasta: caf.getFolioHasta(),
|
|
486
|
+
ambiente: this.ambiente,
|
|
487
|
+
cafFingerprint,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (remaining < requiredCount) {
|
|
491
|
+
resolvedPath = await this.solicitarCafConFallback({
|
|
492
|
+
tipoDte,
|
|
493
|
+
cantidad: requiredCount,
|
|
494
|
+
previousPath: resolvedPath,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return resolvedPath;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Reserva el siguiente folio disponible
|
|
504
|
+
* @param {Object} params - Parámetros del CAF
|
|
505
|
+
* @returns {number}
|
|
506
|
+
*/
|
|
507
|
+
reserveNextFolio({ tipoDte, folioDesde, folioHasta, cafFingerprint }) {
|
|
508
|
+
return this.registry.reserveNextFolio({
|
|
509
|
+
rutEmisor: this.rutEmisor,
|
|
510
|
+
tipoDte,
|
|
511
|
+
folioDesde,
|
|
512
|
+
folioHasta,
|
|
513
|
+
ambiente: this.ambiente,
|
|
514
|
+
cafFingerprint,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Marca un folio como enviado
|
|
520
|
+
* @param {Object} params - Parámetros
|
|
521
|
+
*/
|
|
522
|
+
markFolioSent(params) {
|
|
523
|
+
this.registry.markFolioSent({
|
|
524
|
+
rutEmisor: this.rutEmisor,
|
|
525
|
+
ambiente: this.ambiente,
|
|
526
|
+
...params,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Libera folios del registro
|
|
532
|
+
* @param {Object} params - Parámetros
|
|
533
|
+
*/
|
|
534
|
+
releaseFolios(params) {
|
|
535
|
+
this.registry.releaseFolios({
|
|
536
|
+
rutEmisor: this.rutEmisor,
|
|
537
|
+
ambiente: this.ambiente,
|
|
538
|
+
...params,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ============================================
|
|
543
|
+
// MÉTODOS PRIVADOS DE PARSING
|
|
544
|
+
// ============================================
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* @private
|
|
548
|
+
*/
|
|
549
|
+
_parseAnulacionTable(html) {
|
|
550
|
+
const rows = html.match(/<tr[^>]*>[\s\S]*?<\/tr>/gi) || [];
|
|
551
|
+
const ranges = [];
|
|
552
|
+
|
|
553
|
+
rows.forEach((row) => {
|
|
554
|
+
const cols = row.match(/<td[^>]*>[\s\S]*?<\/td>/gi) || [];
|
|
555
|
+
if (cols.length < 4) return;
|
|
556
|
+
|
|
557
|
+
const selectionTag = SiiSession.extractInputTags(row).find((tag) => {
|
|
558
|
+
const name = String(tag.name || '');
|
|
559
|
+
const value = String(tag.value || '');
|
|
560
|
+
return /selecc/i.test(name) || /selecc/i.test(value);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const formFields = SiiSession.extractFormInputsByName(row);
|
|
564
|
+
const formAction = SiiSession.extractFormAction(row);
|
|
565
|
+
|
|
566
|
+
const values = cols.map((c) => SiiSession.stripHtml(c));
|
|
567
|
+
const folioDesde = SiiSession.parseIntFromText(values[2]);
|
|
568
|
+
const folioHasta = SiiSession.parseIntFromText(values[3]);
|
|
569
|
+
const cantidad = SiiSession.parseIntFromText(values[1]);
|
|
570
|
+
const fecha = values[0] || null;
|
|
571
|
+
const efectuadoPor = values[4] || null;
|
|
572
|
+
|
|
573
|
+
if (!Number.isFinite(folioDesde) || !Number.isFinite(folioHasta)) return;
|
|
574
|
+
|
|
575
|
+
ranges.push({
|
|
576
|
+
fecha,
|
|
577
|
+
cantidad: Number.isFinite(cantidad) ? cantidad : null,
|
|
578
|
+
folioDesde,
|
|
579
|
+
folioHasta,
|
|
580
|
+
efectuadoPor,
|
|
581
|
+
selection: selectionTag && selectionTag.name ? { name: selectionTag.name, value: selectionTag.value || '' } : null,
|
|
582
|
+
formFields,
|
|
583
|
+
formAction,
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const ultimoFolioFinal = ranges.reduce((acc, r) => (r.folioHasta > acc ? r.folioHasta : acc), 0);
|
|
588
|
+
return { ranges, ultimoFolioFinal: ultimoFolioFinal || null };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
_findNextButton(html) {
|
|
595
|
+
const inputs = SiiSession.extractInputTags(html);
|
|
596
|
+
const next = inputs.find((tag) => {
|
|
597
|
+
const value = String(tag.value || '').toLowerCase();
|
|
598
|
+
const name = String(tag.name || '').toLowerCase();
|
|
599
|
+
return value.includes('ver siguiente') || value.includes('siguiente') || name.includes('siguiente') || name.includes('next');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
if (!next || !next.name) return null;
|
|
603
|
+
|
|
604
|
+
let page = null;
|
|
605
|
+
if (next.onClick) {
|
|
606
|
+
const match = String(next.onClick).match(/cambiapag\((\d+)\)/i);
|
|
607
|
+
if (match) page = Number(match[1]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return { name: next.name, value: next.value || '', page };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* @private
|
|
615
|
+
*/
|
|
616
|
+
_mergeRanges(base, extra) {
|
|
617
|
+
const out = [...base];
|
|
618
|
+
extra.forEach((range) => {
|
|
619
|
+
if (!out.some((r) => r.folioDesde === range.folioDesde && r.folioHasta === range.folioHasta)) {
|
|
620
|
+
out.push(range);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
out.sort((a, b) => b.folioHasta - a.folioHasta);
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* @private
|
|
629
|
+
*/
|
|
630
|
+
_setFolioFields(fields, folioDesde, folioHasta) {
|
|
631
|
+
let setDesde = false;
|
|
632
|
+
let setHasta = false;
|
|
633
|
+
|
|
634
|
+
Object.keys(fields).forEach((key) => {
|
|
635
|
+
if (/folio.*(ini|desde)/i.test(key)) {
|
|
636
|
+
fields[key] = String(folioDesde);
|
|
637
|
+
setDesde = true;
|
|
638
|
+
}
|
|
639
|
+
if (/folio.*(fin|hasta)/i.test(key)) {
|
|
640
|
+
fields[key] = String(folioHasta);
|
|
641
|
+
setHasta = true;
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (!setDesde) fields.FOLIO_INICIAL = String(folioDesde);
|
|
646
|
+
if (!setHasta) fields.FOLIO_FINAL = String(folioHasta);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* @private
|
|
651
|
+
*/
|
|
652
|
+
_setMotivoField(fields, motivo) {
|
|
653
|
+
let set = false;
|
|
654
|
+
|
|
655
|
+
Object.keys(fields).forEach((key) => {
|
|
656
|
+
if (/motivo|glosa|razon|coment/i.test(key)) {
|
|
657
|
+
fields[key] = motivo;
|
|
658
|
+
set = true;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
if (!set) fields.MOTIVO = motivo;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* @private
|
|
667
|
+
*/
|
|
668
|
+
_parseAnulacionResult(html) {
|
|
669
|
+
const text = String(html || '').toLowerCase();
|
|
670
|
+
|
|
671
|
+
if (text.includes('recepcionad')) {
|
|
672
|
+
return { ok: false, reason: 'recepcionado' };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (
|
|
676
|
+
text.includes('ya fue anulado') ||
|
|
677
|
+
text.includes('anulado anteriormente') ||
|
|
678
|
+
(text.includes('anulad') && text.includes('ya'))
|
|
679
|
+
) {
|
|
680
|
+
return { ok: false, reason: 'ya-anulado' };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (
|
|
684
|
+
text.includes('ha autorizado la anulaci') ||
|
|
685
|
+
text.includes('ha autorizado la anulacion') ||
|
|
686
|
+
text.includes('solicitud anulacion de folios')
|
|
687
|
+
) {
|
|
688
|
+
return { ok: true, reason: 'anulado' };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (text.includes('anulaci') && (text.includes('exit') || text.includes('realiz') || text.includes('correct'))) {
|
|
692
|
+
return { ok: true, reason: 'anulado' };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (text.includes('anulaci') && text.includes('no')) {
|
|
696
|
+
return { ok: false, reason: 'rechazado' };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return { ok: null, reason: 'desconocido' };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
module.exports = FolioService;
|