@devlas/dte-sii 2.5.11 → 2.5.13

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.
@@ -348,6 +348,16 @@ class CertRunner {
348
348
  const result = await this.siiCert.declararAvance({ sets });
349
349
  lastResult = result;
350
350
 
351
+ // Guardar el form pe_avance2 (antes del POST) para debug
352
+ if (result.formHtml) {
353
+ fs.writeFileSync(
354
+ path.join(this.debugDir, `${debugPrefix}-pe_avance2-${intento}.html`),
355
+ result.formHtml,
356
+ 'utf8'
357
+ );
358
+ }
359
+
360
+ // Guardar la respuesta pe_avance3 (despues del POST) para debug
351
361
  if (result.rawHtml) {
352
362
  fs.writeFileSync(
353
363
  path.join(this.debugDir, `${debugPrefix}-${intento}.html`),
@@ -370,6 +380,11 @@ class CertRunner {
370
380
  if (noProcessedError && intento < maxIntentos) {
371
381
  console.log(` ⏳ SII aún procesando, reintentando en ${intervalo / 1000}s...`);
372
382
  await sleep(intervalo);
383
+ } else if (result.verificado === false && intento < maxIntentos) {
384
+ // Verificación post-declaración falló: los campos quedaron vacíos en el portal
385
+ console.log(` ⚠️ Verificación fallida: ${result.error}`);
386
+ console.log(` 🔄 Reintentando declaración en ${intervalo / 1000}s...`);
387
+ await sleep(intervalo);
373
388
  } else if (!result.success) {
374
389
  console.log(` ⚠️ Error declarando ${label}: ${result.error || 'desconocido'}`);
375
390
  break;
@@ -460,6 +475,7 @@ class CertRunner {
460
475
  * @param {Object} [options] - Opciones
461
476
  * @param {Object} [options.setBasicoResult] - Resultado del SetBasico
462
477
  * @param {Object} [options.setGuiaResult] - Resultado del SetGuia
478
+ * @param {Object} [options.setsResultados] - Track IDs de los sets (basico/guia/exenta/compra) para incluirlos en la declaración conjunta
463
479
  * @returns {Promise<Object>} Resultado con todos los libros
464
480
  */
465
481
  async ejecutarFase4Libros(options = {}) {
@@ -471,12 +487,49 @@ class CertRunner {
471
487
  const resultados = {};
472
488
  const errores = [];
473
489
 
490
+ // Verificar cuáles libros ya están REVISADO CONFORME en el portal (no re-enviar)
491
+ let _estadoActual = {};
492
+ try {
493
+ const _consultaPrevia = await this.siiCert.consultarEstadoSets();
494
+ if (_consultaPrevia.success) _estadoActual = _consultaPrevia.estadoSets || {};
495
+ const _yaConformes = Object.entries(_estadoActual)
496
+ .filter(([k, v]) => k.toUpperCase().includes('LIBRO') && v === 'REVISADO CONFORME')
497
+ .map(([k]) => k);
498
+ if (_yaConformes.length) {
499
+ console.log(` ℹ️ Ya en REVISADO CONFORME (se omitirán): ${_yaConformes.join(', ')}`);
500
+ }
501
+ } catch (_e) { /* ignorar error de consulta previa */ }
502
+
503
+ const _estaConforme = (nombre) => {
504
+ const nombreUpper = nombre.toUpperCase();
505
+ const e = Object.entries(_estadoActual).find(([k]) => {
506
+ const ku = k.toUpperCase();
507
+ if (nombreUpper === 'LIBRO DE COMPRAS') return ku.includes('LIBRO DE COMPRAS') && !ku.includes('EXENTOS');
508
+ return ku.includes(nombreUpper);
509
+ });
510
+ return e && e[1] === 'REVISADO CONFORME';
511
+ };
512
+
513
+ // Helper: guardar resultados parciales a disco tras cada envío exitoso
514
+ const _resultadosLibrosPath = path.join(this.debugDir, 'resultados-libros.json');
515
+ const _guardarResultadosParciales = () => {
516
+ try {
517
+ fs.writeFileSync(_resultadosLibrosPath, JSON.stringify(resultados, null, 2));
518
+ } catch (_e) { /* ignorar */ }
519
+ };
520
+
474
521
  try {
475
522
  // 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}`);
523
+ if (_estaConforme('LIBRO DE COMPRAS')) {
524
+ console.log('\n✅ Libro de Compras ya está REVISADO CONFORME — omitiendo');
525
+ } else {
526
+ console.log('\n📖 Enviando Libro de Compras...');
527
+ resultados.libroCompras = await this.ejecutarLibroCompras(options);
528
+ if (!resultados.libroCompras.success) {
529
+ errores.push(`Libro Compras: ${resultados.libroCompras.error}`);
530
+ } else {
531
+ _guardarResultadosParciales();
532
+ }
480
533
  }
481
534
  } catch (e) {
482
535
  errores.push(`Libro Compras: ${e.message}`);
@@ -484,10 +537,16 @@ class CertRunner {
484
537
 
485
538
  try {
486
539
  // 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}`);
540
+ if (_estaConforme('LIBRO DE VENTAS')) {
541
+ console.log('\n✅ Libro de Ventas ya está REVISADO CONFORME — omitiendo');
542
+ } else {
543
+ console.log('\n📖 Enviando Libro de Ventas...');
544
+ resultados.libroVentas = await this.ejecutarLibroVentas(options);
545
+ if (!resultados.libroVentas.success) {
546
+ errores.push(`Libro Ventas: ${resultados.libroVentas.error}`);
547
+ } else {
548
+ _guardarResultadosParciales();
549
+ }
491
550
  }
492
551
  } catch (e) {
493
552
  errores.push(`Libro Ventas: ${e.message}`);
@@ -495,22 +554,34 @@ class CertRunner {
495
554
 
496
555
  try {
497
556
  // 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}`);
557
+ if (_estaConforme('LIBRO DE GUIAS')) {
558
+ console.log('\n✅ Libro de Guías ya está REVISADO CONFORME — omitiendo');
559
+ } else {
560
+ console.log('\n📖 Enviando Libro de Guías...');
561
+ resultados.libroGuias = await this.ejecutarLibroGuias(options);
562
+ if (!resultados.libroGuias.success) {
563
+ errores.push(`Libro Guías: ${resultados.libroGuias.error}`);
564
+ } else {
565
+ _guardarResultadosParciales();
566
+ }
502
567
  }
503
568
  } catch (e) {
504
569
  errores.push(`Libro Guías: ${e.message}`);
505
570
  }
506
571
 
507
- // 4. Libro de Compras para Exentos (solo si el SII lo entregó)
572
+ // 4. Libro de Compras para Exentos (solo si el SII lo entregó y no está ya aprobado)
508
573
  if (this._estructuras?.libroComprasExentos) {
509
574
  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}`);
575
+ if (_estaConforme('LIBRO DE COMPRAS PARA EXENTOS')) {
576
+ console.log('\n✅ Libro Compras Exentos ya está REVISADO CONFORME — omitiendo');
577
+ } else {
578
+ console.log('\n📖 Enviando Libro de Compras para Exentos...');
579
+ resultados.libroComprasExentos = await this.ejecutarLibroComprasExentos(options);
580
+ if (!resultados.libroComprasExentos.success) {
581
+ errores.push(`Libro Compras Exentos: ${resultados.libroComprasExentos.error}`);
582
+ } else {
583
+ _guardarResultadosParciales();
584
+ }
514
585
  }
515
586
  } catch (e) {
516
587
  errores.push(`Libro Compras Exentos: ${e.message}`);
@@ -518,18 +589,65 @@ class CertRunner {
518
589
  }
519
590
 
520
591
  // Contar libros obligatorios (ventas + compras + guías)
592
+ // Un libro cuenta como OK si fue enviado exitosamente O si ya era REVISADO CONFORME (omitido)
521
593
  const librosObligatorios = ['libroVentas', 'libroCompras', 'libroGuias'];
522
- const librosEnviados = librosObligatorios.filter(k => resultados[k]?.success).length;
594
+ const librosEnviados = librosObligatorios.filter(k => {
595
+ if (resultados[k]?.success) return true;
596
+ // Mapeo clave→nombre para consultar _estaConforme
597
+ const nombreMap = { libroVentas: 'LIBRO DE VENTAS', libroCompras: 'LIBRO DE COMPRAS', libroGuias: 'LIBRO DE GUIAS' };
598
+ return _estaConforme(nombreMap[k]);
599
+ }).length;
523
600
 
524
601
  if (librosEnviados === 3) {
525
- // 4. Declarar los libros
602
+ // 4. Declarar los libros (incluyendo sets para evitar que pe_avance3 los resetee)
526
603
  console.log('\n📝 Declarando libros...');
527
604
  try {
528
- const declaracion = await this.declararLibros();
605
+ const declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
529
606
  resultados.declaracion = declaracion;
530
607
 
531
608
  if (declaracion.success) {
532
- console.log('\n✅ FASE 4 COMPLETADA: Todos los libros enviados y declarados');
609
+ console.log('\n✅ Libros declarados esperando revisión del SII...');
610
+
611
+ // Polling real: esperar hasta REVISADO CONFORME o S25 (máx 20 intentos x 15s = 5 min)
612
+ const _librosAEsperar = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
613
+ if (this._estructuras?.libroComprasExentos) _librosAEsperar.push('LIBRO DE COMPRAS PARA EXENTOS');
614
+ // Solo esperar los libros que realmente enviamos (no los que ya eran REVISADO CONFORME)
615
+ const _librosEnviados = _librosAEsperar.filter(n => !_estaConforme(n));
616
+
617
+ console.log(`\n⏳ Esperando aprobación del SII para: ${_librosEnviados.join(', ')}`);
618
+ let _aprobados = false;
619
+ for (let _i = 0; _i < 20; _i++) {
620
+ await sleep(15000);
621
+ const _poll = await this.siiCert.consultarEstadoSets();
622
+ if (!_poll.success) continue;
623
+ const _ss = _poll.estadoSets || {};
624
+ const _info = Object.entries(_ss)
625
+ .filter(([k]) => k.toUpperCase().includes('LIBRO'))
626
+ .map(([k, v]) => `${k.trim()}: ${v}`);
627
+ if (_info.length) console.log(` 🔄 Intento ${_i + 1}/20: ${_info.join(' | ')}`);
628
+
629
+ const _todosOk = _librosEnviados.every(nombre => {
630
+ const e = Object.entries(_ss).find(([k]) => k.toUpperCase().includes(nombre));
631
+ return e && (e[1] === 'REVISADO CONFORME' || e[1] === 'S25');
632
+ });
633
+ const _algunError = _librosEnviados.some(nombre => {
634
+ const e = Object.entries(_ss).find(([k]) => k.toUpperCase().includes(nombre));
635
+ return e && (e[1] === 'LNC' || e[1] === 'LRH' || e[1].includes('RECHAZADO') || e[1].includes('ERROR'));
636
+ });
637
+
638
+ if (_todosOk) {
639
+ console.log('\n🎉 ¡LIBROS APROBADOS POR EL SII!');
640
+ _aprobados = true;
641
+ break;
642
+ }
643
+ if (_algunError) {
644
+ console.log('\n❌ Hay libros rechazados. Revisar emails del SII.');
645
+ break;
646
+ }
647
+ }
648
+ if (!_aprobados) {
649
+ console.log('\n⚠️ Timeout (5 min). El SII aún no responde. Verifica con --avance más tarde.');
650
+ }
533
651
  } else {
534
652
  console.log(`\n⚠️ Libros enviados pero declaración con error: ${declaracion.error}`);
535
653
  }
@@ -563,6 +681,12 @@ class CertRunner {
563
681
  const sets = {};
564
682
 
565
683
  const mapping = {
684
+ // Sets — incluirlos para que pe_avance3 no los resetee a S01
685
+ basico: 'setBasico',
686
+ guia: 'setGuiaDespacho',
687
+ exenta: 'setFacturaExenta',
688
+ compra: 'setFacturaCompra',
689
+ // Libros
566
690
  libroVentas: 'libroVentas',
567
691
  libroCompras: 'libroCompras',
568
692
  libroGuias: 'libroGuias',
@@ -1217,6 +1341,14 @@ class CertRunner {
1217
1341
  return { success: true, etapa: 'INTERCAMBIO' };
1218
1342
  }
1219
1343
 
1344
+ // ✅ TAMBIÉN: Etapas que vienen DESPUÉS de INTERCAMBIO (simulación + intercambio ya completos)
1345
+ const ETAPAS_POST_INTERCAMBIO = ['DOCUMENTOS IMPRESOS', 'MUESTRAS IMPRESAS', 'BOLETA', 'AUTORIZADO', 'COMPLETADO'];
1346
+ if (avance.etapaActual && ETAPAS_POST_INTERCAMBIO.some(e => avance.etapaActual.toUpperCase().includes(e))) {
1347
+ console.log(` 📍 Etapa actual: ${avance.etapaActual}`);
1348
+ console.log('\n 🎉 ¡SIMULACIÓN + INTERCAMBIO COMPLETADOS! Empresa en etapa: ' + avance.etapaActual);
1349
+ return { success: true, etapa: avance.etapaActual, postIntercambio: true };
1350
+ }
1351
+
1220
1352
  // ✅ SEGUNDO: Verificar indicador de formulario de confirmación (simulación aprobada pendiente confirmar)
1221
1353
  if (avance.simulacionAprobadaIndicador) {
1222
1354
  console.log(` ✅ Formulario de confirmación detectado`);
@@ -1,131 +1,131 @@
1
- // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
- // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
- /**
4
- * ConfigLoader - Carga configuración de certificación desde .env
5
- *
6
- * Centraliza la carga de configuración para que los runners (CLI)
7
- * no necesiten módulos intermedios.
8
- *
9
- * @module dte-sii/cert/ConfigLoader
10
- */
11
-
12
- const path = require('path');
13
- const fs = require('fs');
14
-
15
- /**
16
- * Carga configuración desde .env y retorna objetos estructurados
17
- *
18
- * @param {Object} options
19
- * @param {string} [options.envPath] - Ruta al archivo .env (por defecto busca en static/nodejs/.env)
20
- * @param {string} [options.baseDir] - Directorio base para resolver rutas relativas
21
- * @returns {Object} Configuración estructurada { EMISOR, RECEPTOR, CERT_PATH, CERT_PASS, AMBIENTE, BASE_DIR, ... }
22
- */
23
- function loadConfig(options = {}) {
24
- // Determinar baseDir
25
- const baseDir = options.baseDir || path.resolve(__dirname, '..', '..', '..');
26
-
27
- // Cargar .env (opcional — las vars pueden venir de process.env inyectadas por el proceso padre)
28
- const envPath = options.envPath || path.join(baseDir, '.env');
29
-
30
- if (fs.existsSync(envPath)) {
31
- require('dotenv').config({ path: envPath });
32
- } else {
33
- console.warn(`[ConfigLoader] .env no encontrado en ${envPath} — usando variables de entorno del proceso.`);
34
- }
35
-
36
- // Resolver rutas relativas
37
- const resolvePath = (filePath) => {
38
- if (!filePath) return null;
39
- return path.isAbsolute(filePath) ? filePath : path.resolve(baseDir, filePath);
40
- };
41
-
42
- // Validar variables requeridas (solo CERT_PATH es estrictamente necesario;
43
- // el resto puede no estar si los datos vienen del portal SII vía getEmisorFromPortal)
44
- if (!process.env.CERT_PATH) {
45
- throw new Error('Variable de entorno faltante: CERT_PATH');
46
- }
47
-
48
- // Construir objetos de configuración
49
- const AMBIENTE = process.env.SII_AMBIENTE || 'certificacion';
50
-
51
- const fch_resol = process.env.FECHA_RESOLUCION || new Date().toISOString().slice(0, 10);
52
- const nro_resol = parseInt(process.env.NRO_RESOLUCION || '0', 10);
53
-
54
- console.log(`[ConfigLoader] ✅ Resolución: NroResol=${nro_resol} FchResol=${fch_resol} (AMBIENTE=${AMBIENTE})`);
55
-
56
- const EMISOR = {
57
- rut: process.env.EMISOR_RUT,
58
- razon_social: process.env.EMISOR_RAZON_SOCIAL,
59
- giro: process.env.EMISOR_GIRO || 'ACTIVIDADES DE PROGRAMACION INFORMATICA',
60
- acteco: process.env.EMISOR_ACTECO || '620200',
61
- direccion: process.env.EMISOR_DIRECCION || '',
62
- comuna: process.env.EMISOR_COMUNA || 'Santiago',
63
- ciudad: process.env.EMISOR_CIUDAD || 'Santiago',
64
- fch_resol,
65
- nro_resol,
66
- };
67
-
68
- const RECEPTOR = {
69
- rut: process.env.RECEPTOR_RUT || '66666666-6',
70
- razon_social: process.env.RECEPTOR_RAZON_SOCIAL || 'CLIENTE TEST',
71
- giro: process.env.RECEPTOR_GIRO || 'ACTIVIDADES VARIAS',
72
- direccion: process.env.RECEPTOR_DIRECCION || 'AVENIDA PRINCIPAL 123',
73
- comuna: process.env.RECEPTOR_COMUNA || 'Santiago',
74
- ciudad: process.env.RECEPTOR_CIUDAD || 'Santiago',
75
- };
76
-
77
- const CERT_PATH = resolvePath(process.env.CERT_PATH);
78
- const CERT_PASS = process.env.CERT_PASS || '';
79
-
80
- // Configuración SII
81
- const SII_CONFIG = {
82
- sendDelayMs: parseInt(process.env.SII_SEND_DELAY_MS || '8000', 10),
83
- sendRetries: parseInt(process.env.SII_SEND_RETRIES || '6', 10),
84
- cafRetries: parseInt(process.env.SII_CAF_RETRIES || '3', 10),
85
- reuseFolios: process.env.SII_REUSE_FOLIOS === 'true',
86
- };
87
-
88
- // Enviador (quien firma/envía)
89
- const ENVIADOR = {
90
- rut: process.env.ENVIADOR_RUT || EMISOR.rut,
91
- };
92
-
93
- return {
94
- EMISOR,
95
- RECEPTOR,
96
- CERT_PATH,
97
- CERT_PASS,
98
- AMBIENTE,
99
- BASE_DIR: baseDir,
100
- SII_CONFIG,
101
- ENVIADOR,
102
-
103
- // Helper para crear config de CertRunner
104
- toCertRunnerConfig() {
105
- return {
106
- certificado: { path: CERT_PATH, password: CERT_PASS },
107
- emisor: EMISOR,
108
- receptor: RECEPTOR,
109
- ambiente: AMBIENTE,
110
- resolucion: { fecha: EMISOR.fch_resol, numero: EMISOR.nro_resol },
111
- baseDir: baseDir,
112
- debugDir: path.join(baseDir, 'debug', 'cert-v2'),
113
- };
114
- },
115
- };
116
- }
117
-
118
- /**
119
- * Imprime banner de sección para CLI
120
- * @param {string} titulo
121
- */
122
- function printBanner(titulo) {
123
- console.log('\n' + '═'.repeat(60));
124
- console.log(` ${titulo}`);
125
- console.log('═'.repeat(60));
126
- }
127
-
128
- module.exports = {
129
- loadConfig,
130
- printBanner,
131
- };
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * ConfigLoader - Carga configuración de certificación desde .env
5
+ *
6
+ * Centraliza la carga de configuración para que los runners (CLI)
7
+ * no necesiten módulos intermedios.
8
+ *
9
+ * @module dte-sii/cert/ConfigLoader
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ /**
16
+ * Carga configuración desde .env y retorna objetos estructurados
17
+ *
18
+ * @param {Object} options
19
+ * @param {string} [options.envPath] - Ruta al archivo .env (por defecto busca en static/nodejs/.env)
20
+ * @param {string} [options.baseDir] - Directorio base para resolver rutas relativas
21
+ * @returns {Object} Configuración estructurada { EMISOR, RECEPTOR, CERT_PATH, CERT_PASS, AMBIENTE, BASE_DIR, ... }
22
+ */
23
+ function loadConfig(options = {}) {
24
+ // Determinar baseDir
25
+ const baseDir = options.baseDir || path.resolve(__dirname, '..', '..', '..');
26
+
27
+ // Cargar .env (opcional — las vars pueden venir de process.env inyectadas por el proceso padre)
28
+ const envPath = options.envPath || path.join(baseDir, '.env');
29
+
30
+ if (fs.existsSync(envPath)) {
31
+ require('dotenv').config({ path: envPath });
32
+ } else {
33
+ console.warn(`[ConfigLoader] .env no encontrado en ${envPath} — usando variables de entorno del proceso.`);
34
+ }
35
+
36
+ // Resolver rutas relativas
37
+ const resolvePath = (filePath) => {
38
+ if (!filePath) return null;
39
+ return path.isAbsolute(filePath) ? filePath : path.resolve(baseDir, filePath);
40
+ };
41
+
42
+ // Validar variables requeridas (solo CERT_PATH es estrictamente necesario;
43
+ // el resto puede no estar si los datos vienen del portal SII vía getEmisorFromPortal)
44
+ if (!process.env.CERT_PATH) {
45
+ throw new Error('Variable de entorno faltante: CERT_PATH');
46
+ }
47
+
48
+ // Construir objetos de configuración
49
+ const AMBIENTE = process.env.SII_AMBIENTE || 'certificacion';
50
+
51
+ const fch_resol = process.env.FECHA_RESOLUCION || new Date().toISOString().slice(0, 10);
52
+ const nro_resol = parseInt(process.env.NRO_RESOLUCION || '0', 10);
53
+
54
+ console.log(`[ConfigLoader] ✅ Resolución: NroResol=${nro_resol} FchResol=${fch_resol} (AMBIENTE=${AMBIENTE})`);
55
+
56
+ const EMISOR = {
57
+ rut: process.env.EMISOR_RUT,
58
+ razon_social: process.env.EMISOR_RAZON_SOCIAL,
59
+ giro: process.env.EMISOR_GIRO || 'ACTIVIDADES DE PROGRAMACION INFORMATICA',
60
+ acteco: process.env.EMISOR_ACTECO || '620200',
61
+ direccion: process.env.EMISOR_DIRECCION || '',
62
+ comuna: process.env.EMISOR_COMUNA || 'Santiago',
63
+ ciudad: process.env.EMISOR_CIUDAD || 'Santiago',
64
+ fch_resol,
65
+ nro_resol,
66
+ };
67
+
68
+ const RECEPTOR = {
69
+ rut: process.env.RECEPTOR_RUT || '66666666-6',
70
+ razon_social: process.env.RECEPTOR_RAZON_SOCIAL || 'CLIENTE TEST',
71
+ giro: process.env.RECEPTOR_GIRO || 'ACTIVIDADES VARIAS',
72
+ direccion: process.env.RECEPTOR_DIRECCION || 'AVENIDA PRINCIPAL 123',
73
+ comuna: process.env.RECEPTOR_COMUNA || 'Santiago',
74
+ ciudad: process.env.RECEPTOR_CIUDAD || 'Santiago',
75
+ };
76
+
77
+ const CERT_PATH = resolvePath(process.env.CERT_PATH);
78
+ const CERT_PASS = process.env.CERT_PASS || '';
79
+
80
+ // Configuración SII
81
+ const SII_CONFIG = {
82
+ sendDelayMs: parseInt(process.env.SII_SEND_DELAY_MS || '8000', 10),
83
+ sendRetries: parseInt(process.env.SII_SEND_RETRIES || '6', 10),
84
+ cafRetries: parseInt(process.env.SII_CAF_RETRIES || '3', 10),
85
+ reuseFolios: process.env.SII_REUSE_FOLIOS === 'true',
86
+ };
87
+
88
+ // Enviador (quien firma/envía)
89
+ const ENVIADOR = {
90
+ rut: process.env.ENVIADOR_RUT || EMISOR.rut,
91
+ };
92
+
93
+ return {
94
+ EMISOR,
95
+ RECEPTOR,
96
+ CERT_PATH,
97
+ CERT_PASS,
98
+ AMBIENTE,
99
+ BASE_DIR: baseDir,
100
+ SII_CONFIG,
101
+ ENVIADOR,
102
+
103
+ // Helper para crear config de CertRunner
104
+ toCertRunnerConfig() {
105
+ return {
106
+ certificado: { path: CERT_PATH, password: CERT_PASS },
107
+ emisor: EMISOR,
108
+ receptor: RECEPTOR,
109
+ ambiente: AMBIENTE,
110
+ resolucion: { fecha: EMISOR.fch_resol, numero: EMISOR.nro_resol },
111
+ baseDir: baseDir,
112
+ debugDir: path.join(baseDir, 'debug', 'cert-v2'),
113
+ };
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Imprime banner de sección para CLI
120
+ * @param {string} titulo
121
+ */
122
+ function printBanner(titulo) {
123
+ console.log('\n' + '═'.repeat(60));
124
+ console.log(` ${titulo}`);
125
+ console.log('═'.repeat(60));
126
+ }
127
+
128
+ module.exports = {
129
+ loadConfig,
130
+ printBanner,
131
+ };