@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.
Files changed (60) hide show
  1. package/BoletaService.js +109 -0
  2. package/CAF.js +173 -0
  3. package/CafSolicitor.js +380 -0
  4. package/Certificado.js +123 -0
  5. package/ConsumoFolio.js +376 -0
  6. package/DTE.js +399 -0
  7. package/EnviadorSII.js +1304 -0
  8. package/Envio.js +196 -0
  9. package/FolioRegistry.js +553 -0
  10. package/FolioService.js +703 -0
  11. package/LICENSE +27 -0
  12. package/LibroBase.js +134 -0
  13. package/LibroCompraVenta.js +205 -0
  14. package/LibroGuia.js +225 -0
  15. package/README.md +239 -0
  16. package/Signer.js +94 -0
  17. package/SiiCertificacion.js +1189 -0
  18. package/SiiPortalAuth.js +460 -0
  19. package/SiiSession.js +499 -0
  20. package/cert/BoletaCert.js +731 -0
  21. package/cert/CertFolioHelper.js +185 -0
  22. package/cert/CertRunner.js +2658 -0
  23. package/cert/ConfigLoader.js +133 -0
  24. package/cert/IntercambioCert.js +429 -0
  25. package/cert/LibroCompras.js +359 -0
  26. package/cert/LibroGuias.js +171 -0
  27. package/cert/LibroVentas.js +153 -0
  28. package/cert/MuestrasImpresas.js +676 -0
  29. package/cert/SetBase.js +321 -0
  30. package/cert/SetBasico.js +413 -0
  31. package/cert/SetCompra.js +472 -0
  32. package/cert/SetExenta.js +490 -0
  33. package/cert/SetGuia.js +283 -0
  34. package/cert/SetParser.js +1184 -0
  35. package/cert/SetsProvider.js +499 -0
  36. package/cert/Simulacion.js +521 -0
  37. package/cert/comunaOficina.js +460 -0
  38. package/cert/index.js +124 -0
  39. package/cert/types.js +330 -0
  40. package/dte-sii.d.ts +458 -0
  41. package/index.js +428 -0
  42. package/package.json +48 -0
  43. package/utils/c14n.js +275 -0
  44. package/utils/calculo.js +396 -0
  45. package/utils/config.js +276 -0
  46. package/utils/constants.js +302 -0
  47. package/utils/emisor.js +174 -0
  48. package/utils/endpoints.js +225 -0
  49. package/utils/error.js +235 -0
  50. package/utils/index.js +339 -0
  51. package/utils/logger.js +239 -0
  52. package/utils/pfx.js +203 -0
  53. package/utils/receptor.js +218 -0
  54. package/utils/referencia.js +169 -0
  55. package/utils/resolucion.js +119 -0
  56. package/utils/rut.js +169 -0
  57. package/utils/sanitize.js +124 -0
  58. package/utils/tokenCache.js +214 -0
  59. package/utils/xml.js +358 -0
  60. package/utils.js +4 -0
