@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.
Files changed (60) hide show
  1. package/BoletaService.js +109 -0
  2. package/CAF.js +173 -0
  3. package/CafSolicitor.js +380 -0
  4. package/Certificado.js +123 -0
  5. package/ConsumoFolio.js +376 -0
  6. package/DTE.js +399 -0
  7. package/EnviadorSII.js +1304 -0
  8. package/Envio.js +196 -0
  9. package/FolioRegistry.js +553 -0
  10. package/FolioService.js +703 -0
  11. package/LICENSE +27 -0
  12. package/LibroBase.js +134 -0
  13. package/LibroCompraVenta.js +205 -0
  14. package/LibroGuia.js +225 -0
  15. package/README.md +239 -0
  16. package/Signer.js +94 -0
  17. package/SiiCertificacion.js +1189 -0
  18. package/SiiPortalAuth.js +460 -0
  19. package/SiiSession.js +499 -0
  20. package/cert/BoletaCert.js +731 -0
  21. package/cert/CertFolioHelper.js +185 -0
  22. package/cert/CertRunner.js +2658 -0
  23. package/cert/ConfigLoader.js +133 -0
  24. package/cert/IntercambioCert.js +429 -0
  25. package/cert/LibroCompras.js +359 -0
  26. package/cert/LibroGuias.js +171 -0
  27. package/cert/LibroVentas.js +153 -0
  28. package/cert/MuestrasImpresas.js +676 -0
  29. package/cert/SetBase.js +321 -0
  30. package/cert/SetBasico.js +413 -0
  31. package/cert/SetCompra.js +472 -0
  32. package/cert/SetExenta.js +490 -0
  33. package/cert/SetGuia.js +283 -0
  34. package/cert/SetParser.js +1184 -0
  35. package/cert/SetsProvider.js +499 -0
  36. package/cert/Simulacion.js +521 -0
  37. package/cert/comunaOficina.js +460 -0
  38. package/cert/index.js +124 -0
  39. package/cert/types.js +330 -0
  40. package/dte-sii.d.ts +458 -0
  41. package/index.js +428 -0
  42. package/package.json +48 -0
  43. package/utils/c14n.js +275 -0
  44. package/utils/calculo.js +396 -0
  45. package/utils/config.js +276 -0
  46. package/utils/constants.js +302 -0
  47. package/utils/emisor.js +174 -0
  48. package/utils/endpoints.js +225 -0
  49. package/utils/error.js +235 -0
  50. package/utils/index.js +339 -0
  51. package/utils/logger.js +239 -0
  52. package/utils/pfx.js +203 -0
  53. package/utils/receptor.js +218 -0
  54. package/utils/referencia.js +169 -0
  55. package/utils/resolucion.js +119 -0
  56. package/utils/rut.js +169 -0
  57. package/utils/sanitize.js +124 -0
  58. package/utils/tokenCache.js +214 -0
  59. package/utils/xml.js +358 -0
  60. package/utils.js +4 -0
