@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 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;