@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
package/EnviadorSII.js
ADDED
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* EnviadorSII.js
|
|
5
|
+
*
|
|
6
|
+
* Comunicación con los servicios web del SII para:
|
|
7
|
+
* - Autenticación (semilla/token)
|
|
8
|
+
* - Envío de boletas (API REST)
|
|
9
|
+
* - Envío de DTEs (SOAP/DTEUpload)
|
|
10
|
+
* - Envío de RCOF (SOAP/DTEUpload)
|
|
11
|
+
* - Envío de Libros (SOAP/DTEUpload)
|
|
12
|
+
* - Consulta de estado de envíos
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const forge = require('node-forge');
|
|
17
|
+
const FormData = require('form-data');
|
|
18
|
+
const {
|
|
19
|
+
saveEnvioArtifacts,
|
|
20
|
+
siiError,
|
|
21
|
+
ERROR_CODES,
|
|
22
|
+
createScopedLogger,
|
|
23
|
+
getConfigSection,
|
|
24
|
+
withRetry,
|
|
25
|
+
isRetryableError,
|
|
26
|
+
isRetryableStatus,
|
|
27
|
+
getCachedToken,
|
|
28
|
+
setCachedToken,
|
|
29
|
+
invalidateToken,
|
|
30
|
+
// Nuevas utilidades centralizadas
|
|
31
|
+
parseXml,
|
|
32
|
+
parseXmlNoNs,
|
|
33
|
+
decodeXmlEntities,
|
|
34
|
+
extractTagContent,
|
|
35
|
+
SOAP_ENDPOINTS,
|
|
36
|
+
REST_ENDPOINTS,
|
|
37
|
+
validateAmbiente,
|
|
38
|
+
} = require('./utils');
|
|
39
|
+
|
|
40
|
+
const log = createScopedLogger('EnviadorSII');
|
|
41
|
+
|
|
42
|
+
class EnviadorSII {
|
|
43
|
+
/**
|
|
44
|
+
* @param {Object} certificado - Instancia de Certificado (OBLIGATORIO)
|
|
45
|
+
* @param {string} ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
|
|
46
|
+
* @param {Object} [options] - Opciones adicionales
|
|
47
|
+
* @param {boolean} [options.useTokenCache=true] - Usar cache de tokens
|
|
48
|
+
*/
|
|
49
|
+
constructor(certificado, ambiente, options = {}) {
|
|
50
|
+
// Validar parámetros obligatorios (multi-tenant: nunca usar defaults)
|
|
51
|
+
if (!certificado) {
|
|
52
|
+
throw siiError('EnviadorSII: certificado es obligatorio', ERROR_CODES.CONFIG_MISSING);
|
|
53
|
+
}
|
|
54
|
+
if (!ambiente) {
|
|
55
|
+
throw siiError('EnviadorSII: ambiente es obligatorio', ERROR_CODES.CONFIG_MISSING);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Usar validador centralizado
|
|
59
|
+
this.ambiente = validateAmbiente(ambiente);
|
|
60
|
+
this.certificado = certificado;
|
|
61
|
+
this.token = null; // Token REST (boletas)
|
|
62
|
+
this.tokenSoap = null; // Token SOAP (DTEUpload)
|
|
63
|
+
|
|
64
|
+
// Opciones
|
|
65
|
+
this.useTokenCache = options.useTokenCache !== false;
|
|
66
|
+
|
|
67
|
+
// RUT del certificado para cache
|
|
68
|
+
this.rutCert = certificado.rut || 'unknown';
|
|
69
|
+
|
|
70
|
+
// URLs centralizadas desde utils/endpoints.js
|
|
71
|
+
this.urls = {
|
|
72
|
+
certificacion: {
|
|
73
|
+
...REST_ENDPOINTS.certificacion,
|
|
74
|
+
rcof: SOAP_ENDPOINTS.certificacion.upload,
|
|
75
|
+
semillaSoap: SOAP_ENDPOINTS.certificacion.seed,
|
|
76
|
+
tokenSoap: SOAP_ENDPOINTS.certificacion.token,
|
|
77
|
+
},
|
|
78
|
+
produccion: {
|
|
79
|
+
...REST_ENDPOINTS.produccion,
|
|
80
|
+
rcof: SOAP_ENDPOINTS.produccion.upload,
|
|
81
|
+
semillaSoap: SOAP_ENDPOINTS.produccion.seed,
|
|
82
|
+
tokenSoap: SOAP_ENDPOINTS.produccion.token,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================
|
|
88
|
+
// AUTENTICACIÓN REST (Boletas)
|
|
89
|
+
// ============================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Obtener semilla de autenticación del SII (API REST)
|
|
93
|
+
*/
|
|
94
|
+
async getSemilla() {
|
|
95
|
+
const url = this.urls[this.ambiente].semilla;
|
|
96
|
+
|
|
97
|
+
log.log(' Solicitando semilla a:', url);
|
|
98
|
+
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
headers: {
|
|
102
|
+
'Accept': 'application/xml',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`Error obteniendo semilla: ${response.status}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const xml = await response.text();
|
|
111
|
+
log.log(' XML semilla recibido:', xml.substring(0, 500));
|
|
112
|
+
|
|
113
|
+
// Usar parser centralizado
|
|
114
|
+
const data = parseXml(xml);
|
|
115
|
+
|
|
116
|
+
// Estructura con namespaces SII
|
|
117
|
+
const respuesta = data['SII:RESPUESTA'];
|
|
118
|
+
if (respuesta?.['SII:RESP_BODY']?.SEMILLA) {
|
|
119
|
+
return respuesta['SII:RESP_BODY'].SEMILLA.toString();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (data.SII?.RESP_BODY?.SEMILLA) {
|
|
123
|
+
return data.SII.RESP_BODY.SEMILLA.toString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (data.RESPUESTA?.RESP_BODY?.SEMILLA) {
|
|
127
|
+
return data.RESPUESTA.RESP_BODY.SEMILLA.toString();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (data.getToken?.item?.Semilla) {
|
|
131
|
+
return data.getToken.item.Semilla.toString();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
log.debug(' Estructura XML parseada:', JSON.stringify(data, null, 2));
|
|
135
|
+
throw new Error('No se pudo obtener semilla del SII');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Firmar semilla y obtener token (API REST)
|
|
140
|
+
*/
|
|
141
|
+
async getToken() {
|
|
142
|
+
const semilla = await this.getSemilla();
|
|
143
|
+
log.log('Semilla obtenida:', semilla);
|
|
144
|
+
|
|
145
|
+
const xmlSemilla = this._crearXMLSemilla(semilla);
|
|
146
|
+
|
|
147
|
+
const url = this.urls[this.ambiente].token;
|
|
148
|
+
|
|
149
|
+
const response = await fetch(url, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/xml',
|
|
153
|
+
'Accept': 'application/xml',
|
|
154
|
+
},
|
|
155
|
+
body: xmlSemilla,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const errorText = await response.text();
|
|
160
|
+
throw new Error(`Error obteniendo token: ${response.status} - ${errorText}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const xml = await response.text();
|
|
164
|
+
log.log('Respuesta token:', xml);
|
|
165
|
+
|
|
166
|
+
// Usar parser centralizado con namespaces removidos
|
|
167
|
+
const data = parseXmlNoNs(xml);
|
|
168
|
+
|
|
169
|
+
const respuesta = data.RESPUESTA || data['SII:RESPUESTA'];
|
|
170
|
+
if (respuesta) {
|
|
171
|
+
const respBody = respuesta.RESP_BODY || respuesta['SII:RESP_BODY'];
|
|
172
|
+
if (respBody && respBody.TOKEN) {
|
|
173
|
+
this.token = respBody.TOKEN;
|
|
174
|
+
log.log('✅ Token obtenido:', this.token);
|
|
175
|
+
return this.token;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw new Error('No se pudo obtener token del SII');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================
|
|
183
|
+
// AUTENTICACIÓN SOAP (DTEUpload)
|
|
184
|
+
// ============================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Obtener semilla del servicio SOAP para DTEUpload
|
|
188
|
+
* Usa configuración centralizada para reintentos
|
|
189
|
+
*/
|
|
190
|
+
async getSemillaSoap() {
|
|
191
|
+
const url = this.urls[this.ambiente].semillaSoap;
|
|
192
|
+
log.log(' Solicitando semilla SOAP a:', url);
|
|
193
|
+
const retryConfig = getConfigSection('retry');
|
|
194
|
+
const maxRetries = retryConfig?.maxRetries || 6;
|
|
195
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
|
|
197
|
+
const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
|
|
198
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
|
199
|
+
<soapenv:Body>
|
|
200
|
+
<getSeed/>
|
|
201
|
+
</soapenv:Body>
|
|
202
|
+
</soapenv:Envelope>`;
|
|
203
|
+
|
|
204
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
205
|
+
try {
|
|
206
|
+
if (attempt > 1) {
|
|
207
|
+
log.log(` 🔄 Reintento semilla SOAP ${attempt}/${maxRetries}...`);
|
|
208
|
+
await wait(attempt * 1000);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const response = await fetch(url, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: {
|
|
214
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
215
|
+
'SOAPAction': '',
|
|
216
|
+
},
|
|
217
|
+
body: soapEnvelope,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
if (isRetryableStatus(response.status) && attempt < maxRetries) {
|
|
222
|
+
log.log(` ⚠️ Error semilla SOAP (${response.status}), reintentando...`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
throw siiError(`Error obteniendo semilla SOAP: ${response.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const xml = await response.text();
|
|
229
|
+
log.log(' Respuesta semilla SOAP (primeros 400 chars):', xml.substring(0, 400));
|
|
230
|
+
|
|
231
|
+
// Usar utilidad centralizada para decodificar entidades
|
|
232
|
+
const decodedXml = decodeXmlEntities(xml);
|
|
233
|
+
|
|
234
|
+
// Usar utilidad centralizada para extraer contenido de etiqueta
|
|
235
|
+
const semilla = extractTagContent(decodedXml, 'SEMILLA');
|
|
236
|
+
if (semilla) {
|
|
237
|
+
log.log(' Semilla extraída:', semilla);
|
|
238
|
+
return semilla;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const estado = extractTagContent(decodedXml, 'ESTADO');
|
|
242
|
+
if (estado && estado !== '00') {
|
|
243
|
+
throw siiError(`Error del SII al obtener semilla: Estado ${estado}`, ERROR_CODES.SII_INVALID_RESPONSE);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
throw siiError('No se pudo extraer semilla de la respuesta SOAP', ERROR_CODES.SII_INVALID_RESPONSE);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
249
|
+
log.log(` ⚠️ Error de conexión semilla SOAP (${error.cause?.code || 'socket'}), reintentando...`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw siiError('Semilla SOAP falló después de múltiples reintentos', ERROR_CODES.SII_TIMEOUT);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Obtener token del servicio SOAP para DTEUpload
|
|
261
|
+
* Usa cache de tokens para evitar solicitudes innecesarias
|
|
262
|
+
*/
|
|
263
|
+
async getTokenSoap() {
|
|
264
|
+
// Verificar cache primero
|
|
265
|
+
if (this.useTokenCache) {
|
|
266
|
+
const cached = getCachedToken(this.ambiente, 'soap', this.rutCert);
|
|
267
|
+
if (cached) {
|
|
268
|
+
log.log(' ✅ Token SOAP desde cache');
|
|
269
|
+
this.tokenSoap = cached;
|
|
270
|
+
return cached;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const retryConfig = getConfigSection('retry');
|
|
275
|
+
const maxRetries = retryConfig?.maxRetries || 6;
|
|
276
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
277
|
+
const url = this.urls[this.ambiente].tokenSoap;
|
|
278
|
+
|
|
279
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
280
|
+
try {
|
|
281
|
+
if (attempt > 1) {
|
|
282
|
+
log.log(` 🔄 Reintento token SOAP ${attempt}/${maxRetries}...`);
|
|
283
|
+
await wait(attempt * 1000);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const semilla = await this.getSemillaSoap();
|
|
287
|
+
log.log(' Semilla SOAP obtenida:', semilla);
|
|
288
|
+
|
|
289
|
+
const xmlSemilla = this._crearXMLSemilla(semilla);
|
|
290
|
+
log.log(' Obteniendo token SOAP de:', url);
|
|
291
|
+
|
|
292
|
+
const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
|
|
293
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
|
294
|
+
<soapenv:Body>
|
|
295
|
+
<getToken>
|
|
296
|
+
<pszXml>${xmlSemilla.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}</pszXml>
|
|
297
|
+
</getToken>
|
|
298
|
+
</soapenv:Body>
|
|
299
|
+
</soapenv:Envelope>`;
|
|
300
|
+
|
|
301
|
+
const response = await fetch(url, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: {
|
|
304
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
305
|
+
'SOAPAction': '',
|
|
306
|
+
},
|
|
307
|
+
body: soapEnvelope,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const errorText = await response.text();
|
|
312
|
+
if (isRetryableStatus(response.status) && attempt < maxRetries) {
|
|
313
|
+
log.log(` ⚠️ Error token SOAP (${response.status}), reintentando...`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
throw siiError(`Error obteniendo token SOAP: ${response.status} - ${errorText}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const xml = await response.text();
|
|
320
|
+
log.log(' Respuesta token SOAP (primeros 600 chars):', xml.substring(0, 600));
|
|
321
|
+
|
|
322
|
+
const decodedXml = xml
|
|
323
|
+
.replace(/</g, '<')
|
|
324
|
+
.replace(/>/g, '>')
|
|
325
|
+
.replace(/"/g, '"')
|
|
326
|
+
.replace(/&/g, '&');
|
|
327
|
+
|
|
328
|
+
const tokenMatch = decodedXml.match(/<TOKEN>([^<]+)<\/TOKEN>/i);
|
|
329
|
+
if (tokenMatch) {
|
|
330
|
+
this.tokenSoap = tokenMatch[1];
|
|
331
|
+
|
|
332
|
+
// Guardar en cache
|
|
333
|
+
if (this.useTokenCache) {
|
|
334
|
+
setCachedToken(this.ambiente, 'soap', this.rutCert, this.tokenSoap);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
log.log(' ✅ Token SOAP obtenido:', this.tokenSoap);
|
|
338
|
+
return this.tokenSoap;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const estadoMatch = decodedXml.match(/<ESTADO>(\d+)<\/ESTADO>/);
|
|
342
|
+
const glosaMatch = decodedXml.match(/<GLOSA>([^<]+)<\/GLOSA>/);
|
|
343
|
+
if (estadoMatch && estadoMatch[1] !== '00') {
|
|
344
|
+
throw siiError(`Error del SII: Estado ${estadoMatch[1]} - ${glosaMatch ? glosaMatch[1] : 'Sin detalle'}`, ERROR_CODES.SII_AUTH_FAILED);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw siiError('No se pudo obtener token SOAP del SII', ERROR_CODES.SII_INVALID_RESPONSE);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
350
|
+
log.log(` ⚠️ Error de conexión token SOAP (${error.cause?.code || 'socket'}), reintentando...`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
throw siiError('Token SOAP falló después de múltiples reintentos', ERROR_CODES.SII_TIMEOUT);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Invalidar token cacheado (para forzar renovación)
|
|
362
|
+
*/
|
|
363
|
+
invalidateCachedToken(tipo = 'soap') {
|
|
364
|
+
invalidateToken(this.ambiente, tipo, this.rutCert);
|
|
365
|
+
if (tipo === 'soap') {
|
|
366
|
+
this.tokenSoap = null;
|
|
367
|
+
} else {
|
|
368
|
+
this.token = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============================================
|
|
373
|
+
// HELPERS DE FIRMA
|
|
374
|
+
// ============================================
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Crear XML de semilla firmado
|
|
378
|
+
*/
|
|
379
|
+
_crearXMLSemilla(semilla) {
|
|
380
|
+
const xmlContent = `<getToken><item><Semilla>${semilla}</Semilla></item></getToken>`;
|
|
381
|
+
|
|
382
|
+
const md = forge.md.sha1.create();
|
|
383
|
+
md.update(xmlContent, 'utf8');
|
|
384
|
+
const digestValue = forge.util.encode64(md.digest().bytes());
|
|
385
|
+
|
|
386
|
+
const signedInfoParaFirmar = `<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>${digestValue}</DigestValue></Reference></SignedInfo>`;
|
|
387
|
+
|
|
388
|
+
const mdSign = forge.md.sha1.create();
|
|
389
|
+
mdSign.update(signedInfoParaFirmar, 'utf8');
|
|
390
|
+
const signature = this.certificado.privateKey.sign(mdSign);
|
|
391
|
+
const signatureValue = this._wordwrap(forge.util.encode64(signature), 64);
|
|
392
|
+
|
|
393
|
+
const modulus = this._wordwrap(this.certificado.getModulus(), 64);
|
|
394
|
+
const exponent = this.certificado.getExponent();
|
|
395
|
+
const cert = this._wordwrap(this.certificado.getCertificateBase64(), 64);
|
|
396
|
+
|
|
397
|
+
log.log('SignedInfo para firmar (length):', signedInfoParaFirmar.length);
|
|
398
|
+
|
|
399
|
+
const xmlFirmado = `<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
400
|
+
<getToken><item><Semilla>${semilla}</Semilla></item><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>${digestValue}</DigestValue></Reference></SignedInfo><SignatureValue>${signatureValue}</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>${modulus}</Modulus><Exponent>${exponent}</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>${cert}</X509Certificate></X509Data></KeyInfo></Signature></getToken>`;
|
|
401
|
+
|
|
402
|
+
log.log('DigestValue:', digestValue);
|
|
403
|
+
|
|
404
|
+
return xmlFirmado;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Wordwrap para base64 (como PHP)
|
|
409
|
+
*/
|
|
410
|
+
_wordwrap(str, width) {
|
|
411
|
+
const lines = [];
|
|
412
|
+
for (let i = 0; i < str.length; i += width) {
|
|
413
|
+
lines.push(str.substring(i, i + width));
|
|
414
|
+
}
|
|
415
|
+
return lines.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================
|
|
419
|
+
// ENVÍO DE BOLETAS (API REST)
|
|
420
|
+
// ============================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Enviar EnvioBOLETA al SII (API REST para Boletas)
|
|
424
|
+
*/
|
|
425
|
+
async enviar(envioBoleta, rutEmisor, rutEnvia) {
|
|
426
|
+
if (!this.token) {
|
|
427
|
+
await this.getToken();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!envioBoleta.xml) {
|
|
431
|
+
envioBoleta.setCaratula({
|
|
432
|
+
RutEmisor: rutEmisor,
|
|
433
|
+
RutEnvia: rutEnvia,
|
|
434
|
+
FchResol: envioBoleta.config?.fchResol || '2014-08-22',
|
|
435
|
+
NroResol: envioBoleta.config?.nroResol !== undefined ? envioBoleta.config.nroResol : 0,
|
|
436
|
+
});
|
|
437
|
+
envioBoleta.generar();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const url = this.urls[this.ambiente].envio;
|
|
441
|
+
let xml = envioBoleta.getXML();
|
|
442
|
+
|
|
443
|
+
if (!xml) {
|
|
444
|
+
throw new Error('No se pudo generar el XML del EnvioBOLETA');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!xml.startsWith('<?xml')) {
|
|
448
|
+
xml = '<?xml version="1.0" encoding="ISO-8859-1"?>\n' + xml;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const [rutSenderStr, dvSender] = rutEnvia.split('-');
|
|
452
|
+
const [rutCompanyStr, dvCompany] = rutEmisor.split('-');
|
|
453
|
+
const rutSender = parseInt(rutSenderStr.replace(/\./g, ''), 10);
|
|
454
|
+
const rutCompany = parseInt(rutCompanyStr.replace(/\./g, ''), 10);
|
|
455
|
+
|
|
456
|
+
const formData = new FormData();
|
|
457
|
+
formData.append('rutSender', rutSender.toString());
|
|
458
|
+
formData.append('dvSender', dvSender.toUpperCase());
|
|
459
|
+
formData.append('rutCompany', rutCompany.toString());
|
|
460
|
+
formData.append('dvCompany', dvCompany.toUpperCase());
|
|
461
|
+
|
|
462
|
+
const xmlBuffer = Buffer.from(xml, 'utf-8');
|
|
463
|
+
formData.append('archivo', xmlBuffer, {
|
|
464
|
+
filename: 'EnvioBOLETA.xml',
|
|
465
|
+
contentType: 'application/xml',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
log.log('Enviando al SII:', url);
|
|
469
|
+
log.log('RUT Sender:', rutSender, '-', dvSender.toUpperCase());
|
|
470
|
+
log.log('RUT Company:', rutCompany, '-', dvCompany.toUpperCase());
|
|
471
|
+
log.log('XML Length:', xmlBuffer.length, 'bytes');
|
|
472
|
+
|
|
473
|
+
const urlObj = new URL(url);
|
|
474
|
+
const formBuffer = formData.getBuffer();
|
|
475
|
+
const formHeaders = formData.getHeaders();
|
|
476
|
+
|
|
477
|
+
const responseText = await new Promise((resolve, reject) => {
|
|
478
|
+
const req = https.request({
|
|
479
|
+
hostname: urlObj.hostname,
|
|
480
|
+
path: urlObj.pathname,
|
|
481
|
+
method: 'POST',
|
|
482
|
+
headers: {
|
|
483
|
+
...formHeaders,
|
|
484
|
+
'Content-Length': formBuffer.length,
|
|
485
|
+
'User-Agent': 'Mozilla/4.0 ( compatible; PROG 1.0; Windows NT)',
|
|
486
|
+
'Accept': 'application/json',
|
|
487
|
+
'Cookie': `TOKEN=${this.token}`,
|
|
488
|
+
},
|
|
489
|
+
}, (res) => {
|
|
490
|
+
let data = '';
|
|
491
|
+
res.on('data', chunk => data += chunk);
|
|
492
|
+
res.on('end', () => {
|
|
493
|
+
log.log('HTTP Status:', res.statusCode);
|
|
494
|
+
resolve({ text: data, status: res.statusCode });
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
req.on('error', reject);
|
|
499
|
+
req.write(formBuffer);
|
|
500
|
+
req.end();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
log.log('Respuesta SII:', responseText.text);
|
|
504
|
+
|
|
505
|
+
const resultado = this._parsearRespuestaEnvio(responseText.text, responseText.status);
|
|
506
|
+
saveEnvioArtifacts({
|
|
507
|
+
xml,
|
|
508
|
+
responseText: responseText.text,
|
|
509
|
+
responseOk: resultado.ok,
|
|
510
|
+
responseStatus: responseText.status,
|
|
511
|
+
trackId: resultado.trackId || null,
|
|
512
|
+
ambiente: this.ambiente,
|
|
513
|
+
tipoEnvio: 'EnvioBOLETA-REST',
|
|
514
|
+
error: resultado.error || null,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return resultado;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Enviar XML ya generado directamente al SII
|
|
522
|
+
*/
|
|
523
|
+
async enviarXmlDirecto(xml, rutEmisor, rutEnvia) {
|
|
524
|
+
if (!this.token) {
|
|
525
|
+
await this.getToken();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const url = this.urls[this.ambiente].envio;
|
|
529
|
+
|
|
530
|
+
if (!xml.startsWith('<?xml')) {
|
|
531
|
+
xml = '<?xml version="1.0" encoding="ISO-8859-1"?>\n' + xml;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const [rutSenderStr, dvSender] = rutEnvia.split('-');
|
|
535
|
+
const [rutCompanyStr, dvCompany] = rutEmisor.split('-');
|
|
536
|
+
const rutSender = parseInt(rutSenderStr.replace(/\./g, ''), 10);
|
|
537
|
+
const rutCompany = parseInt(rutCompanyStr.replace(/\./g, ''), 10);
|
|
538
|
+
|
|
539
|
+
const formData = new FormData();
|
|
540
|
+
formData.append('rutSender', rutSender.toString());
|
|
541
|
+
formData.append('dvSender', dvSender.toUpperCase());
|
|
542
|
+
formData.append('rutCompany', rutCompany.toString());
|
|
543
|
+
formData.append('dvCompany', dvCompany.toUpperCase());
|
|
544
|
+
|
|
545
|
+
const xmlBuffer = Buffer.from(xml, 'utf-8');
|
|
546
|
+
formData.append('archivo', xmlBuffer, {
|
|
547
|
+
filename: 'EnvioBOLETA.xml',
|
|
548
|
+
contentType: 'application/xml',
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
log.log('Enviando XML directo al SII:', url);
|
|
552
|
+
log.log('RUT Sender:', rutSender, '-', dvSender.toUpperCase());
|
|
553
|
+
log.log('RUT Company:', rutCompany, '-', dvCompany.toUpperCase());
|
|
554
|
+
log.log('XML Length:', xmlBuffer.length, 'bytes');
|
|
555
|
+
|
|
556
|
+
const urlObj = new URL(url);
|
|
557
|
+
const formBuffer = formData.getBuffer();
|
|
558
|
+
const formHeaders = formData.getHeaders();
|
|
559
|
+
|
|
560
|
+
const responseText = await new Promise((resolve, reject) => {
|
|
561
|
+
const req = https.request({
|
|
562
|
+
hostname: urlObj.hostname,
|
|
563
|
+
path: urlObj.pathname,
|
|
564
|
+
method: 'POST',
|
|
565
|
+
headers: {
|
|
566
|
+
...formHeaders,
|
|
567
|
+
'Content-Length': formBuffer.length,
|
|
568
|
+
'User-Agent': 'Mozilla/4.0 ( compatible; PROG 1.0; Windows NT)',
|
|
569
|
+
'Accept': 'application/json',
|
|
570
|
+
'Cookie': `TOKEN=${this.token}`,
|
|
571
|
+
},
|
|
572
|
+
}, (res) => {
|
|
573
|
+
let data = '';
|
|
574
|
+
res.on('data', chunk => data += chunk);
|
|
575
|
+
res.on('end', () => {
|
|
576
|
+
log.log('HTTP Status:', res.statusCode);
|
|
577
|
+
resolve({ text: data, status: res.statusCode });
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
req.on('error', reject);
|
|
582
|
+
req.write(formBuffer);
|
|
583
|
+
req.end();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
log.log('Respuesta SII:', responseText.text);
|
|
587
|
+
const resultado = this._parsearRespuestaEnvio(responseText.text, responseText.status);
|
|
588
|
+
saveEnvioArtifacts({
|
|
589
|
+
xml,
|
|
590
|
+
responseText: responseText.text,
|
|
591
|
+
responseOk: resultado.ok,
|
|
592
|
+
responseStatus: responseText.status,
|
|
593
|
+
trackId: resultado.trackId || null,
|
|
594
|
+
ambiente: this.ambiente,
|
|
595
|
+
tipoEnvio: 'EnvioBOLETA-REST',
|
|
596
|
+
error: resultado.error || null,
|
|
597
|
+
});
|
|
598
|
+
return resultado;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Parsear respuesta del envío (JSON de la API REST)
|
|
603
|
+
*/
|
|
604
|
+
_parsearRespuestaEnvio(responseText, httpStatus) {
|
|
605
|
+
if (httpStatus !== 200) {
|
|
606
|
+
return {
|
|
607
|
+
ok: false,
|
|
608
|
+
status: httpStatus,
|
|
609
|
+
trackId: null,
|
|
610
|
+
mensaje: `Error HTTP ${httpStatus}`,
|
|
611
|
+
respuesta: responseText,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
const json = JSON.parse(responseText);
|
|
617
|
+
|
|
618
|
+
if (json.trackid) {
|
|
619
|
+
return {
|
|
620
|
+
ok: true,
|
|
621
|
+
status: 0,
|
|
622
|
+
trackId: json.trackid.toString(),
|
|
623
|
+
mensaje: `✅ Enviado al SII - TrackID: ${json.trackid}`,
|
|
624
|
+
respuesta: json,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (json.error || json.mensaje) {
|
|
629
|
+
return {
|
|
630
|
+
ok: false,
|
|
631
|
+
status: json.codigo || json.status || -1,
|
|
632
|
+
trackId: null,
|
|
633
|
+
mensaje: json.error || json.mensaje || 'Error desconocido',
|
|
634
|
+
respuesta: json,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
status: -1,
|
|
641
|
+
trackId: null,
|
|
642
|
+
mensaje: 'Respuesta JSON sin trackid',
|
|
643
|
+
respuesta: json,
|
|
644
|
+
};
|
|
645
|
+
} catch (e) {
|
|
646
|
+
const statusMatch = responseText.match(/<STATUS>(\d+)<\/STATUS>/);
|
|
647
|
+
const trackIdMatch = responseText.match(/<TRACKID>(\d+)<\/TRACKID>/);
|
|
648
|
+
|
|
649
|
+
const status = statusMatch ? parseInt(statusMatch[1]) : null;
|
|
650
|
+
const trackId = trackIdMatch ? trackIdMatch[1] : null;
|
|
651
|
+
|
|
652
|
+
if (status === 0 && trackId) {
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
status: status,
|
|
656
|
+
trackId: trackId,
|
|
657
|
+
mensaje: `✅ Enviado al SII - TrackID: ${trackId}`,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
status: status,
|
|
664
|
+
trackId: trackId,
|
|
665
|
+
mensaje: 'Error parseando respuesta',
|
|
666
|
+
respuesta: responseText,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ============================================
|
|
672
|
+
// CONSULTA DE ESTADO
|
|
673
|
+
// ============================================
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Consultar estado de envío (API REST)
|
|
677
|
+
*/
|
|
678
|
+
async consultarEstado(trackId, rutEmisor, rutEnvia = null) {
|
|
679
|
+
if (!this.token) {
|
|
680
|
+
await this.getToken();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const rutEmisorLimpio = rutEmisor.replace(/\./g, '');
|
|
684
|
+
const url = `${this.urls[this.ambiente].estado}${rutEmisorLimpio}-${trackId}`;
|
|
685
|
+
|
|
686
|
+
log.log('Consultando estado:', url);
|
|
687
|
+
|
|
688
|
+
const response = await fetch(url, {
|
|
689
|
+
method: 'GET',
|
|
690
|
+
headers: {
|
|
691
|
+
'Cookie': `TOKEN=${this.token}`,
|
|
692
|
+
'Accept': 'application/json',
|
|
693
|
+
'User-Agent': 'Mozilla/4.0 ( compatible; PROG 1.0; Windows NT)',
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const responseText = await response.text();
|
|
698
|
+
log.log('Respuesta estado:', responseText);
|
|
699
|
+
|
|
700
|
+
if (!response.ok) {
|
|
701
|
+
return {
|
|
702
|
+
ok: false,
|
|
703
|
+
error: `Error HTTP ${response.status}`,
|
|
704
|
+
respuesta: responseText,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
const json = JSON.parse(responseText);
|
|
710
|
+
|
|
711
|
+
let mensaje = '';
|
|
712
|
+
const estado = json.estado;
|
|
713
|
+
if (estado === 'REC') mensaje = '📥 Envío recibido';
|
|
714
|
+
else if (estado === 'SOK') mensaje = '✅ Esquema validado';
|
|
715
|
+
else if (estado === 'FOK') mensaje = '✅ Firma de envío validada';
|
|
716
|
+
else if (estado === 'PRD') mensaje = '⏳ Envío en proceso';
|
|
717
|
+
else if (estado === 'CRT') mensaje = '✅ Carátula OK';
|
|
718
|
+
else if (estado === 'EPR') mensaje = '✅ Envío procesado';
|
|
719
|
+
else if (estado === 'RPT') mensaje = '❌ Rechazado por schema';
|
|
720
|
+
else if (estado === 'RFR') mensaje = '❌ Rechazado por error en firma';
|
|
721
|
+
else if (estado === 'VOF') mensaje = '❌ Error interno en SII';
|
|
722
|
+
else if (estado === 'RCT') mensaje = '❌ Rechazado por error en carátula';
|
|
723
|
+
else if (estado === 'RPR') mensaje = '⚠️ Aceptado con reparos';
|
|
724
|
+
else if (estado === 'RLV') mensaje = '✅ Aceptado - Documento(s) válido(s)';
|
|
725
|
+
else if (estado === 'RCH') mensaje = `❌ Rechazado: ${json.glosa || json.descripcion || 'Sin detalle'}`;
|
|
726
|
+
else mensaje = `Estado: ${estado}`;
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
ok: true,
|
|
730
|
+
trackId: trackId,
|
|
731
|
+
estado: estado,
|
|
732
|
+
descripcion: json.descripcion || json.glosa,
|
|
733
|
+
mensaje,
|
|
734
|
+
...json,
|
|
735
|
+
};
|
|
736
|
+
} catch (e) {
|
|
737
|
+
return {
|
|
738
|
+
ok: false,
|
|
739
|
+
error: 'Error parseando respuesta',
|
|
740
|
+
mensaje: 'Error parseando respuesta del SII',
|
|
741
|
+
respuesta: responseText,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Consultar estado de envío via SOAP (QueryEstUp.jws)
|
|
748
|
+
*/
|
|
749
|
+
async consultarEstadoSoap(trackId, rutEmisor) {
|
|
750
|
+
if (!this.tokenSoap) {
|
|
751
|
+
await this.getTokenSoap();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const servidor = this.ambiente === 'produccion' ? 'palena' : 'maullin';
|
|
755
|
+
const [rutNum, dv] = rutEmisor.replace(/\./g, '').split('-');
|
|
756
|
+
|
|
757
|
+
const urlQueryEstUp = `https://${servidor}.sii.cl/DTEWS/QueryEstUp.jws`;
|
|
758
|
+
|
|
759
|
+
const soapBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
760
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
|
761
|
+
<soapenv:Body>
|
|
762
|
+
<getEstUp>
|
|
763
|
+
<RutEmpresa>${rutNum}</RutEmpresa>
|
|
764
|
+
<DvEmpresa>${dv}</DvEmpresa>
|
|
765
|
+
<TrackId>${trackId}</TrackId>
|
|
766
|
+
<Token>${this.tokenSoap}</Token>
|
|
767
|
+
</getEstUp>
|
|
768
|
+
</soapenv:Body>
|
|
769
|
+
</soapenv:Envelope>`;
|
|
770
|
+
|
|
771
|
+
log.log('📊 Consultando estado SOAP:', urlQueryEstUp);
|
|
772
|
+
log.log(' TrackID:', trackId, 'RUT:', rutEmisor);
|
|
773
|
+
|
|
774
|
+
const response = await fetch(urlQueryEstUp, {
|
|
775
|
+
method: 'POST',
|
|
776
|
+
headers: {
|
|
777
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
778
|
+
'SOAPAction': '',
|
|
779
|
+
},
|
|
780
|
+
body: soapBody,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const text = await response.text();
|
|
784
|
+
|
|
785
|
+
// Usar decodeXmlEntities centralizado
|
|
786
|
+
const decoded = decodeXmlEntities(text).replace(/
/g, '\n');
|
|
787
|
+
|
|
788
|
+
log.log(' Respuesta SOAP estado:', decoded.substring(0, 500));
|
|
789
|
+
|
|
790
|
+
const estadoMatch = decoded.match(/<ESTADO>([^<]+)<\/ESTADO>/i);
|
|
791
|
+
const glosaMatch = decoded.match(/<GLOSA>([^<]+)<\/GLOSA>/i);
|
|
792
|
+
const numAtencionMatch = decoded.match(/<NUM_ATENCION>([^<]+)<\/NUM_ATENCION>/i);
|
|
793
|
+
const errCodeMatch = decoded.match(/<ERR_CODE>([^<]+)<\/ERR_CODE>/i);
|
|
794
|
+
|
|
795
|
+
const estado = estadoMatch ? estadoMatch[1] : null;
|
|
796
|
+
const glosa = glosaMatch ? glosaMatch[1] : null;
|
|
797
|
+
|
|
798
|
+
if (!estado) {
|
|
799
|
+
if (errCodeMatch) {
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
error: `Error ${errCodeMatch[1]}`,
|
|
803
|
+
glosa: glosa || 'Error desconocido',
|
|
804
|
+
respuesta: decoded,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
ok: false,
|
|
809
|
+
error: 'No se pudo obtener estado',
|
|
810
|
+
respuesta: decoded,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let mensaje = '';
|
|
815
|
+
let esExitoso = false;
|
|
816
|
+
let esIntermedio = false;
|
|
817
|
+
let esRechazado = false;
|
|
818
|
+
|
|
819
|
+
switch (estado) {
|
|
820
|
+
case 'EPR':
|
|
821
|
+
mensaje = '✅ Envío Procesado - ¡Listo para declarar cumplimiento!';
|
|
822
|
+
esExitoso = true;
|
|
823
|
+
break;
|
|
824
|
+
case 'RPR':
|
|
825
|
+
mensaje = '⚠️ Aceptado con Reparos';
|
|
826
|
+
esExitoso = true;
|
|
827
|
+
break;
|
|
828
|
+
case 'REC':
|
|
829
|
+
mensaje = '⏳ Envío Recibido - Esperando validación';
|
|
830
|
+
esIntermedio = true;
|
|
831
|
+
break;
|
|
832
|
+
case 'SOK':
|
|
833
|
+
mensaje = '⏳ Schema OK - Validando firma...';
|
|
834
|
+
esIntermedio = true;
|
|
835
|
+
break;
|
|
836
|
+
case 'FOK':
|
|
837
|
+
mensaje = '⏳ Firma Validada - Procesando envío...';
|
|
838
|
+
esIntermedio = true;
|
|
839
|
+
break;
|
|
840
|
+
case 'PRD':
|
|
841
|
+
mensaje = '⏳ Envío en Proceso - Validando carátula...';
|
|
842
|
+
esIntermedio = true;
|
|
843
|
+
break;
|
|
844
|
+
case 'CRT':
|
|
845
|
+
mensaje = '⏳ Carátula OK - Finalizando proceso...';
|
|
846
|
+
esIntermedio = true;
|
|
847
|
+
break;
|
|
848
|
+
case 'DNK':
|
|
849
|
+
mensaje = '⏳ En proceso de revisión';
|
|
850
|
+
esIntermedio = true;
|
|
851
|
+
break;
|
|
852
|
+
case 'RPT':
|
|
853
|
+
mensaje = `❌ Rechazado por Schema: ${glosa || 'Error en estructura XML'}`;
|
|
854
|
+
esRechazado = true;
|
|
855
|
+
break;
|
|
856
|
+
case 'RFR':
|
|
857
|
+
mensaje = `❌ Rechazado por Firma: ${glosa || 'Error en firma digital'}`;
|
|
858
|
+
esRechazado = true;
|
|
859
|
+
break;
|
|
860
|
+
case 'VOF':
|
|
861
|
+
mensaje = `❌ Error Interno del SII: ${glosa || 'Reintentar más tarde'}`;
|
|
862
|
+
esRechazado = true;
|
|
863
|
+
break;
|
|
864
|
+
case 'RCT':
|
|
865
|
+
mensaje = `❌ Rechazado por Error en Carátula: ${glosa || 'Verificar datos del emisor'}`;
|
|
866
|
+
esRechazado = true;
|
|
867
|
+
break;
|
|
868
|
+
case 'RCH':
|
|
869
|
+
mensaje = `❌ Rechazado: ${glosa || 'Sin detalle'}`;
|
|
870
|
+
esRechazado = true;
|
|
871
|
+
break;
|
|
872
|
+
case 'RLV':
|
|
873
|
+
mensaje = `❌ Rechazado: ${glosa || 'Error en documento'}`;
|
|
874
|
+
esRechazado = true;
|
|
875
|
+
break;
|
|
876
|
+
default:
|
|
877
|
+
mensaje = `Estado: ${estado} - ${glosa || 'Sin descripción'}`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
ok: true,
|
|
882
|
+
esExitoso,
|
|
883
|
+
esIntermedio,
|
|
884
|
+
esRechazado,
|
|
885
|
+
trackId,
|
|
886
|
+
estado,
|
|
887
|
+
glosa,
|
|
888
|
+
mensaje,
|
|
889
|
+
numAtencion: numAtencionMatch ? numAtencionMatch[1] : null,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ============================================
|
|
894
|
+
// ENVÍO SOAP/DTEUpload
|
|
895
|
+
// ============================================
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Enviar EnvioBOLETA al SII via SOAP/DTEUpload
|
|
899
|
+
*/
|
|
900
|
+
async enviarBoletaSoap(envioBoleta) {
|
|
901
|
+
const { DOMParser } = require('@xmldom/xmldom');
|
|
902
|
+
|
|
903
|
+
if (!this.tokenSoap) {
|
|
904
|
+
log.log('📡 Obteniendo token SOAP para DTEUpload...');
|
|
905
|
+
await this.getTokenSoap();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const xml = envioBoleta.getXML();
|
|
909
|
+
if (!xml) {
|
|
910
|
+
throw new Error('El EnvioBoleta no tiene XML generado. Llame a generar() primero.');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const parser = new DOMParser();
|
|
914
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
915
|
+
const rutEmisor = doc.getElementsByTagName('RutEmisor')[0]?.textContent;
|
|
916
|
+
const rutEnvia = doc.getElementsByTagName('RutEnvia')[0]?.textContent;
|
|
917
|
+
|
|
918
|
+
if (!rutEmisor || !rutEnvia) {
|
|
919
|
+
throw new Error('No se pudo extraer RutEmisor o RutEnvia del XML');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const url = this.urls[this.ambiente].rcof;
|
|
923
|
+
|
|
924
|
+
log.log('📤 Enviando EnvioBOLETA via SOAP/DTEUpload...');
|
|
925
|
+
log.log(' URL:', url);
|
|
926
|
+
log.log(' XML Length:', xml.length, 'bytes');
|
|
927
|
+
|
|
928
|
+
const [rutNum, dv] = rutEmisor.split('-');
|
|
929
|
+
const [rutEnviaNum, dvEnvia] = rutEnvia.split('-');
|
|
930
|
+
|
|
931
|
+
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
932
|
+
|
|
933
|
+
let body = '';
|
|
934
|
+
body += `--${boundary}\r\n`;
|
|
935
|
+
body += `Content-Disposition: form-data; name="rutSender"\r\n\r\n`;
|
|
936
|
+
body += `${parseInt(rutEnviaNum.replace(/\./g, ''), 10)}\r\n`;
|
|
937
|
+
|
|
938
|
+
body += `--${boundary}\r\n`;
|
|
939
|
+
body += `Content-Disposition: form-data; name="dvSender"\r\n\r\n`;
|
|
940
|
+
body += `${dvEnvia.toUpperCase()}\r\n`;
|
|
941
|
+
|
|
942
|
+
body += `--${boundary}\r\n`;
|
|
943
|
+
body += `Content-Disposition: form-data; name="rutCompany"\r\n\r\n`;
|
|
944
|
+
body += `${parseInt(rutNum.replace(/\./g, ''), 10)}\r\n`;
|
|
945
|
+
|
|
946
|
+
body += `--${boundary}\r\n`;
|
|
947
|
+
body += `Content-Disposition: form-data; name="dvCompany"\r\n\r\n`;
|
|
948
|
+
body += `${dv.toUpperCase()}\r\n`;
|
|
949
|
+
|
|
950
|
+
body += `--${boundary}\r\n`;
|
|
951
|
+
body += `Content-Disposition: form-data; name="archivo"; filename="EnvioBOLETA.xml"\r\n`;
|
|
952
|
+
body += `Content-Type: text/xml\r\n\r\n`;
|
|
953
|
+
body += xml;
|
|
954
|
+
body += `\r\n--${boundary}--\r\n`;
|
|
955
|
+
|
|
956
|
+
return await this._enviarMultipart(url, body, boundary, xml, 'EnvioBOLETA');
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Enviar EnvioDTE al SII via SOAP/DTEUpload
|
|
961
|
+
*/
|
|
962
|
+
async enviarDteSoap(envioDte) {
|
|
963
|
+
const { DOMParser } = require('@xmldom/xmldom');
|
|
964
|
+
|
|
965
|
+
if (!this.tokenSoap) {
|
|
966
|
+
log.log('📡 Obteniendo token SOAP para DTEUpload...');
|
|
967
|
+
await this.getTokenSoap();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const xml = envioDte.getXML();
|
|
971
|
+
if (!xml) {
|
|
972
|
+
throw new Error('El EnvioDTE no tiene XML generado. Llame a generar() primero.');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const parser = new DOMParser();
|
|
976
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
977
|
+
const rutEmisor = doc.getElementsByTagName('RutEmisor')[0]?.textContent;
|
|
978
|
+
const rutEnvia = doc.getElementsByTagName('RutEnvia')[0]?.textContent;
|
|
979
|
+
|
|
980
|
+
if (!rutEmisor || !rutEnvia) {
|
|
981
|
+
throw new Error('No se pudo extraer RutEmisor o RutEnvia del XML');
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const url = this.urls[this.ambiente].rcof;
|
|
985
|
+
|
|
986
|
+
const xmlBuffer = Buffer.from(xml, 'latin1');
|
|
987
|
+
|
|
988
|
+
log.log('📤 Enviando EnvioDTE via SOAP/DTEUpload...');
|
|
989
|
+
log.log(' URL:', url);
|
|
990
|
+
log.log(' XML Length:', xmlBuffer.length, 'bytes');
|
|
991
|
+
|
|
992
|
+
const [rutNum, dv] = rutEmisor.split('-');
|
|
993
|
+
const [rutEnviaNum, dvEnvia] = rutEnvia.split('-');
|
|
994
|
+
|
|
995
|
+
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
996
|
+
|
|
997
|
+
const bodyParts = [];
|
|
998
|
+
bodyParts.push(Buffer.from(`--${boundary}\r\n`, 'utf8'));
|
|
999
|
+
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="rutSender"\r\n\r\n', 'utf8'));
|
|
1000
|
+
bodyParts.push(Buffer.from(`${parseInt(rutEnviaNum.replace(/\./g, ''), 10)}\r\n`, 'utf8'));
|
|
1001
|
+
|
|
1002
|
+
bodyParts.push(Buffer.from(`--${boundary}\r\n`, 'utf8'));
|
|
1003
|
+
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="dvSender"\r\n\r\n', 'utf8'));
|
|
1004
|
+
bodyParts.push(Buffer.from(`${dvEnvia.toUpperCase()}\r\n`, 'utf8'));
|
|
1005
|
+
|
|
1006
|
+
bodyParts.push(Buffer.from(`--${boundary}\r\n`, 'utf8'));
|
|
1007
|
+
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="rutCompany"\r\n\r\n', 'utf8'));
|
|
1008
|
+
bodyParts.push(Buffer.from(`${parseInt(rutNum.replace(/\./g, ''), 10)}\r\n`, 'utf8'));
|
|
1009
|
+
|
|
1010
|
+
bodyParts.push(Buffer.from(`--${boundary}\r\n`, 'utf8'));
|
|
1011
|
+
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="dvCompany"\r\n\r\n', 'utf8'));
|
|
1012
|
+
bodyParts.push(Buffer.from(`${dv.toUpperCase()}\r\n`, 'utf8'));
|
|
1013
|
+
|
|
1014
|
+
bodyParts.push(Buffer.from(`--${boundary}\r\n`, 'utf8'));
|
|
1015
|
+
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="archivo"; filename="EnvioDTE.xml"\r\n', 'utf8'));
|
|
1016
|
+
bodyParts.push(Buffer.from('Content-Type: text/xml\r\n\r\n', 'utf8'));
|
|
1017
|
+
bodyParts.push(xmlBuffer);
|
|
1018
|
+
bodyParts.push(Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8'));
|
|
1019
|
+
|
|
1020
|
+
const body = Buffer.concat(bodyParts);
|
|
1021
|
+
|
|
1022
|
+
return await this._enviarMultipartBuffer(url, body, boundary, xml, 'EnvioDTE');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Enviar Consumo de Folios (RCOF/RVD) al SII via SOAP/CGI
|
|
1027
|
+
*/
|
|
1028
|
+
async enviarConsumoFolios(consumoFolio) {
|
|
1029
|
+
const { DOMParser } = require('@xmldom/xmldom');
|
|
1030
|
+
|
|
1031
|
+
if (!this.tokenSoap) {
|
|
1032
|
+
log.log('📡 Obteniendo token SOAP para DTEUpload...');
|
|
1033
|
+
await this.getTokenSoap();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const xml = consumoFolio.getXML();
|
|
1037
|
+
if (!xml) {
|
|
1038
|
+
throw new Error('El ConsumoFolio no tiene XML generado. Llame a generar() primero.');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const parser = new DOMParser();
|
|
1042
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
1043
|
+
const rutEmisor = doc.getElementsByTagName('RutEmisor')[0]?.textContent;
|
|
1044
|
+
|
|
1045
|
+
if (!rutEmisor) {
|
|
1046
|
+
throw new Error('No se pudo extraer RutEmisor del XML');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const url = this.urls[this.ambiente].rcof;
|
|
1050
|
+
|
|
1051
|
+
log.log('Enviando RCOF a:', url);
|
|
1052
|
+
log.log('XML Length:', xml.length, 'bytes');
|
|
1053
|
+
|
|
1054
|
+
const [rutNum, dv] = rutEmisor.split('-');
|
|
1055
|
+
const [rutEnviaNum, dvEnvia] = this.certificado.rut.split('-');
|
|
1056
|
+
|
|
1057
|
+
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
1058
|
+
|
|
1059
|
+
let body = '';
|
|
1060
|
+
body += `--${boundary}\r\n`;
|
|
1061
|
+
body += `Content-Disposition: form-data; name="rutSender"\r\n\r\n`;
|
|
1062
|
+
body += `${parseInt(rutEnviaNum.replace(/\./g, ''), 10)}\r\n`;
|
|
1063
|
+
|
|
1064
|
+
body += `--${boundary}\r\n`;
|
|
1065
|
+
body += `Content-Disposition: form-data; name="dvSender"\r\n\r\n`;
|
|
1066
|
+
body += `${dvEnvia.toUpperCase()}\r\n`;
|
|
1067
|
+
|
|
1068
|
+
body += `--${boundary}\r\n`;
|
|
1069
|
+
body += `Content-Disposition: form-data; name="rutCompany"\r\n\r\n`;
|
|
1070
|
+
body += `${parseInt(rutNum.replace(/\./g, ''), 10)}\r\n`;
|
|
1071
|
+
|
|
1072
|
+
body += `--${boundary}\r\n`;
|
|
1073
|
+
body += `Content-Disposition: form-data; name="dvCompany"\r\n\r\n`;
|
|
1074
|
+
body += `${dv.toUpperCase()}\r\n`;
|
|
1075
|
+
|
|
1076
|
+
body += `--${boundary}\r\n`;
|
|
1077
|
+
body += `Content-Disposition: form-data; name="archivo"; filename="ConsumoFolios.xml"\r\n`;
|
|
1078
|
+
body += `Content-Type: text/xml\r\n\r\n`;
|
|
1079
|
+
body += xml;
|
|
1080
|
+
body += `\r\n--${boundary}--\r\n`;
|
|
1081
|
+
|
|
1082
|
+
return await this._enviarMultipart(url, body, boundary, xml, 'RCOF');
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Enviar Libro (Compra/Venta o Guías) al SII via SOAP/CGI
|
|
1087
|
+
*/
|
|
1088
|
+
async enviarLibro(libro, nombreArchivo = 'LibroCV.xml') {
|
|
1089
|
+
const { DOMParser } = require('@xmldom/xmldom');
|
|
1090
|
+
|
|
1091
|
+
if (!this.tokenSoap) {
|
|
1092
|
+
log.log('📡 Obteniendo token SOAP para DTEUpload...');
|
|
1093
|
+
await this.getTokenSoap();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const xml = libro.getXML();
|
|
1097
|
+
if (!xml) {
|
|
1098
|
+
throw new Error('El Libro no tiene XML generado. Llame a generar() primero.');
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const parser = new DOMParser();
|
|
1102
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
1103
|
+
const rutEmisor = doc.getElementsByTagName('RutEmisorLibro')[0]?.textContent
|
|
1104
|
+
|| doc.getElementsByTagName('RutEmisor')[0]?.textContent;
|
|
1105
|
+
const rutEnvia = doc.getElementsByTagName('RutEnvia')[0]?.textContent;
|
|
1106
|
+
|
|
1107
|
+
if (!rutEmisor || !rutEnvia) {
|
|
1108
|
+
throw new Error('No se pudo extraer RutEmisorLibro o RutEnvia del XML');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const url = this.urls[this.ambiente].rcof;
|
|
1112
|
+
|
|
1113
|
+
log.log('📤 Enviando Libro via SOAP/DTEUpload...');
|
|
1114
|
+
log.log(' URL:', url);
|
|
1115
|
+
log.log(' XML Length:', xml.length, 'bytes');
|
|
1116
|
+
|
|
1117
|
+
const [rutNum, dv] = rutEmisor.split('-');
|
|
1118
|
+
const [rutEnviaNum, dvEnvia] = rutEnvia.split('-');
|
|
1119
|
+
|
|
1120
|
+
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
1121
|
+
|
|
1122
|
+
let body = '';
|
|
1123
|
+
body += `--${boundary}\r\n`;
|
|
1124
|
+
body += 'Content-Disposition: form-data; name="rutSender"\r\n\r\n';
|
|
1125
|
+
body += `${parseInt(rutEnviaNum.replace(/\./g, ''), 10)}\r\n`;
|
|
1126
|
+
|
|
1127
|
+
body += `--${boundary}\r\n`;
|
|
1128
|
+
body += 'Content-Disposition: form-data; name="dvSender"\r\n\r\n';
|
|
1129
|
+
body += `${dvEnvia.toUpperCase()}\r\n`;
|
|
1130
|
+
|
|
1131
|
+
body += `--${boundary}\r\n`;
|
|
1132
|
+
body += 'Content-Disposition: form-data; name="rutCompany"\r\n\r\n';
|
|
1133
|
+
body += `${parseInt(rutNum.replace(/\./g, ''), 10)}\r\n`;
|
|
1134
|
+
|
|
1135
|
+
body += `--${boundary}\r\n`;
|
|
1136
|
+
body += 'Content-Disposition: form-data; name="dvCompany"\r\n\r\n';
|
|
1137
|
+
body += `${dv.toUpperCase()}\r\n`;
|
|
1138
|
+
|
|
1139
|
+
body += `--${boundary}\r\n`;
|
|
1140
|
+
const fileNameLibro = nombreArchivo || 'LibroCV.xml';
|
|
1141
|
+
body += `Content-Disposition: form-data; name="archivo"; filename="${fileNameLibro}"\r\n`;
|
|
1142
|
+
body += 'Content-Type: text/xml\r\n\r\n';
|
|
1143
|
+
body += xml;
|
|
1144
|
+
body += `\r\n--${boundary}--\r\n`;
|
|
1145
|
+
|
|
1146
|
+
return await this._enviarMultipart(url, body, boundary, xml, 'Libro');
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// ============================================
|
|
1150
|
+
// HELPERS DE ENVÍO
|
|
1151
|
+
// ============================================
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Enviar multipart con reintentos (unificado para string y buffer)
|
|
1155
|
+
* Usa isRetryableError centralizado de utils
|
|
1156
|
+
*
|
|
1157
|
+
* @param {string} url - URL de envío
|
|
1158
|
+
* @param {string|Buffer} body - Body del request
|
|
1159
|
+
* @param {string} boundary - Boundary del multipart
|
|
1160
|
+
* @param {string} xml - XML original para debug
|
|
1161
|
+
* @param {string} tipoEnvio - Tipo de envío para logs
|
|
1162
|
+
* @returns {Object} Resultado del envío
|
|
1163
|
+
*/
|
|
1164
|
+
async _enviarMultipart(url, body, boundary, xml, tipoEnvio) {
|
|
1165
|
+
const retryConfig = getConfigSection('retry');
|
|
1166
|
+
const maxRetries = retryConfig?.maxRetries || 8;
|
|
1167
|
+
const initialDelay = retryConfig?.initialDelay || 2000;
|
|
1168
|
+
const backoffMultiplier = retryConfig?.backoffMultiplier || 1.8;
|
|
1169
|
+
let lastError = null;
|
|
1170
|
+
|
|
1171
|
+
// Calcular Content-Length según tipo de body
|
|
1172
|
+
const contentLength = Buffer.isBuffer(body)
|
|
1173
|
+
? body.length.toString()
|
|
1174
|
+
: Buffer.byteLength(body).toString();
|
|
1175
|
+
|
|
1176
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1177
|
+
const controller = new AbortController();
|
|
1178
|
+
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
1179
|
+
|
|
1180
|
+
try {
|
|
1181
|
+
if (attempt > 1) {
|
|
1182
|
+
const delay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt - 2), 15000);
|
|
1183
|
+
log.log(` 🔄 Reintento ${tipoEnvio} ${attempt}/${maxRetries} (delay: ${Math.round(delay/1000)}s)...`);
|
|
1184
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const response = await fetch(url, {
|
|
1188
|
+
method: 'POST',
|
|
1189
|
+
headers: {
|
|
1190
|
+
'Cookie': `TOKEN=${this.tokenSoap}`,
|
|
1191
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
1192
|
+
'User-Agent': 'Mozilla/4.0 ( compatible; PROG 1.0; Windows NT)',
|
|
1193
|
+
'Accept': '*/*',
|
|
1194
|
+
'Connection': 'keep-alive',
|
|
1195
|
+
'Content-Length': contentLength,
|
|
1196
|
+
},
|
|
1197
|
+
body: body,
|
|
1198
|
+
signal: controller.signal,
|
|
1199
|
+
});
|
|
1200
|
+
clearTimeout(timeout);
|
|
1201
|
+
|
|
1202
|
+
const responseText = await response.text();
|
|
1203
|
+
log.log(` Respuesta ${tipoEnvio}:`, responseText);
|
|
1204
|
+
|
|
1205
|
+
const result = this._parsearRespuestaSoap(responseText, response.ok, response.status, tipoEnvio);
|
|
1206
|
+
|
|
1207
|
+
saveEnvioArtifacts({
|
|
1208
|
+
xml,
|
|
1209
|
+
responseText,
|
|
1210
|
+
responseOk: response.ok,
|
|
1211
|
+
responseStatus: response.status,
|
|
1212
|
+
trackId: result.trackId,
|
|
1213
|
+
ambiente: this.ambiente,
|
|
1214
|
+
tipoEnvio,
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
return result;
|
|
1218
|
+
} catch (fetchError) {
|
|
1219
|
+
clearTimeout(timeout);
|
|
1220
|
+
lastError = fetchError;
|
|
1221
|
+
|
|
1222
|
+
// Usar isRetryableError centralizado de utils
|
|
1223
|
+
if (isRetryableError(fetchError) && attempt < maxRetries) {
|
|
1224
|
+
log.log(` ⚠️ Error de conexión ${tipoEnvio} (${fetchError.cause?.code || 'socket'}), reintentando...`);
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
saveEnvioArtifacts({
|
|
1229
|
+
xml,
|
|
1230
|
+
responseText: `ERROR_FETCH: ${fetchError.message}`,
|
|
1231
|
+
responseOk: false,
|
|
1232
|
+
responseStatus: null,
|
|
1233
|
+
trackId: null,
|
|
1234
|
+
ambiente: this.ambiente,
|
|
1235
|
+
tipoEnvio,
|
|
1236
|
+
error: fetchError.cause || fetchError,
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
log.error('Fetch error details:', fetchError.cause || fetchError);
|
|
1240
|
+
throw fetchError;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
throw lastError || new Error(`${tipoEnvio} falló después de múltiples reintentos`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Alias para compatibilidad con código existente que usa _enviarMultipartBuffer
|
|
1249
|
+
* @deprecated Usar _enviarMultipart directamente
|
|
1250
|
+
*/
|
|
1251
|
+
async _enviarMultipartBuffer(url, body, boundary, xml, tipoEnvio) {
|
|
1252
|
+
return this._enviarMultipart(url, body, boundary, xml, tipoEnvio);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Parsear respuesta SOAP/CGI
|
|
1257
|
+
*/
|
|
1258
|
+
_parsearRespuestaSoap(responseText, responseOk, httpStatus, tipoEnvio) {
|
|
1259
|
+
if (!responseOk) {
|
|
1260
|
+
return {
|
|
1261
|
+
ok: false,
|
|
1262
|
+
error: `Error HTTP ${httpStatus}`,
|
|
1263
|
+
respuesta: responseText,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const statusMatch = responseText.match(/<STATUS>(\d+)<\/STATUS>/i);
|
|
1268
|
+
const trackIdMatch = responseText.match(/<TRACKID>(\d+)<\/TRACKID>/i);
|
|
1269
|
+
const fileMatch = responseText.match(/<FILE>([^<]*)<\/FILE>/i);
|
|
1270
|
+
|
|
1271
|
+
const status = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
1272
|
+
const trackId = trackIdMatch ? trackIdMatch[1] : null;
|
|
1273
|
+
const fileName = fileMatch ? fileMatch[1] : null;
|
|
1274
|
+
|
|
1275
|
+
if (status === 0 && trackId) {
|
|
1276
|
+
return {
|
|
1277
|
+
ok: true,
|
|
1278
|
+
status: status,
|
|
1279
|
+
trackId: trackId,
|
|
1280
|
+
archivo: fileName,
|
|
1281
|
+
mensaje: `✅ ${tipoEnvio} Enviado - TrackID: ${trackId}`,
|
|
1282
|
+
respuesta: responseText,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const errorMessages = {
|
|
1287
|
+
1: 'Error de autenticación',
|
|
1288
|
+
2: 'Error en RUT',
|
|
1289
|
+
3: 'Error en XML',
|
|
1290
|
+
4: 'Error de firma',
|
|
1291
|
+
5: 'Error de sistema',
|
|
1292
|
+
99: 'Error desconocido',
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
return {
|
|
1296
|
+
ok: false,
|
|
1297
|
+
status: status,
|
|
1298
|
+
error: errorMessages[status] || `Error del SII - Status: ${status}`,
|
|
1299
|
+
respuesta: responseText,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
module.exports = EnviadorSII;
|