@@ -0,0 +1,109 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * BoletaService
5
+ *
6
+ * Servicio simplificado para crear boletas electrónicas
7
+ */
8
+
9
+ const Certificado = require('./Certificado');
10
+ const CAF = require('./CAF');
11
+ const DTE = require('./DTE');
12
+ const {
13
+ sanitizeSiiText,
14
+ buildDetalle,
15
+ calcularTotalesDesdeItems,
16
+ buildReceptorConsumidorFinal,
17
+ dteError,
18
+ ERROR_CODES,
19
+ createScopedLogger,
20
+ TIPOS_DTE,
21
+ esBoleta,
22
+ } = require('./utils');
23
+
24
+ const log = createScopedLogger('BoletaService');
25
+
26
+ class BoletaService {
27
+ constructor(config) {
28
+ this.config = config;
29
+ this.certificado = null;
30
+ this.caf = null;
31
+ }
32
+
33
+ /**
34
+ * Cargar certificado desde buffer PFX
35
+ */
36
+ cargarCertificado(pfxBuffer, password) {
37
+ this.certificado = new Certificado(pfxBuffer, password);
38
+ return this;
39
+ }
40
+
41
+ /**
42
+ * Cargar CAF desde string XML
43
+ */
44
+ cargarCAF(xmlContent) {
45
+ this.caf = new CAF(xmlContent);
46
+ return this;
47
+ }
48
+
49
+ /**
50
+ * Crear y firmar una boleta
51
+ * @param {Array} items - Items de la boleta [{NmbItem, QtyItem, PrcItem}]
52
+ * @param {number} folio - Número de folio
53
+ * @param {Object} receptor - Datos del receptor (opcional)
54
+ * @returns {Object} - Resultado con XML firmado
55
+ */
56
+ crearBoleta(items, folio, receptor = null) {
57
+ if (!this.certificado) {
58
+ throw dteError('Certificado no cargado', ERROR_CODES.CERT_INVALID);
59
+ }
60
+ if (!this.caf) {
61
+ throw dteError('CAF no cargado', ERROR_CODES.CAF_INVALID);
62
+ }
63
+
64
+ // Usar utilidades centralizadas para construir detalle y calcular totales
65
+ const detalle = buildDetalle(items);
66
+ const totales = calcularTotalesDesdeItems(items);
67
+
68
+ const datos = {
69
+ Encabezado: {
70
+ IdDoc: {
71
+ TipoDTE: TIPOS_DTE.BOLETA,
72
+ Folio: folio,
73
+ FchEmis: new Date().toISOString().split('T')[0],
74
+ IndServicio: 3,
75
+ },
76
+ Emisor: {
77
+ RUTEmisor: this.config.rutEmisor,
78
+ RznSocEmisor: this.config.razonSocial,
79
+ GiroEmisor: this.config.giro,
80
+ DirOrigen: this.config.direccion,
81
+ CmnaOrigen: this.config.comuna,
82
+ },
83
+ Receptor: receptor || buildReceptorConsumidorFinal(),
84
+ Totales: {
85
+ MntTotal: totales.MntTotal,
86
+ },
87
+ },
88
+ Detalle: detalle,
89
+ };
90
+
91
+ const dte = new DTE(datos);
92
+ dte.generarXML();
93
+ dte.timbrar(this.caf);
94
+ dte.firmar(this.certificado);
95
+
96
+ log.log(`✅ Boleta creada: Folio ${folio}, Monto $${totales.MntTotal}`);
97
+
98
+ return {
99
+ ok: true,
100
+ folio,
101
+ tipo_dte: TIPOS_DTE.BOLETA,
102
+ fecha: datos.Encabezado.IdDoc.FchEmis,
103
+ monto_total: totales.MntTotal,
104
+ xml: dte.getXML(),
105
+ };
106
+ }
107
+ }
108
+
109
+ module.exports = BoletaService;
package/CAF.js ADDED
@@ -0,0 +1,173 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * CAF (Código de Autorización de Folios)
5
+ *
6
+ * Maneja los archivos CAF del SII para timbraje de documentos
7
+ */
8
+
9
+ const forge = require('node-forge');
10
+ const {
11
+ cafError,
12
+ ERROR_CODES,
13
+ createScopedLogger,
14
+ parseXml,
15
+ buildXml,
16
+ IDK_CERTIFICACION,
17
+ getNombreDte,
18
+ } = require('./utils');
19
+
20
+ const log = createScopedLogger('CAF');
21
+
22
+ class CAF {
23
+ /**
24
+ * @param {string} xmlContent - Contenido XML del archivo CAF
25
+ * @throws {DteSiiError} Si el CAF es inválido
26
+ */
27
+ constructor(xmlContent) {
28
+ if (!xmlContent || typeof xmlContent !== 'string') {
29
+ throw cafError('CAF XML content es requerido', ERROR_CODES.CAF_INVALID, { xmlContent });
30
+ }
31
+
32
+ // Usar parser centralizado
33
+ try {
34
+ this.data = parseXml(xmlContent);
35
+ } catch (err) {
36
+ throw cafError(`Error parseando CAF XML: ${err.message}`, ERROR_CODES.CAF_INVALID, { originalError: err });
37
+ }
38
+
39
+ // Validar estructura
40
+ if (!this.data.AUTORIZACION) {
41
+ throw cafError('CAF inválido: falta elemento AUTORIZACION', ERROR_CODES.CAF_INVALID);
42
+ }
43
+
44
+ this.autorizacion = this.data.AUTORIZACION;
45
+ this.caf = this.autorizacion.CAF;
46
+
47
+ if (!this.caf) {
48
+ throw cafError('CAF inválido: falta elemento CAF', ERROR_CODES.CAF_INVALID);
49
+ }
50
+
51
+ this.da = this.caf.DA;
52
+
53
+ if (!this.da) {
54
+ throw cafError('CAF inválido: falta elemento DA (Datos de Autorización)', ERROR_CODES.CAF_INVALID);
55
+ }
56
+
57
+ // Propiedades principales
58
+ this.tipo = parseInt(this.da.TD, 10);
59
+ this.folioDesde = parseInt(this.da.RNG?.D, 10);
60
+ this.folioHasta = parseInt(this.da.RNG?.H, 10);
61
+ this.rutEmisor = this.da.RE;
62
+
63
+ if (isNaN(this.folioDesde) || isNaN(this.folioHasta)) {
64
+ throw cafError('CAF inválido: rango de folios no válido', ERROR_CODES.CAF_INVALID, {
65
+ folioDesde: this.da.RNG?.D,
66
+ folioHasta: this.da.RNG?.H,
67
+ });
68
+ }
69
+
70
+ // Clave privada del CAF
71
+ if (!this.autorizacion.RSASK) {
72
+ throw cafError('CAF inválido: falta clave privada RSASK', ERROR_CODES.CAF_INVALID);
73
+ }
74
+
75
+ this.privateKeyPem = this.autorizacion.RSASK.trim();
76
+
77
+ try {
78
+ this.privateKey = forge.pki.privateKeyFromPem(this.privateKeyPem);
79
+ } catch (err) {
80
+ throw cafError(`Error cargando clave privada del CAF: ${err.message}`, ERROR_CODES.CAF_INVALID, { originalError: err });
81
+ }
82
+
83
+ // CAF XML original (el SII exige formato exacto)
84
+ this._originalCafXml = this._extractCafXml(xmlContent);
85
+
86
+ log.debug(`CAF cargado: Tipo ${this.tipo}, Folios ${this.folioDesde}-${this.folioHasta}`);
87
+ }
88
+
89
+ /**
90
+ * Extraer el CAF XML exacto del archivo original
91
+ */
92
+ _extractCafXml(xmlContent) {
93
+ const cafMatch = xmlContent.match(/<CAF[^>]*>[\s\S]*?<\/CAF>/);
94
+ if (cafMatch) {
95
+ // Normalizar: quitar espacios entre tags
96
+ return cafMatch[0].replace(/>\s+</g, '><');
97
+ }
98
+ return null;
99
+ }
100
+
101
+ // Getters
102
+ getRutEmisor() { return this.da.RE; }
103
+ getTipoDTE() { return parseInt(this.da.TD, 10); }
104
+ getFolioDesde() { return parseInt(this.da.RNG.D, 10); }
105
+ getFolioHasta() { return parseInt(this.da.RNG.H, 10); }
106
+ getIDK() { return parseInt(this.da.IDK, 10); }
107
+ esCertificacion() { return this.getIDK() === IDK_CERTIFICACION; }
108
+
109
+ /**
110
+ * Obtener nombre del tipo de DTE
111
+ * @param {boolean} [corto=false] - Si usar nombre corto
112
+ * @returns {string}
113
+ */
114
+ getNombreTipoDTE(corto = false) {
115
+ return getNombreDte(this.tipo, corto);
116
+ }
117
+
118
+ /**
119
+ * Obtener CAF XML para insertar en TED
120
+ */
121
+ getCafXml() {
122
+ if (this._originalCafXml) {
123
+ return this._originalCafXml;
124
+ }
125
+ // Fallback: reconstruir usando builder centralizado
126
+ return buildXml({ CAF: this.caf });
127
+ }
128
+
129
+ /**
130
+ * Firmar datos con la clave privada del CAF (para TED)
131
+ * IMPORTANTE: Usamos 'latin1' porque así interpreta forge los bytes
132
+ * y debe coincidir con lo que se pasa al PDF417
133
+ */
134
+ sign(data) {
135
+ const md = forge.md.sha1.create();
136
+ md.update(data, 'latin1');
137
+ const signature = this.privateKey.sign(md);
138
+ return forge.util.encode64(signature);
139
+ }
140
+
141
+ /**
142
+ * Verificar si un folio está dentro del rango autorizado
143
+ * @param {number} folio - Número de folio
144
+ * @returns {boolean} True si es válido
145
+ */
146
+ isFolioValido(folio) {
147
+ return folio >= this.folioDesde && folio <= this.folioHasta;
148
+ }
149
+
150
+ /**
151
+ * Validar folio y lanzar error si no es válido
152
+ * @param {number} folio - Número de folio
153
+ * @throws {DteSiiError} Si el folio está fuera de rango
154
+ */
155
+ validarFolio(folio) {
156
+ if (!this.isFolioValido(folio)) {
157
+ throw cafError(
158
+ `Folio ${folio} fuera de rango autorizado (${this.folioDesde}-${this.folioHasta})`,
159
+ ERROR_CODES.FOLIO_OUT_OF_RANGE,
160
+ { folio, folioDesde: this.folioDesde, folioHasta: this.folioHasta, tipo: this.tipo }
161
+ );
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Obtener folios disponibles
167
+ */
168
+ getFoliosDisponibles() {
169
+ return this.folioHasta - this.folioDesde + 1;
170
+ }
171
+ }
172
+
173
+ module.exports = CAF;
@@ -0,0 +1,380 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
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;