@devlas/dte-sii 2.9.8 → 2.11.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/EnviadorSII.js +100 -64
- package/LICENSE +27 -27
- package/LibroCompraVenta.js +141 -17
- package/LibroGuia.js +36 -25
- package/SiiCertificacion.js +85 -3
- package/SiiPortalAuth.js +85 -19
- package/WsReclamo.js +434 -434
- package/cert/BoletaCert.js +41 -4
- package/cert/CertRunner.js +1123 -1209
- package/cert/LibroCompras.js +3 -2
- package/cert/LibroGuias.js +2 -1
- package/cert/LibroVentas.js +2 -1
- package/cert/MuestrasImpresas.js +831 -131
- package/cert/comunaOficina.js +458 -458
- package/cert/index.js +122 -122
- package/cert/types.js +328 -328
- package/package.json +2 -3
- package/test-muestras.js +180 -0
- package/test-qdetestlibro.js +174 -0
- package/utils/progress.js +4 -0
- package/utils/browser.js +0 -79
package/LibroGuia.js
CHANGED
|
@@ -40,8 +40,10 @@ class LibroGuia extends LibroBase {
|
|
|
40
40
|
throw new Error('Debe establecer la carátula antes de generar');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const tipoEnvio = String(this.caratula?.TipoEnvio || '').toUpperCase();
|
|
43
44
|
const caratulaXml = this._renderCaratula();
|
|
44
45
|
const resumenXml = this._renderResumen();
|
|
46
|
+
// AJUSTE con documentos: Detalle + ResumenSegmento + ResumenPeriodo
|
|
45
47
|
const detalleXml = this._renderDetalle();
|
|
46
48
|
|
|
47
49
|
const schemaLoc = 'http://www.sii.cl/SiiDte LibroGuia_v10.xsd';
|
|
@@ -87,7 +89,9 @@ class LibroGuia extends LibroBase {
|
|
|
87
89
|
_renderResumen() {
|
|
88
90
|
const resumen = this._buildResumenPeriodo();
|
|
89
91
|
if (!resumen) return '';
|
|
90
|
-
|
|
92
|
+
|
|
93
|
+
const tipoEnvio = String(this.caratula?.TipoEnvio || '').toUpperCase();
|
|
94
|
+
|
|
91
95
|
const order = [
|
|
92
96
|
'TotFolAnulado',
|
|
93
97
|
'TotGuiaAnulada',
|
|
@@ -95,30 +99,37 @@ class LibroGuia extends LibroBase {
|
|
|
95
99
|
'TotMntGuiaVta',
|
|
96
100
|
'TotTraslado',
|
|
97
101
|
];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
102
|
+
|
|
103
|
+
const _renderSection = (tag) => {
|
|
104
|
+
let xml = `<${tag}>`;
|
|
105
|
+
order.forEach((key) => {
|
|
106
|
+
const value = resumen[key];
|
|
107
|
+
if (value === undefined || value === null || value === '' || value === false) return;
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
value.forEach((item) => {
|
|
110
|
+
if (item && typeof item === 'object') {
|
|
111
|
+
xml += `<${key}>`;
|
|
112
|
+
Object.keys(item).forEach((itemKey) => {
|
|
113
|
+
if (item[itemKey] !== undefined && item[itemKey] !== null && item[itemKey] !== '') {
|
|
114
|
+
xml += `<${itemKey}>${this._escapeXmlText(String(item[itemKey]))}</${itemKey}>`;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
xml += `</${key}>`;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
xml += `<${key}>${this._escapeXmlText(String(value))}</${key}>`;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
xml += `</${tag}>`;
|
|
125
|
+
return xml;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// AJUSTE con documentos: ResumenSegmento (delta) + ResumenPeriodo (acumulado)
|
|
129
|
+
if (tipoEnvio === 'AJUSTE' && this.detalle.length > 0) {
|
|
130
|
+
return _renderSection('ResumenSegmento') + _renderSection('ResumenPeriodo');
|
|
131
|
+
}
|
|
132
|
+
return _renderSection('ResumenPeriodo');
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
_renderDetalle() {
|
package/SiiCertificacion.js
CHANGED
|
@@ -458,6 +458,53 @@ class SiiCertificacion {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Consulta los libros IECV ya enviados al SII para un año dado.
|
|
463
|
+
* Usa el portal QEstLibro (DTEauth?7) para determinar si cada período tiene
|
|
464
|
+
* un envío MENSUAL (TOTAL/LTC) o RECTIFICA (AJUSTE) por tipo de operación.
|
|
465
|
+
*
|
|
466
|
+
* @param {number|string} year - Año (ej: 2026)
|
|
467
|
+
* @returns {Promise<Object>} Mapa con estructura:
|
|
468
|
+
* { '2026-04': { COMPRA: { periodicidad: 'MENSUAL', estado: 'Recibido', codigo: 'COMPRA-...' }, VENTA: {...} } }
|
|
469
|
+
*/
|
|
470
|
+
async consultarLibrosExistentes(year) {
|
|
471
|
+
const year4 = String(year || new Date().getFullYear());
|
|
472
|
+
|
|
473
|
+
await this.session.ensureSession('/cgi_dte/UPL/DTEauth?7');
|
|
474
|
+
|
|
475
|
+
const url = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
|
|
476
|
+
`?rutCompany=${this.rutEmpresa}&dvCompany=${this.dvEmpresa}` +
|
|
477
|
+
`&TrackId=&year=${year4}&month=00&tipo=TODOS`;
|
|
478
|
+
|
|
479
|
+
const resp = await this.session.request(url);
|
|
480
|
+
const html = resp.body || '';
|
|
481
|
+
|
|
482
|
+
if (html.includes('NO ESTA AUTORIZADO') || html.includes('SESION HA EXPIRADO')) {
|
|
483
|
+
throw new Error('QEstLibro: sesión no autorizada o expirada');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Cada fila tiene 6 <td>: periodo, operacion, estado, tipoLibro, periodicidad, cantidad+link
|
|
487
|
+
// Capturamos los 5 primeros <td> de texto plano + el Codigo de la URL del link
|
|
488
|
+
const result = {};
|
|
489
|
+
const rowRegex =
|
|
490
|
+
/<tr><td[^>]*>([^<]+)<\/td><td[^>]*>([^<]+)<\/td><td[^>]*>([^<]+)<\/td><td[^>]*>([^<]+)<\/td><td[^>]*>([^<]+)<\/td><td[^>]*>[^<]*<a href=QDetEstLibro\?Codigo=([^&"'\s>]+)/gi;
|
|
491
|
+
|
|
492
|
+
let m;
|
|
493
|
+
while ((m = rowRegex.exec(html)) !== null) {
|
|
494
|
+
const periodo = m[1].trim();
|
|
495
|
+
const tipo = m[2].trim().toUpperCase(); // COMPRA | VENTA
|
|
496
|
+
const estado = m[3].trim(); // Recibido | ...
|
|
497
|
+
const periodicidad = m[5].trim().toUpperCase(); // MENSUAL | RECTIFICA
|
|
498
|
+
const codigo = m[6].trim();
|
|
499
|
+
|
|
500
|
+
if (!/^\d{4}-\d{2}$/.test(periodo)) continue;
|
|
501
|
+
if (!result[periodo]) result[periodo] = {};
|
|
502
|
+
result[periodo][tipo] = { periodicidad, estado, codigo };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
|
|
461
508
|
/**
|
|
462
509
|
* Declara avance en una etapa de certificación con TrackIds específicos
|
|
463
510
|
* @param {Object} options - Opciones
|
|
@@ -859,15 +906,50 @@ class SiiCertificacion {
|
|
|
859
906
|
console.log(` [!] No se pudo verificar la declaración: ${verifyErr.message}`);
|
|
860
907
|
}
|
|
861
908
|
|
|
862
|
-
//
|
|
909
|
+
// conErrores: el SII procesó el envío pero encontró errores en el contenido del XML.
|
|
910
|
+
// Detectar en el body de la respuesta de pe_avance3 (no en la re-verificación de pe_avance2).
|
|
911
|
+
const _erroresBody = body || '';
|
|
912
|
+
const _nombresConError = [];
|
|
913
|
+
const _patronesNombre = [
|
|
914
|
+
'LIBRO DE VENTAS', 'LIBRO DE COMPRAS PARA EXENTOS', 'LIBRO DE COMPRAS',
|
|
915
|
+
'LIBRO DE GUIAS', 'SET BASICO', 'SET GUIA DE DESPACHO', 'SET FACTURA EXENTA',
|
|
916
|
+
'SET CASO GENERAL FACTURA COMPRA', 'SET DE SIMULACION',
|
|
917
|
+
];
|
|
918
|
+
for (const _nom of _patronesNombre) {
|
|
919
|
+
// Buscar fila que contenga el nombre Y "ENVIO CON ERRORES O REPAROS".
|
|
920
|
+
// Se usa un lookahead negativo para que "LIBRO DE COMPRAS" no haga match dentro de
|
|
921
|
+
// "LIBRO DE COMPRAS PARA EXENTOS", evitando falsos positivos.
|
|
922
|
+
const _nomEscapado = _nom.replace(/ /g, '\\s+');
|
|
923
|
+
// Si el nombre es un prefijo de otro nombre más largo, agregar un límite tras él.
|
|
924
|
+
const _otrosNombres = _patronesNombre.filter(n => n !== _nom && n.startsWith(_nom));
|
|
925
|
+
let _patronNom = _nomEscapado;
|
|
926
|
+
if (_otrosNombres.length > 0) {
|
|
927
|
+
// Lookahead negativo: no debe seguir el texto diferenciador de los nombres más largos
|
|
928
|
+
const _sufijos = _otrosNombres.map(n => n.slice(_nom.length).replace(/ /g, '\\s+').trim());
|
|
929
|
+
_patronNom += `(?!\\s+(?:${_sufijos.join('|')}))`;
|
|
930
|
+
}
|
|
931
|
+
const _re = new RegExp(_patronNom + '[\\s\\S]{0,200}?ENVIO CON ERRORES O REPAROS', 'i');
|
|
932
|
+
if (_re.test(_erroresBody)) _nombresConError.push(_nom);
|
|
933
|
+
}
|
|
934
|
+
const conErrores = _nombresConError.length > 0;
|
|
935
|
+
if (conErrores) {
|
|
936
|
+
console.log(` [ERR] SII rechazó por ERRORES DE CONTENIDO: ${_nombresConError.join(', ')}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// allRejected: SII rechazó todos los sets declarados — período incorrecto, no tiene sentido reintentar.
|
|
940
|
+
// Solo aplica cuando NO hay errores de contenido explícitos (esos deben manejarse por separado).
|
|
863
941
|
const _declaredCount = Object.keys(fieldMapping).filter(k => sets[k]).length;
|
|
864
|
-
const allRejected = !enRevision && camposVacios.length > 0 && camposVacios.length >= _declaredCount && _declaredCount > 0;
|
|
942
|
+
const allRejected = !enRevision && !conErrores && camposVacios.length > 0 && camposVacios.length >= _declaredCount && _declaredCount > 0;
|
|
865
943
|
|
|
866
944
|
return {
|
|
867
945
|
success: verificado || enRevision,
|
|
868
|
-
error:
|
|
946
|
+
error: conErrores
|
|
947
|
+
? `ENVIO CON ERRORES O REPAROS: ${_nombresConError.join(', ')}`
|
|
948
|
+
: (verificacionError || errorMsg || undefined),
|
|
869
949
|
verificado,
|
|
870
950
|
enRevision,
|
|
951
|
+
conErrores,
|
|
952
|
+
nombresConError: _nombresConError,
|
|
871
953
|
allRejected,
|
|
872
954
|
status: declareResponse.status,
|
|
873
955
|
rawHtml: body,
|
package/SiiPortalAuth.js
CHANGED
|
@@ -83,7 +83,7 @@ class SiiPortalAuth {
|
|
|
83
83
|
if (!pfxBuffer) throw new Error('SiiPortalAuth: pfxBuffer es obligatorio');
|
|
84
84
|
if (pfxPassword === undefined) throw new Error('SiiPortalAuth: pfxPassword es obligatorio');
|
|
85
85
|
|
|
86
|
-
const { certPem, keyPem } = SiiPortalAuth._extractPems(pfxBuffer, pfxPassword);
|
|
86
|
+
const { certPem, keyPem, chainPem } = SiiPortalAuth._extractPems(pfxBuffer, pfxPassword);
|
|
87
87
|
this._certPem = certPem;
|
|
88
88
|
this._keyPem = keyPem;
|
|
89
89
|
// Huella para identificar de qué cert es la sesión cacheada
|
|
@@ -96,7 +96,11 @@ class SiiPortalAuth {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
this._agentePlano = new https.Agent(SII_TLS_OPTS);
|
|
99
|
-
|
|
99
|
+
// chainPem incluye hoja + intermedios + raíz extraídos por forge.
|
|
100
|
+
// Necesario para PFX con cadena completa (ej. IDOK) — SII necesita los
|
|
101
|
+
// intermedios para verificar la firma. tls.createSecureContext({ pfx }) no se
|
|
102
|
+
// usa porque OpenSSL 3 (Node 24) rechaza ciertos formatos PKCS12 modernos.
|
|
103
|
+
this._agenteCert = new https.Agent({ ...SII_TLS_OPTS, cert: chainPem, key: keyPem });
|
|
100
104
|
|
|
101
105
|
_instanceRegistry.set(this._certHash, this);
|
|
102
106
|
}
|
|
@@ -106,24 +110,86 @@ class SiiPortalAuth {
|
|
|
106
110
|
* (node-forge soporta PKCS12 moderno AES-256 que Node nativo rechaza)
|
|
107
111
|
* @private
|
|
108
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* Extrae clave privada y cadena de certificados de un PFX/PKCS12.
|
|
115
|
+
* Soporta: RSA y EC, 1 a N certificados, cadenas desordenadas,
|
|
116
|
+
* pkcs8ShroudedKeyBag y keyBag, algoritmos legacy (RC2, 3DES) y modernos.
|
|
117
|
+
* Usa forge (no tls.createSecureContext) para ser compatible con formatos
|
|
118
|
+
* PKCS12 que OpenSSL 3 rechaza (ej. IDOK con SHA-1 MAC o AES-256 encryption).
|
|
119
|
+
*
|
|
120
|
+
* @returns {{ certPem: string, keyPem: string, chainPem: string }}
|
|
121
|
+
* certPem - solo el certificado hoja (para _certHash)
|
|
122
|
+
* keyPem - clave privada PEM
|
|
123
|
+
* chainPem - cadena completa ordenada: hoja → intermedios → raíz
|
|
124
|
+
*/
|
|
109
125
|
static _extractPems(pfxBuffer, password) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
// ── 1. Parsear PKCS12 ────────────────────────────────────────────────────
|
|
127
|
+
let p12;
|
|
128
|
+
try {
|
|
129
|
+
p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(pfxBuffer.toString('binary')), password);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
throw new Error(`SiiPortalAuth: no se pudo parsear el PFX — ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── 2. Clave privada: probar todos los tipos de key bag ──────────────────
|
|
135
|
+
let keyObj = null;
|
|
136
|
+
for (const oid of [forge.pki.oids.pkcs8ShroudedKeyBag, forge.pki.oids.keyBag]) {
|
|
137
|
+
const bags = p12.getBags({ bagType: oid })[oid] || [];
|
|
138
|
+
const found = bags.find(b => b.key);
|
|
139
|
+
if (found) { keyObj = found.key; break; }
|
|
140
|
+
}
|
|
141
|
+
if (!keyObj) throw new Error('SiiPortalAuth: no se encontró clave privada en el PFX');
|
|
142
|
+
const keyPem = forge.pki.privateKeyToPem(keyObj);
|
|
143
|
+
|
|
144
|
+
// ── 3. Certificados: recopilar todos ─────────────────────────────────────
|
|
145
|
+
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag] || [];
|
|
146
|
+
const allCerts = certBags.map(b => b.cert).filter(Boolean);
|
|
147
|
+
if (!allCerts.length) throw new Error('SiiPortalAuth: no se encontró certificado en el PFX');
|
|
148
|
+
|
|
149
|
+
// ── 4. Identificar el certificado hoja ───────────────────────────────────
|
|
150
|
+
// Estrategia 1: el cert cuya clave pública coincide con la clave privada
|
|
151
|
+
let leafCert = null;
|
|
152
|
+
try {
|
|
153
|
+
// Funciona para RSA y EC mientras forge pueda derivar la clave pública
|
|
154
|
+
const pubFromKey = forge.pki.setRsaPublicKey
|
|
155
|
+
? forge.pki.setRsaPublicKey(keyObj.n, keyObj.e) // RSA
|
|
156
|
+
: null;
|
|
157
|
+
if (pubFromKey) {
|
|
158
|
+
const pubPem = forge.pki.publicKeyToPem(pubFromKey);
|
|
159
|
+
leafCert = allCerts.find(c => forge.pki.publicKeyToPem(c.publicKey) === pubPem) ?? null;
|
|
160
|
+
}
|
|
161
|
+
} catch (_) { /* EC u otro — caer a estrategia 2 */ }
|
|
162
|
+
|
|
163
|
+
// Estrategia 2: el primer cert que NO sea CA (basicConstraints.cA = false/absent)
|
|
164
|
+
if (!leafCert) {
|
|
165
|
+
leafCert = allCerts.find(c => {
|
|
166
|
+
const bc = c.getExtension('basicConstraints');
|
|
167
|
+
return !bc || bc.cA !== true;
|
|
168
|
+
}) ?? allCerts[0];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── 5. Ordenar cadena: hoja → intermedios → raíz ─────────────────────────
|
|
172
|
+
// Construir la cadena siguiendo relaciones issuer→subject
|
|
173
|
+
const remaining = allCerts.filter(c => c !== leafCert);
|
|
174
|
+
const chain = [leafCert];
|
|
175
|
+
let current = leafCert;
|
|
176
|
+
|
|
177
|
+
while (remaining.length > 0) {
|
|
178
|
+
// El siguiente eslabón es el cert cuyo subject coincide con el issuer del actual
|
|
179
|
+
const issuerHash = current.issuer.hash;
|
|
180
|
+
const idx = remaining.findIndex(c => c.subject.hash === issuerHash);
|
|
181
|
+
if (idx < 0) break; // fin de cadena conocida
|
|
182
|
+
current = remaining.splice(idx, 1)[0];
|
|
183
|
+
if (chain.includes(current)) break; // ciclo (self-signed raíz)
|
|
184
|
+
chain.push(current);
|
|
185
|
+
}
|
|
186
|
+
// Appender cualquier cert restante que no pudimos encadenar
|
|
187
|
+
chain.push(...remaining);
|
|
188
|
+
|
|
189
|
+
const certPem = forge.pki.certificateToPem(leafCert);
|
|
190
|
+
const chainPem = chain.map(c => forge.pki.certificateToPem(c)).join('');
|
|
191
|
+
|
|
192
|
+
return { certPem, keyPem, chainPem };
|
|
127
193
|
}
|
|
128
194
|
|
|
129
195
|
// ─── HTTP helpers ────────────────────────────────────────────────────────────
|