@devlas/dte-sii 2.8.2 → 2.9.2
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/CafSolicitor.js +22 -2
- package/DTE.js +25 -15
- package/SiiPortalAuth.js +114 -1
- package/WsReclamo.js +434 -0
- package/index.js +8 -0
- package/package.json +1 -1
- package/utils/endpoints.js +87 -0
package/CafSolicitor.js
CHANGED
|
@@ -200,6 +200,11 @@ class CafSolicitor {
|
|
|
200
200
|
return { success: false, error: 'El SII devolvió página de autenticación' };
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// Detectar error de límite diario del SII
|
|
204
|
+
if (response.body && response.body.includes('menor o igual al m')) {
|
|
205
|
+
return { success: false, error: 'Límite diario de folios SII agotado (MAX_AUTOR). Reintenta mañana.' };
|
|
206
|
+
}
|
|
207
|
+
|
|
203
208
|
return { success: false, error: 'No se obtuvo CAF en la respuesta' };
|
|
204
209
|
|
|
205
210
|
} catch (err) {
|
|
@@ -233,11 +238,15 @@ class CafSolicitor {
|
|
|
233
238
|
// Selección de tipo de documento
|
|
234
239
|
if (currentHtml.includes('COD_DOCTO')) {
|
|
235
240
|
const selectInputs = SiiSession.extractInputValues(currentHtml);
|
|
241
|
+
// Respetar MAX_AUTOR que el SII informa en el formulario.
|
|
242
|
+
const maxAutorSelect = parseInt(selectInputs.MAX_AUTOR || '999', 10);
|
|
243
|
+
const cantSelect = Math.min(cantidad, maxAutorSelect);
|
|
236
244
|
const selectFields = {
|
|
237
245
|
...selectInputs,
|
|
238
246
|
RUT_EMP: rut,
|
|
239
247
|
DV_EMP: dv,
|
|
240
248
|
COD_DOCTO: tipoDte,
|
|
249
|
+
CANT_DOCTOS: cantSelect,
|
|
241
250
|
};
|
|
242
251
|
|
|
243
252
|
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios_dcto', selectFields);
|
|
@@ -262,12 +271,15 @@ class CafSolicitor {
|
|
|
262
271
|
const formAction3 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
263
272
|
const inputs3 = SiiSession.extractInputValues(currentHtml);
|
|
264
273
|
|
|
274
|
+
// Respetar MAX_AUTOR si viene en este paso también
|
|
275
|
+
const maxAutorStep3 = parseInt(inputs3.MAX_AUTOR || '999', 10);
|
|
276
|
+
const cantStep3 = Math.min(cantidad, maxAutorStep3);
|
|
265
277
|
const step3Fields = {
|
|
266
278
|
...inputs3,
|
|
267
279
|
RUT_EMP: rut,
|
|
268
280
|
DV_EMP: dv,
|
|
269
281
|
COD_DOCTO: tipoDte,
|
|
270
|
-
CANT_DOCTOS:
|
|
282
|
+
CANT_DOCTOS: cantStep3,
|
|
271
283
|
ACEPTAR: 'Solicitar Numeración',
|
|
272
284
|
};
|
|
273
285
|
|
|
@@ -321,9 +333,17 @@ class CafSolicitor {
|
|
|
321
333
|
|
|
322
334
|
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_folio';
|
|
323
335
|
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
324
|
-
|
|
336
|
+
|
|
337
|
+
// Respetar el máximo autorizado por el SII (puede ser menor a lo solicitado)
|
|
338
|
+
const maxAutor = parseInt(inputs.MAX_AUTOR || '999', 10);
|
|
339
|
+
const folioIni = parseInt(inputs.FOLIO_INI || '1', 10);
|
|
340
|
+
const cantOriginal = parseInt(inputs.CANT_DOCTOS || '1', 10);
|
|
341
|
+
const cantReal = Math.min(cantOriginal, maxAutor);
|
|
342
|
+
|
|
325
343
|
const fields = {
|
|
326
344
|
...inputs,
|
|
345
|
+
CANT_DOCTOS: String(cantReal),
|
|
346
|
+
FOLIO_FIN: String(folioIni + cantReal - 1),
|
|
327
347
|
ACEPTAR: 'Obtener Folios',
|
|
328
348
|
};
|
|
329
349
|
|
package/DTE.js
CHANGED
|
@@ -71,8 +71,9 @@ class DTE {
|
|
|
71
71
|
|
|
72
72
|
_convertirDatosSimplificados(d) {
|
|
73
73
|
const esExenta = d.tipo === 41;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
74
|
+
const precioConIva = d.precioConIva === true;
|
|
75
|
+
const { detalle, mntNeto, mntExento } = this._procesarItems(d.items, esExenta, precioConIva);
|
|
76
|
+
const totales = this._calcularTotales(mntNeto, mntExento, esExenta);
|
|
76
77
|
|
|
77
78
|
const resultado = {
|
|
78
79
|
Encabezado: {
|
|
@@ -86,6 +87,7 @@ class DTE {
|
|
|
86
87
|
Receptor: {
|
|
87
88
|
RUTRecep: d.receptor?.RUTRecep ?? '66666666-6',
|
|
88
89
|
RznSocRecep: sanitizeSiiText(d.receptor?.RznSocRecep ?? 'Consumidor Final'),
|
|
90
|
+
...(d.receptor?.GiroRecep ? { GiroRecep: d.receptor.GiroRecep } : {}),
|
|
89
91
|
DirRecep: sanitizeSiiText(d.receptor?.DirRecep ?? 'Sin Direccion'),
|
|
90
92
|
CmnaRecep: d.receptor?.CmnaRecep ?? 'Santiago',
|
|
91
93
|
},
|
|
@@ -105,24 +107,29 @@ class DTE {
|
|
|
105
107
|
return resultado;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
_procesarItems(items, esExenta) {
|
|
109
|
-
let
|
|
110
|
+
_procesarItems(items, esExenta, precioConIva = false) {
|
|
111
|
+
let mntNeto = 0;
|
|
110
112
|
let mntExento = 0;
|
|
111
113
|
|
|
112
114
|
const detalle = items.map((item, idx) => {
|
|
113
115
|
const qty = item.QtyItem || 1;
|
|
114
|
-
const
|
|
116
|
+
const esItemExento = esExenta || item.IndExe === 1;
|
|
117
|
+
// Si precioConIva=true y el ítem no es exento, el PrcItem viene con IVA incluido
|
|
118
|
+
// (precio al consumidor del POS) → convertir a neto dividiendo por 1+TASA_IVA.
|
|
119
|
+
const prc = (precioConIva && !esItemExento)
|
|
120
|
+
? Math.round(item.PrcItem / (1 + TASA_IVA / 100))
|
|
121
|
+
: item.PrcItem;
|
|
115
122
|
const montoItem = Math.round(qty * prc);
|
|
116
123
|
|
|
117
|
-
if (
|
|
124
|
+
if (esItemExento) {
|
|
118
125
|
mntExento += montoItem;
|
|
119
126
|
} else {
|
|
120
|
-
|
|
127
|
+
mntNeto += montoItem;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
const det = {
|
|
124
131
|
NroLinDet: idx + 1,
|
|
125
|
-
...(
|
|
132
|
+
...(esItemExento ? { IndExe: 1 } : {}),
|
|
126
133
|
NmbItem: sanitizeSiiText(item.NmbItem),
|
|
127
134
|
QtyItem: qty,
|
|
128
135
|
...(item.UnmdItem ? { UnmdItem: item.UnmdItem } : {}),
|
|
@@ -133,18 +140,21 @@ class DTE {
|
|
|
133
140
|
return det;
|
|
134
141
|
});
|
|
135
142
|
|
|
136
|
-
return { detalle,
|
|
143
|
+
return { detalle, mntNeto, mntExento };
|
|
137
144
|
}
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
146
|
+
// PrcItem = precio neto unitario (o precio con IVA si se usa precioConIva:true).
|
|
147
|
+
// IVA = mntNeto * TasaIVA.
|
|
148
|
+
_calcularTotales(mntNeto, mntExento, esExenta) {
|
|
149
|
+
const neto = esExenta ? 0 : mntNeto;
|
|
150
|
+
const iva = esExenta ? 0 : Math.round(neto * TASA_IVA / 100);
|
|
151
|
+
const mntTotal = neto + iva + mntExento;
|
|
143
152
|
|
|
144
153
|
const totales = {};
|
|
145
|
-
if (
|
|
154
|
+
if (neto > 0) totales.MntNeto = neto;
|
|
155
|
+
if (neto > 0) totales.TasaIVA = TASA_IVA; // requerido en facturas
|
|
146
156
|
if (mntExento > 0) totales.MntExe = mntExento;
|
|
147
|
-
if (
|
|
157
|
+
if (neto > 0) totales.IVA = iva;
|
|
148
158
|
totales.MntTotal = mntTotal;
|
|
149
159
|
|
|
150
160
|
return totales;
|
package/SiiPortalAuth.js
CHANGED
|
@@ -121,7 +121,9 @@ class SiiPortalAuth {
|
|
|
121
121
|
if (cookieStr) options.headers['Cookie'] = cookieStr;
|
|
122
122
|
|
|
123
123
|
if (body) {
|
|
124
|
-
options.headers['Content-Type']
|
|
124
|
+
if (!options.headers['Content-Type']) {
|
|
125
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
126
|
+
}
|
|
125
127
|
options.headers['Content-Length'] = Buffer.byteLength(body);
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -493,6 +495,117 @@ if (!fs.existsSync(SESSION_CACHE_PATH)) {
|
|
|
493
495
|
fecha_autorizacion: datos['Fecha Autorización'] || null,
|
|
494
496
|
};
|
|
495
497
|
}
|
|
498
|
+
|
|
499
|
+
// ─── Consemitidos (www4.sii.cl/consemitidosinternetui) ───────────────────────
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Navega a consemitidosinternetui para obtener el TOKEN de sesión.
|
|
503
|
+
* El TOKEN es el mismo valor que CSESSIONID y va como conversationId en el body.
|
|
504
|
+
* @private
|
|
505
|
+
*/
|
|
506
|
+
async _obtenerTokenConsemitidos(cookieJar) {
|
|
507
|
+
await this._request('https://www4.sii.cl/consemitidosinternetui/', { cookieJar });
|
|
508
|
+
const token = cookieJar['TOKEN'] || cookieJar['CSESSIONID'];
|
|
509
|
+
if (!token) {
|
|
510
|
+
throw new Error('SiiPortalAuth: no se pudo obtener TOKEN de sesión de consemitidosinternetui');
|
|
511
|
+
}
|
|
512
|
+
return token;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Llama a un endpoint JSON de la API consemitidosinternetui.
|
|
517
|
+
* @private
|
|
518
|
+
*/
|
|
519
|
+
async _callConsemitidos(method, data, token, cookieJar) {
|
|
520
|
+
const body = JSON.stringify({
|
|
521
|
+
metaData: {
|
|
522
|
+
namespace: `cl.sii.sdi.lob.diii.consemitidos.data.api.interfaces.FacadeService/${method}`,
|
|
523
|
+
conversationId: token,
|
|
524
|
+
transactionId: crypto.randomUUID(),
|
|
525
|
+
page: null,
|
|
526
|
+
},
|
|
527
|
+
data,
|
|
528
|
+
});
|
|
529
|
+
const res = await this._request(
|
|
530
|
+
`https://www4.sii.cl/consemitidosinternetui/services/data/facadeService/${method}`,
|
|
531
|
+
{
|
|
532
|
+
method: 'POST',
|
|
533
|
+
body,
|
|
534
|
+
cookieJar,
|
|
535
|
+
headers: {
|
|
536
|
+
'Content-Type': 'application/json',
|
|
537
|
+
'Accept': 'application/json, text/plain, */*',
|
|
538
|
+
'Origin': 'https://www4.sii.cl',
|
|
539
|
+
'Referer': 'https://www4.sii.cl/consemitidosinternetui/',
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
try {
|
|
544
|
+
return JSON.parse(res.body);
|
|
545
|
+
} catch {
|
|
546
|
+
throw new Error(`SiiPortalAuth: respuesta no-JSON de ${method}: ${res.body.slice(0, 300)}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Obtiene el detalle de DTEs emitidos o recibidos desde www4.sii.cl.
|
|
552
|
+
*
|
|
553
|
+
* @param {string} rut - RUT sin DV (ej: "78206276")
|
|
554
|
+
* @param {string} dv - DV (ej: "K")
|
|
555
|
+
* @param {string} periodo - Período YYYY-MM (ej: "2026-05")
|
|
556
|
+
* @param {number} operacion - 1 = compras / recibidos, 2 = ventas / emitidos
|
|
557
|
+
* @param {Object} [cookieJar] - Sesión ya autenticada (opcional)
|
|
558
|
+
* @returns {Promise<{ resumen: Array, detalles: Array }>}
|
|
559
|
+
*/
|
|
560
|
+
async obtenerDetalleDtes(rut, dv, periodo, operacion = 2, cookieJar = null) {
|
|
561
|
+
const jar = cookieJar || await this.autenticar();
|
|
562
|
+
const token = await this._obtenerTokenConsemitidos(jar);
|
|
563
|
+
|
|
564
|
+
// Mapeo de convención interna → convención SII:
|
|
565
|
+
// interno: 1 = compras/recibidos, 2 = ventas/emitidos
|
|
566
|
+
// SII: 1 = emitidos, 2 = recibidos
|
|
567
|
+
const siiOperacion = operacion === 2 ? 1 : 2;
|
|
568
|
+
const esEmitidos = siiOperacion === 1;
|
|
569
|
+
|
|
570
|
+
// 1. Resumen mensual — saber qué tipos de DTE hay en el período
|
|
571
|
+
const resumenResp = await this._callConsemitidos('getResumen', {
|
|
572
|
+
periodo,
|
|
573
|
+
rutContribuyente: rut,
|
|
574
|
+
dvContribuyente: dv,
|
|
575
|
+
operacion: siiOperacion,
|
|
576
|
+
}, token, jar);
|
|
577
|
+
|
|
578
|
+
const resumen = resumenResp.data?.resumenDte ?? [];
|
|
579
|
+
if (resumen.length === 0) return { resumen: [], detalles: [] };
|
|
580
|
+
|
|
581
|
+
// 2. Detalle por cada tipo DTE encontrado en el resumen
|
|
582
|
+
const detallesArr = await Promise.all(
|
|
583
|
+
resumen.map(async (t) => {
|
|
584
|
+
// Para emitidos tipo 33/34, el SII expone un método específico
|
|
585
|
+
const metodo = esEmitidos && (t.tipoDoc === 33 || t.tipoDoc === 34)
|
|
586
|
+
? 'getDetalleEmitidos3334'
|
|
587
|
+
: 'getDetalleRecibidos';
|
|
588
|
+
const resp = await this._callConsemitidos(metodo, {
|
|
589
|
+
tipoDoc: String(t.tipoDoc),
|
|
590
|
+
rut,
|
|
591
|
+
dv,
|
|
592
|
+
periodo,
|
|
593
|
+
operacion: siiOperacion,
|
|
594
|
+
derrCodigo: String(t.tipoDoc),
|
|
595
|
+
refNCD: '0',
|
|
596
|
+
}, token, jar);
|
|
597
|
+
const items = resp.dataResp?.detalles ?? [];
|
|
598
|
+
// Completar tipoDoc y tipoDocDesc desde el resumen si no vienen en el detalle
|
|
599
|
+
return items.map((d) => ({
|
|
600
|
+
...d,
|
|
601
|
+
tipoDoc: t.tipoDoc,
|
|
602
|
+
tipoDocDesc: d.descTipoDoc || t.tipoDocDesc,
|
|
603
|
+
}));
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
return { resumen, detalles: detallesArr.flat() };
|
|
608
|
+
}
|
|
496
609
|
}
|
|
497
610
|
|
|
498
611
|
module.exports = SiiPortalAuth;
|
package/WsReclamo.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* WsReclamo.js — Cliente SOAP para el WS de Aceptación/Reclamo de DTEs del SII
|
|
5
|
+
*
|
|
6
|
+
* Implementa el "WS Consulta y Registro de Aceptación/Reclamo a DTE recibido" v1.2
|
|
7
|
+
* Fuente oficial: https://www.sii.cl/factura_electronica/factura_mercado/WSREGISTRORECLAMODTESERVICIO.pdf
|
|
8
|
+
*
|
|
9
|
+
* Métodos expuestos:
|
|
10
|
+
* listarEventosHistDoc(rutEmisor, dvEmisor, tipoDoc, folio) → { codResp, descResp, eventos[] }
|
|
11
|
+
* consultarEstadoReceptor(rutEmisor, dvEmisor, tipoDoc, folio) → 'sin_accion'|'aceptada'|'tacita'|'reclamada'
|
|
12
|
+
* ingresarAceptacion(rutEmisor, dvEmisor, tipoDoc, folio, accion) → { codResp, descResp }
|
|
13
|
+
*
|
|
14
|
+
* Autenticación: TOKEN SII vía flujo seed/firma SOAP (mismo que EnviadorSII.getTokenSoap).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const forge = require('node-forge');
|
|
18
|
+
const {
|
|
19
|
+
SOAP_ENDPOINTS,
|
|
20
|
+
WSRECLAMO_ENDPOINTS,
|
|
21
|
+
validateAmbiente,
|
|
22
|
+
createScopedLogger,
|
|
23
|
+
getCachedToken,
|
|
24
|
+
setCachedToken,
|
|
25
|
+
extractTagContent,
|
|
26
|
+
decodeXmlEntities,
|
|
27
|
+
parseXmlNoNs,
|
|
28
|
+
siiError,
|
|
29
|
+
ERROR_CODES,
|
|
30
|
+
} = require('./utils');
|
|
31
|
+
|
|
32
|
+
const log = createScopedLogger('WsReclamo');
|
|
33
|
+
|
|
34
|
+
// ─── Acciones válidas para ingresarAceptacionReclamoDoc ───────────────────────
|
|
35
|
+
/** @typedef {'ACD'|'ERM'|'RCD'|'RFP'|'RFT'} AccionReclamo */
|
|
36
|
+
|
|
37
|
+
// ─── Mapeo codEvento → estado normalizado ────────────────────────────────────
|
|
38
|
+
// Mapeo de codEvento WSRECLAMO → estado receptor
|
|
39
|
+
// 'tacita' NO es un evento del WS — es una presunción legal cuando pasan 8 días
|
|
40
|
+
// sin eventos (codResp=16). Se calcula en capa de negocio, no aquí.
|
|
41
|
+
const ESTADO_POR_EVENTO = {
|
|
42
|
+
ACD: 'aceptada', // Acepta Contenido del Documento (explícito)
|
|
43
|
+
ERM: 'acuse_recibo', // Otorga Recibo de Mercaderías/Servicios (explícito)
|
|
44
|
+
RCD: 'reclamada', // Reclamo al Contenido del Documento
|
|
45
|
+
RFP: 'reclamada', // Reclamo por Falta Parcial de Mercaderías
|
|
46
|
+
RFT: 'reclamada', // Reclamo por Falta Total de Mercaderías
|
|
47
|
+
NCA: 'sin_accion', // NC de anulación que referencia el doc (no registrable por WS)
|
|
48
|
+
ENC: 'sin_accion', // NC distinta de anulación que referencia el doc (no registrable)
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
class WsReclamo {
|
|
52
|
+
/**
|
|
53
|
+
* @param {Object} certificado — instancia de Certificado con privateKey y cert
|
|
54
|
+
* @param {string} ambiente — 'certificacion' | 'produccion'
|
|
55
|
+
* @param {Object} [options]
|
|
56
|
+
* @param {boolean} [options.useTokenCache=true]
|
|
57
|
+
*/
|
|
58
|
+
constructor(certificado, ambiente, options = {}) {
|
|
59
|
+
if (!certificado) throw new Error('WsReclamo: certificado es obligatorio');
|
|
60
|
+
this.certificado = certificado;
|
|
61
|
+
this.ambiente = validateAmbiente(ambiente);
|
|
62
|
+
this.useTokenCache = options.useTokenCache !== false;
|
|
63
|
+
|
|
64
|
+
this.rutCert = certificado.rut || 'unknown';
|
|
65
|
+
|
|
66
|
+
this._tokenSoap = null;
|
|
67
|
+
|
|
68
|
+
// URLs
|
|
69
|
+
this._seedUrl = SOAP_ENDPOINTS[this.ambiente].seed;
|
|
70
|
+
this._tokenUrl = SOAP_ENDPOINTS[this.ambiente].token;
|
|
71
|
+
this._wsUrl = WSRECLAMO_ENDPOINTS[this.ambiente].replace('?wsdl', '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
75
|
+
// AUTENTICACIÓN — mismo flujo que EnviadorSII.getTokenSoap
|
|
76
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
/** Obtiene semilla del servicio SOAP del SII */
|
|
79
|
+
async _getSemilla() {
|
|
80
|
+
const envelope = `<?xml version="1.0" encoding="UTF-8"?>
|
|
81
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
|
82
|
+
<soapenv:Body><getSeed/></soapenv:Body>
|
|
83
|
+
</soapenv:Envelope>`;
|
|
84
|
+
|
|
85
|
+
const res = await fetch(this._seedUrl, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '' },
|
|
88
|
+
body: envelope,
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) throw siiError(`Error semilla: ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
91
|
+
|
|
92
|
+
const xml = await res.text();
|
|
93
|
+
const semilla = extractTagContent(decodeXmlEntities(xml), 'SEMILLA');
|
|
94
|
+
if (!semilla) throw siiError('No se obtuvo semilla del SII', ERROR_CODES.SII_INVALID_RESPONSE);
|
|
95
|
+
return semilla;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Firma la semilla y obtiene el TOKEN SOAP */
|
|
99
|
+
async _fetchTokenSoap() {
|
|
100
|
+
const semilla = await this._getSemilla();
|
|
101
|
+
const xmlFirmado = this._firmarSemilla(semilla);
|
|
102
|
+
|
|
103
|
+
const escaped = xmlFirmado
|
|
104
|
+
.replace(/&/g, '&')
|
|
105
|
+
.replace(/</g, '<')
|
|
106
|
+
.replace(/>/g, '>');
|
|
107
|
+
|
|
108
|
+
const envelope = `<?xml version="1.0" encoding="UTF-8"?>
|
|
109
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
|
110
|
+
<soapenv:Body>
|
|
111
|
+
<getToken>
|
|
112
|
+
<pszXml>${escaped}</pszXml>
|
|
113
|
+
</getToken>
|
|
114
|
+
</soapenv:Body>
|
|
115
|
+
</soapenv:Envelope>`;
|
|
116
|
+
|
|
117
|
+
const res = await fetch(this._tokenUrl, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '' },
|
|
120
|
+
body: envelope,
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) throw siiError(`Error token: ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
123
|
+
|
|
124
|
+
const xml = await res.text();
|
|
125
|
+
const decoded = xml.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
126
|
+
const token = extractTagContent(decoded, 'TOKEN');
|
|
127
|
+
if (!token) throw siiError('No se obtuvo TOKEN del SII', ERROR_CODES.SII_AUTH_FAILED);
|
|
128
|
+
|
|
129
|
+
return token;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Obtiene o reutiliza el token SOAP (con cache opcional) */
|
|
133
|
+
async _ensureToken() {
|
|
134
|
+
if (this.useTokenCache) {
|
|
135
|
+
const cached = getCachedToken(this.ambiente, 'soap', this.rutCert);
|
|
136
|
+
if (cached) {
|
|
137
|
+
this._tokenSoap = cached;
|
|
138
|
+
return cached;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const token = await this._fetchTokenSoap();
|
|
143
|
+
this._tokenSoap = token;
|
|
144
|
+
|
|
145
|
+
if (this.useTokenCache) {
|
|
146
|
+
setCachedToken(this.ambiente, 'soap', this.rutCert, token);
|
|
147
|
+
}
|
|
148
|
+
return token;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Invalida el token cacheado para forzar renovación */
|
|
152
|
+
invalidarToken() {
|
|
153
|
+
this._tokenSoap = null;
|
|
154
|
+
// invalidateToken no está disponible en utils — limpiamos el caché con un token vacío
|
|
155
|
+
// o simplemente seteamos nulo; el cache expira por TTL
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
// FIRMA DE SEMILLA — idéntico a EnviadorSII._crearXMLSemilla
|
|
160
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
_firmarSemilla(semilla) {
|
|
163
|
+
const xmlContent = `<getToken><item><Semilla>${semilla}</Semilla></item></getToken>`;
|
|
164
|
+
|
|
165
|
+
const md = forge.md.sha1.create();
|
|
166
|
+
md.update(xmlContent, 'utf8');
|
|
167
|
+
const digestValue = forge.util.encode64(md.digest().bytes());
|
|
168
|
+
|
|
169
|
+
const signedInfoParaFirmar = [
|
|
170
|
+
'<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">',
|
|
171
|
+
'<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>',
|
|
172
|
+
'<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod>',
|
|
173
|
+
'<Reference URI=""><Transforms>',
|
|
174
|
+
'<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform>',
|
|
175
|
+
'</Transforms>',
|
|
176
|
+
'<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod>',
|
|
177
|
+
`<DigestValue>${digestValue}</DigestValue>`,
|
|
178
|
+
'</Reference></SignedInfo>',
|
|
179
|
+
].join('');
|
|
180
|
+
|
|
181
|
+
const mdSign = forge.md.sha1.create();
|
|
182
|
+
mdSign.update(signedInfoParaFirmar, 'utf8');
|
|
183
|
+
const signature = this.certificado.privateKey.sign(mdSign);
|
|
184
|
+
const signatureValue = this._wordwrap(forge.util.encode64(signature), 64);
|
|
185
|
+
|
|
186
|
+
const modulus = this._wordwrap(this.certificado.getModulus(), 64);
|
|
187
|
+
const exponent = this.certificado.getExponent();
|
|
188
|
+
const cert = this._wordwrap(this.certificado.getCertificateBase64(), 64);
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
192
|
+
`<getToken><item><Semilla>${semilla}</Semilla></item>`,
|
|
193
|
+
'<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">',
|
|
194
|
+
'<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">',
|
|
195
|
+
'<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>',
|
|
196
|
+
'<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>',
|
|
197
|
+
'<Reference URI=""><Transforms>',
|
|
198
|
+
'<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>',
|
|
199
|
+
'</Transforms>',
|
|
200
|
+
'<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>',
|
|
201
|
+
`<DigestValue>${digestValue}</DigestValue>`,
|
|
202
|
+
'</Reference></SignedInfo>',
|
|
203
|
+
`<SignatureValue>${signatureValue}</SignatureValue>`,
|
|
204
|
+
'<KeyInfo><KeyValue><RSAKeyValue>',
|
|
205
|
+
`<Modulus>${modulus}</Modulus>`,
|
|
206
|
+
`<Exponent>${exponent}</Exponent>`,
|
|
207
|
+
'</RSAKeyValue></KeyValue>',
|
|
208
|
+
`<X509Data><X509Certificate>${cert}</X509Certificate></X509Data>`,
|
|
209
|
+
'</KeyInfo></Signature></getToken>',
|
|
210
|
+
].join('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_wordwrap(str, width) {
|
|
214
|
+
const lines = [];
|
|
215
|
+
for (let i = 0; i < str.length; i += width) {
|
|
216
|
+
lines.push(str.substring(i, i + width));
|
|
217
|
+
}
|
|
218
|
+
return lines.join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
222
|
+
// LLAMADAS SOAP AL WSRECLAMO
|
|
223
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
224
|
+
|
|
225
|
+
// Namespace oficial del servicio (fuente: WSDL producción/certificación)
|
|
226
|
+
static NS = 'http://ws.registroreclamodte.diii.sdi.sii.cl';
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Realiza una llamada SOAP al WSRECLAMO con autenticación via TOKEN cookie.
|
|
230
|
+
* Namespace verificado desde: https://ws2.sii.cl/WSREGISTRORECLAMODTECERT/registroreclamodteservice?wsdl
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
async _llamar(metodo, params, reintentar = true) {
|
|
234
|
+
const token = await this._ensureToken();
|
|
235
|
+
const ns = WsReclamo.NS;
|
|
236
|
+
|
|
237
|
+
const innerXml = Object.entries(params)
|
|
238
|
+
.map(([k, v]) => `<${k}>${v}</${k}>`)
|
|
239
|
+
.join('');
|
|
240
|
+
|
|
241
|
+
// El body usa el prefijo ws: con el namespace del servicio
|
|
242
|
+
const envelope = `<?xml version="1.0" encoding="UTF-8"?>
|
|
243
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="${ns}">
|
|
244
|
+
<soapenv:Header/>
|
|
245
|
+
<soapenv:Body>
|
|
246
|
+
<ws:${metodo}>${innerXml}</ws:${metodo}>
|
|
247
|
+
</soapenv:Body>
|
|
248
|
+
</soapenv:Envelope>`;
|
|
249
|
+
|
|
250
|
+
const res = await fetch(this._wsUrl, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: {
|
|
253
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
254
|
+
SOAPAction: '',
|
|
255
|
+
Cookie: `TOKEN=${token}`,
|
|
256
|
+
},
|
|
257
|
+
body: envelope,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Si el SII devuelve 403/401, el token puede haber expirado — reintentar una vez
|
|
261
|
+
if ((res.status === 401 || res.status === 403) && reintentar) {
|
|
262
|
+
log.log(`[WsReclamo] Token expirado (${res.status}), renovando...`);
|
|
263
|
+
this._tokenSoap = null;
|
|
264
|
+
return this._llamar(metodo, params, false);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!res.ok) {
|
|
268
|
+
throw siiError(`WSRECLAMO ${metodo}: HTTP ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const xml = await res.text();
|
|
272
|
+
return xml;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Lista todos los eventos históricos de aceptación/reclamo para un DTE.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} rutEmisor — RUT sin DV ni puntos, ej: '76354771'
|
|
279
|
+
* @param {string} dvEmisor — DV del RUT, ej: 'K'
|
|
280
|
+
* @param {number} tipoDoc — Tipo de DTE, ej: 33 (factura afecta)
|
|
281
|
+
* @param {number} folio — Número de folio
|
|
282
|
+
* @returns {Promise<{codResp: number, descResp: string, eventos: Array<{codEvento: string, descEvento: string, rutResponsable: string, dvResponsable: string, fechaEvento: string}>}>}
|
|
283
|
+
*/
|
|
284
|
+
async listarEventosHistDoc(rutEmisor, dvEmisor, tipoDoc, folio) {
|
|
285
|
+
log.log(`[WsReclamo] listarEventosHistDoc RUT=${rutEmisor}-${dvEmisor} tipo=${tipoDoc} folio=${folio}`);
|
|
286
|
+
|
|
287
|
+
const xml = await this._llamar('listarEventosHistDoc', {
|
|
288
|
+
rutEmisor,
|
|
289
|
+
dvEmisor,
|
|
290
|
+
tipoDoc,
|
|
291
|
+
folio,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return this._parsearRespuestaEventos(xml);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Consulta el estado resumido del receptor para un DTE emitido.
|
|
299
|
+
* Devuelve el estado derivado del evento más reciente.
|
|
300
|
+
*
|
|
301
|
+
* Códigos relevantes de listarEventosHistDoc (doc SII v1.2):
|
|
302
|
+
* 15 = Listado de eventos del documento (hay eventos)
|
|
303
|
+
* 16 = Documento no presenta eventos de reclamos o acuse de recibo
|
|
304
|
+
* 18 = Documento no ha sido recibido por el receptor
|
|
305
|
+
*
|
|
306
|
+
* @returns {Promise<'sin_accion'|'aceptada'|'tacita'|'reclamada'>}
|
|
307
|
+
*/
|
|
308
|
+
async consultarEstadoReceptor(rutEmisor, dvEmisor, tipoDoc, folio) {
|
|
309
|
+
const { codResp, eventos } = await this.listarEventosHistDoc(rutEmisor, dvEmisor, tipoDoc, folio);
|
|
310
|
+
|
|
311
|
+
// 16 = sin eventos de reclamo/acuse | 18 = no recibido aún
|
|
312
|
+
if (codResp === 16 || codResp === 18) {
|
|
313
|
+
return 'sin_accion';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 15 = hay eventos — tomar el más reciente
|
|
317
|
+
if (codResp === 15 && eventos && eventos.length > 0) {
|
|
318
|
+
const ultimo = eventos[eventos.length - 1];
|
|
319
|
+
return ESTADO_POR_EVENTO[ultimo.codEvento] ?? 'sin_accion';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return 'sin_accion';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Ingresa una acción de aceptación o reclamo sobre un DTE recibido.
|
|
327
|
+
* Uso típico: el receptor registra su decisión.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} rutEmisor
|
|
330
|
+
* @param {string} dvEmisor
|
|
331
|
+
* @param {number} tipoDoc
|
|
332
|
+
* @param {number} folio
|
|
333
|
+
* @param {AccionReclamo} accionDoc — 'ACD'|'ERM'|'RCD'|'RFP'|'RFT'
|
|
334
|
+
* @returns {Promise<{codResp: number, descResp: string}>}
|
|
335
|
+
*/
|
|
336
|
+
async ingresarAceptacion(rutEmisor, dvEmisor, tipoDoc, folio, accionDoc) {
|
|
337
|
+
const ACCIONES_VALIDAS = ['ACD', 'ERM', 'RCD', 'RFP', 'RFT'];
|
|
338
|
+
if (!ACCIONES_VALIDAS.includes(accionDoc)) {
|
|
339
|
+
throw new Error(`WsReclamo: accionDoc inválida '${accionDoc}'. Debe ser una de: ${ACCIONES_VALIDAS.join(', ')}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
log.log(`[WsReclamo] ingresarAceptacion RUT=${rutEmisor}-${dvEmisor} tipo=${tipoDoc} folio=${folio} accion=${accionDoc}`);
|
|
343
|
+
|
|
344
|
+
const xml = await this._llamar('ingresarAceptacionReclamoDoc', {
|
|
345
|
+
rutEmisor,
|
|
346
|
+
dvEmisor,
|
|
347
|
+
tipoDoc,
|
|
348
|
+
folio,
|
|
349
|
+
accionDoc,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return this._parsearRespuestaSimple(xml);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
356
|
+
// PARSERS XML
|
|
357
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Parsea respuesta de listarEventosHistDoc.
|
|
361
|
+
*
|
|
362
|
+
* Estructura real (verificada en doc SII v1.2 y SoapUI screenshot):
|
|
363
|
+
* <return>
|
|
364
|
+
* <codResp>15</codResp>
|
|
365
|
+
* <descResp>Listado de eventos del documento</descResp>
|
|
366
|
+
* <listaEventosDoc>
|
|
367
|
+
* <codEvento>ACD</codEvento>
|
|
368
|
+
* <descEvento>Acepta Contenido del Documento</descEvento>
|
|
369
|
+
* <rutResponsable>45000055</rutResponsable>
|
|
370
|
+
* <dvResponsable>8</dvResponsable>
|
|
371
|
+
* <fechaEvento>29-12-2016 12:05:36</fechaEvento>
|
|
372
|
+
* </listaEventosDoc>
|
|
373
|
+
* </return>
|
|
374
|
+
*/
|
|
375
|
+
_parsearRespuestaEventos(xml) {
|
|
376
|
+
// Decode HTML entities del wrapper SOAP
|
|
377
|
+
const decoded = xml
|
|
378
|
+
.replace(/</g, '<')
|
|
379
|
+
.replace(/>/g, '>')
|
|
380
|
+
.replace(/&/g, '&')
|
|
381
|
+
.replace(/"/g, '"');
|
|
382
|
+
|
|
383
|
+
// Extraer el bloque <return>...</return>
|
|
384
|
+
const returnMatch = decoded.match(/<return>([\s\S]*?)<\/return>/i);
|
|
385
|
+
const bloque = returnMatch ? returnMatch[1] : decoded;
|
|
386
|
+
|
|
387
|
+
const codResp = parseInt(extractTagContent(bloque, 'codResp') ?? '16', 10);
|
|
388
|
+
const descResp = extractTagContent(bloque, 'descResp') ?? '';
|
|
389
|
+
|
|
390
|
+
// Extraer cada <listaEventosDoc>...</listaEventosDoc>
|
|
391
|
+
const eventos = [];
|
|
392
|
+
const itemRegex = /<listaEventosDoc>([\s\S]*?)<\/listaEventosDoc>/gi;
|
|
393
|
+
let match;
|
|
394
|
+
while ((match = itemRegex.exec(bloque)) !== null) {
|
|
395
|
+
const item = match[1];
|
|
396
|
+
eventos.push({
|
|
397
|
+
codEvento: extractTagContent(item, 'codEvento') ?? '',
|
|
398
|
+
descEvento: extractTagContent(item, 'descEvento') ?? '',
|
|
399
|
+
rutResponsable: extractTagContent(item, 'rutResponsable') ?? '',
|
|
400
|
+
dvResponsable: extractTagContent(item, 'dvResponsable') ?? '',
|
|
401
|
+
fechaEvento: extractTagContent(item, 'fechaEvento') ?? '',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { codResp, descResp, eventos };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Parsea respuesta de ingresarAceptacionReclamoDoc y consultarDocDteCedible.
|
|
410
|
+
*
|
|
411
|
+
* Estructura:
|
|
412
|
+
* <return>
|
|
413
|
+
* <codResp>0</codResp>
|
|
414
|
+
* <descResp>Acción completada OK</descResp>
|
|
415
|
+
* </return>
|
|
416
|
+
*
|
|
417
|
+
* codResp 0 = OK (ingresarAceptacion)
|
|
418
|
+
*/
|
|
419
|
+
_parsearRespuestaSimple(xml) {
|
|
420
|
+
const decoded = xml
|
|
421
|
+
.replace(/</g, '<')
|
|
422
|
+
.replace(/>/g, '>')
|
|
423
|
+
.replace(/&/g, '&');
|
|
424
|
+
|
|
425
|
+
const returnMatch = decoded.match(/<return>([\s\S]*?)<\/return>/i);
|
|
426
|
+
const bloque = returnMatch ? returnMatch[1] : decoded;
|
|
427
|
+
|
|
428
|
+
const codResp = parseInt(extractTagContent(bloque, 'codResp') ?? '-1', 10);
|
|
429
|
+
const descResp = extractTagContent(bloque, 'descResp') ?? '';
|
|
430
|
+
return { codResp, descResp };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = WsReclamo;
|
package/index.js
CHANGED
|
@@ -35,6 +35,9 @@ const LibroGuia = require('./LibroGuia');
|
|
|
35
35
|
// Helpers para certificación
|
|
36
36
|
const CertFolioHelper = require('./cert/CertFolioHelper');
|
|
37
37
|
|
|
38
|
+
// WS Aceptación/Reclamo DTE (v2.9.0)
|
|
39
|
+
const WsReclamo = require('./WsReclamo');
|
|
40
|
+
|
|
38
41
|
const utils = require('./utils');
|
|
39
42
|
|
|
40
43
|
// Re-exports de utilidades para compatibilidad
|
|
@@ -240,6 +243,11 @@ module.exports = {
|
|
|
240
243
|
// ─────────────────────────────────────────
|
|
241
244
|
CertFolioHelper,
|
|
242
245
|
|
|
246
|
+
// ─────────────────────────────────────────
|
|
247
|
+
// WS Aceptación/Reclamo DTE (v2.9.0)
|
|
248
|
+
// ─────────────────────────────────────────
|
|
249
|
+
WsReclamo,
|
|
250
|
+
|
|
243
251
|
// ─────────────────────────────────────────
|
|
244
252
|
// UTILIDADES (todo el namespace)
|
|
245
253
|
// ─────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.2",
|
|
4
4
|
"description": "Facturación y boletas electrónicas para el SII de Chile. Genera, timbra, firma y envía DTEs, libros electrónicos y automatiza la certificación.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "dte-sii.d.ts",
|
package/utils/endpoints.js
CHANGED
|
@@ -51,6 +51,89 @@ const SOAP_ENDPOINTS = {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Web Service SOAP oficial — Aceptación/Reclamo de DTE recibido
|
|
56
|
+
* Fuente: "WS Consulta y Registro de Aceptación/Reclamo a DTE recibido" v1.2, 07/08/2017, SII
|
|
57
|
+
*
|
|
58
|
+
* Métodos disponibles:
|
|
59
|
+
* ingresarAceptacionReclamoDoc(rutEmisor, dvEmisor, tipoDoc, folio, accionDoc)
|
|
60
|
+
* accionDoc: ACD | ERM | RCD | RFP | RFT
|
|
61
|
+
* listarEventosHistDoc(rutEmisor, dvEmisor, tipoDoc, folio)
|
|
62
|
+
* consultarDocDteCedible(rutEmisor, dvEmisor, tipoDoc, folio)
|
|
63
|
+
* consultarFechaRecepcionSii(rutEmisor, dvEmisor, tipoDoc, folio)
|
|
64
|
+
*
|
|
65
|
+
* Autenticación: token SII vía cookie `TOKEN=...` (mismo flujo seed/token de SOAP_ENDPOINTS,
|
|
66
|
+
* NO usa cookies NETSCAPE_LIVEWIRE del portal). Ver SiiSession.js getToken().
|
|
67
|
+
*
|
|
68
|
+
* Respuestas:
|
|
69
|
+
* ingresarAceptacionReclamoDoc → { codResp: number, descResp: string }
|
|
70
|
+
* listarEventosHistDoc → { codResp, descResp, listaEventosDoc: [{ codEvento, descEvento,
|
|
71
|
+
* rutResponsable, dvResponsable, fechaEvento }] }
|
|
72
|
+
* codEvento extra (solo en lista): NCA | ENC
|
|
73
|
+
* consultarDocDteCedible → { codResp: number, descResp: string }
|
|
74
|
+
* consultarFechaRecepcionSii → string VARCHAR(100), ej: '21-02-2017 19:02:03'
|
|
75
|
+
*/
|
|
76
|
+
const WSRECLAMO_ENDPOINTS = {
|
|
77
|
+
certificacion: 'https://ws2.sii.cl/WSREGISTRORECLAMODTECERT/registroreclamodteservice?wsdl',
|
|
78
|
+
produccion: 'https://ws1.sii.cl/WSREGISTRORECLAMODTE/registroreclamodteservice?wsdl',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Acciones válidas para ingresarAceptacionReclamoDoc
|
|
83
|
+
*/
|
|
84
|
+
const WSRECLAMO_ACCIONES = {
|
|
85
|
+
ACD: 'Acepta Contenido del Documento',
|
|
86
|
+
ERM: 'Otorga Recibo de Mercaderías o Servicios',
|
|
87
|
+
RCD: 'Reclamo al Contenido del Documento',
|
|
88
|
+
RFP: 'Reclamo por Falta Parcial de Mercaderías',
|
|
89
|
+
RFT: 'Reclamo por Falta Total de Mercaderías',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Códigos de respuesta compartidos (ingresarAceptacionReclamoDoc + listarEventosHistDoc)
|
|
94
|
+
* Fuente: WS v1.2, 07/08/2017, tablas de parámetros de salida
|
|
95
|
+
*/
|
|
96
|
+
const WSRECLAMO_CODIGOS = {
|
|
97
|
+
0: 'Acción Completada OK',
|
|
98
|
+
1: 'Rut Emisor Erróneo',
|
|
99
|
+
2: 'Número de Folio Erróneo',
|
|
100
|
+
3: 'Tipo de documento no corresponde (distinto de 33, 34, 43)',
|
|
101
|
+
4: 'Acción inválida',
|
|
102
|
+
5: 'DTE ya está reclamado por XXX (RFP, RFT o RCD)',
|
|
103
|
+
6: 'No se puede acusar recibo de mercadería de DTE previamente reclamado por XXX (RFP, RFT, RCD)',
|
|
104
|
+
7: 'Evento registrado previamente',
|
|
105
|
+
8: 'Pasados 8 días después de la recepción no es posible registrar reclamos o eventos',
|
|
106
|
+
9: 'No existen registros de acuerdo a los parámetros ingresados',
|
|
107
|
+
10: 'Documento no emitido y/o recibido en el SII desde el 14 de enero de 2017 en adelante',
|
|
108
|
+
11: 'No se puede reclamar DTE previamente aceptado',
|
|
109
|
+
12: 'No se puede dar por aceptado DTE previamente rechazado por XXX (RFP, RFT, RCD)',
|
|
110
|
+
13: 'No se puede reclamar DTE previamente registrado como acuso recibo mercadería',
|
|
111
|
+
14: 'Acción autorizada sólo para empresa receptora o emisora',
|
|
112
|
+
15: 'Listado de eventos del documento',
|
|
113
|
+
16: 'Documento no presenta eventos de reclamos o acuse de recibo',
|
|
114
|
+
17: 'Acción autorizada solo para empresa receptora',
|
|
115
|
+
18: 'Documento no ha sido recibido',
|
|
116
|
+
19: 'Reclamo de mercaderías ya realizado',
|
|
117
|
+
'-1': 'Error Interno — Rut Receptor debe reintentar más tarde',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Códigos de respuesta exclusivos de consultarDocDteCedible
|
|
122
|
+
* Fuente: WS v1.2, tabla parámetros de salida consultarDocDteCedible
|
|
123
|
+
*/
|
|
124
|
+
const WSRECLAMO_CODIGOS_CEDIBLE = {
|
|
125
|
+
1: 'Rut Emisor Erróneo',
|
|
126
|
+
2: 'Número de Folio Erróneo',
|
|
127
|
+
10: 'Documento no emitido y/o recibido en el SII desde el 14 de enero de 2017 en adelante',
|
|
128
|
+
18: 'Documento no ha sido recibido',
|
|
129
|
+
20: 'Tipo de documento no es cedible',
|
|
130
|
+
21: 'DTE No cedible — referenciado por nota de crédito de anulación del emisor dentro de los primeros 8 días',
|
|
131
|
+
22: 'No existe registro de reclamo o de recepción de mercadería o servicios',
|
|
132
|
+
23: 'DTE Cedible, sin reclamos',
|
|
133
|
+
24: 'DTE No Cedible — reclamado por el receptor',
|
|
134
|
+
25: 'DTE Cedible — habiendo pasado 8 días se entiende dado acuse de recibo',
|
|
135
|
+
};
|
|
136
|
+
|
|
54
137
|
/**
|
|
55
138
|
* Endpoints REST para Boletas Electrónicas
|
|
56
139
|
*/
|
|
@@ -213,6 +296,10 @@ module.exports = {
|
|
|
213
296
|
SOAP_ENDPOINTS,
|
|
214
297
|
REST_ENDPOINTS,
|
|
215
298
|
CERT_ENDPOINTS,
|
|
299
|
+
WSRECLAMO_ENDPOINTS,
|
|
300
|
+
WSRECLAMO_ACCIONES,
|
|
301
|
+
WSRECLAMO_CODIGOS,
|
|
302
|
+
WSRECLAMO_CODIGOS_CEDIBLE,
|
|
216
303
|
|
|
217
304
|
// Funciones
|
|
218
305
|
getHost,
|