@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
|
@@ -0,0 +1,2658 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* CertRunner - Orquestador del proceso de certificación SII
|
|
5
|
+
*
|
|
6
|
+
* Encapsula todo el flujo de certificación:
|
|
7
|
+
* - Obtención de sets de prueba
|
|
8
|
+
* - Solicitud de CAFs
|
|
9
|
+
* - Ejecución de sets (Básico, Guía, Exenta, Compra)
|
|
10
|
+
* - Envío de libros
|
|
11
|
+
* - Declaración de avance
|
|
12
|
+
* - Polling de estado
|
|
13
|
+
*
|
|
14
|
+
* @module dte-sii/cert/CertRunner
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// Core
|
|
21
|
+
const { Certificado, EnvioDTE } = require('../index');
|
|
22
|
+
const EnviadorSII = require('../EnviadorSII');
|
|
23
|
+
const FolioService = require('../FolioService');
|
|
24
|
+
const SiiCertificacion = require('../SiiCertificacion');
|
|
25
|
+
|
|
26
|
+
// Módulo cert
|
|
27
|
+
const SetsProvider = require('./SetsProvider');
|
|
28
|
+
const CertFolioHelper = require('./CertFolioHelper');
|
|
29
|
+
const SetBasico = require('./SetBasico');
|
|
30
|
+
const SetGuia = require('./SetGuia');
|
|
31
|
+
const SetExenta = require('./SetExenta');
|
|
32
|
+
const SetCompra = require('./SetCompra');
|
|
33
|
+
|
|
34
|
+
// Libros (Fase 4)
|
|
35
|
+
const LibroVentas = require('./LibroVentas');
|
|
36
|
+
const LibroCompras = require('./LibroCompras');
|
|
37
|
+
const LibroGuias = require('./LibroGuias');
|
|
38
|
+
|
|
39
|
+
// Simulación (Fase 6)
|
|
40
|
+
const Simulacion = require('./Simulacion');
|
|
41
|
+
|
|
42
|
+
// Intercambio (Fase 7)
|
|
43
|
+
const IntercambioCert = require('./IntercambioCert');
|
|
44
|
+
|
|
45
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} CertConfig
|
|
49
|
+
* @property {Object} certificado - { path, password }
|
|
50
|
+
* @property {Object} emisor - { rut, razon_social, giro, acteco, direccion, comuna, ciudad, fch_resol, nro_resol }
|
|
51
|
+
* @property {Object} receptor - { rut, razon_social, giro, direccion, comuna, ciudad }
|
|
52
|
+
* @property {string} [ambiente='certificacion']
|
|
53
|
+
* @property {string} [debugDir]
|
|
54
|
+
* @property {string} [sessionPath]
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
class CertRunner {
|
|
58
|
+
/**
|
|
59
|
+
* @param {CertConfig} config - Configuración del runner
|
|
60
|
+
*/
|
|
61
|
+
constructor(config) {
|
|
62
|
+
this._validateConfig(config);
|
|
63
|
+
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.ambiente = config.ambiente || 'certificacion';
|
|
66
|
+
this.debugDir = config.debugDir || path.join(process.cwd(), 'debug', 'cert-v2');
|
|
67
|
+
this.sessionPath = config.sessionPath || path.join(this.debugDir, 'session.json');
|
|
68
|
+
|
|
69
|
+
// Componentes (lazy init)
|
|
70
|
+
this._certificado = null;
|
|
71
|
+
this._folioService = null;
|
|
72
|
+
this._folioHelper = null;
|
|
73
|
+
this._siiCert = null;
|
|
74
|
+
this._setsProvider = null;
|
|
75
|
+
this._estructuras = null;
|
|
76
|
+
|
|
77
|
+
// Caché de sesión SII en memoria (evita logins múltiples durante la misma ejecución)
|
|
78
|
+
// Se puede inyectar un cookieJar ya obtenido vía config.cookieJar para reutilizar sesión.
|
|
79
|
+
this._siiCookieJar = config.cookieJar || null;
|
|
80
|
+
|
|
81
|
+
// Resultados de ejecución
|
|
82
|
+
this.resultados = {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_validateConfig(config) {
|
|
86
|
+
if (!config.certificado?.path) throw new Error('CertRunner: config.certificado.path es obligatorio');
|
|
87
|
+
if (!config.certificado?.password && config.certificado?.password !== '') {
|
|
88
|
+
throw new Error('CertRunner: config.certificado.password es obligatorio');
|
|
89
|
+
}
|
|
90
|
+
if (!config.emisor?.rut) throw new Error('CertRunner: config.emisor.rut es obligatorio');
|
|
91
|
+
// receptor solo es obligatorio para flujos de emisión DTE (no para métodos de portal Puppeteer)
|
|
92
|
+
if (config.receptor && !config.receptor.rut) {
|
|
93
|
+
throw new Error('CertRunner: config.receptor.rut es obligatorio cuando se proporciona config.receptor');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════
|
|
98
|
+
// Getters lazy para componentes
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
get certificado() {
|
|
102
|
+
if (!this._certificado) {
|
|
103
|
+
const pfxBuffer = fs.readFileSync(this.config.certificado.path);
|
|
104
|
+
this._certificado = new Certificado(pfxBuffer, this.config.certificado.password);
|
|
105
|
+
}
|
|
106
|
+
return this._certificado;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get folioService() {
|
|
110
|
+
if (!this._folioService) {
|
|
111
|
+
this._folioService = new FolioService({
|
|
112
|
+
ambiente: this.ambiente,
|
|
113
|
+
rutEmisor: this.config.emisor.rut,
|
|
114
|
+
pfxPath: this.config.certificado.path,
|
|
115
|
+
pfxPassword: this.config.certificado.password,
|
|
116
|
+
debugDir: this.debugDir,
|
|
117
|
+
sessionPath: this.sessionPath, // Usar sesión compartida
|
|
118
|
+
retries: 3,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return this._folioService;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get folioHelper() {
|
|
125
|
+
if (!this._folioHelper) {
|
|
126
|
+
this._folioHelper = new CertFolioHelper({ ambiente: this.ambiente });
|
|
127
|
+
}
|
|
128
|
+
return this._folioHelper;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get siiCert() {
|
|
132
|
+
if (!this._siiCert) {
|
|
133
|
+
const [rut, dv] = this.config.emisor.rut.split('-');
|
|
134
|
+
this._siiCert = new SiiCertificacion({
|
|
135
|
+
pfxPath: this.config.certificado.path,
|
|
136
|
+
pfxPassword: this.config.certificado.password,
|
|
137
|
+
rutEmpresa: rut,
|
|
138
|
+
dvEmpresa: dv,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return this._siiCert;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get setsProvider() {
|
|
145
|
+
if (!this._setsProvider) {
|
|
146
|
+
const [rut, dv] = this.config.emisor.rut.split('-');
|
|
147
|
+
this._setsProvider = new SetsProvider({
|
|
148
|
+
pfxPath: this.config.certificado.path,
|
|
149
|
+
pfxPassword: this.config.certificado.password,
|
|
150
|
+
rutEmpresa: rut,
|
|
151
|
+
dvEmpresa: dv,
|
|
152
|
+
sessionPath: this.sessionPath,
|
|
153
|
+
debugDir: this.debugDir,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return this._setsProvider;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ═══════════════════════════════════════════════════════════════
|
|
160
|
+
// Métodos públicos
|
|
161
|
+
// ═══════════════════════════════════════════════════════════════
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Obtiene los sets de prueba del SII
|
|
165
|
+
* @param {Object} [options] - { setsOpcionales }
|
|
166
|
+
* @returns {Promise<Object>} { success, estructuras, error }
|
|
167
|
+
*/
|
|
168
|
+
async obtenerSets(options = {}) {
|
|
169
|
+
const resultado = await this.setsProvider.obtenerSets({
|
|
170
|
+
setsOpcionales: options.setsOpcionales || {
|
|
171
|
+
SET03: 'S', // SET GUIA DE DESPACHO
|
|
172
|
+
SET06: 'S', // SET FACTURA EXENTA
|
|
173
|
+
SET07: 'S', // LIBRO DE VENTAS
|
|
174
|
+
SET08: 'S', // LIBRO DE COMPRAS
|
|
175
|
+
SET09: 'S', // LIBRO DE GUIAS
|
|
176
|
+
SET15: 'S', // LIBRO DE COMPRAS PARA EXENTOS
|
|
177
|
+
SET72: 'S', // SET CASO GENERAL FACTURA COMPRA
|
|
178
|
+
},
|
|
179
|
+
forceRefresh: true, // siempre fresco — nunca reusar caché de ejecuciones anteriores
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!resultado.success) {
|
|
183
|
+
return { success: false, error: resultado.error || 'Error desconocido al obtener sets' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this._estructuras = resultado.estructuras;
|
|
187
|
+
|
|
188
|
+
// Guardar debug — solo si hay estructuras reales (no sobreescribir con null)
|
|
189
|
+
if (resultado.estructuras) {
|
|
190
|
+
fs.mkdirSync(this.debugDir, { recursive: true });
|
|
191
|
+
fs.writeFileSync(
|
|
192
|
+
path.join(this.debugDir, 'estructuras.json'),
|
|
193
|
+
JSON.stringify(resultado.estructuras, null, 2)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { success: true, estructuras: resultado.estructuras };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Solicita CAFs frescos para los tipos especificados
|
|
202
|
+
* @param {Object} cafRequired - { 33: 4, 56: 1, 61: 3 }
|
|
203
|
+
* @returns {Promise<Object>} { 33: cafPath, 56: cafPath, ... }
|
|
204
|
+
*/
|
|
205
|
+
async solicitarCafs(cafRequired) {
|
|
206
|
+
const cafs = {};
|
|
207
|
+
|
|
208
|
+
// Limpiar contadores para nuevos CAFs
|
|
209
|
+
this.folioHelper.counters.clear();
|
|
210
|
+
this.folioHelper.usedFolios.clear();
|
|
211
|
+
|
|
212
|
+
for (const [tipoDte, cantidad] of Object.entries(cafRequired)) {
|
|
213
|
+
console.log(` Tipo ${tipoDte}: ${cantidad} folios...`);
|
|
214
|
+
|
|
215
|
+
// Usar solicitarCafConFallback que solicita y retorna el path
|
|
216
|
+
const cafPath = await this.folioService.solicitarCafConFallback({
|
|
217
|
+
tipoDte: Number(tipoDte),
|
|
218
|
+
cantidad: Number(cantidad),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!cafPath) {
|
|
222
|
+
throw new Error(`No se pudo obtener CAF para tipo ${tipoDte}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cafs[tipoDte] = cafPath;
|
|
226
|
+
console.log(` ✓ CAF tipo ${tipoDte}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return cafs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Crea un enviador para enviar DTEs al SII
|
|
234
|
+
* @param {string} [setName] - Nombre del set para guardar XML (ej: 'basico', 'guia')
|
|
235
|
+
* @returns {Object} Enviador compatible con Sets
|
|
236
|
+
*/
|
|
237
|
+
_createEnviador(setName = null) {
|
|
238
|
+
const enviador = new EnviadorSII(this.certificado, this.ambiente);
|
|
239
|
+
const debugDir = this.debugDir;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
async enviar(envio) {
|
|
243
|
+
// Guardar XML del set antes de enviar (para muestras impresas)
|
|
244
|
+
if (setName && debugDir && envio.xml) {
|
|
245
|
+
const setsDir = path.join(debugDir, 'sets-prueba');
|
|
246
|
+
fs.mkdirSync(setsDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
// Guardar envío consolidado
|
|
249
|
+
const envioPath = path.join(setsDir, `envio-set-${setName}.xml`);
|
|
250
|
+
fs.writeFileSync(envioPath, envio.xml, 'utf8');
|
|
251
|
+
console.log(` 📄 XML guardado: ${envioPath}`);
|
|
252
|
+
|
|
253
|
+
// Guardar DTEs individuales
|
|
254
|
+
if (envio.dtes && envio.dtes.length > 0) {
|
|
255
|
+
const dtesDir = path.join(setsDir, 'dtes');
|
|
256
|
+
fs.mkdirSync(dtesDir, { recursive: true });
|
|
257
|
+
for (const dte of envio.dtes) {
|
|
258
|
+
const tipoDte = dte.getTipoDTE ? dte.getTipoDTE() : dte.tipoDte;
|
|
259
|
+
const folio = dte.getFolio ? dte.getFolio() : dte.folio;
|
|
260
|
+
const dteXml = dte.xml || (dte.getXml ? dte.getXml() : null);
|
|
261
|
+
if (dteXml) {
|
|
262
|
+
const filename = `dte-${String(tipoDte).padStart(2, '0')}-${String(folio).padStart(6, '0')}.xml`;
|
|
263
|
+
fs.writeFileSync(path.join(dtesDir, filename), dteXml, 'utf8');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const resultado = await enviador.enviarDteSoap(envio);
|
|
270
|
+
return {
|
|
271
|
+
success: !!resultado?.trackId,
|
|
272
|
+
trackId: resultado?.trackId,
|
|
273
|
+
error: resultado?.error,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Ejecuta un set genérico — toda la lógica común entre los 4 sets.
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
async _ejecutarSet(ClaseSet, estructuraKey, resultadoKey, cafFallback, enviadorNombre, casosExterno) {
|
|
284
|
+
const setData = casosExterno || this._estructuras?.[estructuraKey];
|
|
285
|
+
if (!setData) {
|
|
286
|
+
throw new Error(`No hay casos para ${ClaseSet.name}. Ejecutar obtenerSets() primero.`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const cafRequired = setData.cafRequired ||
|
|
290
|
+
(typeof cafFallback === 'function' ? cafFallback(setData) : cafFallback);
|
|
291
|
+
const cafs = await this.solicitarCafs(cafRequired);
|
|
292
|
+
|
|
293
|
+
const set = new ClaseSet({
|
|
294
|
+
config: {
|
|
295
|
+
emisor: this.config.emisor,
|
|
296
|
+
receptor: this.config.receptor,
|
|
297
|
+
certificado: this.config.certificado,
|
|
298
|
+
ambiente: this.ambiente,
|
|
299
|
+
resolucion: {
|
|
300
|
+
fecha: this.config.emisor.fch_resol,
|
|
301
|
+
numero: this.config.emisor.nro_resol,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
cafManager: { ensureCaf: ({ tipoDte }) => cafs[tipoDte] },
|
|
305
|
+
folioHelper: this.folioHelper,
|
|
306
|
+
enviador: this._createEnviador(enviadorNombre),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const resultado = await set.ejecutar(setData, cafs);
|
|
310
|
+
this.resultados[resultadoKey] = resultado;
|
|
311
|
+
return resultado;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async ejecutarSetBasico(casos) {
|
|
315
|
+
return this._ejecutarSet(SetBasico, 'setBasico', 'basico', { 33: 4, 56: 1, 61: 3 }, 'basico', casos);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async ejecutarSetGuia(casos) {
|
|
319
|
+
return this._ejecutarSet(SetGuia, 'setGuiaDespacho', 'guia',
|
|
320
|
+
(setData) => ({ 52: setData.casos?.length || 1 }), 'guia', casos);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async ejecutarSetExenta(casos) {
|
|
324
|
+
return this._ejecutarSet(SetExenta, 'setFacturaExenta', 'exenta', { 34: 3, 56: 1, 61: 4 }, 'exenta', casos);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async ejecutarSetCompra(casos) {
|
|
328
|
+
return this._ejecutarSet(SetCompra, 'setFacturaCompra', 'compra', { 46: 1, 56: 1, 61: 1 }, 'compra', casos);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Bucle de reintentos común para declarar avance/libros/simulación.
|
|
333
|
+
* @private
|
|
334
|
+
* @param {Object} sets - Sets a declarar (pasados a siiCert.declararAvance)
|
|
335
|
+
* @param {string} debugPrefix - Prefijo del archivo HTML de debug (ej: 'declaracion-response')
|
|
336
|
+
* @param {Object} [options] - { maxIntentos, intervalo, label }
|
|
337
|
+
*/
|
|
338
|
+
async _declararConReintentos(sets, debugPrefix, options = {}) {
|
|
339
|
+
const { maxIntentos = 10, intervalo = 5000, label = 'avance' } = options;
|
|
340
|
+
|
|
341
|
+
console.log(` ⏳ Esperando 10s para que SII procese los envíos...`);
|
|
342
|
+
await sleep(10000);
|
|
343
|
+
|
|
344
|
+
let lastResult = null;
|
|
345
|
+
for (let intento = 1; intento <= maxIntentos; intento++) {
|
|
346
|
+
console.log(` 🔄 Declarando ${label} (intento ${intento}/${maxIntentos})...`);
|
|
347
|
+
|
|
348
|
+
const result = await this.siiCert.declararAvance({ sets });
|
|
349
|
+
lastResult = result;
|
|
350
|
+
|
|
351
|
+
if (result.rawHtml) {
|
|
352
|
+
fs.writeFileSync(
|
|
353
|
+
path.join(this.debugDir, `${debugPrefix}-${intento}.html`),
|
|
354
|
+
result.rawHtml,
|
|
355
|
+
'utf8'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const html = result.rawHtml || '';
|
|
360
|
+
const noProcessedError =
|
|
361
|
+
html.includes('no ha sido procesado') ||
|
|
362
|
+
html.includes('aún no está disponible') ||
|
|
363
|
+
html.includes('intente más tarde') ||
|
|
364
|
+
html.includes('no se encuentra');
|
|
365
|
+
|
|
366
|
+
if (result.success && !noProcessedError) {
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (noProcessedError && intento < maxIntentos) {
|
|
371
|
+
console.log(` ⏳ SII aún procesando, reintentando en ${intervalo / 1000}s...`);
|
|
372
|
+
await sleep(intervalo);
|
|
373
|
+
} else if (!result.success) {
|
|
374
|
+
console.log(` ⚠️ Error declarando ${label}: ${result.error || 'desconocido'}`);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return lastResult;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Declara avance de los sets ejecutados con reintentos automáticos
|
|
384
|
+
* @param {Object} [resultadosExt] - Resultados externos (usa this.resultados si no se pasa)
|
|
385
|
+
* @param {Object} [estructurasExt] - Estructuras externas (usa this._estructuras si no se pasa)
|
|
386
|
+
* @param {Object} [options] - { maxIntentos, intervalo }
|
|
387
|
+
* @returns {Promise<Object>} Resultado de la declaración
|
|
388
|
+
*/
|
|
389
|
+
async declararAvance(resultadosExt, estructurasExt, options = {}) {
|
|
390
|
+
const resultados = resultadosExt || this.resultados;
|
|
391
|
+
const estructuras = estructurasExt || this._estructuras;
|
|
392
|
+
const { maxIntentos = 10, intervalo = 5000 } = options;
|
|
393
|
+
|
|
394
|
+
if (!estructuras) {
|
|
395
|
+
throw new Error('No hay estructuras. Ejecutar obtenerSets() primero.');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const fecha = this._getFechaHoy();
|
|
399
|
+
const sets = {};
|
|
400
|
+
|
|
401
|
+
const mapping = {
|
|
402
|
+
basico: 'setBasico',
|
|
403
|
+
guia: 'setGuiaDespacho',
|
|
404
|
+
exenta: 'setFacturaExenta',
|
|
405
|
+
compra: 'setFacturaCompra',
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
for (const [resKey, setKey] of Object.entries(mapping)) {
|
|
409
|
+
if (resultados[resKey]?.trackId) {
|
|
410
|
+
sets[setKey] = {
|
|
411
|
+
trackId: resultados[resKey].trackId,
|
|
412
|
+
fecha,
|
|
413
|
+
numeroAtencion: estructuras[setKey]?.numeroAtencion,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (Object.keys(sets).length === 0) {
|
|
419
|
+
return { success: false, error: 'No hay sets para declarar' };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const result = await this._declararConReintentos(sets, 'declaracion-response', { maxIntentos, intervalo, label: 'avance de sets' });
|
|
423
|
+
if (result?.success) console.log(' ✓ Declaración de sets enviada');
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Espera a que los sets sean aprobados
|
|
429
|
+
* @param {string[]} [sets] - Sets a esperar (default: todos los ejecutados)
|
|
430
|
+
* @param {Object} [options] - { maxIntentos, intervalo, onProgress }
|
|
431
|
+
* @returns {Promise<Object>} Resultado del polling
|
|
432
|
+
*/
|
|
433
|
+
async esperarAprobacion(sets, options = {}) {
|
|
434
|
+
const setsAEsperar = sets || Object.entries(this.resultados)
|
|
435
|
+
.filter(([_, r]) => r?.trackId)
|
|
436
|
+
.map(([k]) => {
|
|
437
|
+
const mapping = { basico: 'setBasico', guia: 'setGuiaDespacho', exenta: 'setFacturaExenta', compra: 'setFacturaCompra' };
|
|
438
|
+
return mapping[k];
|
|
439
|
+
})
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
|
|
442
|
+
return this.siiCert.waitForApproval(setsAEsperar, options);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Consulta el estado actual de avance
|
|
447
|
+
* @returns {Promise<Object>} Estados parseados
|
|
448
|
+
*/
|
|
449
|
+
async consultarAvance() {
|
|
450
|
+
return this.siiCert.verAvanceParsed();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ═══════════════════════════════════════════════════════════════
|
|
454
|
+
// LIBROS (Fase 4)
|
|
455
|
+
// ═══════════════════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Ejecuta toda la Fase 4: Envío y declaración de libros
|
|
459
|
+
* NOTA: Cada libro decrementa su propio período para evitar "LNC - Libro Cerrado"
|
|
460
|
+
* @param {Object} [options] - Opciones
|
|
461
|
+
* @param {Object} [options.setBasicoResult] - Resultado del SetBasico
|
|
462
|
+
* @param {Object} [options.setGuiaResult] - Resultado del SetGuia
|
|
463
|
+
* @returns {Promise<Object>} Resultado con todos los libros
|
|
464
|
+
*/
|
|
465
|
+
async ejecutarFase4Libros(options = {}) {
|
|
466
|
+
// NOTA: Ya NO decrementamos aquí - cada libro decrementa su propio período
|
|
467
|
+
console.log('\n' + '═'.repeat(60));
|
|
468
|
+
console.log(`📚 FASE 4: LIBROS (cada libro usará período diferente)`);
|
|
469
|
+
console.log('═'.repeat(60) + '\n');
|
|
470
|
+
|
|
471
|
+
const resultados = {};
|
|
472
|
+
const errores = [];
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
// 1. Libro de Compras (usa datos del SII)
|
|
476
|
+
console.log('\n📖 Enviando Libro de Compras...');
|
|
477
|
+
resultados.libroCompras = await this.ejecutarLibroCompras(options);
|
|
478
|
+
if (!resultados.libroCompras.success) {
|
|
479
|
+
errores.push(`Libro Compras: ${resultados.libroCompras.error}`);
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
errores.push(`Libro Compras: ${e.message}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
// 2. Libro de Ventas (usa SetBasico)
|
|
487
|
+
console.log('\n📖 Enviando Libro de Ventas...');
|
|
488
|
+
resultados.libroVentas = await this.ejecutarLibroVentas(options);
|
|
489
|
+
if (!resultados.libroVentas.success) {
|
|
490
|
+
errores.push(`Libro Ventas: ${resultados.libroVentas.error}`);
|
|
491
|
+
}
|
|
492
|
+
} catch (e) {
|
|
493
|
+
errores.push(`Libro Ventas: ${e.message}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
// 3. Libro de Guías (usa SetGuia)
|
|
498
|
+
console.log('\n📖 Enviando Libro de Guías...');
|
|
499
|
+
resultados.libroGuias = await this.ejecutarLibroGuias(options);
|
|
500
|
+
if (!resultados.libroGuias.success) {
|
|
501
|
+
errores.push(`Libro Guías: ${resultados.libroGuias.error}`);
|
|
502
|
+
}
|
|
503
|
+
} catch (e) {
|
|
504
|
+
errores.push(`Libro Guías: ${e.message}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 4. Libro de Compras para Exentos (solo si el SII lo entregó)
|
|
508
|
+
if (this._estructuras?.libroComprasExentos) {
|
|
509
|
+
try {
|
|
510
|
+
console.log('\n📖 Enviando Libro de Compras para Exentos...');
|
|
511
|
+
resultados.libroComprasExentos = await this.ejecutarLibroComprasExentos(options);
|
|
512
|
+
if (!resultados.libroComprasExentos.success) {
|
|
513
|
+
errores.push(`Libro Compras Exentos: ${resultados.libroComprasExentos.error}`);
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
errores.push(`Libro Compras Exentos: ${e.message}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Contar libros obligatorios (ventas + compras + guías)
|
|
521
|
+
const librosObligatorios = ['libroVentas', 'libroCompras', 'libroGuias'];
|
|
522
|
+
const librosEnviados = librosObligatorios.filter(k => resultados[k]?.success).length;
|
|
523
|
+
|
|
524
|
+
if (librosEnviados === 3) {
|
|
525
|
+
// 4. Declarar los libros
|
|
526
|
+
console.log('\n📝 Declarando libros...');
|
|
527
|
+
try {
|
|
528
|
+
const declaracion = await this.declararLibros();
|
|
529
|
+
resultados.declaracion = declaracion;
|
|
530
|
+
|
|
531
|
+
if (declaracion.success) {
|
|
532
|
+
console.log('\n✅ FASE 4 COMPLETADA: Todos los libros enviados y declarados');
|
|
533
|
+
} else {
|
|
534
|
+
console.log(`\n⚠️ Libros enviados pero declaración con error: ${declaracion.error}`);
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
console.log(`\n⚠️ Error declarando libros: ${e.message}`);
|
|
538
|
+
resultados.declaracion = { success: false, error: e.message };
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
console.log(`\n⚠️ Solo ${librosEnviados}/3 libros enviados. Errores: ${errores.join('; ')}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
success: librosEnviados === 3,
|
|
546
|
+
librosEnviados,
|
|
547
|
+
resultados,
|
|
548
|
+
errores,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Declara avance de los libros ejecutados con reintentos automáticos
|
|
554
|
+
* @param {Object} [resultadosExt] - Resultados externos (usa this.resultados si no se pasa)
|
|
555
|
+
* @param {Object} [options] - { maxIntentos, intervalo }
|
|
556
|
+
* @returns {Promise<Object>} Resultado de la declaración
|
|
557
|
+
*/
|
|
558
|
+
async declararLibros(resultadosExt, options = {}) {
|
|
559
|
+
const resultados = resultadosExt || this.resultados;
|
|
560
|
+
const { maxIntentos = 10, intervalo = 5000 } = options;
|
|
561
|
+
|
|
562
|
+
const fecha = this._getFechaHoy();
|
|
563
|
+
const sets = {};
|
|
564
|
+
|
|
565
|
+
const mapping = {
|
|
566
|
+
libroVentas: 'libroVentas',
|
|
567
|
+
libroCompras: 'libroCompras',
|
|
568
|
+
libroGuias: 'libroGuias',
|
|
569
|
+
libroComprasExentos: 'libroComprasExentos',
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
for (const [resKey, setKey] of Object.entries(mapping)) {
|
|
573
|
+
if (resultados[resKey]?.trackId) {
|
|
574
|
+
sets[setKey] = { trackId: resultados[resKey].trackId, fecha };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (Object.keys(sets).length === 0) {
|
|
579
|
+
return { success: false, error: 'No hay libros para declarar' };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const result = await this._declararConReintentos(sets, 'declaracion-libros-response', { maxIntentos, intervalo, label: 'libros' });
|
|
583
|
+
if (result?.success) {
|
|
584
|
+
const declarados = result.setsDeclarados || [];
|
|
585
|
+
console.log(` ✅ Libros declarados: ${declarados.join(', ')}`);
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Obtiene el período para libros (período fijo para certificación)
|
|
592
|
+
* El período se decrementa cada vez que el SII rechaza con "LNC - Libro Cerrado"
|
|
593
|
+
* Se guarda el período en un archivo de estado para persistir entre ejecuciones
|
|
594
|
+
* @returns {string} Período en formato YYYY-MM
|
|
595
|
+
*/
|
|
596
|
+
_getPeriodoLibros() {
|
|
597
|
+
const stateFile = path.join(this.debugDir, 'periodo-libros.json');
|
|
598
|
+
|
|
599
|
+
// Cargar estado existente o crear nuevo
|
|
600
|
+
let state = { periodo: null, lastRun: null };
|
|
601
|
+
try {
|
|
602
|
+
if (fs.existsSync(stateFile)) {
|
|
603
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
604
|
+
}
|
|
605
|
+
} catch (e) {
|
|
606
|
+
// Usar default
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Si no hay período guardado, usar el mes anterior al actual
|
|
610
|
+
if (!state.periodo) {
|
|
611
|
+
const now = new Date();
|
|
612
|
+
let year = now.getFullYear();
|
|
613
|
+
let month = now.getMonth(); // 0-indexed, así que es el mes anterior
|
|
614
|
+
|
|
615
|
+
if (month < 1) {
|
|
616
|
+
month = 12;
|
|
617
|
+
year -= 1;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
state.periodo = `${year}-${String(month).padStart(2, '0')}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return state.periodo;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Decrementa el período de libros (llamar cuando falla con LNC)
|
|
628
|
+
* @returns {string} Nuevo período
|
|
629
|
+
*/
|
|
630
|
+
_decrementarPeriodoLibros() {
|
|
631
|
+
const stateFile = path.join(this.debugDir, 'periodo-libros.json');
|
|
632
|
+
const currentPeriodo = this._getPeriodoLibros();
|
|
633
|
+
|
|
634
|
+
const [year, month] = currentPeriodo.split('-').map(Number);
|
|
635
|
+
let newMonth = month - 1;
|
|
636
|
+
let newYear = year;
|
|
637
|
+
|
|
638
|
+
if (newMonth < 1) {
|
|
639
|
+
newMonth = 12;
|
|
640
|
+
newYear -= 1;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const newPeriodo = `${newYear}-${String(newMonth).padStart(2, '0')}`;
|
|
644
|
+
|
|
645
|
+
const state = {
|
|
646
|
+
periodo: newPeriodo,
|
|
647
|
+
lastRun: new Date().toISOString(),
|
|
648
|
+
previousPeriodo: currentPeriodo,
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
653
|
+
console.log(` 📅 Período decrementado: ${currentPeriodo} → ${newPeriodo}`);
|
|
654
|
+
} catch (e) {
|
|
655
|
+
console.warn(` ⚠️ No se pudo guardar período: ${e.message}`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return newPeriodo;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Resetea el período de libros a un valor específico
|
|
663
|
+
* @param {string} periodo - Período en formato YYYY-MM
|
|
664
|
+
*/
|
|
665
|
+
resetPeriodoLibros(periodo) {
|
|
666
|
+
const stateFile = path.join(this.debugDir, 'periodo-libros.json');
|
|
667
|
+
const state = { periodo, lastRun: new Date().toISOString() };
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
671
|
+
console.log(` 📅 Período reseteado a: ${periodo}`);
|
|
672
|
+
} catch (e) {
|
|
673
|
+
console.warn(` ⚠️ No se pudo guardar período: ${e.message}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Crea un enviador de libros
|
|
679
|
+
* @private
|
|
680
|
+
*/
|
|
681
|
+
_createLibroEnviador() {
|
|
682
|
+
return new EnviadorSII(this.certificado, this.ambiente);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Ejecuta el Libro de Ventas
|
|
687
|
+
* @param {Object} [options] - Opciones
|
|
688
|
+
* @param {Object} [options.setBasicoResult] - Resultado del SetBasico (usa this.resultados.basico si no se pasa)
|
|
689
|
+
* @param {string} [options.signoNC='POSITIVO'] - Signo para NC
|
|
690
|
+
* @returns {Promise<Object>} Resultado con trackId
|
|
691
|
+
*/
|
|
692
|
+
async ejecutarLibroVentas(options = {}) {
|
|
693
|
+
const setBasicoResult = options.setBasicoResult || this.resultados.basico;
|
|
694
|
+
|
|
695
|
+
if (!setBasicoResult?.documentos) {
|
|
696
|
+
throw new Error('No hay resultado del SetBasico. Ejecutar ejecutarSetBasico() primero.');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// IMPORTANTE: Decrementar período ANTES de usar para evitar "LNC - Libro Cerrado"
|
|
700
|
+
this._decrementarPeriodoLibros();
|
|
701
|
+
const periodo = this._getPeriodoLibros();
|
|
702
|
+
console.log(` 📚 Generando Libro de Ventas para período ${periodo}...`);
|
|
703
|
+
|
|
704
|
+
const libroVentas = new LibroVentas({
|
|
705
|
+
emisor: this.config.emisor,
|
|
706
|
+
receptor: this.config.receptor,
|
|
707
|
+
periodo,
|
|
708
|
+
certificado: this.certificado,
|
|
709
|
+
signoNC: options.signoNC || 'POSITIVO',
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const { libro, xml, detalle, resumen } = libroVentas.generar(setBasicoResult);
|
|
713
|
+
|
|
714
|
+
// Guardar XML de debug
|
|
715
|
+
const outPath = path.join(this.debugDir, 'libro-ventas.xml');
|
|
716
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
717
|
+
console.log(` XML guardado: ${outPath}`);
|
|
718
|
+
|
|
719
|
+
// Enviar al SII
|
|
720
|
+
const enviador = this._createLibroEnviador();
|
|
721
|
+
const resultado = await enviador.enviarLibro(libro, 'LibroCV.xml');
|
|
722
|
+
|
|
723
|
+
const result = {
|
|
724
|
+
success: !!resultado?.trackId,
|
|
725
|
+
trackId: resultado?.trackId,
|
|
726
|
+
error: resultado?.error,
|
|
727
|
+
periodo,
|
|
728
|
+
totalDetalle: detalle.length,
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
this.resultados.libroVentas = result;
|
|
732
|
+
|
|
733
|
+
if (result.success) {
|
|
734
|
+
console.log(` ✅ Libro de Ventas enviado - TrackId: ${result.trackId}`);
|
|
735
|
+
} else {
|
|
736
|
+
console.log(` ❌ Error enviando Libro de Ventas: ${result.error}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Ejecuta el Libro de Compras
|
|
744
|
+
* @param {Object} [options] - Opciones
|
|
745
|
+
* @param {Object} [options.libroComprasData] - Datos del libro (usa this._estructuras.libroCompras si no se pasa)
|
|
746
|
+
* @returns {Promise<Object>} Resultado con trackId
|
|
747
|
+
*/
|
|
748
|
+
async ejecutarLibroCompras(options = {}) {
|
|
749
|
+
const libroComprasData = options.libroComprasData || this._estructuras?.libroCompras;
|
|
750
|
+
|
|
751
|
+
// IMPORTANTE: Decrementar período ANTES de usar para evitar "LNC - Libro Cerrado"
|
|
752
|
+
this._decrementarPeriodoLibros();
|
|
753
|
+
const periodo = this._getPeriodoLibros();
|
|
754
|
+
|
|
755
|
+
const libroCompras = new LibroCompras({
|
|
756
|
+
emisor: this.config.emisor,
|
|
757
|
+
periodo,
|
|
758
|
+
certificado: this.certificado,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
if (!libroComprasData?.detalle) {
|
|
762
|
+
throw new Error('No hay datos del libro de compras. El SII no entregó el set LIBRO_COMPRAS al obtener las estructuras.');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
console.log(` 📚 Generando Libro de Compras para período ${periodo} (${libroComprasData.detalle.length} documentos del SII)...`);
|
|
766
|
+
const { libro, xml, detalle, resumen } = libroCompras.generarDesdeEstructuras(libroComprasData, periodo);
|
|
767
|
+
|
|
768
|
+
// Guardar XML de debug
|
|
769
|
+
const outPath = path.join(this.debugDir, 'libro-compras.xml');
|
|
770
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
771
|
+
console.log(` XML guardado: ${outPath}`);
|
|
772
|
+
|
|
773
|
+
// Enviar al SII
|
|
774
|
+
const enviador = this._createLibroEnviador();
|
|
775
|
+
const resultado = await enviador.enviarLibro(libro, 'LibroCV.xml');
|
|
776
|
+
|
|
777
|
+
const result = {
|
|
778
|
+
success: !!resultado?.trackId,
|
|
779
|
+
trackId: resultado?.trackId,
|
|
780
|
+
error: resultado?.error,
|
|
781
|
+
periodo,
|
|
782
|
+
totalDetalle: detalle.length,
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
this.resultados.libroCompras = result;
|
|
786
|
+
|
|
787
|
+
if (result.success) {
|
|
788
|
+
console.log(` ✅ Libro de Compras enviado - TrackId: ${result.trackId}`);
|
|
789
|
+
} else {
|
|
790
|
+
console.log(` ❌ Error enviando Libro de Compras: ${result.error}`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Ejecuta el Libro de Compras para Exentos (SET15)
|
|
798
|
+
* Misma lógica que ejecutarLibroCompras pero usando estructuras.libroComprasExentos
|
|
799
|
+
*/
|
|
800
|
+
async ejecutarLibroComprasExentos(options = {}) {
|
|
801
|
+
const libroData = options.libroComprasExentosData || this._estructuras?.libroComprasExentos;
|
|
802
|
+
|
|
803
|
+
if (!libroData?.detalle) {
|
|
804
|
+
throw new Error('No hay datos del libro de compras para exentos. El SII no entregó el set LIBRO_COMPRAS_EXENTOS.');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
this._decrementarPeriodoLibros();
|
|
808
|
+
const periodo = this._getPeriodoLibros();
|
|
809
|
+
|
|
810
|
+
const libroCompras = new LibroCompras({
|
|
811
|
+
emisor: this.config.emisor,
|
|
812
|
+
periodo,
|
|
813
|
+
certificado: this.certificado,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
console.log(` 📚 Generando Libro de Compras para Exentos para período ${periodo} (${libroData.detalle.length} documentos del SII)...`);
|
|
817
|
+
const { libro, xml, detalle } = libroCompras.generarDesdeEstructuras(libroData, periodo);
|
|
818
|
+
|
|
819
|
+
const outPath = path.join(this.debugDir, 'libro-compras-exentos.xml');
|
|
820
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
821
|
+
console.log(` XML guardado: ${outPath}`);
|
|
822
|
+
|
|
823
|
+
const enviador = this._createLibroEnviador();
|
|
824
|
+
const resultado = await enviador.enviarLibro(libro, 'LibroCVExentos.xml');
|
|
825
|
+
|
|
826
|
+
const result = {
|
|
827
|
+
success: !!resultado?.trackId,
|
|
828
|
+
trackId: resultado?.trackId,
|
|
829
|
+
error: resultado?.error,
|
|
830
|
+
periodo,
|
|
831
|
+
totalDetalle: detalle.length,
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
this.resultados.libroComprasExentos = result;
|
|
835
|
+
|
|
836
|
+
if (result.success) {
|
|
837
|
+
console.log(` ✅ Libro de Compras para Exentos enviado - TrackId: ${result.trackId}`);
|
|
838
|
+
} else {
|
|
839
|
+
console.log(` ❌ Error enviando Libro de Compras para Exentos: ${result.error}`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Ejecuta el Libro de Guías
|
|
847
|
+
* @param {Object} [options] - Opciones
|
|
848
|
+
* @param {Object} [options.setGuiaResult] - Resultado del SetGuia (usa this.resultados.guia si no se pasa)
|
|
849
|
+
* @param {number} [options.folioNotificacion=3] - Folio de notificación
|
|
850
|
+
* @returns {Promise<Object>} Resultado con trackId
|
|
851
|
+
*/
|
|
852
|
+
async ejecutarLibroGuias(options = {}) {
|
|
853
|
+
const setGuiaResult = options.setGuiaResult || this.resultados.guia;
|
|
854
|
+
|
|
855
|
+
if (!setGuiaResult?.documentos) {
|
|
856
|
+
throw new Error('No hay resultado del SetGuia. Ejecutar ejecutarSetGuia() primero.');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// IMPORTANTE: Decrementar período ANTES de usar para evitar "LNC - Libro Cerrado"
|
|
860
|
+
this._decrementarPeriodoLibros();
|
|
861
|
+
const periodo = this._getPeriodoLibros();
|
|
862
|
+
console.log(` 📚 Generando Libro de Guías para período ${periodo}...`);
|
|
863
|
+
|
|
864
|
+
const libroGuias = new LibroGuias({
|
|
865
|
+
emisor: this.config.emisor,
|
|
866
|
+
receptor: this.config.receptor,
|
|
867
|
+
periodo,
|
|
868
|
+
certificado: this.certificado,
|
|
869
|
+
folioNotificacion: options.folioNotificacion || 3,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const { libro, xml, detalle } = libroGuias.generar(setGuiaResult, {
|
|
873
|
+
casosLibro: options.casosLibro,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Guardar XML de debug
|
|
877
|
+
const outPath = path.join(this.debugDir, 'libro-guias.xml');
|
|
878
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
879
|
+
console.log(` XML guardado: ${outPath}`);
|
|
880
|
+
|
|
881
|
+
// Enviar al SII
|
|
882
|
+
const enviador = this._createLibroEnviador();
|
|
883
|
+
const resultado = await enviador.enviarLibro(libro, 'LibroGuia.xml');
|
|
884
|
+
|
|
885
|
+
const result = {
|
|
886
|
+
success: !!resultado?.trackId,
|
|
887
|
+
trackId: resultado?.trackId,
|
|
888
|
+
error: resultado?.error,
|
|
889
|
+
periodo,
|
|
890
|
+
totalDetalle: detalle.length,
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
this.resultados.libroGuias = result;
|
|
894
|
+
|
|
895
|
+
if (result.success) {
|
|
896
|
+
console.log(` ✅ Libro de Guías enviado - TrackId: ${result.trackId}`);
|
|
897
|
+
} else {
|
|
898
|
+
console.log(` ❌ Error enviando Libro de Guías: ${result.error}`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return result;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════
|
|
905
|
+
// AVANZAR SIGUIENTE PASO (Fase 5)
|
|
906
|
+
// ═══════════════════════════════════════════════════════════════
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Avanza al siguiente paso de certificación cuando todos los ítems están REVISADO CONFORME
|
|
910
|
+
* @returns {Promise<Object>} Resultado del avance
|
|
911
|
+
*/
|
|
912
|
+
async avanzarSiguientePaso() {
|
|
913
|
+
console.log('\n' + '═'.repeat(60));
|
|
914
|
+
console.log('🚀 AVANZAR SIGUIENTE PASO');
|
|
915
|
+
console.log('═'.repeat(60) + '\n');
|
|
916
|
+
|
|
917
|
+
try {
|
|
918
|
+
console.log(' 📋 Enviando solicitud de avance...');
|
|
919
|
+
const result = await this.siiCert.avanzarSiguientePaso();
|
|
920
|
+
|
|
921
|
+
if (result.rawHtml) {
|
|
922
|
+
fs.writeFileSync(
|
|
923
|
+
path.join(this.debugDir, 'avanzar-siguiente-paso-response.html'),
|
|
924
|
+
result.rawHtml,
|
|
925
|
+
'utf8'
|
|
926
|
+
);
|
|
927
|
+
console.log(` 📄 Respuesta guardada en: ${path.join(this.debugDir, 'avanzar-siguiente-paso-response.html')}`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (result.success) {
|
|
931
|
+
console.log(' ✅ Avance al siguiente paso exitoso');
|
|
932
|
+
this.resultados.avanceSiguientePaso = { success: true };
|
|
933
|
+
} else {
|
|
934
|
+
console.log(` ❌ Error en avance: ${result.error || 'Error desconocido'}`);
|
|
935
|
+
this.resultados.avanceSiguientePaso = { success: false, error: result.error };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return result;
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.log(` ❌ Error: ${error.message}`);
|
|
941
|
+
this.resultados.avanceSiguientePaso = { success: false, error: error.message };
|
|
942
|
+
return { success: false, error: error.message };
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Espera a que los libros sean aprobados y luego avanza al siguiente paso
|
|
948
|
+
* @param {Object} [options] - { maxIntentos, intervalo }
|
|
949
|
+
* @returns {Promise<Object>} Resultado del avance
|
|
950
|
+
*/
|
|
951
|
+
async esperarLibrosYAvanzar(options = {}) {
|
|
952
|
+
const { maxIntentos = 30, intervalo = 10000 } = options;
|
|
953
|
+
|
|
954
|
+
console.log('\n⏳ Esperando aprobación de libros...');
|
|
955
|
+
|
|
956
|
+
for (let i = 1; i <= maxIntentos; i++) {
|
|
957
|
+
console.log(`\n 🔄 Intento ${i}/${maxIntentos}...`);
|
|
958
|
+
|
|
959
|
+
const avance = await this.siiCert.verAvanceParsed();
|
|
960
|
+
|
|
961
|
+
if (!avance.success) {
|
|
962
|
+
console.log(` ⚠️ Error consultando avance: ${avance.error}`);
|
|
963
|
+
await sleep(intervalo);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const libros = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
|
|
968
|
+
const estados = avance.sets || [];
|
|
969
|
+
|
|
970
|
+
let todosAprobados = true;
|
|
971
|
+
let hayRechazados = false;
|
|
972
|
+
|
|
973
|
+
for (const libro of libros) {
|
|
974
|
+
const estado = estados.find(s => s.nombre?.toUpperCase().includes(libro.replace('DE ', '')));
|
|
975
|
+
if (!estado) continue;
|
|
976
|
+
|
|
977
|
+
const esAprobado = estado.estado?.toUpperCase().includes('REVISADO CONFORME');
|
|
978
|
+
const esRechazado = estado.estado?.toUpperCase().includes('RECHAZADO') ||
|
|
979
|
+
estado.estado?.toUpperCase().includes('REPARO');
|
|
980
|
+
|
|
981
|
+
if (esAprobado) {
|
|
982
|
+
console.log(` ✅ ${libro}: REVISADO CONFORME`);
|
|
983
|
+
} else if (esRechazado) {
|
|
984
|
+
console.log(` ❌ ${libro}: ${estado.estado}`);
|
|
985
|
+
hayRechazados = true;
|
|
986
|
+
} else {
|
|
987
|
+
console.log(` 🔄 ${libro}: ${estado.estado || 'EN REVISION'}`);
|
|
988
|
+
todosAprobados = false;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (hayRechazados) {
|
|
993
|
+
console.log('\n ❌ Hay libros rechazados. No se puede avanzar.');
|
|
994
|
+
return { success: false, error: 'Hay libros rechazados' };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (todosAprobados) {
|
|
998
|
+
console.log('\n 🎉 ¡Todos los libros aprobados!');
|
|
999
|
+
return await this.avanzarSiguientePaso();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
await sleep(intervalo);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
console.log('\n ⚠️ Timeout esperando aprobación de libros');
|
|
1006
|
+
return { success: false, error: 'Timeout esperando aprobación' };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1010
|
+
// SIMULACIÓN (Fase 6)
|
|
1011
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Ejecuta el Set de Simulación (Fase 6)
|
|
1015
|
+
* Genera un envío con todos los DTEs de las estructuras
|
|
1016
|
+
* @param {Object} [options] - Opciones
|
|
1017
|
+
* @param {Object} [options.estructuras] - Estructuras (usa this._estructuras si no se pasa)
|
|
1018
|
+
* @returns {Promise<Object>} Resultado con trackId
|
|
1019
|
+
*/
|
|
1020
|
+
async ejecutarSimulacion(options = {}) {
|
|
1021
|
+
const estructuras = options.estructuras || this._estructuras;
|
|
1022
|
+
if (!estructuras) {
|
|
1023
|
+
throw new Error('No hay estructuras para simulación. Ejecutar obtenerSets() primero.');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
console.log('\n' + '═'.repeat(60));
|
|
1027
|
+
console.log('🧪 FASE 6: SIMULACIÓN');
|
|
1028
|
+
console.log('═'.repeat(60) + '\n');
|
|
1029
|
+
|
|
1030
|
+
// Calcular CAFs necesarios
|
|
1031
|
+
const cafRequired = this._calcularCafsSimulacion(estructuras);
|
|
1032
|
+
console.log(' Solicitando CAFs para simulación...');
|
|
1033
|
+
|
|
1034
|
+
// Solicitar CAFs frescos
|
|
1035
|
+
const cafs = await this.solicitarCafs(cafRequired);
|
|
1036
|
+
|
|
1037
|
+
// Cargar objetos CAF
|
|
1038
|
+
const cafObjects = {};
|
|
1039
|
+
const { CAF } = require('../index');
|
|
1040
|
+
for (const [tipo, cafPath] of Object.entries(cafs)) {
|
|
1041
|
+
const cafXml = fs.readFileSync(cafPath, 'utf8');
|
|
1042
|
+
cafObjects[tipo] = new CAF(cafXml);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Crear simulación
|
|
1046
|
+
const simulacion = new Simulacion({
|
|
1047
|
+
emisor: this.config.emisor,
|
|
1048
|
+
receptor: this.config.receptor,
|
|
1049
|
+
certificado: this.certificado,
|
|
1050
|
+
resolucion: {
|
|
1051
|
+
fecha: this.config.emisor.fch_resol,
|
|
1052
|
+
numero: this.config.emisor.nro_resol,
|
|
1053
|
+
},
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Generar
|
|
1057
|
+
console.log(' Generando DTEs de simulación...');
|
|
1058
|
+
const { envioDte, dtes, xml, plan, tiposUsados } = simulacion.generar(
|
|
1059
|
+
estructuras,
|
|
1060
|
+
cafObjects,
|
|
1061
|
+
this.folioHelper,
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
console.log(` 📦 Plan de simulación: ${plan.length} documentos`);
|
|
1065
|
+
console.log(` 📄 Tipos usados: ${tiposUsados.join(', ')}`);
|
|
1066
|
+
|
|
1067
|
+
// Guardar XML de debug
|
|
1068
|
+
const runDir = path.join(this.debugDir, 'simulacion');
|
|
1069
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
1070
|
+
const outPath = path.join(runDir, 'envio-simulacion.xml');
|
|
1071
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
1072
|
+
console.log(` XML guardado: ${outPath}`);
|
|
1073
|
+
|
|
1074
|
+
// Guardar DTEs individuales
|
|
1075
|
+
const dtesDir = path.join(runDir, 'dtes');
|
|
1076
|
+
fs.mkdirSync(dtesDir, { recursive: true });
|
|
1077
|
+
dtes.forEach((dteItem) => {
|
|
1078
|
+
const filename = `dte-${String(dteItem.tipoDte).padStart(2, '0')}-${String(dteItem.folio).padStart(6, '0')}.xml`;
|
|
1079
|
+
fs.writeFileSync(path.join(dtesDir, filename), dteItem.xml, 'utf8');
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// Enviar al SII
|
|
1083
|
+
console.log('\n 📤 Enviando al SII...');
|
|
1084
|
+
const enviador = this._createEnviador();
|
|
1085
|
+
const resultado = await enviador.enviar(envioDte);
|
|
1086
|
+
|
|
1087
|
+
const result = {
|
|
1088
|
+
success: !!resultado?.trackId,
|
|
1089
|
+
trackId: resultado?.trackId,
|
|
1090
|
+
error: resultado?.error,
|
|
1091
|
+
documentos: plan.length,
|
|
1092
|
+
tiposUsados,
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
this.resultados.simulacion = result;
|
|
1096
|
+
|
|
1097
|
+
// Guardar info
|
|
1098
|
+
const infoPath = path.join(runDir, 'envio-simulacion-info.json');
|
|
1099
|
+
fs.writeFileSync(infoPath, JSON.stringify({
|
|
1100
|
+
...result,
|
|
1101
|
+
dtes: dtes.map(d => ({ tipoDte: d.tipoDte, folio: d.folio })),
|
|
1102
|
+
}, null, 2), 'utf8');
|
|
1103
|
+
|
|
1104
|
+
if (result.success) {
|
|
1105
|
+
console.log(`\n✅ Simulación enviada - TrackId: ${result.trackId}`);
|
|
1106
|
+
} else {
|
|
1107
|
+
console.log(`\n❌ Error en simulación: ${result.error}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return result;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Calcula los CAFs necesarios para simulación
|
|
1115
|
+
* @private
|
|
1116
|
+
*/
|
|
1117
|
+
_calcularCafsSimulacion(estructuras) {
|
|
1118
|
+
const cafRequired = {};
|
|
1119
|
+
|
|
1120
|
+
const contarTipo = (tipo) => {
|
|
1121
|
+
cafRequired[tipo] = (cafRequired[tipo] || 0) + 1;
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// Set Básico
|
|
1125
|
+
const sb = estructuras?.setBasico;
|
|
1126
|
+
if (sb) {
|
|
1127
|
+
(sb.casosFactura || []).forEach(() => contarTipo(33));
|
|
1128
|
+
(sb.casosNC || []).forEach(() => contarTipo(61));
|
|
1129
|
+
(sb.casosND || []).forEach(() => contarTipo(56));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Set Exenta
|
|
1133
|
+
const se = estructuras?.setFacturaExenta;
|
|
1134
|
+
if (se) {
|
|
1135
|
+
(se.casosFactura || []).forEach(() => contarTipo(34));
|
|
1136
|
+
(se.casosNC || []).forEach(() => contarTipo(61));
|
|
1137
|
+
(se.casosND || []).forEach(() => contarTipo(56));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Set Guía
|
|
1141
|
+
const sg = estructuras?.setGuiaDespacho;
|
|
1142
|
+
if (sg) {
|
|
1143
|
+
(sg.casos || []).forEach(() => contarTipo(52));
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Set Compra
|
|
1147
|
+
const sc = estructuras?.setFacturaCompra;
|
|
1148
|
+
if (sc) {
|
|
1149
|
+
if (sc.casoFactura) contarTipo(46);
|
|
1150
|
+
if (sc.casoNC) contarTipo(61);
|
|
1151
|
+
if (sc.casoND) contarTipo(56);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return cafRequired;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Declara avance del set de simulación con reintentos automáticos
|
|
1159
|
+
* @param {Object} [resultadosExt] - Resultados externos
|
|
1160
|
+
* @param {Object} [options] - { maxIntentos, intervalo }
|
|
1161
|
+
* @returns {Promise<Object>} Resultado de la declaración
|
|
1162
|
+
*/
|
|
1163
|
+
async declararSimulacion(resultadosExt, options = {}) {
|
|
1164
|
+
const resultados = resultadosExt || this.resultados;
|
|
1165
|
+
const { maxIntentos = 10, intervalo = 5000 } = options;
|
|
1166
|
+
|
|
1167
|
+
if (!resultados.simulacion?.trackId) {
|
|
1168
|
+
return { success: false, error: 'No hay TrackId de simulación para declarar' };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Verificar si ya pasamos a INTERCAMBIO (simulación ya aprobada)
|
|
1172
|
+
console.log(' 🔍 Verificando etapa actual...');
|
|
1173
|
+
const avance = await this.siiCert.verAvanceParsed();
|
|
1174
|
+
if (avance.rawHtml && /paso\s*<b>\s*INTERCAMBIO/i.test(avance.rawHtml)) {
|
|
1175
|
+
console.log(' ✅ Simulación ya aprobada - empresa en etapa INTERCAMBIO');
|
|
1176
|
+
return { success: true, skipped: true, message: 'Ya en etapa INTERCAMBIO' };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const fecha = this._getFechaHoy();
|
|
1180
|
+
const sets = {
|
|
1181
|
+
setSimulacion: {
|
|
1182
|
+
trackId: resultados.simulacion.trackId,
|
|
1183
|
+
fecha,
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
const result = await this._declararConReintentos(sets, 'declaracion-simulacion-response', { maxIntentos, intervalo, label: 'simulación' });
|
|
1188
|
+
if (result?.success) console.log(' ✅ Simulación declarada exitosamente');
|
|
1189
|
+
return result;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Espera a que la simulación sea aprobada
|
|
1194
|
+
* @param {Object} [options] - { maxIntentos, intervalo }
|
|
1195
|
+
* @returns {Promise<Object>} Resultado del polling
|
|
1196
|
+
*/
|
|
1197
|
+
async esperarSimulacionAprobada(options = {}) {
|
|
1198
|
+
const { maxIntentos = 30, intervalo = 10000 } = options;
|
|
1199
|
+
|
|
1200
|
+
console.log('\n⏳ Esperando aprobación de simulación...');
|
|
1201
|
+
|
|
1202
|
+
for (let i = 1; i <= maxIntentos; i++) {
|
|
1203
|
+
console.log(`\n 🔄 Intento ${i}/${maxIntentos}...`);
|
|
1204
|
+
|
|
1205
|
+
const avance = await this.siiCert.verAvanceParsed();
|
|
1206
|
+
|
|
1207
|
+
if (!avance.success) {
|
|
1208
|
+
console.log(` ⚠️ Error consultando avance: ${avance.error}`);
|
|
1209
|
+
await sleep(intervalo);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// ✅ PRIMERO: Verificar si ya pasó a INTERCAMBIO (significa que simulación fue aprobada)
|
|
1214
|
+
if (avance.etapaActual && avance.etapaActual.includes('INTERCAMBIO')) {
|
|
1215
|
+
console.log(` ✅ Etapa actual: ${avance.etapaActual}`);
|
|
1216
|
+
console.log('\n 🎉 ¡SIMULACIÓN APROBADA! Empresa pasó a etapa INTERCAMBIO.');
|
|
1217
|
+
return { success: true, etapa: 'INTERCAMBIO' };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// ✅ SEGUNDO: Verificar indicador de formulario de confirmación (simulación aprobada pendiente confirmar)
|
|
1221
|
+
if (avance.simulacionAprobadaIndicador) {
|
|
1222
|
+
console.log(` ✅ Formulario de confirmación detectado`);
|
|
1223
|
+
|
|
1224
|
+
// Confirmar automáticamente la simulación
|
|
1225
|
+
if (this.resultados.simulacion?.trackId) {
|
|
1226
|
+
console.log(`\n 📝 Confirmando revisión de simulación (TrackId: ${this.resultados.simulacion.trackId})...`);
|
|
1227
|
+
|
|
1228
|
+
const fecha = this._getFechaHoy();
|
|
1229
|
+
const confirmResult = await this.siiCert.declararAvance({
|
|
1230
|
+
sets: {
|
|
1231
|
+
setSimulacion: {
|
|
1232
|
+
trackId: this.resultados.simulacion.trackId,
|
|
1233
|
+
fecha,
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
if (confirmResult.success) {
|
|
1239
|
+
console.log(' ✅ Confirmación enviada exitosamente');
|
|
1240
|
+
|
|
1241
|
+
// Revalidar contra SII para evitar falso positivo de confirmación
|
|
1242
|
+
const verificacion = await this.siiCert.verAvanceParsed();
|
|
1243
|
+
const estadoSim = verificacion?.estados?.setSimulacion;
|
|
1244
|
+
const sigueFormulario = Boolean(verificacion?.simulacionAprobadaIndicador);
|
|
1245
|
+
const yaIntercambio = Boolean(verificacion?.etapaActual?.includes('INTERCAMBIO'));
|
|
1246
|
+
const simConforme = Boolean(estadoSim?.esConforme || estadoSim?.estado?.toUpperCase()?.includes('REVISADO CONFORME'));
|
|
1247
|
+
|
|
1248
|
+
if (yaIntercambio || simConforme || !sigueFormulario) {
|
|
1249
|
+
console.log('\n 🎉 ¡SIMULACIÓN CONFIRMADA! Certificación completa.');
|
|
1250
|
+
return { success: true, confirmada: true };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
console.log(' ⚠️ SII aún mantiene formulario de simulación pendiente; se reintentará...');
|
|
1254
|
+
await sleep(intervalo);
|
|
1255
|
+
continue;
|
|
1256
|
+
} else {
|
|
1257
|
+
console.log(` ⚠️ Error en confirmación: ${confirmResult.error}`);
|
|
1258
|
+
// Continuar el loop para reintentar
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
console.log('\n 🎉 ¡SIMULACIÓN APROBADA! Lista para confirmar revisión.');
|
|
1262
|
+
return { success: true, pendienteConfirmar: true };
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Buscar estado de simulación en los estados parseados
|
|
1267
|
+
const estados = avance.estados || {};
|
|
1268
|
+
const simKey = Object.keys(estados).find(k =>
|
|
1269
|
+
k.toLowerCase().includes('simulacion') ||
|
|
1270
|
+
estados[k]?.nombre?.toUpperCase().includes('SIMULACION') ||
|
|
1271
|
+
estados[k]?.nombre?.toUpperCase().includes('SIMULACIÓN')
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
if (simKey && estados[simKey]) {
|
|
1275
|
+
const simEstado = estados[simKey];
|
|
1276
|
+
const esAprobado = simEstado.esConforme || simEstado.estado?.toUpperCase().includes('REVISADO CONFORME');
|
|
1277
|
+
const esRechazado = simEstado.esRechazado ||
|
|
1278
|
+
simEstado.estado?.toUpperCase().includes('RECHAZADO') ||
|
|
1279
|
+
simEstado.estado?.toUpperCase().includes('REPARO');
|
|
1280
|
+
|
|
1281
|
+
if (esAprobado) {
|
|
1282
|
+
console.log(` ✅ SIMULACIÓN: REVISADO CONFORME`);
|
|
1283
|
+
console.log('\n 🎉 ¡SIMULACIÓN APROBADA! Certificación completa.');
|
|
1284
|
+
return { success: true };
|
|
1285
|
+
} else if (esRechazado) {
|
|
1286
|
+
console.log(` ❌ SIMULACIÓN: ${simEstado.estado}`);
|
|
1287
|
+
return { success: false, error: 'Simulación rechazada' };
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log(` 🔄 SIMULACIÓN: ${simEstado.estado || 'EN REVISION'}`);
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
// No hay estado de simulación, pero verificar etapa actual
|
|
1293
|
+
if (avance.etapaActual) {
|
|
1294
|
+
console.log(` 📍 Etapa actual: ${avance.etapaActual}`);
|
|
1295
|
+
} else {
|
|
1296
|
+
console.log(' ⏳ Simulación aún no registrada...');
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
await sleep(intervalo);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
console.log('\n ⚠️ Timeout esperando aprobación de simulación');
|
|
1304
|
+
return { success: false, error: 'Timeout esperando aprobación' };
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1308
|
+
// Helpers privados
|
|
1309
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1310
|
+
|
|
1311
|
+
_getFechaHoy() {
|
|
1312
|
+
const now = new Date();
|
|
1313
|
+
return `${String(now.getDate()).padStart(2, '0')}-${String(now.getMonth() + 1).padStart(2, '0')}-${now.getFullYear()}`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1317
|
+
// FASE 7: INTERCAMBIO DE INFORMACIÓN
|
|
1318
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Ejecuta el proceso completo de intercambio de información DTE.
|
|
1322
|
+
* 1. Descarga el SET de intercambio desde www4.sii.cl/pfeInternet (auto o manual)
|
|
1323
|
+
* 2. Genera los 3 XMLs de respuesta firmados
|
|
1324
|
+
* 3. Sube las respuestas al portal
|
|
1325
|
+
* @param {Object} [options]
|
|
1326
|
+
* @param {string} [options.inputPath] - Ruta manual al XML del SET de intercambio
|
|
1327
|
+
* @returns {Promise<Object>}
|
|
1328
|
+
*/
|
|
1329
|
+
async ejecutarFase7Intercambio(options = {}) {
|
|
1330
|
+
const intercambioDir = path.join(this.debugDir, 'intercambio');
|
|
1331
|
+
fs.mkdirSync(intercambioDir, { recursive: true });
|
|
1332
|
+
|
|
1333
|
+
console.log('\n' + '═'.repeat(60));
|
|
1334
|
+
console.log('📬 FASE 7: INTERCAMBIO DE INFORMACIÓN');
|
|
1335
|
+
console.log('═'.repeat(60));
|
|
1336
|
+
|
|
1337
|
+
// ── PASO 1: Obtener el SET XML ─────────────────────────────
|
|
1338
|
+
const setInputPath = options.inputPath ||
|
|
1339
|
+
path.join(intercambioDir, 'set-intercambio.xml');
|
|
1340
|
+
|
|
1341
|
+
let setXml = null;
|
|
1342
|
+
|
|
1343
|
+
// Ruta persistente donde siempre guardamos el XML (independiente de options.inputPath)
|
|
1344
|
+
const setDownloadPath = path.join(intercambioDir, 'set-intercambio.xml');
|
|
1345
|
+
|
|
1346
|
+
if (setInputPath && fs.existsSync(setInputPath)) {
|
|
1347
|
+
console.log(`\n📂 Leyendo SET desde: ${setInputPath}`);
|
|
1348
|
+
setXml = fs.readFileSync(setInputPath, 'utf8');
|
|
1349
|
+
console.log(` ✓ ${setXml.length} bytes`);
|
|
1350
|
+
} else if (fs.existsSync(setDownloadPath)) {
|
|
1351
|
+
console.log(`\n📂 Leyendo SET guardado: ${setDownloadPath}`);
|
|
1352
|
+
setXml = fs.readFileSync(setDownloadPath, 'utf8');
|
|
1353
|
+
console.log(` ✓ ${setXml.length} bytes`);
|
|
1354
|
+
} else {
|
|
1355
|
+
console.log('\n📡 Descargando SET desde www4.sii.cl/pfeInternet...');
|
|
1356
|
+
const dl = await this._descargarSetPfeInternet(intercambioDir);
|
|
1357
|
+
if (dl.success) {
|
|
1358
|
+
setXml = dl.xml;
|
|
1359
|
+
fs.writeFileSync(setDownloadPath, setXml, 'utf8');
|
|
1360
|
+
console.log(` ✅ SET descargado (${setXml.length} bytes) → ${setDownloadPath}`);
|
|
1361
|
+
} else {
|
|
1362
|
+
console.log(` ⚠️ No se pudo descargar: ${dl.error}`);
|
|
1363
|
+
console.log('\n' + '─'.repeat(60));
|
|
1364
|
+
console.log('📋 DESCARGA MANUAL REQUERIDA:');
|
|
1365
|
+
console.log(' 1. Si aparece error de sesiones: ingresa a https://www4.sii.cl/ → Cerrar Sesión');
|
|
1366
|
+
console.log(' 2. Ir a: https://www4.sii.cl/pfeInternet/ y descargar el SET XML');
|
|
1367
|
+
console.log(` 3. Guardarlo en: ${setDownloadPath}`);
|
|
1368
|
+
console.log(' 4. Volver a ejecutar el runner');
|
|
1369
|
+
console.log('─'.repeat(60));
|
|
1370
|
+
return { success: false, error: 'SET no disponible - descarga manual requerida', requiresManual: true, manualPath: setInputPath };
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// ── PASO 2: Generar XMLs de respuesta ─────────────────────
|
|
1375
|
+
console.log('\n📝 Generando respuestas firmadas...');
|
|
1376
|
+
const intercambioCert = new IntercambioCert({
|
|
1377
|
+
certificado: this.certificado,
|
|
1378
|
+
emisor: {
|
|
1379
|
+
rut: this.config.emisor.rut,
|
|
1380
|
+
razonSocial: this.config.emisor.razon_social || this.config.emisor.razonSocial || '',
|
|
1381
|
+
},
|
|
1382
|
+
contacto: this.config.contacto || {},
|
|
1383
|
+
debugDir: intercambioDir,
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
const genResult = await intercambioCert.generarIntercambio(setXml, { outDir: intercambioDir });
|
|
1387
|
+
if (!genResult.success) {
|
|
1388
|
+
return { success: false, error: genResult.error || 'Error generando XMLs' };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ── PASO 3: Subir respuestas ───────────────────────────────
|
|
1392
|
+
console.log('\n📤 Subiendo respuestas a www4.sii.cl/pfeInternet...');
|
|
1393
|
+
const uploadResult = await this._subirRespuestasPfeInternet({
|
|
1394
|
+
recepcionXml: fs.readFileSync(genResult.files.recepcion, 'utf8'),
|
|
1395
|
+
aprobacionXml: fs.readFileSync(genResult.files.aprobacion, 'utf8'),
|
|
1396
|
+
recibosXml: fs.readFileSync(genResult.files.recibos, 'utf8'),
|
|
1397
|
+
debugDir: intercambioDir,
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
if (uploadResult.success) {
|
|
1401
|
+
console.log('\n' + '═'.repeat(60));
|
|
1402
|
+
console.log('✅ INTERCAMBIO COMPLETADO');
|
|
1403
|
+
console.log('═'.repeat(60));
|
|
1404
|
+
if (uploadResult.resultado) console.log(` Resultado SII: ${uploadResult.resultado}`);
|
|
1405
|
+
} else {
|
|
1406
|
+
console.log(` ⚠️ No se pudo subir automáticamente: ${uploadResult.error}`);
|
|
1407
|
+
console.log('\n' + '─'.repeat(60));
|
|
1408
|
+
console.log('📋 SUBIDA MANUAL REQUERIDA:');
|
|
1409
|
+
console.log(' 1. Ir a: https://www4.sii.cl/pfeInternet/ → "Subir archivos"');
|
|
1410
|
+
console.log(` 2. Subir: ${genResult.files.recepcion}`);
|
|
1411
|
+
console.log(` 3. Subir: ${genResult.files.aprobacion}`);
|
|
1412
|
+
console.log(` 4. Subir: ${genResult.files.recibos}`);
|
|
1413
|
+
console.log('─'.repeat(60));
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return {
|
|
1417
|
+
success: true,
|
|
1418
|
+
files: genResult.files,
|
|
1419
|
+
meta: genResult.meta,
|
|
1420
|
+
uploaded: uploadResult.success,
|
|
1421
|
+
requiresManual: !uploadResult.success,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Obtiene el cookieJar de sesión SII reutilizando caché en memoria primero,
|
|
1427
|
+
* luego caché en disco (TTL 25 min) gestionada por SiiPortalAuth.
|
|
1428
|
+
* Evita crear múltiples sesiones simultáneas (el SII limita a ~3).
|
|
1429
|
+
* @private
|
|
1430
|
+
* @returns {Promise<Object>} cookieJar con cookies NETSCAPE_LIVEWIRE.*
|
|
1431
|
+
*/
|
|
1432
|
+
async _obtenerCookiesSII() {
|
|
1433
|
+
if (this._siiCookieJar) {
|
|
1434
|
+
console.log('[SII Auth] ♻️ Reutilizando sesión SII en memoria');
|
|
1435
|
+
return this._siiCookieJar;
|
|
1436
|
+
}
|
|
1437
|
+
const SiiPortalAuth = require('../SiiPortalAuth');
|
|
1438
|
+
const pfxBuffer = fs.readFileSync(this.config.certificado.path);
|
|
1439
|
+
const password = this.config.certificado.password;
|
|
1440
|
+
const siiAuth = new SiiPortalAuth({ pfxBuffer, pfxPassword: password });
|
|
1441
|
+
this._siiCookieJar = await siiAuth.autenticar();
|
|
1442
|
+
const nSession = Object.keys(this._siiCookieJar).filter(k => k.startsWith('NETSCAPE')).length;
|
|
1443
|
+
console.log(`[SII Auth] ✅ Sesión SII activa (cookies NETSCAPE: ${nSession})`);
|
|
1444
|
+
return this._siiCookieJar;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Autentica contra pfeInternet reutilizando la sesión cacheada por SiiPortalAuth.
|
|
1449
|
+
* Tras obtener las cookies, hace un GET inicial a www4.sii.cl/pfeInternet/ para
|
|
1450
|
+
* inicializar el contexto del portal GWT del lado del servidor (igual que el browser).
|
|
1451
|
+
* @private
|
|
1452
|
+
*/
|
|
1453
|
+
async _autenticarPfeInternet() {
|
|
1454
|
+
const https = require('https');
|
|
1455
|
+
const crypto = require('crypto');
|
|
1456
|
+
const { URL } = require('url');
|
|
1457
|
+
|
|
1458
|
+
const tlsOpts = {
|
|
1459
|
+
rejectUnauthorized: false,
|
|
1460
|
+
maxVersion: 'TLSv1.2',
|
|
1461
|
+
secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
|
|
1462
|
+
| crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// makeReq: helper HTTP para requests pfeInternet — solo lleva las cookies, no hace auth
|
|
1466
|
+
const makeReq = (urlStr, { method = 'GET', body = null, headers = {}, cookies: reqCookies = '' }) =>
|
|
1467
|
+
new Promise((resolve, reject) => {
|
|
1468
|
+
const u = new URL(urlStr);
|
|
1469
|
+
const agent = new https.Agent(tlsOpts);
|
|
1470
|
+
const opts = {
|
|
1471
|
+
hostname: u.hostname, port: u.port || 443,
|
|
1472
|
+
path: u.pathname + u.search, method, agent,
|
|
1473
|
+
headers: {
|
|
1474
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
1475
|
+
'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8',
|
|
1476
|
+
'Connection': 'keep-alive',
|
|
1477
|
+
...(reqCookies ? { 'Cookie': reqCookies } : {}),
|
|
1478
|
+
...headers,
|
|
1479
|
+
},
|
|
1480
|
+
};
|
|
1481
|
+
if (body) opts.headers['Content-Length'] = Buffer.byteLength(body);
|
|
1482
|
+
const req = https.request(opts, (res) => {
|
|
1483
|
+
let data = '';
|
|
1484
|
+
res.on('data', c => data += c);
|
|
1485
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data, headers: res.headers }));
|
|
1486
|
+
});
|
|
1487
|
+
req.on('error', reject);
|
|
1488
|
+
if (body) req.write(body);
|
|
1489
|
+
req.end();
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
const collectNewCookies = (headers, existing) => {
|
|
1493
|
+
const merged = {};
|
|
1494
|
+
existing.split(';').forEach(c => { const [k, v] = c.trim().split('='); if (k) merged[k.trim()] = (v||'').trim(); });
|
|
1495
|
+
for (const c of (headers['set-cookie'] || [])) {
|
|
1496
|
+
const [kv] = c.split(';');
|
|
1497
|
+
const eq = kv.indexOf('=');
|
|
1498
|
+
if (eq > 0) merged[kv.slice(0, eq).trim()] = kv.slice(eq + 1).trim();
|
|
1499
|
+
}
|
|
1500
|
+
return Object.entries(merged).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
// Reutiliza sesión en memoria o disco via _obtenerCookiesSII()
|
|
1504
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
1505
|
+
let cookies = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
1506
|
+
|
|
1507
|
+
// Warm-up: visitar www4.sii.cl/pfeInternet/ para inicializar contexto del portal GWT
|
|
1508
|
+
// en el servidor, exactamente como haría el browser al navegar a la página.
|
|
1509
|
+
try {
|
|
1510
|
+
const warmup = await makeReq('https://www4.sii.cl/pfeInternet/', { cookies });
|
|
1511
|
+
cookies = collectNewCookies(warmup.headers, cookies);
|
|
1512
|
+
// Si redirige (302), seguir el redirect una vez más
|
|
1513
|
+
if ((warmup.status === 301 || warmup.status === 302) && warmup.headers?.location) {
|
|
1514
|
+
const loc = warmup.headers.location;
|
|
1515
|
+
const absLoc = loc.startsWith('http') ? loc : `https://www4.sii.cl${loc}`;
|
|
1516
|
+
const warmup2 = await makeReq(absLoc, { cookies });
|
|
1517
|
+
cookies = collectNewCookies(warmup2.headers, cookies);
|
|
1518
|
+
}
|
|
1519
|
+
console.log(`[pfeInternet Auth] warm-up → HTTP ${warmup.status}`);
|
|
1520
|
+
} catch (e) {
|
|
1521
|
+
console.log(`[pfeInternet Auth] warm-up falló (no crítico): ${e.message}`);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
return { cookies, makeReq };
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Descarga el SET de intercambio desde www4.sii.cl/pfeInternet
|
|
1529
|
+
* @private
|
|
1530
|
+
*/
|
|
1531
|
+
async _descargarSetPfeInternet(debugDir) {
|
|
1532
|
+
try {
|
|
1533
|
+
const { cookies, makeReq } = await this._autenticarPfeInternet();
|
|
1534
|
+
const [rutNum, dv] = this.config.emisor.rut.split('-');
|
|
1535
|
+
|
|
1536
|
+
// Endpoint real capturado via F12:
|
|
1537
|
+
// POST https://www4.sii.cl/pfeInternet/downloadFile?re={rutSinDv}&dve={dv}
|
|
1538
|
+
// Body: multipart vacío (solo el closing boundary, sin campos de formulario)
|
|
1539
|
+
const boundary = `----WebKitFormBoundary${Date.now()}`;
|
|
1540
|
+
const emptyMultipartBody = `--${boundary}--\r\n`;
|
|
1541
|
+
|
|
1542
|
+
console.log(` → Descargando SET desde pfeInternet/downloadFile (RUT ${rutNum}-${dv})...`);
|
|
1543
|
+
|
|
1544
|
+
const r = await makeReq(
|
|
1545
|
+
`https://www4.sii.cl/pfeInternet/downloadFile?re=${rutNum}&dve=${dv}`,
|
|
1546
|
+
{
|
|
1547
|
+
method: 'POST',
|
|
1548
|
+
body: emptyMultipartBody,
|
|
1549
|
+
headers: {
|
|
1550
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
1551
|
+
'Referer': 'https://www4.sii.cl/pfeInternet/',
|
|
1552
|
+
'Origin': 'https://www4.sii.cl',
|
|
1553
|
+
},
|
|
1554
|
+
cookies,
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
// Guardar respuesta raw para debugging
|
|
1559
|
+
fs.writeFileSync(path.join(debugDir, `pfe-download-${Date.now()}.xml`), r.body, 'utf8');
|
|
1560
|
+
|
|
1561
|
+
if (r.status === 200 && (
|
|
1562
|
+
r.body.includes('<EnvioDTE') ||
|
|
1563
|
+
r.body.includes('<SetDTE') ||
|
|
1564
|
+
r.body.includes('<?xml')
|
|
1565
|
+
)) {
|
|
1566
|
+
console.log(` ✓ SET descargado correctamente (${r.body.length} bytes)`);
|
|
1567
|
+
return { success: true, xml: r.body };
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const errMsg = `pfeInternet/downloadFile respondió HTTP ${r.status} sin XML válido`;
|
|
1571
|
+
console.log(` ✗ ${errMsg}`);
|
|
1572
|
+
fs.writeFileSync(path.join(debugDir, `pfe-download-error-${Date.now()}.html`), r.body, 'utf8');
|
|
1573
|
+
return { success: false, error: errMsg };
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
return { success: false, error: err.message };
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Sube las 3 respuestas de intercambio a www4.sii.cl/pfeInternet usando Puppeteer.
|
|
1581
|
+
* El portal GWT requiere que el JavaScript inicialice la sesión antes de aceptar uploads.
|
|
1582
|
+
* Con Puppeteer el browser real ejecuta el JS del portal y los uploads quedan registrados.
|
|
1583
|
+
* @private
|
|
1584
|
+
*/
|
|
1585
|
+
async _subirRespuestasPfeInternet({ recepcionXml, aprobacionXml, recibosXml, debugDir }) {
|
|
1586
|
+
const puppeteer = require('puppeteer');
|
|
1587
|
+
const os = require('os');
|
|
1588
|
+
|
|
1589
|
+
// Guardar XMLs en archivos temporales para que Puppeteer pueda subirlos
|
|
1590
|
+
const tmpDir = debugDir || path.join(os.tmpdir(), 'pfe-intercambio');
|
|
1591
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1592
|
+
// Los labels deben coincidir con el texto del portal GWT (Archivo N: ...)
|
|
1593
|
+
const archivos = [
|
|
1594
|
+
{ label: 'Respuesta de Intercambio', filename: 'respuesta-recepcion-envio.xml', content: recepcionXml, uploadN: 1 },
|
|
1595
|
+
{ label: 'Recibo de Mercaderias', filename: 'envio-recibos.xml', content: recibosXml, uploadN: 2 },
|
|
1596
|
+
{ label: 'Resultado Aprobaci\u00f3n Comercial de Documento', filename: 'respuesta-aprobacion-comercial.xml', content: aprobacionXml, uploadN: 3 },
|
|
1597
|
+
];
|
|
1598
|
+
for (const a of archivos) {
|
|
1599
|
+
fs.writeFileSync(path.join(tmpDir, a.filename), a.content, 'utf8');
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Obtener cookies de sesión SII (reutiliza caché en memoria/disco)
|
|
1603
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
1604
|
+
|
|
1605
|
+
// Convertir cookieJar a formato Puppeteer para dominio .sii.cl
|
|
1606
|
+
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
1607
|
+
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
1608
|
+
}));
|
|
1609
|
+
|
|
1610
|
+
let browser;
|
|
1611
|
+
try {
|
|
1612
|
+
browser = await puppeteer.launch({
|
|
1613
|
+
headless: true,
|
|
1614
|
+
ignoreHTTPSErrors: true,
|
|
1615
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
const page = await browser.newPage();
|
|
1619
|
+
await page.setCookie(...puppeteerCookies);
|
|
1620
|
+
|
|
1621
|
+
// Navegar al portal pfeInternet
|
|
1622
|
+
console.log(' → Cargando portal pfeInternet...');
|
|
1623
|
+
await page.goto('https://www4.sii.cl/pfeInternet/', {
|
|
1624
|
+
waitUntil: 'networkidle2',
|
|
1625
|
+
timeout: 60000,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// Dar 3 segundos extra para que GWT renderice el menú inicial
|
|
1629
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1630
|
+
|
|
1631
|
+
// Hacer click en el enlace "Subir archivos XML de respuesta de Intercambio"
|
|
1632
|
+
// El href es javascript:openForm('opt-ingresoEmpresaUp') — necesita click real para GWT
|
|
1633
|
+
console.log(' → Clickeando "Subir archivos XML de respuesta de Intercambio"...');
|
|
1634
|
+
const linkClicked = await page.click('a[href*="ingresoEmpresaUp"]').then(() => true).catch(() => false);
|
|
1635
|
+
if (!linkClicked) {
|
|
1636
|
+
// Fallback: evaluar click con dispatchEvent
|
|
1637
|
+
await page.evaluate(() => {
|
|
1638
|
+
const link = document.querySelector('a[href*="ingresoEmpresaUp"]');
|
|
1639
|
+
if (link) link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Esperar a que GWT complete el RPC y re-renderice la vista de upload
|
|
1644
|
+
await page.waitForNetworkIdle({ timeout: 15000, idleTime: 1000 }).catch(() => {});
|
|
1645
|
+
|
|
1646
|
+
// === PASO INTERMEDIO: ingresar RUT y confirmar empresa ===
|
|
1647
|
+
// GWT muestra "Ingrese el RUT de la empresa" antes de mostrar el formulario de upload
|
|
1648
|
+
const rutInput = await page.$('input.gwt-TextBox[maxlength="10"]');
|
|
1649
|
+
if (rutInput) {
|
|
1650
|
+
const [rutNum, dv] = this.config.emisor.rut.split('-');
|
|
1651
|
+
const rutConDv = `${rutNum}-${dv}`;
|
|
1652
|
+
console.log(` → Ingresando RUT empresa: ${rutConDv}`);
|
|
1653
|
+
await rutInput.click({ clickCount: 3 }); // seleccionar todo
|
|
1654
|
+
await rutInput.type(rutConDv);
|
|
1655
|
+
|
|
1656
|
+
// Click en "Confirmar Empresa"
|
|
1657
|
+
const confirmBtn = await page.evaluateHandle(() => {
|
|
1658
|
+
return Array.from(document.querySelectorAll('button.gwt-Button'))
|
|
1659
|
+
.find(b => b.textContent.trim() === 'Confirmar Empresa');
|
|
1660
|
+
});
|
|
1661
|
+
if (confirmBtn) {
|
|
1662
|
+
await confirmBtn.asElement().click();
|
|
1663
|
+
console.log(' → Click "Confirmar Empresa", esperando formulario de upload...');
|
|
1664
|
+
await page.waitForNetworkIdle({ timeout: 15000, idleTime: 1000 }).catch(() => {});
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Esperar que GWT renderice el formulario con los 3 inputs de upload
|
|
1669
|
+
const inputFound = await page.waitForSelector('input[name="uploadFormElement"]', { timeout: 30000 }).catch(() => null);
|
|
1670
|
+
if (!inputFound) {
|
|
1671
|
+
if (debugDir) {
|
|
1672
|
+
await page.screenshot({ path: path.join(debugDir, 'pfeInternet-error.png'), fullPage: true }).catch(() => {});
|
|
1673
|
+
fs.writeFileSync(path.join(debugDir, 'pfeInternet-error.html'), await page.content().catch(() => ''), 'utf8');
|
|
1674
|
+
}
|
|
1675
|
+
const pageText = await page.$eval('body', el => el.textContent).catch(() => '');
|
|
1676
|
+
if (pageText.includes('DOCUMENTOS IMPRESOS') || pageText.includes('fue cargado exitosamente')) {
|
|
1677
|
+
throw new Error('PASO_YA_COMPLETADO');
|
|
1678
|
+
}
|
|
1679
|
+
throw new Error('pfeInternet no mostró formulario de upload tras openForm — ver pfeInternet-error.png/.html');
|
|
1680
|
+
}
|
|
1681
|
+
console.log(' → Formulario de upload listo');
|
|
1682
|
+
|
|
1683
|
+
// ── DEBUG: screenshot del formulario con los inputs listos ──
|
|
1684
|
+
if (debugDir) {
|
|
1685
|
+
await page.screenshot({ path: path.join(debugDir, 'pfeInternet-form-listo.png'), fullPage: true }).catch(() => {});
|
|
1686
|
+
fs.writeFileSync(path.join(debugDir, 'pfeInternet-form-listo.html'), await page.content().catch(() => ''), 'utf8');
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Subir cada archivo en secuencia.
|
|
1690
|
+
// GWT mantiene solo los inputs de archivos pendientes: "procesado con exito anteriormente"
|
|
1691
|
+
// reemplaza al input. Así que iteramos solo los archivos pendientes en orden.
|
|
1692
|
+
for (const archivo of archivos) {
|
|
1693
|
+
const filePath = path.join(tmpDir, archivo.filename);
|
|
1694
|
+
|
|
1695
|
+
// Asegurarse de que no haya diálogo abierto del upload anterior
|
|
1696
|
+
await page.waitForFunction(
|
|
1697
|
+
() => !document.querySelector('.gwt-DialogBox'),
|
|
1698
|
+
{ timeout: 10000 }
|
|
1699
|
+
).catch(() => {});
|
|
1700
|
+
|
|
1701
|
+
// Verificar si este archivo ya fue procesado (GWT reemplaza el input con texto)
|
|
1702
|
+
// Estructura DOM: <td class="filter-label">Archivo N: Label</td> en una <tr>
|
|
1703
|
+
// La siguiente <tr> tiene <td class="filter-widget"> con el input O el texto "procesado"
|
|
1704
|
+
const yaProcessado = await page.evaluate((labelText) => {
|
|
1705
|
+
const allTds = Array.from(document.querySelectorAll('td.filter-label'));
|
|
1706
|
+
const labelTd = allTds.find(el => el.textContent.includes(labelText));
|
|
1707
|
+
if (!labelTd) return false;
|
|
1708
|
+
// Subir al <tr> padre y tomar el siguiente <tr>
|
|
1709
|
+
const tr = labelTd.closest('tr');
|
|
1710
|
+
if (!tr) return false;
|
|
1711
|
+
const nextTr = tr.nextElementSibling;
|
|
1712
|
+
if (!nextTr) return false;
|
|
1713
|
+
return nextTr.textContent.includes('procesado con exito anteriormente');
|
|
1714
|
+
}, archivo.label);
|
|
1715
|
+
|
|
1716
|
+
if (yaProcessado) {
|
|
1717
|
+
console.log(` → ${archivo.filename}: ya procesado anteriormente, saltando...`);
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
console.log(` → Subiendo ${archivo.filename}...`);
|
|
1722
|
+
|
|
1723
|
+
// Cada archivo tiene su propio form con action uploadFile1/2/3
|
|
1724
|
+
// Usamos el selector específico para no confundir entre los 3 inputs que pueden
|
|
1725
|
+
// estar presentes simultáneamente en el DOM
|
|
1726
|
+
const formSel = `form[action*="uploadFile${archivo.uploadN}"]`;
|
|
1727
|
+
await page.waitForSelector(`${formSel} input[name="uploadFormElement"]`, { timeout: 15000 });
|
|
1728
|
+
|
|
1729
|
+
const input = await page.$(`${formSel} input[name="uploadFormElement"]`);
|
|
1730
|
+
if (!input) throw new Error(`No se encontró input uploadFile${archivo.uploadN}`);
|
|
1731
|
+
await input.uploadFile(filePath);
|
|
1732
|
+
|
|
1733
|
+
const submitBtn = await page.$(`${formSel} button.button-little`);
|
|
1734
|
+
if (!submitBtn) throw new Error(`No se encontró botón Subir para uploadFile${archivo.uploadN}`);
|
|
1735
|
+
await submitBtn.click();
|
|
1736
|
+
|
|
1737
|
+
// Esperar el diálogo GWT de confirmación
|
|
1738
|
+
await page.waitForSelector('.gwt-DialogBox .msgeDialogBox', { timeout: 30000 });
|
|
1739
|
+
const msgText = await page.$eval('.gwt-DialogBox .msgeDialogBox', el => el.textContent.trim());
|
|
1740
|
+
console.log(` ✓ ${msgText}`);
|
|
1741
|
+
|
|
1742
|
+
if (debugDir) {
|
|
1743
|
+
fs.writeFileSync(
|
|
1744
|
+
path.join(debugDir, `upload-resp-${archivo.filename}.txt`),
|
|
1745
|
+
`Puppeteer: ${msgText}`, 'utf8'
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (msgText.toLowerCase().includes('error') || msgText.toLowerCase().includes('rechaz')) {
|
|
1750
|
+
throw new Error(`Error en archivo ${archivo.filename}: ${msgText}`);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Cerrar el diálogo usando el botón "Cerrar" dentro del gwt-DialogBox
|
|
1754
|
+
await page.evaluate(() => {
|
|
1755
|
+
const dlg = document.querySelector('.gwt-DialogBox');
|
|
1756
|
+
if (!dlg) return;
|
|
1757
|
+
const btn = Array.from(dlg.querySelectorAll('button')).find(
|
|
1758
|
+
b => b.textContent.trim() === 'Cerrar'
|
|
1759
|
+
);
|
|
1760
|
+
if (btn) btn.click();
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
// Esperar que el diálogo desaparezca antes del siguiente archivo
|
|
1764
|
+
await page.waitForFunction(
|
|
1765
|
+
() => !document.querySelector('.gwt-DialogBox'),
|
|
1766
|
+
{ timeout: 10000 }
|
|
1767
|
+
).catch(() => {});
|
|
1768
|
+
|
|
1769
|
+
// Esperar que GWT termine de actualizar el estado (RPC post-upload)
|
|
1770
|
+
await page.waitForNetworkIdle({ timeout: 10000, idleTime: 500 }).catch(() => {});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
return { success: true, resultado: 'Los 3 archivos subidos y registrados correctamente' };
|
|
1774
|
+
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
return { success: false, error: err.message };
|
|
1777
|
+
} finally {
|
|
1778
|
+
if (browser) await browser.close();
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1783
|
+
// FASE 8: MUESTRAS IMPRESAS — subir PDFs a pe_avance5
|
|
1784
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Consulta el estado actual del portal pdfdteInternet sin subir nada.
|
|
1788
|
+
* Útil para saltarse la generación de PDFs si ya están enviados.
|
|
1789
|
+
* @returns {Promise<{estado: string|null, error?: string}>}
|
|
1790
|
+
*/
|
|
1791
|
+
async verificarEstadoPortalMuestras() {
|
|
1792
|
+
const puppeteer = require('puppeteer');
|
|
1793
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
1794
|
+
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
1795
|
+
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
1796
|
+
}));
|
|
1797
|
+
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
1798
|
+
let browser;
|
|
1799
|
+
try {
|
|
1800
|
+
browser = await puppeteer.launch({
|
|
1801
|
+
headless: true,
|
|
1802
|
+
ignoreHTTPSErrors: true,
|
|
1803
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
1804
|
+
});
|
|
1805
|
+
const page = await browser.newPage();
|
|
1806
|
+
await page.setCookie(...puppeteerCookies);
|
|
1807
|
+
await page.goto('https://www4.sii.cl/pdfdteInternet/', { waitUntil: 'networkidle2', timeout: 60000 });
|
|
1808
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1809
|
+
|
|
1810
|
+
const rutInputs = await page.$$('input[name="rut"]');
|
|
1811
|
+
const dvInputs = await page.$$('input[name="dv"]');
|
|
1812
|
+
if (!rutInputs.length) return { estado: null, error: 'sin campos RUT (¿sesión expirada?)' };
|
|
1813
|
+
|
|
1814
|
+
// Solo RUT empresa → "Rut": el portal ya muestra "Estado de la Revisión" en el DOM
|
|
1815
|
+
await rutInputs[0].click({ clickCount: 3 }); await rutInputs[0].type(rutNum);
|
|
1816
|
+
await dvInputs[0].click({ clickCount: 3 }); await dvInputs[0].type(dvChar);
|
|
1817
|
+
await page.evaluate((t) => {
|
|
1818
|
+
const btn = Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
1819
|
+
.find(b => b.textContent.trim() === t && !b.disabled && b.getAttribute('aria-disabled') !== 'true');
|
|
1820
|
+
if (btn) btn.click();
|
|
1821
|
+
}, 'Rut');
|
|
1822
|
+
|
|
1823
|
+
// Descartar diálogo "ya existe revisión" si aparece
|
|
1824
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
1825
|
+
await page.evaluate(() => {
|
|
1826
|
+
const si = Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
1827
|
+
.find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
1828
|
+
if (si) si.click();
|
|
1829
|
+
}).catch(() => {});
|
|
1830
|
+
|
|
1831
|
+
// Esperar hasta 8s a que aparezca el estado en el DOM
|
|
1832
|
+
await page.waitForFunction(() => {
|
|
1833
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
1834
|
+
return t.includes('ESTADO DE LA REVISI') ||
|
|
1835
|
+
t.includes('POR REVISAR') || t.includes('APROBADO') ||
|
|
1836
|
+
t.includes('EN REVISI') || t.includes('RECHAZADO');
|
|
1837
|
+
}, { timeout: 8000, polling: 500 }).catch(() => {});
|
|
1838
|
+
|
|
1839
|
+
const estado = await page.evaluate(() => {
|
|
1840
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
1841
|
+
if (t.includes('APROBADO')) return 'APROBADO';
|
|
1842
|
+
if (t.includes('POR REVISAR')) return 'POR REVISAR';
|
|
1843
|
+
if (t.includes('EN REVISI')) return 'EN REVISIÓN';
|
|
1844
|
+
if (t.includes('RECHAZADO')) return 'RECHAZADO';
|
|
1845
|
+
if (t.includes('ENVIADO AL SII')) return 'ENVIADO AL SII';
|
|
1846
|
+
return null;
|
|
1847
|
+
}).catch(() => null);
|
|
1848
|
+
|
|
1849
|
+
return { estado };
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
return { estado: null, error: err.message };
|
|
1852
|
+
} finally {
|
|
1853
|
+
if (browser) await browser.close().catch(() => {});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Sube los PDFs de muestras impresas al portal pe_avance5 via Puppeteer.
|
|
1859
|
+
* @param {Object} opts
|
|
1860
|
+
* @param {string} opts.pdfDir - Directorio con los PDFs generados
|
|
1861
|
+
* @returns {Promise<Object>} { success, error? }
|
|
1862
|
+
*/
|
|
1863
|
+
async ejecutarFase8Muestras({ pdfDir }) {
|
|
1864
|
+
const _collectPdfs = (dir) => {
|
|
1865
|
+
if (!fs.existsSync(dir)) return [];
|
|
1866
|
+
return fs.readdirSync(dir).flatMap(f => {
|
|
1867
|
+
const full = path.join(dir, f);
|
|
1868
|
+
return fs.statSync(full).isDirectory() ? _collectPdfs(full) : f.endsWith('.pdf') ? [full] : [];
|
|
1869
|
+
});
|
|
1870
|
+
};
|
|
1871
|
+
const pdfPaths = _collectPdfs(pdfDir);
|
|
1872
|
+
|
|
1873
|
+
if (!pdfPaths.length) throw new Error(`No se encontraron PDFs en: ${pdfDir}`);
|
|
1874
|
+
|
|
1875
|
+
console.log('\n' + '═'.repeat(60));
|
|
1876
|
+
console.log(`📄 FASE 8: MUESTRAS IMPRESAS (${pdfPaths.length} PDFs)`);
|
|
1877
|
+
console.log('═'.repeat(60));
|
|
1878
|
+
|
|
1879
|
+
return this._subirMuestrasImpresasPortal({ pdfPaths, debugDir: pdfDir });
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* Sube PDFs al portal https://www4.sii.cl/pdfdteInternet/ via Puppeteer.
|
|
1884
|
+
* El portal usa ExtJS 3 — botones buscados por texto, no por ID dinámico.
|
|
1885
|
+
* @private
|
|
1886
|
+
*/
|
|
1887
|
+
async _subirMuestrasImpresasPortal({ pdfPaths, debugDir }) {
|
|
1888
|
+
const puppeteer = require('puppeteer');
|
|
1889
|
+
|
|
1890
|
+
// Reutiliza sesión SII en memoria/disco (misma sesión que intercambio u otros pasos)
|
|
1891
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
1892
|
+
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
1893
|
+
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
1894
|
+
}));
|
|
1895
|
+
|
|
1896
|
+
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
1897
|
+
|
|
1898
|
+
// Helper: click botón ExtJS por texto
|
|
1899
|
+
const clickBoton = (page, texto) => page.evaluate((t) => {
|
|
1900
|
+
const btn = Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
1901
|
+
.find(b => b.textContent.trim() === t && !b.disabled && b.getAttribute('aria-disabled') !== 'true');
|
|
1902
|
+
if (btn) { btn.click(); return true; }
|
|
1903
|
+
return false;
|
|
1904
|
+
}, texto);
|
|
1905
|
+
|
|
1906
|
+
let browser;
|
|
1907
|
+
try {
|
|
1908
|
+
browser = await puppeteer.launch({
|
|
1909
|
+
headless: true,
|
|
1910
|
+
ignoreHTTPSErrors: true,
|
|
1911
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
1912
|
+
});
|
|
1913
|
+
const page = await browser.newPage();
|
|
1914
|
+
await page.setCookie(...puppeteerCookies);
|
|
1915
|
+
|
|
1916
|
+
// Navegar directamente a www4.sii.cl/pdfdteInternet/ con las cookies de sesión SII
|
|
1917
|
+
console.log(' → Cargando portal pdfdteInternet...');
|
|
1918
|
+
await page.goto('https://www4.sii.cl/pdfdteInternet/', {
|
|
1919
|
+
waitUntil: 'networkidle2', timeout: 60000,
|
|
1920
|
+
});
|
|
1921
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1922
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-01-loaded.png'), fullPage: true }).catch(() => {});
|
|
1923
|
+
|
|
1924
|
+
// Paso 1: RUT Empresa
|
|
1925
|
+
const rutInputs = await page.$$('input[name="rut"]');
|
|
1926
|
+
const dvInputs = await page.$$('input[name="dv"]');
|
|
1927
|
+
if (!rutInputs.length) {
|
|
1928
|
+
if (debugDir) {
|
|
1929
|
+
await page.screenshot({ path: path.join(debugDir, 'pdfte-error-no-rut.png'), fullPage: true }).catch(() => {});
|
|
1930
|
+
fs.writeFileSync(path.join(debugDir, 'pdfte-error.html'), await page.content(), 'utf8');
|
|
1931
|
+
}
|
|
1932
|
+
throw new Error('pdfdteInternet: no se encontraron campos de RUT (¿sesión expirada?)');
|
|
1933
|
+
}
|
|
1934
|
+
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
1935
|
+
await rutInputs[0].click({ clickCount: 3 }); await rutInputs[0].type(rutNum);
|
|
1936
|
+
await dvInputs[0].click({ clickCount: 3 }); await dvInputs[0].type(dvChar);
|
|
1937
|
+
await clickBoton(page, 'Rut');
|
|
1938
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
1939
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-02-after-rut.png'), fullPage: true }).catch(() => {});
|
|
1940
|
+
|
|
1941
|
+
// Paso 2: Diálogo "ya existe revisión" → click "Sí"
|
|
1942
|
+
const hayDialog = await page.evaluate(() => {
|
|
1943
|
+
const dlg = document.querySelector('.x-window');
|
|
1944
|
+
return !!(dlg && dlg.offsetParent !== null);
|
|
1945
|
+
});
|
|
1946
|
+
if (hayDialog) {
|
|
1947
|
+
console.log(' → Diálogo de revisión existente → haciendo click en "Sí"');
|
|
1948
|
+
const clicked = await page.evaluate(() => {
|
|
1949
|
+
const si = Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
1950
|
+
.find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
1951
|
+
if (si) { si.click(); return true; }
|
|
1952
|
+
return false;
|
|
1953
|
+
});
|
|
1954
|
+
if (!clicked) await page.evaluate(() => { const b = document.querySelector('.x-window button'); if (b) b.click(); });
|
|
1955
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Paso 3: RUT Proveedor (mismo RUT empresa)
|
|
1959
|
+
await page.waitForFunction(() => {
|
|
1960
|
+
const ins = document.querySelectorAll('input[name="rut"]');
|
|
1961
|
+
return ins.length >= 2 && !ins[1].disabled;
|
|
1962
|
+
}, { timeout: 15000 }).catch(() => {});
|
|
1963
|
+
|
|
1964
|
+
const rutNow = await page.$$('input[name="rut"]');
|
|
1965
|
+
const dvNow = await page.$$('input[name="dv"]');
|
|
1966
|
+
const pRut = rutNow.length >= 2 ? rutNow[1] : rutNow[0];
|
|
1967
|
+
const pDv = dvNow.length >= 2 ? dvNow[1] : dvNow[0];
|
|
1968
|
+
console.log(` → Ingresando RUT proveedor: ${rutNum}-${dvChar}`);
|
|
1969
|
+
await pRut.click({ clickCount: 3 }); await pRut.type(rutNum);
|
|
1970
|
+
await pDv.click({ clickCount: 3 }); await pDv.type(dvChar);
|
|
1971
|
+
await clickBoton(page, 'Consultar');
|
|
1972
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
1973
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-03-after-consultar.png'), fullPage: true }).catch(() => {});
|
|
1974
|
+
|
|
1975
|
+
// ── Re-ejecución: detectar estado terminal antes de proceder ──
|
|
1976
|
+
const _estadoYaSubido = await page.evaluate(() => {
|
|
1977
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
1978
|
+
if (t.includes('APROBADO')) return 'APROBADO';
|
|
1979
|
+
if (t.includes('POR REVISAR')) return 'POR REVISAR';
|
|
1980
|
+
if (t.includes('EN REVISI')) return 'EN REVISIÓN';
|
|
1981
|
+
if (t.includes('RECHAZADO')) return 'RECHAZADO';
|
|
1982
|
+
if (t.includes('ENVIADO AL SII')) return 'ENVIADO AL SII';
|
|
1983
|
+
return null;
|
|
1984
|
+
}).catch(() => null);
|
|
1985
|
+
if (_estadoYaSubido) {
|
|
1986
|
+
console.log(` ✅ Portal ya muestra estado "${_estadoYaSubido}" — muestras subidas previamente. Proceso completado.`);
|
|
1987
|
+
return { success: true, alreadyCompleted: true, estado: _estadoYaSubido };
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Paso 4: "Crear" → habilita el input de archivo
|
|
1991
|
+
console.log(' → Click "Crear"...');
|
|
1992
|
+
await clickBoton(page, 'Crear');
|
|
1993
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
1994
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-04-after-crear.png'), fullPage: true }).catch(() => {});
|
|
1995
|
+
|
|
1996
|
+
// Paso 5: Verificar que el input de archivo existe antes de empezar
|
|
1997
|
+
const inputCheck = await page.waitForSelector('input.gwt-FileUpload', { timeout: 30000 }).catch(() => null);
|
|
1998
|
+
if (!inputCheck) {
|
|
1999
|
+
if (debugDir) {
|
|
2000
|
+
await page.screenshot({ path: path.join(debugDir, 'pdfte-error-no-fileinput.png'), fullPage: true }).catch(() => {});
|
|
2001
|
+
fs.writeFileSync(path.join(debugDir, 'pdfte-error.html'), await page.content(), 'utf8');
|
|
2002
|
+
}
|
|
2003
|
+
throw new Error('pdfdteInternet: no apareció el input de archivo tras "Crear"');
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// El portal sólo acepta un PDF a la vez (input sin atributo "multiple").
|
|
2007
|
+
// Tras el submit GWT no recrea el input dentro de la misma carga de página.
|
|
2008
|
+
// Solución: re-navegar al portal antes de cada archivo ≥ 2 (misma revisión,
|
|
2009
|
+
// misma sesión con cookies); el diálogo "ya existe revisión → Sí" la abre.
|
|
2010
|
+
const navegarAlFormulario = async () => {
|
|
2011
|
+
await page.goto('https://www4.sii.cl/pdfdteInternet/', { waitUntil: 'networkidle2', timeout: 60000 });
|
|
2012
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2013
|
+
const _ruts = await page.$$('input[name="rut"]');
|
|
2014
|
+
const _dvs = await page.$$('input[name="dv"]');
|
|
2015
|
+
if (!_ruts.length) throw new Error('pdfdteInternet: sin campos RUT en re-navegación (¿sesión expirada?)');
|
|
2016
|
+
await _ruts[0].click({ clickCount: 3 }); await _ruts[0].type(rutNum);
|
|
2017
|
+
await _dvs[0].click({ clickCount: 3 }); await _dvs[0].type(dvChar);
|
|
2018
|
+
await clickBoton(page, 'Rut');
|
|
2019
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2020
|
+
// Diálogo "ya existe revisión" → click "Sí" para abrirla
|
|
2021
|
+
const _dlg = await page.evaluate(() => { const d = document.querySelector('.x-window'); return !!(d && d.offsetParent !== null); });
|
|
2022
|
+
if (_dlg) {
|
|
2023
|
+
const _ok = await page.evaluate(() => {
|
|
2024
|
+
const si = Array.from(document.querySelectorAll('button.x-btn-text')).find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
2025
|
+
if (si) { si.click(); return true; }
|
|
2026
|
+
return false;
|
|
2027
|
+
});
|
|
2028
|
+
if (!_ok) await page.evaluate(() => { const b = document.querySelector('.x-window button'); if (b) b.click(); });
|
|
2029
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2030
|
+
}
|
|
2031
|
+
// RUT proveedor (mismo que empresa)
|
|
2032
|
+
await page.waitForFunction(() => { const ins = document.querySelectorAll('input[name="rut"]'); return ins.length >= 2 && !ins[1].disabled; }, { timeout: 10000 }).catch(() => {});
|
|
2033
|
+
const _rutsN = await page.$$('input[name="rut"]');
|
|
2034
|
+
const _dvsN = await page.$$('input[name="dv"]');
|
|
2035
|
+
const _pRut = _rutsN.length >= 2 ? _rutsN[1] : _rutsN[0];
|
|
2036
|
+
const _pDv = _dvsN.length >= 2 ? _dvsN[1] : _dvsN[0];
|
|
2037
|
+
await _pRut.click({ clickCount: 3 }); await _pRut.type(rutNum);
|
|
2038
|
+
await _pDv.click({ clickCount: 3 }); await _pDv.type(dvChar);
|
|
2039
|
+
await clickBoton(page, 'Consultar');
|
|
2040
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
2041
|
+
// Si aún no hay formulario de subida (primera vez), crear revisión
|
|
2042
|
+
const _hayInp = await page.$('input.gwt-FileUpload').catch(() => null);
|
|
2043
|
+
if (!_hayInp) { await clickBoton(page, 'Crear'); await new Promise(r => setTimeout(r, 2500)); }
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// Cargar todos los PDFs como base64 en Node.js y soltarlos en el drop zone de GWT
|
|
2047
|
+
// de una sola vez via DataTransfer. GWT los procesa en secuencia internamente:
|
|
2048
|
+
// drop → por cada file: submit form al iframe → respuesta → leeImpresoById → tick verde
|
|
2049
|
+
// Esto evita la re-navegación entre archivos y garantiza que la validación
|
|
2050
|
+
// (Timbre/CAF/TED) ocurra antes de salir de la página.
|
|
2051
|
+
console.log(` → Cargando ${pdfPaths.length} PDFs para drop en el portal...`);
|
|
2052
|
+
const _fileDataList = pdfPaths.map(p => ({
|
|
2053
|
+
name: path.basename(p),
|
|
2054
|
+
b64: fs.readFileSync(p).toString('base64'),
|
|
2055
|
+
}));
|
|
2056
|
+
|
|
2057
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-04b-antes-drop.png'), fullPage: true }).catch(() => {});
|
|
2058
|
+
|
|
2059
|
+
console.log(` → Ejecutando drop de ${pdfPaths.length} PDFs sobre el portal...`);
|
|
2060
|
+
const _dropped = await page.evaluate((files) => {
|
|
2061
|
+
const dt = new DataTransfer();
|
|
2062
|
+
for (const f of files) {
|
|
2063
|
+
const bin = atob(f.b64);
|
|
2064
|
+
const arr = new Uint8Array(bin.length);
|
|
2065
|
+
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
2066
|
+
dt.items.add(new File([arr], f.name, { type: 'application/pdf' }));
|
|
2067
|
+
}
|
|
2068
|
+
const dz = document.querySelector('.dropFilesLabel');
|
|
2069
|
+
if (!dz) return 0;
|
|
2070
|
+
dz.dispatchEvent(new DragEvent('dragenter', { dataTransfer: dt, bubbles: true, cancelable: true }));
|
|
2071
|
+
dz.dispatchEvent(new DragEvent('dragover', { dataTransfer: dt, bubbles: true, cancelable: true }));
|
|
2072
|
+
dz.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true }));
|
|
2073
|
+
return dt.files.length;
|
|
2074
|
+
}, _fileDataList);
|
|
2075
|
+
|
|
2076
|
+
if (_dropped === 0) throw new Error('pdfdteInternet: drop zone no encontrado (.dropFilesLabel)');
|
|
2077
|
+
console.log(` → Drop ejecutado (${_dropped} archivos). Esperando procesamiento...`);
|
|
2078
|
+
|
|
2079
|
+
// ── Fase 1: esperar hasta 45s por primera señal de progreso o estado terminal ──
|
|
2080
|
+
// Si el portal ya está en "POR REVISAR" (re-ejecución), lo detectamos aquí inmediatamente.
|
|
2081
|
+
// Si el drop inició normalmente, "Procesados 1" aparece en pocos segundos.
|
|
2082
|
+
await page.waitForFunction(() => {
|
|
2083
|
+
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2084
|
+
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2085
|
+
if (m && +m[1] > 0) return true;
|
|
2086
|
+
}
|
|
2087
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
2088
|
+
if (t.includes('APROBADO') || t.includes('POR REVISAR') || t.includes('EN REVISI') || t.includes('RECHAZADO')) return true;
|
|
2089
|
+
return false;
|
|
2090
|
+
}, { timeout: 45000, polling: 1000 }).catch(() => {});
|
|
2091
|
+
|
|
2092
|
+
// Leer estado real tras la fase 1
|
|
2093
|
+
const _fase1 = await page.evaluate(() => {
|
|
2094
|
+
let procesados = 0;
|
|
2095
|
+
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2096
|
+
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2097
|
+
if (m) { procesados = +m[1]; break; }
|
|
2098
|
+
}
|
|
2099
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
2100
|
+
let estado = null;
|
|
2101
|
+
if (t.includes('APROBADO')) estado = 'APROBADO';
|
|
2102
|
+
else if (t.includes('POR REVISAR')) estado = 'POR REVISAR';
|
|
2103
|
+
else if (t.includes('EN REVISI')) estado = 'EN REVISIÓN';
|
|
2104
|
+
else if (t.includes('RECHAZADO')) estado = 'RECHAZADO';
|
|
2105
|
+
return { procesados, estado };
|
|
2106
|
+
}).catch(() => ({ procesados: 0, estado: null }));
|
|
2107
|
+
|
|
2108
|
+
if (_fase1.estado) {
|
|
2109
|
+
console.log(` ✅ Portal en estado "${_fase1.estado}" — muestras ya procesadas previamente.`);
|
|
2110
|
+
return { success: true, alreadyCompleted: true, estado: _fase1.estado };
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
if (_fase1.procesados === 0) {
|
|
2114
|
+
// Sin progreso y sin estado terminal: el portal puede no estar procesando
|
|
2115
|
+
console.warn(' ⚠ Sin progreso en 45s y sin estado terminal. Continuando al paso siguiente...');
|
|
2116
|
+
} else {
|
|
2117
|
+
// ── Fase 2: progreso iniciado — esperar al total ──
|
|
2118
|
+
await page.waitForFunction((total) => {
|
|
2119
|
+
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2120
|
+
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2121
|
+
if (m && +m[1] >= total) return true;
|
|
2122
|
+
}
|
|
2123
|
+
return false;
|
|
2124
|
+
}, { timeout: pdfPaths.length * 15000, polling: 1000 }, pdfPaths.length).catch(async () => {
|
|
2125
|
+
const procesados = await page.evaluate(() => {
|
|
2126
|
+
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2127
|
+
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2128
|
+
if (m) return +m[1];
|
|
2129
|
+
}
|
|
2130
|
+
return 0;
|
|
2131
|
+
}).catch(() => 0);
|
|
2132
|
+
console.warn(` ⚠ Timeout: solo se procesaron ${procesados}/${pdfPaths.length} antes del timeout`);
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// Esperar que todos los requests de leeImpresoById (validación) terminen
|
|
2137
|
+
await page.waitForNetworkIdle({ timeout: 60000, idleTime: 2000 }).catch(() => {});
|
|
2138
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2139
|
+
|
|
2140
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-05-archivos-listos.png'), fullPage: true }).catch(() => {});
|
|
2141
|
+
|
|
2142
|
+
// Paso 6: re-navegar al estado limpio y Enviar al SII
|
|
2143
|
+
console.log(' → Re-navegando para "Enviar al SII"...');
|
|
2144
|
+
await navegarAlFormulario();
|
|
2145
|
+
|
|
2146
|
+
// Esperar a que el botón esté habilitado (aria-disabled="false")
|
|
2147
|
+
await page.waitForFunction(() => {
|
|
2148
|
+
const btn = Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
2149
|
+
.find(b => b.textContent.trim() === 'Enviar al SII' && b.getAttribute('aria-disabled') !== 'true');
|
|
2150
|
+
return !!btn;
|
|
2151
|
+
}, { timeout: 15000 }).catch(() => {});
|
|
2152
|
+
|
|
2153
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-05b-antes-enviar.png'), fullPage: true }).catch(() => {});
|
|
2154
|
+
|
|
2155
|
+
console.log(' → Click "Enviar al SII"...');
|
|
2156
|
+
const enviado = await clickBoton(page, 'Enviar al SII');
|
|
2157
|
+
if (!enviado) throw new Error('pdfdteInternet: botón "Enviar al SII" no disponible o deshabilitado');
|
|
2158
|
+
|
|
2159
|
+
await page.waitForNetworkIdle({ timeout: 30000, idleTime: 1000 }).catch(() => {});
|
|
2160
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2161
|
+
if (debugDir) {
|
|
2162
|
+
await page.screenshot({ path: path.join(debugDir, 'pdfte-06-enviado.png'), fullPage: true }).catch(() => {});
|
|
2163
|
+
fs.writeFileSync(path.join(debugDir, 'pdfte-06-enviado.html'), await page.content(), 'utf8');
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const pageText = await page.$eval('body', el => el.textContent).catch(() => '');
|
|
2167
|
+
const exitoso = /revision.*creada|solicitud.*enviada|documentos.*enviados|fue.*enviado|[eé]xito/i.test(pageText);
|
|
2168
|
+
console.log(` → Resultado: ${exitoso ? '✅ enviado correctamente' : '⚠️ sin confirmación explícita'}`);
|
|
2169
|
+
return { success: exitoso, pageText: pageText.substring(0, 800) };
|
|
2170
|
+
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
return { success: false, error: err.message };
|
|
2173
|
+
} finally {
|
|
2174
|
+
if (browser) await browser.close().catch(() => {});
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2179
|
+
// BOLETA ELECTRÓNICA — automatización portal certBolElectDteInternet
|
|
2180
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Descarga el Set de Pruebas de Boleta Electrónica desde el portal SII.
|
|
2184
|
+
* Flujo real portal:
|
|
2185
|
+
* 1. Ingresa RUT empresa → click "Confirmar Empresa"
|
|
2186
|
+
* 2. Marca checkbox "SET DE BOLETA ELECTRÓNICA AFECTA"
|
|
2187
|
+
* 3. Rellena email proveedor
|
|
2188
|
+
* 4. Click "Bajar Nuevo Set" → descarga archivo .txt
|
|
2189
|
+
* @param {Object} opts
|
|
2190
|
+
* @param {string} [opts.setPath] - Ruta donde guardar el set
|
|
2191
|
+
* @param {string} [opts.correoSet='sii.certificacion@devlas.cl'] - Correo proveedor para el set
|
|
2192
|
+
* @returns {Promise<{success: boolean, setText?: string, error?: string}>}
|
|
2193
|
+
*/
|
|
2194
|
+
async obtenerSetBoletaPortal({ setPath, correoSet = 'sii.certificacion@devlas.cl' } = {}) {
|
|
2195
|
+
const puppeteer = require('puppeteer');
|
|
2196
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
2197
|
+
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
2198
|
+
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
2199
|
+
}));
|
|
2200
|
+
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2201
|
+
|
|
2202
|
+
let browser;
|
|
2203
|
+
try {
|
|
2204
|
+
browser = await puppeteer.launch({
|
|
2205
|
+
headless: true,
|
|
2206
|
+
ignoreHTTPSErrors: true,
|
|
2207
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2208
|
+
});
|
|
2209
|
+
const page = await browser.newPage();
|
|
2210
|
+
await page.setCookie(...puppeteerCookies);
|
|
2211
|
+
page.on('dialog', async dlg => { console.log(` → [dialog SET=1] ${dlg.message()}`); await dlg.accept(); });
|
|
2212
|
+
|
|
2213
|
+
console.log(' → Cargando portal certBolElectDteInternet (SET=1)...');
|
|
2214
|
+
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=1', {
|
|
2215
|
+
waitUntil: 'networkidle2', timeout: 60000,
|
|
2216
|
+
});
|
|
2217
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2218
|
+
|
|
2219
|
+
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2220
|
+
const rutInput = await page.$('input[maxlength="8"]');
|
|
2221
|
+
const dvInput = await page.$('input[maxlength="1"]');
|
|
2222
|
+
if (!rutInput) throw new Error('certBolElectDteInternet/?SET=1: no se encontró campo RUT');
|
|
2223
|
+
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
2224
|
+
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutNum);
|
|
2225
|
+
await dvInput.click({ clickCount: 3 }); await dvInput.type(dvChar);
|
|
2226
|
+
await page.evaluate(() => {
|
|
2227
|
+
const btn = Array.from(document.querySelectorAll('button'))
|
|
2228
|
+
.find(b => /confirmar/i.test(b.textContent));
|
|
2229
|
+
if (btn) btn.click();
|
|
2230
|
+
});
|
|
2231
|
+
console.log(' → Click "Confirmar Empresa"');
|
|
2232
|
+
|
|
2233
|
+
// Esperar checkboxes — GWT dispara ~8-10 POST /facade en paralelo
|
|
2234
|
+
await page.waitForNetworkIdle({ idleTime: 800, timeout: 40000 }).catch(() => {});
|
|
2235
|
+
await page.waitForFunction(() => {
|
|
2236
|
+
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2237
|
+
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2238
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2239
|
+
|
|
2240
|
+
// Paso 2: Marcar todos los checkboxes
|
|
2241
|
+
const nCbs = await page.evaluate(() => {
|
|
2242
|
+
const cbs = Array.from(document.querySelectorAll('input[type="checkbox"]'));
|
|
2243
|
+
cbs.forEach(cb => { if (!cb.checked) cb.click(); });
|
|
2244
|
+
return cbs.length;
|
|
2245
|
+
});
|
|
2246
|
+
console.log(` → ${nCbs} checkbox(es) marcados`);
|
|
2247
|
+
await new Promise(r => setTimeout(r, 300));
|
|
2248
|
+
|
|
2249
|
+
// Paso 3: Rellenar correo proveedor
|
|
2250
|
+
// El campo de email es el input visible que NO tiene maxlength pequeño
|
|
2251
|
+
const allInputs = await page.$$('input[type="text"].form-control');
|
|
2252
|
+
let emailInput = null;
|
|
2253
|
+
for (const inp of allInputs) {
|
|
2254
|
+
const ml = await page.evaluate(el => el.maxLength, inp);
|
|
2255
|
+
const visible = await page.evaluate(el => el.offsetParent !== null, inp);
|
|
2256
|
+
if (visible && (ml <= 0 || ml > 8)) { emailInput = inp; break; }
|
|
2257
|
+
}
|
|
2258
|
+
if (emailInput) {
|
|
2259
|
+
await emailInput.click({ clickCount: 3 });
|
|
2260
|
+
await emailInput.type(correoSet);
|
|
2261
|
+
console.log(` → Correo proveedor: ${correoSet}`);
|
|
2262
|
+
} else {
|
|
2263
|
+
console.log(' ⚠️ No se encontró campo de correo — continuando sin él');
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// Paso 4: Click "Bajar Nuevo Set" — esperar POST /facade (GWT RPC) y luego
|
|
2267
|
+
// construir la URL de DownloadFileServlet directamente con los parámetros conocidos.
|
|
2268
|
+
// La descarga real es un GET:
|
|
2269
|
+
// DownloadFileServlet?rutEmpresa=X&dvEmpresa=X&rutRepre=X&dvRepre=X&mailProvSw=X
|
|
2270
|
+
// donde rutRepre/dvRepre vienen de las cookies NETSCAPE_LIVEWIRE.rut / .dv
|
|
2271
|
+
|
|
2272
|
+
const rutRepreNum = cookieJar['NETSCAPE_LIVEWIRE.rut'] || cookieJar['RUT_NS'] || '';
|
|
2273
|
+
const dvRepreChar = cookieJar['NETSCAPE_LIVEWIRE.dv'] || cookieJar['DV_NS'] || '';
|
|
2274
|
+
if (!rutRepreNum) throw new Error('No se pudo obtener rutRepre de las cookies SII (NETSCAPE_LIVEWIRE.rut)');
|
|
2275
|
+
|
|
2276
|
+
// Registrar listener de facade ANTES de hacer click
|
|
2277
|
+
const facadePromise = page.waitForResponse(
|
|
2278
|
+
resp => resp.url().includes('/certBolElectDteInternet/facade'),
|
|
2279
|
+
{ timeout: 20000 }
|
|
2280
|
+
).catch(() => null);
|
|
2281
|
+
|
|
2282
|
+
console.log(' → Click "Bajar Nuevo Set" — esperando GWT facade...');
|
|
2283
|
+
await page.evaluate(() => {
|
|
2284
|
+
const btn = Array.from(document.querySelectorAll('button'))
|
|
2285
|
+
.find(b => /bajar/i.test(b.textContent));
|
|
2286
|
+
if (btn) btn.click();
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
// Esperar que el facade GWT procese la solicitud
|
|
2290
|
+
await facadePromise;
|
|
2291
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
2292
|
+
|
|
2293
|
+
// Construir URL de descarga e ir directo con https.get + cookies
|
|
2294
|
+
const https = require('https');
|
|
2295
|
+
const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
2296
|
+
const downloadUrl = `https://www4.sii.cl/certBolElectDteInternet/DownloadFileServlet` +
|
|
2297
|
+
`?rutEmpresa=${rutNum}&dvEmpresa=${dvChar}` +
|
|
2298
|
+
`&rutRepre=${rutRepreNum}&dvRepre=${dvRepreChar}` +
|
|
2299
|
+
`&mailProvSw=${encodeURIComponent(correoSet)}`;
|
|
2300
|
+
|
|
2301
|
+
console.log(` → Descargando set directamente: DownloadFileServlet?rutEmpresa=${rutNum}&dvEmpresa=${dvChar}&rutRepre=${rutRepreNum}&dvRepre=${dvRepreChar}&mailProvSw=${correoSet}`);
|
|
2302
|
+
|
|
2303
|
+
const setText = await new Promise((resolve, reject) => {
|
|
2304
|
+
const req = https.get(downloadUrl, {
|
|
2305
|
+
headers: {
|
|
2306
|
+
'Cookie': cookieStr,
|
|
2307
|
+
'Referer': 'https://www4.sii.cl/certBolElectDteInternet/?SET=1',
|
|
2308
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
2309
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
2310
|
+
},
|
|
2311
|
+
rejectUnauthorized: false,
|
|
2312
|
+
}, (res) => {
|
|
2313
|
+
const chunks = [];
|
|
2314
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
2315
|
+
res.on('end', () => {
|
|
2316
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
2317
|
+
resolve(body);
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
2320
|
+
req.on('error', reject);
|
|
2321
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('Timeout descargando DownloadFileServlet')); });
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
if (!setText || setText.trim().length < 10) {
|
|
2325
|
+
if (this.config.debugDir) {
|
|
2326
|
+
fs.mkdirSync(this.config.debugDir, { recursive: true });
|
|
2327
|
+
fs.writeFileSync(path.join(this.config.debugDir, 'boleta-set-debug.txt'), setText || '', 'utf-8');
|
|
2328
|
+
}
|
|
2329
|
+
throw new Error(`DownloadFileServlet devolvió contenido vacío (${setText?.length ?? 0} chars). Verificar cookies.`);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
if (setPath) {
|
|
2333
|
+
const nodePath = require('path');
|
|
2334
|
+
fs.mkdirSync(nodePath.dirname(setPath), { recursive: true });
|
|
2335
|
+
fs.writeFileSync(setPath, setText, 'utf-8');
|
|
2336
|
+
console.log(` ✓ Set guardado en: ${setPath}`);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
console.log(` ✓ Set de pruebas obtenido (${setText.length} chars)`);
|
|
2340
|
+
return { success: true, setText };
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
return { success: false, error: err.message };
|
|
2343
|
+
} finally {
|
|
2344
|
+
if (browser) await browser.close().catch(() => {});
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* Solicita la validación del set de pruebas de Boleta Electrónica al SII (?SET=2).
|
|
2350
|
+
* Flujo real portal:
|
|
2351
|
+
* 1. Ingresa RUT empresa → click "Confirmar Empresa"
|
|
2352
|
+
* 2. Ingresa el TrackId del EnvioBOLETA en "Identificador de Envio"
|
|
2353
|
+
* 3. Click "Solicitar validación"
|
|
2354
|
+
* El SII procesa el set de forma asíncrona y notifica por correo (SOK/SRH).
|
|
2355
|
+
* @param {Object} opts
|
|
2356
|
+
* @param {string} opts.trackId - TrackId del EnvioBOLETA
|
|
2357
|
+
* @returns {Promise<{success: boolean, respuesta?: string, error?: string}>}
|
|
2358
|
+
*/
|
|
2359
|
+
async solicitarValidacionBoletaPortal({ trackId } = {}) {
|
|
2360
|
+
if (!trackId) throw new Error('solicitarValidacionBoletaPortal: trackId es obligatorio');
|
|
2361
|
+
const puppeteer = require('puppeteer');
|
|
2362
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
2363
|
+
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
2364
|
+
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
2365
|
+
}));
|
|
2366
|
+
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2367
|
+
|
|
2368
|
+
let browser;
|
|
2369
|
+
try {
|
|
2370
|
+
browser = await puppeteer.launch({
|
|
2371
|
+
headless: true,
|
|
2372
|
+
ignoreHTTPSErrors: true,
|
|
2373
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2374
|
+
});
|
|
2375
|
+
const page = await browser.newPage();
|
|
2376
|
+
await page.setCookie(...puppeteerCookies);
|
|
2377
|
+
page.on('dialog', async dlg => { console.log(` → [dialog SET=2] ${dlg.message()}`); await dlg.accept(); });
|
|
2378
|
+
|
|
2379
|
+
console.log(' → Cargando portal certBolElectDteInternet (SET=2)...');
|
|
2380
|
+
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=2', {
|
|
2381
|
+
waitUntil: 'networkidle2', timeout: 60000,
|
|
2382
|
+
});
|
|
2383
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2384
|
+
|
|
2385
|
+
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2386
|
+
const rutInput = await page.$('input[maxlength="8"]');
|
|
2387
|
+
const dvInput = await page.$('input[maxlength="1"]');
|
|
2388
|
+
if (!rutInput) throw new Error('certBolElectDteInternet/?SET=2: no se encontró campo RUT');
|
|
2389
|
+
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
2390
|
+
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutNum);
|
|
2391
|
+
await dvInput.click({ clickCount: 3 }); await dvInput.type(dvChar);
|
|
2392
|
+
await page.evaluate(() => {
|
|
2393
|
+
const btn = Array.from(document.querySelectorAll('button'))
|
|
2394
|
+
.find(b => /confirmar/i.test(b.textContent));
|
|
2395
|
+
if (btn) btn.click();
|
|
2396
|
+
});
|
|
2397
|
+
console.log(' → Click "Confirmar Empresa"');
|
|
2398
|
+
|
|
2399
|
+
// Esperar que aparezca el campo "Identificador de Envio" — GWT dispara ~8-10 POST /facade en paralelo
|
|
2400
|
+
await page.waitForNetworkIdle({ idleTime: 800, timeout: 40000 }).catch(() => {});
|
|
2401
|
+
await page.waitForFunction(() => {
|
|
2402
|
+
return document.querySelector('input[maxlength="15"]') !== null;
|
|
2403
|
+
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2404
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2405
|
+
|
|
2406
|
+
// Paso 2: Ingresar TrackId
|
|
2407
|
+
const trackInput = await page.$('input[maxlength="15"]');
|
|
2408
|
+
if (!trackInput) throw new Error('No se encontró campo "Identificador de Envio" en certBolElectDteInternet/?SET=2');
|
|
2409
|
+
console.log(` → Ingresando TrackId: ${trackId}`);
|
|
2410
|
+
await trackInput.click({ clickCount: 3 });
|
|
2411
|
+
await trackInput.type(String(trackId));
|
|
2412
|
+
|
|
2413
|
+
// Paso 3: Click "Solicitar validación"
|
|
2414
|
+
await page.evaluate(() => {
|
|
2415
|
+
const btn = Array.from(document.querySelectorAll('button'))
|
|
2416
|
+
.find(b => /solicitar/i.test(b.textContent));
|
|
2417
|
+
if (btn) btn.click();
|
|
2418
|
+
});
|
|
2419
|
+
console.log(' → Click "Solicitar validación" — esperando respuesta...');
|
|
2420
|
+
|
|
2421
|
+
// Esperar respuesta del portal (puede ser confirmación o error)
|
|
2422
|
+
await page.waitForFunction(() => {
|
|
2423
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
2424
|
+
return t.includes('ENVI') || t.includes('CORREO') || t.includes('ERROR') ||
|
|
2425
|
+
t.includes('VALIDACI') || t.includes('SOLICITUD');
|
|
2426
|
+
}, { timeout: 15000, polling: 500 }).catch(() => {});
|
|
2427
|
+
|
|
2428
|
+
const respuesta = await page.evaluate(() => (document.body.innerText || '').trim().substring(0, 500));
|
|
2429
|
+
console.log(` ✓ Validación solicitada. Respuesta: ${respuesta.substring(0, 120)}`);
|
|
2430
|
+
|
|
2431
|
+
return { success: true, respuesta };
|
|
2432
|
+
} catch (err) {
|
|
2433
|
+
return { success: false, error: err.message };
|
|
2434
|
+
} finally {
|
|
2435
|
+
if (browser) await browser.close().catch(() => {});
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
/**
|
|
2440
|
+
* Completa la declaración de cumplimiento de Boleta Electrónica en el portal SII.
|
|
2441
|
+
* Marca los checkboxes de requisitos y rellena el formulario de proveedor.
|
|
2442
|
+
* @param {Object} opts
|
|
2443
|
+
* @param {string} [opts.linkConsulta='www.sii.cl']
|
|
2444
|
+
* @param {string} [opts.rutProveedor='78206276-K']
|
|
2445
|
+
* @param {string} [opts.nombreProveedor='DEVLAS SPA']
|
|
2446
|
+
* @param {string} [opts.correoProveedor='certificacion.sii@devlas.cl']
|
|
2447
|
+
* @returns {Promise<{success: boolean, mensaje?: string, error?: string}>}
|
|
2448
|
+
*/
|
|
2449
|
+
async completarDeclaracionBoletaPortal({
|
|
2450
|
+
linkConsulta = 'www.sii.cl',
|
|
2451
|
+
rutProveedor = '78206276-K',
|
|
2452
|
+
nombreProveedor = 'DEVLAS SPA',
|
|
2453
|
+
correoProveedor = 'certificacion.sii@devlas.cl',
|
|
2454
|
+
} = {}) {
|
|
2455
|
+
const puppeteer = require('puppeteer');
|
|
2456
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
2457
|
+
|
|
2458
|
+
// RUT empresa desde config (ej: "78206276-K")
|
|
2459
|
+
const rutEmpresaRaw = (this.config.emisor?.rut || '').replace(/\./g, '');
|
|
2460
|
+
const [rutEmpNum, rutEmpDvRaw = 'K'] = rutEmpresaRaw.split('-');
|
|
2461
|
+
const rutEmpDv = rutEmpDvRaw.toUpperCase();
|
|
2462
|
+
|
|
2463
|
+
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
2464
|
+
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
2465
|
+
}));
|
|
2466
|
+
|
|
2467
|
+
let browser;
|
|
2468
|
+
try {
|
|
2469
|
+
browser = await puppeteer.launch({
|
|
2470
|
+
headless: true,
|
|
2471
|
+
ignoreHTTPSErrors: true,
|
|
2472
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2473
|
+
});
|
|
2474
|
+
const page = await browser.newPage();
|
|
2475
|
+
await page.setCookie(...puppeteerCookies);
|
|
2476
|
+
|
|
2477
|
+
// Capturar alerts/confirms de GWT — sin handler quedan bloqueados
|
|
2478
|
+
let dialogMsg = null;
|
|
2479
|
+
page.on('dialog', async dlg => {
|
|
2480
|
+
dialogMsg = dlg.message();
|
|
2481
|
+
console.log(` → [dialog] ${dialogMsg}`);
|
|
2482
|
+
await dlg.accept();
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
// ── PASOS 1+2: Confirmar Empresa → esperar formulario (con retry) ──
|
|
2486
|
+
// El portal GWT a veces responde con error transitorio ("empresa no autorizada")
|
|
2487
|
+
// que se resuelve recargando la página y reintentando.
|
|
2488
|
+
const MAX_INTENTOS = 3;
|
|
2489
|
+
let checkboxOk = false;
|
|
2490
|
+
let lastDialogMsg = null;
|
|
2491
|
+
for (let intento = 1; intento <= MAX_INTENTOS; intento++) {
|
|
2492
|
+
dialogMsg = null; // resetear entre intentos
|
|
2493
|
+
|
|
2494
|
+
console.log(` → Navegando a certBolElectDteInternet (declaración) [intento ${intento}/${MAX_INTENTOS}]...`);
|
|
2495
|
+
await page.goto('https://www4.sii.cl/certBolElectDteInternet/', {
|
|
2496
|
+
waitUntil: 'networkidle2', timeout: 60000,
|
|
2497
|
+
});
|
|
2498
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
2499
|
+
|
|
2500
|
+
// GWT requiere eventos de teclado reales — NO funciona con .value = ...
|
|
2501
|
+
const rutInput = await page.$('input[maxlength="8"]');
|
|
2502
|
+
const dvInput = await page.$('input[maxlength="1"]');
|
|
2503
|
+
if (!rutInput) throw new Error('certBolElectDteInternet/: no se encontró campo RUT empresa');
|
|
2504
|
+
console.log(` → Ingresando RUT empresa: ${rutEmpresaRaw}`);
|
|
2505
|
+
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutEmpNum);
|
|
2506
|
+
await dvInput.click({ clickCount: 3 }); await dvInput.type(rutEmpDv);
|
|
2507
|
+
|
|
2508
|
+
await page.evaluate(() => {
|
|
2509
|
+
const btn = Array.from(document.querySelectorAll('button'))
|
|
2510
|
+
.find(b => /confirmar empresa/i.test(b.textContent));
|
|
2511
|
+
if (btn) btn.click();
|
|
2512
|
+
});
|
|
2513
|
+
console.log(' → Click "Confirmar Empresa"...');
|
|
2514
|
+
|
|
2515
|
+
// GWT dispara ~8-10 POST /facade EN PARALELO al confirmar.
|
|
2516
|
+
// Esperar red inactiva y luego DOM con checkboxes.
|
|
2517
|
+
await page.waitForNetworkIdle({ idleTime: 800, timeout: 40000 }).catch(() => {});
|
|
2518
|
+
await page.waitForFunction(() => {
|
|
2519
|
+
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2520
|
+
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2521
|
+
|
|
2522
|
+
if (dialogMsg) {
|
|
2523
|
+
// Portal lanzó alert — puede ser transitorio. Guardar y reintentar.
|
|
2524
|
+
lastDialogMsg = dialogMsg;
|
|
2525
|
+
console.log(` ⚠️ Portal respondió con alerta en intento ${intento}: ${dialogMsg.substring(0, 100)}`);
|
|
2526
|
+
if (intento < MAX_INTENTOS) {
|
|
2527
|
+
console.log(' → Recargando y reintentando en 3s...');
|
|
2528
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
// Agotados los intentos con alerta — el SII puede requerir esperar SOK
|
|
2532
|
+
return { success: false, pendingSok: true, error: lastDialogMsg };
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
if (await page.$('input[type="checkbox"]')) {
|
|
2536
|
+
checkboxOk = true;
|
|
2537
|
+
break;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
console.log(` ⚠️ Formulario no cargó en intento ${intento}${intento < MAX_INTENTOS ? ` — reintentando...` : ''}`);
|
|
2541
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
if (!checkboxOk) {
|
|
2545
|
+
throw new Error('Formulario de declaración no cargó tras confirmar empresa (3 intentos)');
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const totalCbs = await page.evaluate(() => {
|
|
2549
|
+
const todos = Array.from(document.querySelectorAll('input[type="checkbox"]'));
|
|
2550
|
+
todos.forEach(cb => { if (!cb.checked) cb.click(); });
|
|
2551
|
+
return todos.length;
|
|
2552
|
+
});
|
|
2553
|
+
console.log(` → ${totalCbs} checkbox(es) marcados`);
|
|
2554
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2555
|
+
|
|
2556
|
+
// ── PASO 3: Rellenar campos proveedor software ────────────────
|
|
2557
|
+
// GWT requiere page.type() real — NOT funciona con .value + dispatchEvent
|
|
2558
|
+
const fillByLabel = async (labelFragment, value) => {
|
|
2559
|
+
const handle = await page.evaluateHandle((frag) => {
|
|
2560
|
+
for (const td of document.querySelectorAll('td')) {
|
|
2561
|
+
if (!td.textContent.includes(frag)) continue;
|
|
2562
|
+
const next = td.nextElementSibling;
|
|
2563
|
+
if (!next) continue;
|
|
2564
|
+
const inp = next.querySelector('input[type="text"]');
|
|
2565
|
+
if (inp) return inp;
|
|
2566
|
+
}
|
|
2567
|
+
return null;
|
|
2568
|
+
}, labelFragment);
|
|
2569
|
+
const elem = handle && await handle.asElement();
|
|
2570
|
+
if (!elem) return false;
|
|
2571
|
+
await elem.click({ clickCount: 3 });
|
|
2572
|
+
await elem.type(value);
|
|
2573
|
+
return true;
|
|
2574
|
+
};
|
|
2575
|
+
|
|
2576
|
+
// Link de Consulta (maxlength=100)
|
|
2577
|
+
const linkOk = await fillByLabel('Link de Consulta', linkConsulta);
|
|
2578
|
+
if (!linkOk) {
|
|
2579
|
+
const inp = await page.$('input[type="text"][maxlength="100"]');
|
|
2580
|
+
if (inp) { await inp.click({ clickCount: 3 }); await inp.type(linkConsulta); }
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// RUT Proveedor — dos inputs (num + DV) en la fila "Rut Proveedor"
|
|
2584
|
+
const [rutProvNum, rutProvDvRaw] = rutProveedor.replace(/\./g, '').split('-');
|
|
2585
|
+
const rutProvDv = (rutProvDvRaw || 'K').toUpperCase();
|
|
2586
|
+
const rutProvH = await page.evaluateHandle(() => {
|
|
2587
|
+
for (const td of document.querySelectorAll('td')) {
|
|
2588
|
+
if (!td.textContent.includes('Rut Proveedor')) continue;
|
|
2589
|
+
const next = td.nextElementSibling;
|
|
2590
|
+
if (next) { const ins = next.querySelectorAll('input[type="text"]'); if (ins[0]) return ins[0]; }
|
|
2591
|
+
}
|
|
2592
|
+
return null;
|
|
2593
|
+
});
|
|
2594
|
+
const dvProvH = await page.evaluateHandle(() => {
|
|
2595
|
+
for (const td of document.querySelectorAll('td')) {
|
|
2596
|
+
if (!td.textContent.includes('Rut Proveedor')) continue;
|
|
2597
|
+
const next = td.nextElementSibling;
|
|
2598
|
+
if (next) { const ins = next.querySelectorAll('input[type="text"]'); if (ins[1]) return ins[1]; }
|
|
2599
|
+
}
|
|
2600
|
+
return null;
|
|
2601
|
+
});
|
|
2602
|
+
if (rutProvH && await rutProvH.asElement()) { const e = rutProvH.asElement(); await e.click({ clickCount: 3 }); await e.type(rutProvNum); }
|
|
2603
|
+
if (dvProvH && await dvProvH.asElement()) { const e = dvProvH.asElement(); await e.click({ clickCount: 3 }); await e.type(rutProvDv); }
|
|
2604
|
+
|
|
2605
|
+
// Nombre Proveedor
|
|
2606
|
+
await fillByLabel('Nombre Proveedor', nombreProveedor);
|
|
2607
|
+
|
|
2608
|
+
// Correo Proveedor Software
|
|
2609
|
+
await fillByLabel('Correo electrónico Proveedor', correoProveedor);
|
|
2610
|
+
|
|
2611
|
+
// Captura pre-submit
|
|
2612
|
+
if (this.config.debugDir) {
|
|
2613
|
+
await page.screenshot({ path: path.join(this.config.debugDir, 'boleta-declaracion-pre-submit.png'), fullPage: true }).catch(() => {});
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// ── PASO 4: Click "Grabar Declaración" ───────────────────────
|
|
2617
|
+
const submitOk = await page.evaluate(() => {
|
|
2618
|
+
const btn = Array.from(document.querySelectorAll('button'))
|
|
2619
|
+
.find(b => /grabar declaraci/i.test(b.textContent));
|
|
2620
|
+
if (btn) { btn.click(); return true; }
|
|
2621
|
+
return false;
|
|
2622
|
+
});
|
|
2623
|
+
if (!submitOk) throw new Error('No se encontró botón "Grabar Declaración" en certBolElectDteInternet/');
|
|
2624
|
+
console.log(' → Click "Grabar Declaración"...');
|
|
2625
|
+
|
|
2626
|
+
// Esperar confirmación del SII
|
|
2627
|
+
await page.waitForFunction(() => {
|
|
2628
|
+
const t = (document.body.textContent || '').toUpperCase();
|
|
2629
|
+
return t.includes('GRABADA') || t.includes('GRABADO') || t.includes('COMPLETADA') ||
|
|
2630
|
+
t.includes('EXITOSA') || t.includes('GUARDADA') || t.includes('REGISTRADA');
|
|
2631
|
+
}, { timeout: 20000, polling: 1000 }).catch(() => {});
|
|
2632
|
+
|
|
2633
|
+
const msgFinal = await page.evaluate(() => (document.body.textContent || '').trim().substring(0, 300));
|
|
2634
|
+
console.log(` ✓ Declaración completada. Respuesta: ${msgFinal.substring(0, 150)}`);
|
|
2635
|
+
|
|
2636
|
+
if (this.config.debugDir) {
|
|
2637
|
+
await page.screenshot({ path: path.join(this.config.debugDir, 'boleta-declaracion-post-submit.png'), fullPage: true }).catch(() => {});
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
return { success: true, mensaje: msgFinal };
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
return { success: false, error: err.message };
|
|
2643
|
+
} finally {
|
|
2644
|
+
if (browser) await browser.close().catch(() => {});
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2649
|
+
// Helpers privados
|
|
2650
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2651
|
+
|
|
2652
|
+
_getFechaHoy() {
|
|
2653
|
+
const now = new Date();
|
|
2654
|
+
return `${String(now.getDate()).padStart(2, '0')}-${String(now.getMonth() + 1).padStart(2, '0')}-${now.getFullYear()}`;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
module.exports = CertRunner;
|