@devlas/dte-sii 2.5.10 → 2.5.12
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/CafSolicitor.js +378 -378
- package/ConsumoFolio.js +376 -376
- package/Envio.js +196 -196
- package/LICENSE +27 -27
- package/README.md +273 -239
- package/SiiCertificacion.js +14 -7
- package/SiiPortalAuth.js +474 -468
- package/cert/CertRunner.js +8 -0
- package/cert/ConfigLoader.js +131 -131
- package/cert/IntercambioCert.js +427 -427
- package/cert/LibroCompras.js +357 -357
- package/cert/LibroGuias.js +169 -169
- package/cert/LibroVentas.js +151 -151
- package/cert/MuestrasImpresas.js +674 -674
- package/cert/SetBase.js +319 -319
- package/cert/SetBasico.js +411 -411
- package/cert/SetCompra.js +470 -470
- package/cert/SetExenta.js +488 -488
- package/cert/SetGuia.js +281 -281
- package/cert/SetParser.js +1182 -1182
- package/cert/SetsProvider.js +497 -497
- package/cert/Simulacion.js +519 -519
- package/cert/comunaOficina.js +458 -458
- package/cert/index.js +122 -122
- package/cert/types.js +328 -328
- package/package.json +49 -49
package/CafSolicitor.js
CHANGED
|
@@ -1,380 +1,380 @@
|
|
|
1
1
|
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
2
|
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
-
/**
|
|
4
|
-
* CafSolicitor.js - Solicitador de CAFs al SII
|
|
5
|
-
*
|
|
6
|
-
* Módulo interno del core para solicitar Códigos de Autorización de Folios (CAF)
|
|
7
|
-
* directamente al SII. Usa SiiSession para evitar duplicación de código.
|
|
8
|
-
*
|
|
9
|
-
* Migrado desde: scripts/cert/test-caf-solicitar.js
|
|
10
|
-
* Refactorizado: Usa SiiSession para HTTP y utilidades
|
|
11
|
-
*
|
|
12
|
-
* @module CafSolicitor
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
const SiiSession = require('./SiiSession');
|
|
18
|
-
const { splitRut } = require('./utils/rut');
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Clase para solicitar CAFs al SII
|
|
22
|
-
*/
|
|
23
|
-
class CafSolicitor {
|
|
24
|
-
/**
|
|
25
|
-
* @param {Object} options - Opciones de configuración
|
|
26
|
-
* @param {string} options.ambiente - 'certificacion' o 'produccion'
|
|
27
|
-
* @param {string} options.rutEmisor - RUT del emisor (ej: 76192083-9)
|
|
28
|
-
* @param {string} options.pfxPath - Ruta absoluta al certificado PFX
|
|
29
|
-
* @param {string} options.pfxPassword - Contraseña del certificado
|
|
30
|
-
* @param {string} [options.baseDir] - Directorio base para guardar archivos
|
|
31
|
-
* @param {string} [options.sessionPath] - Ruta al archivo de sesión compartida
|
|
32
|
-
* @param {string} [options.runStamp] - Timestamp de la ejecución
|
|
33
|
-
*/
|
|
34
|
-
constructor(options = {}) {
|
|
35
|
-
if (!options.ambiente) {
|
|
36
|
-
throw new Error('CafSolicitor: options.ambiente es obligatorio');
|
|
37
|
-
}
|
|
38
|
-
if (!options.rutEmisor) {
|
|
39
|
-
throw new Error('CafSolicitor: options.rutEmisor es obligatorio');
|
|
40
|
-
}
|
|
41
|
-
if (!options.pfxPath) {
|
|
42
|
-
throw new Error('CafSolicitor: options.pfxPath es obligatorio');
|
|
43
|
-
}
|
|
44
|
-
if (!options.pfxPassword) {
|
|
45
|
-
throw new Error('CafSolicitor: options.pfxPassword es obligatorio');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.ambiente = options.ambiente.toLowerCase();
|
|
49
|
-
this.rutEmisor = options.rutEmisor;
|
|
50
|
-
this.baseDir = options.baseDir || path.resolve(__dirname, '..', '..');
|
|
51
|
-
this.sessionPath = options.sessionPath || null;
|
|
52
|
-
this.runStamp = options.runStamp || new Date().toISOString().replace(/[:.]/g, '-');
|
|
53
|
-
|
|
54
|
-
// Crear SiiSession para manejar HTTP y cookies
|
|
55
|
-
this.session = new SiiSession({
|
|
56
|
-
ambiente: this.ambiente,
|
|
57
|
-
pfxPath: options.pfxPath,
|
|
58
|
-
pfxPassword: options.pfxPassword,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Cargar sesión compartida si existe
|
|
62
|
-
if (this.sessionPath) {
|
|
63
|
-
const loaded = this.session.loadSession(this.sessionPath);
|
|
64
|
-
if (loaded) {
|
|
65
|
-
console.log('[CafSolicitor] ✓ Usando sesión compartida');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Crea directorio para debug de esta solicitud
|
|
72
|
-
* @private
|
|
73
|
-
*/
|
|
74
|
-
_getDebugDir(tipoDte) {
|
|
75
|
-
const rutClean = String(this.rutEmisor).replace(/\./g, '').toUpperCase();
|
|
76
|
-
const runDir = path.join(this.baseDir, 'debug', 'auto-caf', rutClean, this.runStamp, String(tipoDte));
|
|
77
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
78
|
-
return runDir;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Guarda respuesta de debug
|
|
83
|
-
* @private
|
|
84
|
-
*/
|
|
85
|
-
_saveDebug(debugDir, filename, content) {
|
|
86
|
-
const filePath = path.join(debugDir, filename);
|
|
87
|
-
fs.writeFileSync(filePath, content, 'utf-8');
|
|
88
|
-
console.log(`${filename}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Extrae información del CAF desde XML
|
|
93
|
-
* @private
|
|
94
|
-
*/
|
|
95
|
-
_extractCafInfo(xml, tipoDte) {
|
|
96
|
-
const tdMatch = xml.match(/<TD>(\d+)<\/TD>/i);
|
|
97
|
-
const dMatch = xml.match(/<D>(\d+)<\/D>/i);
|
|
98
|
-
const hMatch = xml.match(/<H>(\d+)<\/H>/i);
|
|
99
|
-
const faMatch = xml.match(/<FA>(\d{4}-\d{2}-\d{2})<\/FA>/i);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
tipoDte: tdMatch ? tdMatch[1] : tipoDte,
|
|
103
|
-
folioDesde: dMatch ? dMatch[1] : 'unknown',
|
|
104
|
-
folioHasta: hMatch ? hMatch[1] : 'unknown',
|
|
105
|
-
fechaAutorizacion: faMatch ? faMatch[1] : new Date().toISOString().slice(0, 10),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Guarda el CAF en ubicación organizada
|
|
111
|
-
* @private
|
|
112
|
-
*/
|
|
113
|
-
_saveCafOrganized(xml, tipoDte) {
|
|
114
|
-
const info = this._extractCafInfo(xml, tipoDte);
|
|
115
|
-
const rutClean = this.rutEmisor.replace(/\./g, '').toUpperCase();
|
|
116
|
-
|
|
117
|
-
const cafDir = path.join(
|
|
118
|
-
this.baseDir, 'debug', 'caf', this.ambiente,
|
|
119
|
-
rutClean, String(info.tipoDte), this.runStamp
|
|
120
|
-
);
|
|
121
|
-
fs.mkdirSync(cafDir, { recursive: true });
|
|
122
|
-
|
|
123
|
-
const cafFileName = `caf-${info.tipoDte}-${info.folioDesde}-${info.folioHasta}.xml`;
|
|
124
|
-
const cafPath = path.join(cafDir, cafFileName);
|
|
125
|
-
fs.writeFileSync(cafPath, xml, 'utf-8');
|
|
126
|
-
|
|
127
|
-
console.log(`✅ CAF guardado: ${cafFileName}`);
|
|
128
|
-
console.log(` Ruta: ${cafPath}`);
|
|
129
|
-
|
|
130
|
-
return cafPath;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Detecta si la respuesta del SII requiere autenticación con certificado.
|
|
135
|
-
* @private
|
|
136
|
-
*/
|
|
137
|
-
_requiresAuthentication(responseBody = '') {
|
|
138
|
-
if (!responseBody || typeof responseBody !== 'string') return false;
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
responseBody.includes('Autenticaci') ||
|
|
142
|
-
responseBody.includes('autInicioDTE.cgi') ||
|
|
143
|
-
responseBody.includes('cgi_AUT2000') ||
|
|
144
|
-
responseBody.includes('302 Found')
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Solicita un CAF al SII
|
|
150
|
-
* @param {Object} params - Parámetros
|
|
151
|
-
* @param {number} params.tipoDte - Tipo de DTE (33, 34, 39, 56, 61, etc.)
|
|
152
|
-
* @param {number} [params.cantidad=1] - Cantidad de folios a solicitar
|
|
153
|
-
* @returns {Promise<Object>} - { success, cafPath, xml, error }
|
|
154
|
-
*/
|
|
155
|
-
async solicitar({ tipoDte, cantidad = 1 }) {
|
|
156
|
-
const { numero: rut, dv } = splitRut(this.rutEmisor);
|
|
157
|
-
const debugDir = this._getDebugDir(tipoDte);
|
|
158
|
-
|
|
159
|
-
console.log('─'.repeat(60));
|
|
160
|
-
console.log(`[CafSolicitor] Solicitando CAF tipo ${tipoDte} x${cantidad}`);
|
|
161
|
-
console.log(` RUT: ${this.rutEmisor} | Ambiente: ${this.ambiente}`);
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
// Paso 1: POST inicial a of_solicita_folios
|
|
165
|
-
const fields = {
|
|
166
|
-
RUT_EMP: rut,
|
|
167
|
-
DV_EMP: dv,
|
|
168
|
-
COD_DOCTO: tipoDte,
|
|
169
|
-
CANTIDAD: cantidad,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
let response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios', fields);
|
|
173
|
-
|
|
174
|
-
// Manejar autenticación si es necesaria (incluye 302 a autInicioDTE)
|
|
175
|
-
if (this._requiresAuthentication(response.body)) {
|
|
176
|
-
const authResult = await this.session.ensureSession('/cvc_cgi/dte/of_solicita_folios');
|
|
177
|
-
if (authResult.body) {
|
|
178
|
-
// Reintentar después de autenticación
|
|
179
|
-
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios', fields);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Guardar sesión para reutilización
|
|
183
|
-
if (this.sessionPath) {
|
|
184
|
-
this.session.saveSession(this.sessionPath);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Procesar flujo multi-paso del SII
|
|
189
|
-
response = await this._processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
190
|
-
|
|
191
|
-
// Guardar respuesta final
|
|
192
|
-
this._saveDebug(debugDir, `caf-final-${this.runStamp}.html`, response.body || '');
|
|
193
|
-
|
|
194
|
-
// Verificar si obtuvimos el CAF
|
|
195
|
-
if (response.body && response.body.includes('<AUTORIZACION')) {
|
|
196
|
-
const cafPath = this._saveCafOrganized(response.body, tipoDte);
|
|
197
|
-
return { success: true, cafPath, xml: response.body };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (response.body && response.body.includes('Autenticaci')) {
|
|
201
|
-
return { success: false, error: 'El SII devolvió página de autenticación' };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { success: false, error: 'No se obtuvo CAF en la respuesta' };
|
|
205
|
-
|
|
206
|
-
} catch (err) {
|
|
207
|
-
console.error(`[CafSolicitor] Error: ${err.message}`);
|
|
208
|
-
return { success: false, error: err.message };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Procesa el flujo multi-paso del SII para obtener CAF
|
|
214
|
-
* @private
|
|
215
|
-
*/
|
|
216
|
-
async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
217
|
-
let currentHtml = response.body || '';
|
|
218
|
-
|
|
219
|
-
// Paso 2: of_solicita_folios_dcto
|
|
220
|
-
if (currentHtml.includes('of_solicita_folios_dcto')) {
|
|
221
|
-
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_solicita_folios_dcto';
|
|
222
|
-
const hiddenInputs = SiiSession.extractInputValues(currentHtml);
|
|
223
|
-
|
|
224
|
-
const step2Fields = {
|
|
225
|
-
...hiddenInputs,
|
|
226
|
-
RUT_EMP: rut,
|
|
227
|
-
DV_EMP: dv,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
response = await this.session.submitForm(formAction, step2Fields);
|
|
231
|
-
currentHtml = response.body || '';
|
|
232
|
-
this._saveDebug(debugDir, `step2-${this.runStamp}.html`, currentHtml);
|
|
233
|
-
|
|
234
|
-
// Selección de tipo de documento
|
|
235
|
-
if (currentHtml.includes('COD_DOCTO')) {
|
|
236
|
-
const selectInputs = SiiSession.extractInputValues(currentHtml);
|
|
237
|
-
const selectFields = {
|
|
238
|
-
...selectInputs,
|
|
239
|
-
RUT_EMP: rut,
|
|
240
|
-
DV_EMP: dv,
|
|
241
|
-
COD_DOCTO: tipoDte,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios_dcto', selectFields);
|
|
245
|
-
currentHtml = response.body || '';
|
|
246
|
-
this._saveDebug(debugDir, `select-${this.runStamp}.html`, currentHtml);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Paso 3: Solicitar numeración
|
|
250
|
-
response = await this._processStep3(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return response;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Procesa paso 3 y siguientes
|
|
258
|
-
* @private
|
|
259
|
-
*/
|
|
260
|
-
async _processStep3(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
261
|
-
let currentHtml = response.body || '';
|
|
262
|
-
|
|
263
|
-
const formAction3 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
264
|
-
const inputs3 = SiiSession.extractInputValues(currentHtml);
|
|
265
|
-
|
|
266
|
-
const step3Fields = {
|
|
267
|
-
...inputs3,
|
|
268
|
-
RUT_EMP: rut,
|
|
269
|
-
DV_EMP: dv,
|
|
270
|
-
COD_DOCTO: tipoDte,
|
|
271
|
-
CANT_DOCTOS: cantidad,
|
|
272
|
-
ACEPTAR: 'Solicitar Numeración',
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
response = await this.session.submitForm(formAction3, step3Fields);
|
|
276
|
-
currentHtml = response.body || '';
|
|
277
|
-
this._saveDebug(debugDir, `step3-${this.runStamp}.html`, currentHtml);
|
|
278
|
-
|
|
279
|
-
// Confirmar folio inicial
|
|
280
|
-
if (currentHtml.includes('of_confirma_folio')) {
|
|
281
|
-
response = await this._processConfirmFolio(response, debugDir);
|
|
282
|
-
} else if (currentHtml.includes('of_genera_folio')) {
|
|
283
|
-
response = await this._processGeneraFolio(response, debugDir);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return response;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Procesa confirmación de folio
|
|
291
|
-
* @private
|
|
292
|
-
*/
|
|
293
|
-
async _processConfirmFolio(response, debugDir) {
|
|
294
|
-
let currentHtml = response.body || '';
|
|
295
|
-
|
|
296
|
-
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
297
|
-
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
298
|
-
|
|
299
|
-
const fields = {
|
|
300
|
-
...inputs,
|
|
301
|
-
FOLIO_INICIAL: inputs.FOLIO_INICIAL || '1',
|
|
302
|
-
ACEPTAR: 'Confirmar Folio Inicial',
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
response = await this.session.submitForm(formAction, fields);
|
|
306
|
-
currentHtml = response.body || '';
|
|
307
|
-
this._saveDebug(debugDir, `confirm-${this.runStamp}.html`, currentHtml);
|
|
308
|
-
|
|
309
|
-
if (currentHtml.includes('of_genera_folio')) {
|
|
310
|
-
response = await this._processGeneraFolio(response, debugDir);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return response;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Procesa generación de folio
|
|
318
|
-
* @private
|
|
319
|
-
*/
|
|
320
|
-
async _processGeneraFolio(response, debugDir) {
|
|
321
|
-
let currentHtml = response.body || '';
|
|
322
|
-
|
|
323
|
-
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_folio';
|
|
324
|
-
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
325
|
-
|
|
326
|
-
const fields = {
|
|
327
|
-
...inputs,
|
|
328
|
-
ACEPTAR: 'Obtener Folios',
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
response = await this.session.submitForm(formAction, fields);
|
|
332
|
-
currentHtml = response.body || '';
|
|
333
|
-
this._saveDebug(debugDir, `genera-${this.runStamp}.html`, currentHtml);
|
|
334
|
-
|
|
335
|
-
// Paso final: of_genera_archivo
|
|
336
|
-
if (!currentHtml.includes('<AUTORIZACION') && currentHtml.includes('of_genera_archivo')) {
|
|
337
|
-
response = await this._processGeneraArchivo(response, debugDir);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return response;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Procesa generación de archivo CAF
|
|
345
|
-
* @private
|
|
346
|
-
*/
|
|
347
|
-
async _processGeneraArchivo(response, debugDir) {
|
|
348
|
-
let currentHtml = response.body || '';
|
|
349
|
-
|
|
350
|
-
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_archivo';
|
|
351
|
-
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
352
|
-
|
|
353
|
-
const fields = {
|
|
354
|
-
...inputs,
|
|
355
|
-
ACEPTAR: 'AQUI',
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
response = await this.session.submitForm(formAction, fields);
|
|
359
|
-
currentHtml = response.body || '';
|
|
360
|
-
this._saveDebug(debugDir, `archivo-${this.runStamp}.xml`, currentHtml);
|
|
361
|
-
|
|
362
|
-
// A veces hay un paso extra
|
|
363
|
-
if (!currentHtml.includes('<AUTORIZACION') && currentHtml.includes('of_genera_archivo')) {
|
|
364
|
-
const formAction2 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_archivo';
|
|
365
|
-
const inputs2 = SiiSession.extractInputValues(currentHtml);
|
|
366
|
-
|
|
367
|
-
const fields2 = {
|
|
368
|
-
...inputs2,
|
|
369
|
-
ACEPTAR: 'AQUI',
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
response = await this.session.submitForm(formAction2, fields2);
|
|
373
|
-
this._saveDebug(debugDir, `archivo2-${this.runStamp}.xml`, response.body || '');
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return response;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
module.exports = CafSolicitor;
|
|
3
|
+
/**
|
|
4
|
+
* CafSolicitor.js - Solicitador de CAFs al SII
|
|
5
|
+
*
|
|
6
|
+
* Módulo interno del core para solicitar Códigos de Autorización de Folios (CAF)
|
|
7
|
+
* directamente al SII. Usa SiiSession para evitar duplicación de código.
|
|
8
|
+
*
|
|
9
|
+
* Migrado desde: scripts/cert/test-caf-solicitar.js
|
|
10
|
+
* Refactorizado: Usa SiiSession para HTTP y utilidades
|
|
11
|
+
*
|
|
12
|
+
* @module CafSolicitor
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const SiiSession = require('./SiiSession');
|
|
18
|
+
const { splitRut } = require('./utils/rut');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Clase para solicitar CAFs al SII
|
|
22
|
+
*/
|
|
23
|
+
class CafSolicitor {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options - Opciones de configuración
|
|
26
|
+
* @param {string} options.ambiente - 'certificacion' o 'produccion'
|
|
27
|
+
* @param {string} options.rutEmisor - RUT del emisor (ej: 76192083-9)
|
|
28
|
+
* @param {string} options.pfxPath - Ruta absoluta al certificado PFX
|
|
29
|
+
* @param {string} options.pfxPassword - Contraseña del certificado
|
|
30
|
+
* @param {string} [options.baseDir] - Directorio base para guardar archivos
|
|
31
|
+
* @param {string} [options.sessionPath] - Ruta al archivo de sesión compartida
|
|
32
|
+
* @param {string} [options.runStamp] - Timestamp de la ejecución
|
|
33
|
+
*/
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
if (!options.ambiente) {
|
|
36
|
+
throw new Error('CafSolicitor: options.ambiente es obligatorio');
|
|
37
|
+
}
|
|
38
|
+
if (!options.rutEmisor) {
|
|
39
|
+
throw new Error('CafSolicitor: options.rutEmisor es obligatorio');
|
|
40
|
+
}
|
|
41
|
+
if (!options.pfxPath) {
|
|
42
|
+
throw new Error('CafSolicitor: options.pfxPath es obligatorio');
|
|
43
|
+
}
|
|
44
|
+
if (!options.pfxPassword) {
|
|
45
|
+
throw new Error('CafSolicitor: options.pfxPassword es obligatorio');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.ambiente = options.ambiente.toLowerCase();
|
|
49
|
+
this.rutEmisor = options.rutEmisor;
|
|
50
|
+
this.baseDir = options.baseDir || path.resolve(__dirname, '..', '..');
|
|
51
|
+
this.sessionPath = options.sessionPath || null;
|
|
52
|
+
this.runStamp = options.runStamp || new Date().toISOString().replace(/[:.]/g, '-');
|
|
53
|
+
|
|
54
|
+
// Crear SiiSession para manejar HTTP y cookies
|
|
55
|
+
this.session = new SiiSession({
|
|
56
|
+
ambiente: this.ambiente,
|
|
57
|
+
pfxPath: options.pfxPath,
|
|
58
|
+
pfxPassword: options.pfxPassword,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Cargar sesión compartida si existe
|
|
62
|
+
if (this.sessionPath) {
|
|
63
|
+
const loaded = this.session.loadSession(this.sessionPath);
|
|
64
|
+
if (loaded) {
|
|
65
|
+
console.log('[CafSolicitor] ✓ Usando sesión compartida');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Crea directorio para debug de esta solicitud
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
_getDebugDir(tipoDte) {
|
|
75
|
+
const rutClean = String(this.rutEmisor).replace(/\./g, '').toUpperCase();
|
|
76
|
+
const runDir = path.join(this.baseDir, 'debug', 'auto-caf', rutClean, this.runStamp, String(tipoDte));
|
|
77
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
78
|
+
return runDir;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Guarda respuesta de debug
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
_saveDebug(debugDir, filename, content) {
|
|
86
|
+
const filePath = path.join(debugDir, filename);
|
|
87
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
88
|
+
console.log(`${filename}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extrae información del CAF desde XML
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_extractCafInfo(xml, tipoDte) {
|
|
96
|
+
const tdMatch = xml.match(/<TD>(\d+)<\/TD>/i);
|
|
97
|
+
const dMatch = xml.match(/<D>(\d+)<\/D>/i);
|
|
98
|
+
const hMatch = xml.match(/<H>(\d+)<\/H>/i);
|
|
99
|
+
const faMatch = xml.match(/<FA>(\d{4}-\d{2}-\d{2})<\/FA>/i);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
tipoDte: tdMatch ? tdMatch[1] : tipoDte,
|
|
103
|
+
folioDesde: dMatch ? dMatch[1] : 'unknown',
|
|
104
|
+
folioHasta: hMatch ? hMatch[1] : 'unknown',
|
|
105
|
+
fechaAutorizacion: faMatch ? faMatch[1] : new Date().toISOString().slice(0, 10),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Guarda el CAF en ubicación organizada
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_saveCafOrganized(xml, tipoDte) {
|
|
114
|
+
const info = this._extractCafInfo(xml, tipoDte);
|
|
115
|
+
const rutClean = this.rutEmisor.replace(/\./g, '').toUpperCase();
|
|
116
|
+
|
|
117
|
+
const cafDir = path.join(
|
|
118
|
+
this.baseDir, 'debug', 'caf', this.ambiente,
|
|
119
|
+
rutClean, String(info.tipoDte), this.runStamp
|
|
120
|
+
);
|
|
121
|
+
fs.mkdirSync(cafDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
const cafFileName = `caf-${info.tipoDte}-${info.folioDesde}-${info.folioHasta}.xml`;
|
|
124
|
+
const cafPath = path.join(cafDir, cafFileName);
|
|
125
|
+
fs.writeFileSync(cafPath, xml, 'utf-8');
|
|
126
|
+
|
|
127
|
+
console.log(`✅ CAF guardado: ${cafFileName}`);
|
|
128
|
+
console.log(` Ruta: ${cafPath}`);
|
|
129
|
+
|
|
130
|
+
return cafPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Detecta si la respuesta del SII requiere autenticación con certificado.
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_requiresAuthentication(responseBody = '') {
|
|
138
|
+
if (!responseBody || typeof responseBody !== 'string') return false;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
responseBody.includes('Autenticaci') ||
|
|
142
|
+
responseBody.includes('autInicioDTE.cgi') ||
|
|
143
|
+
responseBody.includes('cgi_AUT2000') ||
|
|
144
|
+
responseBody.includes('302 Found')
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Solicita un CAF al SII
|
|
150
|
+
* @param {Object} params - Parámetros
|
|
151
|
+
* @param {number} params.tipoDte - Tipo de DTE (33, 34, 39, 56, 61, etc.)
|
|
152
|
+
* @param {number} [params.cantidad=1] - Cantidad de folios a solicitar
|
|
153
|
+
* @returns {Promise<Object>} - { success, cafPath, xml, error }
|
|
154
|
+
*/
|
|
155
|
+
async solicitar({ tipoDte, cantidad = 1 }) {
|
|
156
|
+
const { numero: rut, dv } = splitRut(this.rutEmisor);
|
|
157
|
+
const debugDir = this._getDebugDir(tipoDte);
|
|
158
|
+
|
|
159
|
+
console.log('─'.repeat(60));
|
|
160
|
+
console.log(`[CafSolicitor] Solicitando CAF tipo ${tipoDte} x${cantidad}`);
|
|
161
|
+
console.log(` RUT: ${this.rutEmisor} | Ambiente: ${this.ambiente}`);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Paso 1: POST inicial a of_solicita_folios
|
|
165
|
+
const fields = {
|
|
166
|
+
RUT_EMP: rut,
|
|
167
|
+
DV_EMP: dv,
|
|
168
|
+
COD_DOCTO: tipoDte,
|
|
169
|
+
CANTIDAD: cantidad,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
let response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios', fields);
|
|
173
|
+
|
|
174
|
+
// Manejar autenticación si es necesaria (incluye 302 a autInicioDTE)
|
|
175
|
+
if (this._requiresAuthentication(response.body)) {
|
|
176
|
+
const authResult = await this.session.ensureSession('/cvc_cgi/dte/of_solicita_folios');
|
|
177
|
+
if (authResult.body) {
|
|
178
|
+
// Reintentar después de autenticación
|
|
179
|
+
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios', fields);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Guardar sesión para reutilización
|
|
183
|
+
if (this.sessionPath) {
|
|
184
|
+
this.session.saveSession(this.sessionPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Procesar flujo multi-paso del SII
|
|
189
|
+
response = await this._processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
190
|
+
|
|
191
|
+
// Guardar respuesta final
|
|
192
|
+
this._saveDebug(debugDir, `caf-final-${this.runStamp}.html`, response.body || '');
|
|
193
|
+
|
|
194
|
+
// Verificar si obtuvimos el CAF
|
|
195
|
+
if (response.body && response.body.includes('<AUTORIZACION')) {
|
|
196
|
+
const cafPath = this._saveCafOrganized(response.body, tipoDte);
|
|
197
|
+
return { success: true, cafPath, xml: response.body };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (response.body && response.body.includes('Autenticaci')) {
|
|
201
|
+
return { success: false, error: 'El SII devolvió página de autenticación' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { success: false, error: 'No se obtuvo CAF en la respuesta' };
|
|
205
|
+
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error(`[CafSolicitor] Error: ${err.message}`);
|
|
208
|
+
return { success: false, error: err.message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Procesa el flujo multi-paso del SII para obtener CAF
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
217
|
+
let currentHtml = response.body || '';
|
|
218
|
+
|
|
219
|
+
// Paso 2: of_solicita_folios_dcto
|
|
220
|
+
if (currentHtml.includes('of_solicita_folios_dcto')) {
|
|
221
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_solicita_folios_dcto';
|
|
222
|
+
const hiddenInputs = SiiSession.extractInputValues(currentHtml);
|
|
223
|
+
|
|
224
|
+
const step2Fields = {
|
|
225
|
+
...hiddenInputs,
|
|
226
|
+
RUT_EMP: rut,
|
|
227
|
+
DV_EMP: dv,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
response = await this.session.submitForm(formAction, step2Fields);
|
|
231
|
+
currentHtml = response.body || '';
|
|
232
|
+
this._saveDebug(debugDir, `step2-${this.runStamp}.html`, currentHtml);
|
|
233
|
+
|
|
234
|
+
// Selección de tipo de documento
|
|
235
|
+
if (currentHtml.includes('COD_DOCTO')) {
|
|
236
|
+
const selectInputs = SiiSession.extractInputValues(currentHtml);
|
|
237
|
+
const selectFields = {
|
|
238
|
+
...selectInputs,
|
|
239
|
+
RUT_EMP: rut,
|
|
240
|
+
DV_EMP: dv,
|
|
241
|
+
COD_DOCTO: tipoDte,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios_dcto', selectFields);
|
|
245
|
+
currentHtml = response.body || '';
|
|
246
|
+
this._saveDebug(debugDir, `select-${this.runStamp}.html`, currentHtml);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Paso 3: Solicitar numeración
|
|
250
|
+
response = await this._processStep3(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Procesa paso 3 y siguientes
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
async _processStep3(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
261
|
+
let currentHtml = response.body || '';
|
|
262
|
+
|
|
263
|
+
const formAction3 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
264
|
+
const inputs3 = SiiSession.extractInputValues(currentHtml);
|
|
265
|
+
|
|
266
|
+
const step3Fields = {
|
|
267
|
+
...inputs3,
|
|
268
|
+
RUT_EMP: rut,
|
|
269
|
+
DV_EMP: dv,
|
|
270
|
+
COD_DOCTO: tipoDte,
|
|
271
|
+
CANT_DOCTOS: cantidad,
|
|
272
|
+
ACEPTAR: 'Solicitar Numeración',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
response = await this.session.submitForm(formAction3, step3Fields);
|
|
276
|
+
currentHtml = response.body || '';
|
|
277
|
+
this._saveDebug(debugDir, `step3-${this.runStamp}.html`, currentHtml);
|
|
278
|
+
|
|
279
|
+
// Confirmar folio inicial
|
|
280
|
+
if (currentHtml.includes('of_confirma_folio')) {
|
|
281
|
+
response = await this._processConfirmFolio(response, debugDir);
|
|
282
|
+
} else if (currentHtml.includes('of_genera_folio')) {
|
|
283
|
+
response = await this._processGeneraFolio(response, debugDir);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return response;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Procesa confirmación de folio
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
async _processConfirmFolio(response, debugDir) {
|
|
294
|
+
let currentHtml = response.body || '';
|
|
295
|
+
|
|
296
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
297
|
+
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
298
|
+
|
|
299
|
+
const fields = {
|
|
300
|
+
...inputs,
|
|
301
|
+
FOLIO_INICIAL: inputs.FOLIO_INICIAL || '1',
|
|
302
|
+
ACEPTAR: 'Confirmar Folio Inicial',
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
response = await this.session.submitForm(formAction, fields);
|
|
306
|
+
currentHtml = response.body || '';
|
|
307
|
+
this._saveDebug(debugDir, `confirm-${this.runStamp}.html`, currentHtml);
|
|
308
|
+
|
|
309
|
+
if (currentHtml.includes('of_genera_folio')) {
|
|
310
|
+
response = await this._processGeneraFolio(response, debugDir);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Procesa generación de folio
|
|
318
|
+
* @private
|
|
319
|
+
*/
|
|
320
|
+
async _processGeneraFolio(response, debugDir) {
|
|
321
|
+
let currentHtml = response.body || '';
|
|
322
|
+
|
|
323
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_folio';
|
|
324
|
+
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
325
|
+
|
|
326
|
+
const fields = {
|
|
327
|
+
...inputs,
|
|
328
|
+
ACEPTAR: 'Obtener Folios',
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
response = await this.session.submitForm(formAction, fields);
|
|
332
|
+
currentHtml = response.body || '';
|
|
333
|
+
this._saveDebug(debugDir, `genera-${this.runStamp}.html`, currentHtml);
|
|
334
|
+
|
|
335
|
+
// Paso final: of_genera_archivo
|
|
336
|
+
if (!currentHtml.includes('<AUTORIZACION') && currentHtml.includes('of_genera_archivo')) {
|
|
337
|
+
response = await this._processGeneraArchivo(response, debugDir);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return response;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Procesa generación de archivo CAF
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
async _processGeneraArchivo(response, debugDir) {
|
|
348
|
+
let currentHtml = response.body || '';
|
|
349
|
+
|
|
350
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_archivo';
|
|
351
|
+
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
352
|
+
|
|
353
|
+
const fields = {
|
|
354
|
+
...inputs,
|
|
355
|
+
ACEPTAR: 'AQUI',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
response = await this.session.submitForm(formAction, fields);
|
|
359
|
+
currentHtml = response.body || '';
|
|
360
|
+
this._saveDebug(debugDir, `archivo-${this.runStamp}.xml`, currentHtml);
|
|
361
|
+
|
|
362
|
+
// A veces hay un paso extra
|
|
363
|
+
if (!currentHtml.includes('<AUTORIZACION') && currentHtml.includes('of_genera_archivo')) {
|
|
364
|
+
const formAction2 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_archivo';
|
|
365
|
+
const inputs2 = SiiSession.extractInputValues(currentHtml);
|
|
366
|
+
|
|
367
|
+
const fields2 = {
|
|
368
|
+
...inputs2,
|
|
369
|
+
ACEPTAR: 'AQUI',
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
response = await this.session.submitForm(formAction2, fields2);
|
|
373
|
+
this._saveDebug(debugDir, `archivo2-${this.runStamp}.xml`, response.body || '');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return response;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = CafSolicitor;
|