package/utils/pfx.js ADDED
@@ -0,0 +1,203 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * pfx.js - Loader centralizado de certificados PFX/P12
5
+ *
6
+ * Centraliza la carga y extracción de certificados PFX para evitar
7
+ * duplicación de código entre Certificado.js y SiiSession.js.
8
+ *
9
+ * @module utils/pfx
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const forge = require('node-forge');
14
+ const { certError, ERROR_CODES } = require('./error');
15
+
16
+ /**
17
+ * Resultado de cargar un PFX
18
+ * @typedef {Object} PfxData
19
+ * @property {forge.pki.PrivateKey} privateKey - Clave privada
20
+ * @property {forge.pki.Certificate} certificate - Certificado
21
+ * @property {string} privateKeyPem - Clave privada en formato PEM
22
+ * @property {string} certificatePem - Certificado en formato PEM
23
+ * @property {Object} subject - Campos del subject del certificado
24
+ * @property {string} rut - RUT extraído del certificado (si existe)
25
+ * @property {string} cn - Common Name del certificado
26
+ * @property {Date} notBefore - Fecha de inicio de validez
27
+ * @property {Date} notAfter - Fecha de expiración
28
+ */
29
+
30
+ /**
31
+ * Cargar y parsear un archivo PFX/P12 desde buffer
32
+ * @param {Buffer} pfxBuffer - Buffer del archivo PFX
33
+ * @param {string} password - Contraseña del PFX
34
+ * @returns {PfxData} Datos del certificado
35
+ * @throws {DteSiiError} Si hay error al parsear
36
+ */
37
+ function loadPfxFromBuffer(pfxBuffer, password) {
38
+ if (!pfxBuffer || !Buffer.isBuffer(pfxBuffer)) {
39
+ throw certError('Buffer PFX es requerido', ERROR_CODES.CERT_INVALID);
40
+ }
41
+
42
+ let p12Asn1, p12;
43
+
44
+ try {
45
+ p12Asn1 = forge.asn1.fromDer(pfxBuffer.toString('binary'));
46
+ } catch (err) {
47
+ throw certError(`Error parseando PFX: ${err.message}`, ERROR_CODES.CERT_INVALID, { originalError: err });
48
+ }
49
+
50
+ try {
51
+ p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);
52
+ } catch (err) {
53
+ if (err.message.includes('Invalid password')) {
54
+ throw certError('Contraseña del certificado incorrecta', ERROR_CODES.CERT_PASSWORD_INVALID, { originalError: err });
55
+ }
56
+ throw certError(`Error descifrando PFX: ${err.message}`, ERROR_CODES.CERT_INVALID, { originalError: err });
57
+ }
58
+
59
+ // Extraer certificado y clave privada
60
+ const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
61
+ const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag });
62
+
63
+ const certBag = certBags[forge.pki.oids.certBag];
64
+ const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag];
65
+
66
+ if (!certBag || !certBag.length) {
67
+ throw certError('No se encontró certificado en el archivo PFX', ERROR_CODES.CERT_INVALID);
68
+ }
69
+
70
+ if (!keyBag || !keyBag.length) {
71
+ throw certError('No se encontró clave privada en el archivo PFX', ERROR_CODES.CERT_INVALID);
72
+ }
73
+
74
+ const certificate = certBag[0].cert;
75
+ const privateKey = keyBag[0].key;
76
+
77
+ // Extraer información del subject
78
+ const subject = extractSubjectFields(certificate);
79
+ const rut = extractRutFromCertificate(certificate);
80
+
81
+ return {
82
+ privateKey,
83
+ certificate,
84
+ privateKeyPem: forge.pki.privateKeyToPem(privateKey),
85
+ certificatePem: forge.pki.certificateToPem(certificate),
86
+ subject,
87
+ rut,
88
+ cn: subject.CN || null,
89
+ notBefore: certificate.validity.notBefore,
90
+ notAfter: certificate.validity.notAfter,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Cargar PFX desde archivo
96
+ * @param {string} pfxPath - Ruta al archivo PFX
97
+ * @param {string} password - Contraseña del PFX
98
+ * @returns {PfxData} Datos del certificado
99
+ */
100
+ function loadPfxFromFile(pfxPath, password) {
101
+ if (!fs.existsSync(pfxPath)) {
102
+ throw certError(`Archivo PFX no encontrado: ${pfxPath}`, ERROR_CODES.CERT_NOT_FOUND, { path: pfxPath });
103
+ }
104
+
105
+ const pfxBuffer = fs.readFileSync(pfxPath);
106
+ return loadPfxFromBuffer(pfxBuffer, password);
107
+ }
108
+
109
+ /**
110
+ * Extraer campos del subject del certificado
111
+ * @param {forge.pki.Certificate} cert - Certificado
112
+ * @returns {Object} Campos del subject
113
+ */
114
+ function extractSubjectFields(cert) {
115
+ const fields = {};
116
+
117
+ if (cert.subject && cert.subject.attributes) {
118
+ cert.subject.attributes.forEach((attr) => {
119
+ const name = attr.shortName || attr.name;
120
+ if (name) {
121
+ fields[name] = attr.value;
122
+ }
123
+ });
124
+ }
125
+
126
+ return fields;
127
+ }
128
+
129
+ /**
130
+ * Extraer RUT del certificado (desde serialNumber o CN)
131
+ * @param {forge.pki.Certificate} cert - Certificado
132
+ * @returns {string|null} RUT o null
133
+ */
134
+ function extractRutFromCertificate(cert) {
135
+ const subject = extractSubjectFields(cert);
136
+
137
+ // Intentar desde serialNumber (más confiable)
138
+ if (subject.serialNumber) {
139
+ const clean = subject.serialNumber.replace(/\./g, '').toUpperCase();
140
+ if (/^\d{7,8}-[\dK]$/.test(clean)) {
141
+ return clean;
142
+ }
143
+ }
144
+
145
+ // Intentar desde CN
146
+ if (subject.CN) {
147
+ const match = subject.CN.match(/(\d{7,8}-[\dK])/i);
148
+ if (match) {
149
+ return match[1].toUpperCase();
150
+ }
151
+ }
152
+
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * Verificar si un certificado está expirado
158
+ * @param {Date} notAfter - Fecha de expiración
159
+ * @param {number} [marginDays=0] - Días de margen
160
+ * @returns {boolean} True si está expirado
161
+ */
162
+ function isCertificateExpired(notAfter, marginDays = 0) {
163
+ const now = new Date();
164
+ const expiry = new Date(notAfter);
165
+ expiry.setDate(expiry.getDate() - marginDays);
166
+ return now > expiry;
167
+ }
168
+
169
+ /**
170
+ * Obtener días hasta expiración
171
+ * @param {Date} notAfter - Fecha de expiración
172
+ * @returns {number} Días hasta expiración (negativo si expirado)
173
+ */
174
+ function getDaysUntilExpiry(notAfter) {
175
+ const now = new Date();
176
+ const expiry = new Date(notAfter);
177
+ const diffMs = expiry - now;
178
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24));
179
+ }
180
+
181
+ /**
182
+ * Crear opciones TLS desde datos PFX
183
+ * @param {PfxData} pfxData - Datos del PFX
184
+ * @returns {Object} Opciones TLS para https/got
185
+ */
186
+ function createTlsOptions(pfxData) {
187
+ return {
188
+ key: pfxData.privateKeyPem,
189
+ cert: pfxData.certificatePem,
190
+ certificate: pfxData.certificatePem,
191
+ rejectUnauthorized: false,
192
+ };
193
+ }
194
+
195
+ module.exports = {
196
+ loadPfxFromBuffer,
197
+ loadPfxFromFile,
198
+ extractSubjectFields,
199
+ extractRutFromCertificate,
200
+ isCertificateExpired,
201
+ getDaysUntilExpiry,
202
+ createTlsOptions,
203
+ };
@@ -0,0 +1,218 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Utilidades de Receptor
5
+ *
6
+ * Funciones para construir y validar datos de receptor en DTEs
7
+ *
8
+ * @module dte-sii/utils/receptor
9
+ */
10
+
11
+ const { sanitizeSiiText, sanitizeGiroRecep } = require('./sanitize');
12
+ const { formatRutSii, validarRut } = require('./rut');
13
+
14
+ // ============================================
15
+ // CONSTANTES
16
+ // ============================================
17
+
18
+ /** RUT para consumidor final */
19
+ const RUT_CONSUMIDOR_FINAL = '66666666-6';
20
+
21
+ /** Receptor por defecto para consumidor final */
22
+ const RECEPTOR_CONSUMIDOR_FINAL = {
23
+ RUTRecep: RUT_CONSUMIDOR_FINAL,
24
+ RznSocRecep: 'Consumidor Final',
25
+ DirRecep: 'Sin Direccion',
26
+ CmnaRecep: 'Santiago',
27
+ };
28
+
29
+ // ============================================
30
+ // CONSTRUCCIÓN DE RECEPTOR
31
+ // ============================================
32
+
33
+ /**
34
+ * Construye objeto de Receptor para DTE
35
+ *
36
+ * @param {Object} config - Configuración del receptor
37
+ * @param {string} config.rut - RUT del receptor
38
+ * @param {string} config.razonSocial - Razón social
39
+ * @param {string} [config.giro] - Giro (máximo 40 caracteres)
40
+ * @param {string} config.direccion - Dirección
41
+ * @param {string} config.comuna - Comuna
42
+ * @param {string} [config.ciudad] - Ciudad
43
+ * @param {string} [config.correo] - Correo electrónico
44
+ * @returns {Object} Receptor formateado para DTE
45
+ */
46
+ function buildReceptor(config) {
47
+ const {
48
+ rut,
49
+ razonSocial,
50
+ giro,
51
+ direccion,
52
+ comuna,
53
+ ciudad,
54
+ correo,
55
+ } = config;
56
+
57
+ const receptor = {
58
+ RUTRecep: formatRutSii(rut),
59
+ RznSocRecep: sanitizeSiiText(razonSocial),
60
+ };
61
+
62
+ if (giro) {
63
+ receptor.GiroRecep = sanitizeGiroRecep(giro);
64
+ }
65
+
66
+ receptor.DirRecep = sanitizeSiiText(direccion);
67
+ receptor.CmnaRecep = comuna;
68
+
69
+ if (ciudad) {
70
+ receptor.CiudadRecep = ciudad;
71
+ }
72
+
73
+ if (correo) {
74
+ receptor.CorreoRecep = correo;
75
+ }
76
+
77
+ return receptor;
78
+ }
79
+
80
+ /**
81
+ * Construye receptor para Boleta (campos mínimos)
82
+ *
83
+ * @param {Object} [config] - Configuración del receptor (opcional para boleta)
84
+ * @returns {Object} Receptor formateado para Boleta
85
+ */
86
+ function buildReceptorBoleta(config = {}) {
87
+ const { rut, razonSocial, direccion, comuna } = config;
88
+
89
+ return {
90
+ RUTRecep: rut ? formatRutSii(rut) : '',
91
+ RznSocRecep: sanitizeSiiText(razonSocial || ''),
92
+ DirRecep: sanitizeSiiText(direccion || ''),
93
+ CmnaRecep: comuna || '',
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Crea receptor para consumidor final (RUT 66666666-6)
99
+ *
100
+ * @param {Object} [overrides] - Campos a sobrescribir
101
+ * @returns {Object} Receptor de consumidor final
102
+ */
103
+ function buildReceptorConsumidorFinal(overrides = {}) {
104
+ return {
105
+ ...RECEPTOR_CONSUMIDOR_FINAL,
106
+ ...overrides,
107
+ RUTRecep: RUT_CONSUMIDOR_FINAL, // Siempre mantener este RUT
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Normaliza receptor desde diferentes formatos de entrada
113
+ *
114
+ * @param {Object} receptor - Receptor en cualquier formato
115
+ * @param {boolean} [esBoleta=false] - Si es formato boleta
116
+ * @returns {Object} Receptor normalizado
117
+ */
118
+ function normalizeReceptor(receptor, esBoleta = false) {
119
+ if (!receptor) {
120
+ return esBoleta ? buildReceptorBoleta() : RECEPTOR_CONSUMIDOR_FINAL;
121
+ }
122
+
123
+ if (esBoleta) {
124
+ return {
125
+ RUTRecep: receptor.RUTRecep || '',
126
+ RznSocRecep: sanitizeSiiText(receptor.RznSocRecep || ''),
127
+ DirRecep: sanitizeSiiText(receptor.DirRecep || ''),
128
+ CmnaRecep: receptor.CmnaRecep || '',
129
+ };
130
+ }
131
+
132
+ const giroRecep = receptor.GiroRecep
133
+ ? sanitizeGiroRecep(receptor.GiroRecep)
134
+ : undefined;
135
+
136
+ return {
137
+ RUTRecep: receptor.RUTRecep,
138
+ RznSocRecep: sanitizeSiiText(receptor.RznSocRecep),
139
+ ...(giroRecep ? { GiroRecep: giroRecep } : {}),
140
+ DirRecep: sanitizeSiiText(receptor.DirRecep),
141
+ CmnaRecep: receptor.CmnaRecep,
142
+ ...(receptor.CiudadRecep ? { CiudadRecep: receptor.CiudadRecep } : {}),
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Valida datos de receptor
148
+ *
149
+ * @param {Object} receptor - Datos del receptor
150
+ * @param {Object} [options] - Opciones de validación
151
+ * @param {boolean} [options.requireGiro=false] - Si el giro es requerido
152
+ * @param {boolean} [options.allowConsumidorFinal=true] - Si se permite RUT 66666666-6
153
+ * @returns {{ valid: boolean, errors: string[] }}
154
+ */
155
+ function validarReceptor(receptor, options = {}) {
156
+ const { requireGiro = false, allowConsumidorFinal = true } = options;
157
+ const errors = [];
158
+
159
+ if (!receptor) {
160
+ return { valid: false, errors: ['Receptor no proporcionado'] };
161
+ }
162
+
163
+ if (!receptor.RUTRecep) {
164
+ errors.push('RUT de receptor requerido');
165
+ } else if (receptor.RUTRecep !== RUT_CONSUMIDOR_FINAL && !validarRut(receptor.RUTRecep)) {
166
+ errors.push('RUT de receptor inválido');
167
+ } else if (receptor.RUTRecep === RUT_CONSUMIDOR_FINAL && !allowConsumidorFinal) {
168
+ errors.push('RUT de consumidor final no permitido para este tipo de documento');
169
+ }
170
+
171
+ if (!receptor.RznSocRecep) {
172
+ errors.push('Razón social de receptor requerida');
173
+ }
174
+
175
+ if (requireGiro && !receptor.GiroRecep) {
176
+ errors.push('Giro de receptor requerido');
177
+ }
178
+
179
+ if (!receptor.DirRecep) {
180
+ errors.push('Dirección de receptor requerida');
181
+ }
182
+
183
+ if (!receptor.CmnaRecep) {
184
+ errors.push('Comuna de receptor requerida');
185
+ }
186
+
187
+ return { valid: errors.length === 0, errors };
188
+ }
189
+
190
+ /**
191
+ * Verifica si un receptor es consumidor final
192
+ *
193
+ * @param {Object} receptor - Datos del receptor
194
+ * @returns {boolean}
195
+ */
196
+ function esConsumidorFinal(receptor) {
197
+ return receptor?.RUTRecep === RUT_CONSUMIDOR_FINAL;
198
+ }
199
+
200
+ // ============================================
201
+ // EXPORTS
202
+ // ============================================
203
+
204
+ module.exports = {
205
+ // Constantes
206
+ RUT_CONSUMIDOR_FINAL,
207
+ RECEPTOR_CONSUMIDOR_FINAL,
208
+
209
+ // Construcción
210
+ buildReceptor,
211
+ buildReceptorBoleta,
212
+ buildReceptorConsumidorFinal,
213
+ normalizeReceptor,
214
+
215
+ // Validación
216
+ validarReceptor,
217
+ esConsumidorFinal,
218
+ };
@@ -0,0 +1,169 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Utilidades de Referencia
5
+ *
6
+ * Funciones para construir referencias en DTEs (SET de pruebas, documentos, anulaciones)
7
+ *
8
+ * @module dte-sii/utils/referencia
9
+ */
10
+
11
+ // ============================================
12
+ // REFERENCIAS PARA CERTIFICACIÓN
13
+ // ============================================
14
+
15
+ /**
16
+ * Construye referencia al SET de pruebas del SII
17
+ * Requerida en todos los DTEs de certificación
18
+ *
19
+ * @param {string} casoId - ID del caso (ej: "4668070-1")
20
+ * @param {string} fecha - Fecha de emisión YYYY-MM-DD
21
+ * @param {number} [nroLinRef=1] - Número de línea de referencia
22
+ * @returns {Object} Objeto de referencia formateado
23
+ */
24
+ function buildSetReferencia(casoId, fecha, nroLinRef = 1) {
25
+ return {
26
+ NroLinRef: nroLinRef,
27
+ TpoDocRef: 'SET',
28
+ FolioRef: 1,
29
+ FchRef: fecha,
30
+ RazonRef: `CASO ${casoId}`,
31
+ };
32
+ }
33
+
34
+ // ============================================
35
+ // REFERENCIAS A DOCUMENTOS
36
+ // ============================================
37
+
38
+ /**
39
+ * Construye referencia a documento previo (para NC/ND)
40
+ *
41
+ * @param {Object} params - Parámetros de la referencia
42
+ * @param {number} params.tipoDte - Tipo de documento referenciado
43
+ * @param {number} params.folio - Folio del documento referenciado
44
+ * @param {string} params.fecha - Fecha del documento YYYY-MM-DD
45
+ * @param {number} params.codRef - Código de referencia (1=Anula, 2=Corrige texto, 3=Corrige montos)
46
+ * @param {string} params.razonRef - Razón de la referencia
47
+ * @param {number} [params.nroLinRef=1] - Número de línea
48
+ * @returns {Object} Objeto de referencia
49
+ */
50
+ function buildDocReferencia({ tipoDte, folio, fecha, codRef, razonRef, nroLinRef = 1 }) {
51
+ return {
52
+ NroLinRef: nroLinRef,
53
+ TpoDocRef: tipoDte,
54
+ FolioRef: folio,
55
+ FchRef: fecha,
56
+ CodRef: codRef,
57
+ RazonRef: razonRef,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Construye referencia para anulación de documento
63
+ *
64
+ * @param {Object} params - Parámetros
65
+ * @param {number} params.tipoDte - Tipo de documento a anular
66
+ * @param {number} params.folio - Folio del documento a anular
67
+ * @param {string} params.fecha - Fecha del documento
68
+ * @param {number} [params.nroLinRef=1] - Número de línea
69
+ * @returns {Object} Referencia de anulación
70
+ */
71
+ function buildAnulacionReferencia({ tipoDte, folio, fecha, nroLinRef = 1 }) {
72
+ return buildDocReferencia({
73
+ tipoDte,
74
+ folio,
75
+ fecha,
76
+ codRef: 1, // 1 = Anula documento de referencia
77
+ razonRef: 'ANULA DOCUMENTO',
78
+ nroLinRef,
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Construye referencia para corrección de texto
84
+ *
85
+ * @param {Object} params - Parámetros
86
+ * @param {number} params.tipoDte - Tipo de documento a corregir
87
+ * @param {number} params.folio - Folio del documento
88
+ * @param {string} params.fecha - Fecha del documento
89
+ * @param {string} params.razonRef - Descripción de la corrección
90
+ * @param {number} [params.nroLinRef=1] - Número de línea
91
+ * @returns {Object} Referencia de corrección
92
+ */
93
+ function buildCorreccionTextoReferencia({ tipoDte, folio, fecha, razonRef, nroLinRef = 1 }) {
94
+ return buildDocReferencia({
95
+ tipoDte,
96
+ folio,
97
+ fecha,
98
+ codRef: 2, // 2 = Corrige texto del documento de referencia
99
+ razonRef,
100
+ nroLinRef,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Construye referencia para corrección de montos
106
+ *
107
+ * @param {Object} params - Parámetros
108
+ * @param {number} params.tipoDte - Tipo de documento a corregir
109
+ * @param {number} params.folio - Folio del documento
110
+ * @param {string} params.fecha - Fecha del documento
111
+ * @param {string} params.razonRef - Descripción de la corrección
112
+ * @param {number} [params.nroLinRef=1] - Número de línea
113
+ * @returns {Object} Referencia de corrección de montos
114
+ */
115
+ function buildCorreccionMontosReferencia({ tipoDte, folio, fecha, razonRef, nroLinRef = 1 }) {
116
+ return buildDocReferencia({
117
+ tipoDte,
118
+ folio,
119
+ fecha,
120
+ codRef: 3, // 3 = Corrige montos
121
+ razonRef,
122
+ nroLinRef,
123
+ });
124
+ }
125
+
126
+ // ============================================
127
+ // UTILIDADES
128
+ // ============================================
129
+
130
+ /**
131
+ * Combina referencia SET con referencia a documento
132
+ * Para NC/ND en certificación que necesitan ambas
133
+ *
134
+ * @param {string} casoId - ID del caso de prueba
135
+ * @param {string} fechaEmision - Fecha de emisión del nuevo documento
136
+ * @param {Object} docRef - Referencia al documento (sin NroLinRef)
137
+ * @returns {Array} Array con ambas referencias ordenadas
138
+ */
139
+ function buildReferenciasNcNd(casoId, fechaEmision, docRef) {
140
+ const setRef = buildSetReferencia(casoId, fechaEmision, 1);
141
+ const documentoRef = { ...docRef, NroLinRef: 2 };
142
+ return [setRef, documentoRef];
143
+ }
144
+
145
+ /**
146
+ * Códigos de referencia SII
147
+ */
148
+ const CODIGOS_REFERENCIA = {
149
+ ANULA: 1,
150
+ CORRIGE_TEXTO: 2,
151
+ CORRIGE_MONTOS: 3,
152
+ };
153
+
154
+ // ============================================
155
+ // EXPORTS
156
+ // ============================================
157
+
158
+ module.exports = {
159
+ // Referencias
160
+ buildSetReferencia,
161
+ buildDocReferencia,
162
+ buildAnulacionReferencia,
163
+ buildCorreccionTextoReferencia,
164
+ buildCorreccionMontosReferencia,
165
+ buildReferenciasNcNd,
166
+
167
+ // Constantes
168
+ CODIGOS_REFERENCIA,
169
+ };
@@ -0,0 +1,119 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Utilidades de Resolución SII
5
+ *
6
+ * Funciones para manejo de resoluciones SII (certificación/producción)
7
+ *
8
+ * @module dte-sii/utils/resolucion
9
+ */
10
+
11
+ // ============================================
12
+ // RESOLUCIÓN SII
13
+ // ============================================
14
+
15
+ /**
16
+ * Normaliza fecha de resolución a formato YYYY-MM-DD
17
+ *
18
+ * @param {string} value - Fecha en formato DD-MM-YYYY o YYYY-MM-DD
19
+ * @returns {string} - Fecha normalizada YYYY-MM-DD
20
+ */
21
+ function normalizeFechaResolucion(value) {
22
+ const raw = (value || '').trim();
23
+ if (!raw) return raw;
24
+
25
+ // Ya está en formato ISO
26
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
27
+
28
+ // Formato DD-MM-YYYY
29
+ if (/^\d{2}-\d{2}-\d{4}$/.test(raw)) {
30
+ const [dd, mm, yyyy] = raw.split('-');
31
+ return `${yyyy}-${mm}-${dd}`;
32
+ }
33
+
34
+ // Formato DD/MM/YYYY
35
+ if (/^\d{2}\/\d{2}\/\d{4}$/.test(raw)) {
36
+ const [dd, mm, yyyy] = raw.split('/');
37
+ return `${yyyy}-${mm}-${dd}`;
38
+ }
39
+
40
+ return raw;
41
+ }
42
+
43
+ /**
44
+ * Crea objeto de resolución SII
45
+ *
46
+ * @param {string} fecha - Fecha de resolución (se normaliza automáticamente)
47
+ * @param {number} [numero=0] - Número de resolución (0 para certificación)
48
+ * @returns {{ fch_resol: string, nro_resol: number }}
49
+ */
50
+ function createResolucion(fecha, numero = 0) {
51
+ return {
52
+ fch_resol: normalizeFechaResolucion(fecha),
53
+ nro_resol: Number(numero) || 0,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Crear resolución para ambiente de certificación
59
+ * La fecha debe ser la fecha de autorización en ambiente de certificación
60
+ *
61
+ * @param {string} fecha - Fecha de autorización
62
+ * @returns {{ fch_resol: string, nro_resol: number }}
63
+ */
64
+ function createResolucionCertificacion(fecha) {
65
+ return createResolucion(fecha, 0); // Número 0 para certificación
66
+ }
67
+
68
+ /**
69
+ * Crear resolución para ambiente de producción
70
+ *
71
+ * @param {string} fecha - Fecha de la resolución
72
+ * @param {number} numero - Número de la resolución
73
+ * @returns {{ fch_resol: string, nro_resol: number }}
74
+ */
75
+ function createResolucionProduccion(fecha, numero) {
76
+ if (!numero || numero <= 0) {
77
+ throw new Error('Número de resolución requerido para producción');
78
+ }
79
+ return createResolucion(fecha, numero);
80
+ }
81
+
82
+ /**
83
+ * Validar objeto de resolución
84
+ *
85
+ * @param {Object} resolucion - Objeto de resolución
86
+ * @param {boolean} [requireNumero=false] - Si se requiere número > 0
87
+ * @returns {{ valid: boolean, error?: string }}
88
+ */
89
+ function validarResolucion(resolucion, requireNumero = false) {
90
+ if (!resolucion) {
91
+ return { valid: false, error: 'Resolución no proporcionada' };
92
+ }
93
+
94
+ if (!resolucion.fch_resol) {
95
+ return { valid: false, error: 'Fecha de resolución requerida' };
96
+ }
97
+
98
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(resolucion.fch_resol)) {
99
+ return { valid: false, error: 'Formato de fecha inválido (debe ser YYYY-MM-DD)' };
100
+ }
101
+
102
+ if (requireNumero && (!resolucion.nro_resol || resolucion.nro_resol <= 0)) {
103
+ return { valid: false, error: 'Número de resolución requerido para producción' };
104
+ }
105
+
106
+ return { valid: true };
107
+ }
108
+
109
+ // ============================================
110
+ // EXPORTS
111
+ // ============================================
112
+
113
+ module.exports = {
114
+ normalizeFechaResolucion,
115
+ createResolucion,
116
+ createResolucionCertificacion,
117
+ createResolucionProduccion,
118
+ validarResolucion,
119
+ };