@timbra-ec/sri 0.1.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/dist/clave-acceso.d.ts +31 -0
- package/dist/clave-acceso.d.ts.map +1 -0
- package/dist/clave-acceso.js +70 -0
- package/dist/clave-acceso.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/signing/certificate.d.ts +27 -0
- package/dist/signing/certificate.d.ts.map +1 -0
- package/dist/signing/certificate.js +78 -0
- package/dist/signing/certificate.js.map +1 -0
- package/dist/signing/xades-bes.d.ts +10 -0
- package/dist/signing/xades-bes.d.ts.map +1 -0
- package/dist/signing/xades-bes.js +166 -0
- package/dist/signing/xades-bes.js.map +1 -0
- package/dist/soap/autorizacion.d.ts +21 -0
- package/dist/soap/autorizacion.d.ts.map +1 -0
- package/dist/soap/autorizacion.js +99 -0
- package/dist/soap/autorizacion.js.map +1 -0
- package/dist/soap/client.d.ts +6 -0
- package/dist/soap/client.d.ts.map +1 -0
- package/dist/soap/client.js +37 -0
- package/dist/soap/client.js.map +1 -0
- package/dist/soap/recepcion.d.ts +9 -0
- package/dist/soap/recepcion.d.ts.map +1 -0
- package/dist/soap/recepcion.js +60 -0
- package/dist/soap/recepcion.js.map +1 -0
- package/dist/xml/builder.d.ts +31 -0
- package/dist/xml/builder.d.ts.map +1 -0
- package/dist/xml/builder.js +287 -0
- package/dist/xml/builder.js.map +1 -0
- package/dist/xml/shared.d.ts +21 -0
- package/dist/xml/shared.d.ts.map +1 -0
- package/dist/xml/shared.js +93 -0
- package/dist/xml/shared.js.map +1 -0
- package/dist/xml/types.d.ts +179 -0
- package/dist/xml/types.d.ts.map +1 -0
- package/dist/xml/types.js +2 -0
- package/dist/xml/types.js.map +1 -0
- package/package.json +37 -0
- package/src/clave-acceso.ts +106 -0
- package/src/errors.ts +32 -0
- package/src/index.ts +49 -0
- package/src/signing/certificate.ts +103 -0
- package/src/signing/xades-bes.ts +188 -0
- package/src/soap/autorizacion.ts +139 -0
- package/src/soap/client.ts +45 -0
- package/src/soap/recepcion.ts +83 -0
- package/src/xml/builder.ts +344 -0
- package/src/xml/shared.ts +110 -0
- package/src/xml/types.ts +195 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Format a number to exactly 2 decimal places */
|
|
2
|
+
export function money(value) {
|
|
3
|
+
return value.toFixed(2);
|
|
4
|
+
}
|
|
5
|
+
/** Convert ISO date (YYYY-MM-DD) to SRI format (dd/MM/yyyy) */
|
|
6
|
+
export function toSriDate(isoDate) {
|
|
7
|
+
const [year, month, day] = isoDate.split('-');
|
|
8
|
+
return `${day}/${month}/${year}`;
|
|
9
|
+
}
|
|
10
|
+
/** Aggregate tax totals by codigoPorcentaje across all items */
|
|
11
|
+
export function aggregateTaxTotals(items) {
|
|
12
|
+
const totals = new Map();
|
|
13
|
+
for (const item of items) {
|
|
14
|
+
for (const imp of item.impuestos) {
|
|
15
|
+
const key = `${imp.codigo}-${imp.codigoPorcentaje}`;
|
|
16
|
+
const existing = totals.get(key);
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.baseImponible += imp.baseImponible;
|
|
19
|
+
existing.valor += imp.valor;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
totals.set(key, {
|
|
23
|
+
codigo: imp.codigo,
|
|
24
|
+
codigoPorcentaje: imp.codigoPorcentaje,
|
|
25
|
+
baseImponible: imp.baseImponible,
|
|
26
|
+
valor: imp.valor,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Array.from(totals.values());
|
|
32
|
+
}
|
|
33
|
+
/** Build the infoTributaria section (common to all SRI documents) */
|
|
34
|
+
export function buildInfoTributaria(parent, input) {
|
|
35
|
+
const infoTrib = parent.ele('infoTributaria');
|
|
36
|
+
infoTrib.ele('ambiente').txt(input.ambiente);
|
|
37
|
+
infoTrib.ele('tipoEmision').txt(input.tipoEmision);
|
|
38
|
+
infoTrib.ele('razonSocial').txt(input.razonSocial);
|
|
39
|
+
if (input.nombreComercial) {
|
|
40
|
+
infoTrib.ele('nombreComercial').txt(input.nombreComercial);
|
|
41
|
+
}
|
|
42
|
+
infoTrib.ele('ruc').txt(input.ruc);
|
|
43
|
+
infoTrib.ele('claveAcceso').txt(input.claveAcceso);
|
|
44
|
+
infoTrib.ele('codDoc').txt(input.codDoc);
|
|
45
|
+
infoTrib.ele('estab').txt(input.estab);
|
|
46
|
+
infoTrib.ele('ptoEmi').txt(input.ptoEmi);
|
|
47
|
+
infoTrib.ele('secuencial').txt(input.secuencial);
|
|
48
|
+
infoTrib.ele('dirMatriz').txt(input.direccionMatriz);
|
|
49
|
+
if (input.regimenRimpe === 'emprendedor') {
|
|
50
|
+
infoTrib.ele('contribuyenteRimpe').txt('CONTRIBUYENTE RÉGIMEN RIMPE');
|
|
51
|
+
}
|
|
52
|
+
else if (input.regimenRimpe === 'negocio_popular') {
|
|
53
|
+
infoTrib.ele('contribuyenteRimpe').txt('CONTRIBUYENTE NEGOCIO POPULAR - RÉGIMEN RIMPE');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Build infoAdicional section */
|
|
57
|
+
export function buildInfoAdicional(parent, infoAdicional) {
|
|
58
|
+
if (infoAdicional && Object.keys(infoAdicional).length > 0) {
|
|
59
|
+
const section = parent.ele('infoAdicional');
|
|
60
|
+
for (const [nombre, valor] of Object.entries(infoAdicional)) {
|
|
61
|
+
section.ele('campoAdicional', { nombre }).txt(valor);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Build detalles section from InvoiceItems */
|
|
66
|
+
export function buildDetalles(parent, items) {
|
|
67
|
+
const detalles = parent.ele('detalles');
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
const detalle = detalles.ele('detalle');
|
|
70
|
+
detalle.ele('codigoPrincipal').txt(item.codigoPrincipal);
|
|
71
|
+
detalle.ele('descripcion').txt(item.descripcion);
|
|
72
|
+
detalle.ele('cantidad').txt(money(item.cantidad));
|
|
73
|
+
detalle.ele('precioUnitario').txt(money(item.precioUnitario));
|
|
74
|
+
detalle.ele('descuento').txt(money(item.descuento));
|
|
75
|
+
detalle.ele('precioTotalSinImpuesto').txt(money(item.precioTotalSinImpuesto));
|
|
76
|
+
const impuestos = detalle.ele('impuestos');
|
|
77
|
+
for (const imp of item.impuestos) {
|
|
78
|
+
const impuesto = impuestos.ele('impuesto');
|
|
79
|
+
impuesto.ele('codigo').txt(imp.codigo);
|
|
80
|
+
impuesto.ele('codigoPorcentaje').txt(imp.codigoPorcentaje);
|
|
81
|
+
impuesto.ele('tarifa').txt(money(imp.tarifa));
|
|
82
|
+
impuesto.ele('baseImponible').txt(money(imp.baseImponible));
|
|
83
|
+
impuesto.ele('valor').txt(money(imp.valor));
|
|
84
|
+
}
|
|
85
|
+
if (item.detallesAdicionales && item.detallesAdicionales.length > 0) {
|
|
86
|
+
const detAdicionales = detalle.ele('detallesAdicionales');
|
|
87
|
+
for (const da of item.detallesAdicionales) {
|
|
88
|
+
detAdicionales.ele('detAdicional', { nombre: da.nombre }).txt(da.valor);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/xml/shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { DocumentTypeCode, EmissionType, InvoiceItem, PaymentDetail, SriEnvironment } from '@timbra-ec/types';
|
|
2
|
+
/** Common fields for all SRI documents (used in infoTributaria) */
|
|
3
|
+
export interface InfoTributariaInput {
|
|
4
|
+
ambiente: SriEnvironment;
|
|
5
|
+
tipoEmision: EmissionType;
|
|
6
|
+
ruc: string;
|
|
7
|
+
claveAcceso: string;
|
|
8
|
+
codDoc: DocumentTypeCode;
|
|
9
|
+
estab: string;
|
|
10
|
+
ptoEmi: string;
|
|
11
|
+
secuencial: string;
|
|
12
|
+
razonSocial: string;
|
|
13
|
+
nombreComercial?: string;
|
|
14
|
+
direccionMatriz: string;
|
|
15
|
+
regimenRimpe?: 'emprendedor' | 'negocio_popular' | null;
|
|
16
|
+
}
|
|
17
|
+
/** Input for building a Nota de Credito XML (codDoc=04) */
|
|
18
|
+
export interface NotaCreditoInput {
|
|
19
|
+
infoTributaria: InfoTributariaInput;
|
|
20
|
+
/** Date in YYYY-MM-DD format */
|
|
21
|
+
fechaEmision: string;
|
|
22
|
+
obligadoContabilidad: boolean;
|
|
23
|
+
tipoIdentificacionComprador: string;
|
|
24
|
+
identificacionComprador: string;
|
|
25
|
+
razonSocialComprador: string;
|
|
26
|
+
/** codDoc of the modified document (e.g. '01' for factura) */
|
|
27
|
+
codDocModificado: DocumentTypeCode;
|
|
28
|
+
/** Document number being modified: estab-ptoEmi-secuencial (e.g. '001-001-000000001') */
|
|
29
|
+
numDocModificado: string;
|
|
30
|
+
/** Emission date of the modified document in YYYY-MM-DD */
|
|
31
|
+
fechaEmisionDocSustento: string;
|
|
32
|
+
/** Reason for the credit note */
|
|
33
|
+
motivo: string;
|
|
34
|
+
totalSinImpuestos: number;
|
|
35
|
+
importeTotal: number;
|
|
36
|
+
items: InvoiceItem[];
|
|
37
|
+
pagos: PaymentDetail[];
|
|
38
|
+
infoAdicional?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
/** Motivo entry for a Nota de Debito */
|
|
41
|
+
export interface MotivoDebito {
|
|
42
|
+
razon: string;
|
|
43
|
+
valor: number;
|
|
44
|
+
}
|
|
45
|
+
/** Input for building a Nota de Debito XML (codDoc=05) */
|
|
46
|
+
export interface NotaDebitoInput {
|
|
47
|
+
infoTributaria: InfoTributariaInput;
|
|
48
|
+
fechaEmision: string;
|
|
49
|
+
obligadoContabilidad: boolean;
|
|
50
|
+
tipoIdentificacionComprador: string;
|
|
51
|
+
identificacionComprador: string;
|
|
52
|
+
razonSocialComprador: string;
|
|
53
|
+
codDocModificado: DocumentTypeCode;
|
|
54
|
+
numDocModificado: string;
|
|
55
|
+
fechaEmisionDocSustento: string;
|
|
56
|
+
totalSinImpuestos: number;
|
|
57
|
+
importeTotal: number;
|
|
58
|
+
impuestos: {
|
|
59
|
+
codigo: string;
|
|
60
|
+
codigoPorcentaje: string;
|
|
61
|
+
tarifa: number;
|
|
62
|
+
baseImponible: number;
|
|
63
|
+
valor: number;
|
|
64
|
+
}[];
|
|
65
|
+
motivos: MotivoDebito[];
|
|
66
|
+
pagos: PaymentDetail[];
|
|
67
|
+
infoAdicional?: Record<string, string>;
|
|
68
|
+
}
|
|
69
|
+
/** Destinatario for a Guia de Remision */
|
|
70
|
+
export interface Destinatario {
|
|
71
|
+
identificacionDestinatario: string;
|
|
72
|
+
razonSocialDestinatario: string;
|
|
73
|
+
dirDestinatario: string;
|
|
74
|
+
motivoTraslado: string;
|
|
75
|
+
/** Optional: referencing a factura or liquidacion */
|
|
76
|
+
codDocSustento?: DocumentTypeCode;
|
|
77
|
+
numDocSustento?: string;
|
|
78
|
+
numAutDocSustento?: string;
|
|
79
|
+
fechaEmisionDocSustento?: string;
|
|
80
|
+
detalles: {
|
|
81
|
+
codigoInterno: string;
|
|
82
|
+
descripcion: string;
|
|
83
|
+
cantidad: number;
|
|
84
|
+
}[];
|
|
85
|
+
}
|
|
86
|
+
/** Input for building a Guia de Remision XML (codDoc=06) */
|
|
87
|
+
export interface GuiaRemisionInput {
|
|
88
|
+
infoTributaria: InfoTributariaInput;
|
|
89
|
+
fechaEmision: string;
|
|
90
|
+
obligadoContabilidad: boolean;
|
|
91
|
+
dirPartida: string;
|
|
92
|
+
razonSocialTransportista: string;
|
|
93
|
+
tipoIdentificacionTransportista: string;
|
|
94
|
+
rucTransportista: string;
|
|
95
|
+
placa: string;
|
|
96
|
+
fechaIniTransporte: string;
|
|
97
|
+
fechaFinTransporte: string;
|
|
98
|
+
destinatarios: Destinatario[];
|
|
99
|
+
infoAdicional?: Record<string, string>;
|
|
100
|
+
}
|
|
101
|
+
/** Retention line item */
|
|
102
|
+
export interface RetencionDetalle {
|
|
103
|
+
/** '1' = renta, '2' = IVA, '6' = ISD */
|
|
104
|
+
codigo: string;
|
|
105
|
+
codigoRetencion: string;
|
|
106
|
+
baseImponible: number;
|
|
107
|
+
porcentajeRetener: number;
|
|
108
|
+
valorRetenido: number;
|
|
109
|
+
/** codDoc of the sustaining document */
|
|
110
|
+
codDocSustento: DocumentTypeCode;
|
|
111
|
+
numDocSustento: string;
|
|
112
|
+
fechaEmisionDocSustento: string;
|
|
113
|
+
}
|
|
114
|
+
/** Input for building a Comprobante de Retencion XML (codDoc=07, Version ATS) */
|
|
115
|
+
export interface RetencionInput {
|
|
116
|
+
infoTributaria: InfoTributariaInput;
|
|
117
|
+
fechaEmision: string;
|
|
118
|
+
obligadoContabilidad: boolean;
|
|
119
|
+
tipoIdentificacionSujetoRetenido: string;
|
|
120
|
+
identificacionSujetoRetenido: string;
|
|
121
|
+
razonSocialSujetoRetenido: string;
|
|
122
|
+
periodoFiscal: string;
|
|
123
|
+
impuestos: RetencionDetalle[];
|
|
124
|
+
infoAdicional?: Record<string, string>;
|
|
125
|
+
}
|
|
126
|
+
/** Purchase detail for ATS */
|
|
127
|
+
export interface AtsCompra {
|
|
128
|
+
codSustento: string;
|
|
129
|
+
tpIdProv: string;
|
|
130
|
+
idProv: string;
|
|
131
|
+
tipoComprobante: string;
|
|
132
|
+
parteRel: 'SI' | 'NO';
|
|
133
|
+
fechaRegistro: string;
|
|
134
|
+
establecimiento: string;
|
|
135
|
+
puntoEmision: string;
|
|
136
|
+
secuencial: string;
|
|
137
|
+
fechaEmision: string;
|
|
138
|
+
autorizacion: string;
|
|
139
|
+
baseNoGraIva: number;
|
|
140
|
+
baseImponible: number;
|
|
141
|
+
baseImpGrav: number;
|
|
142
|
+
montoIva: number;
|
|
143
|
+
valRetBien10: number;
|
|
144
|
+
valRetServ20: number;
|
|
145
|
+
valorRetBienes: number;
|
|
146
|
+
valRetServ50: number;
|
|
147
|
+
valorRetServicios: number;
|
|
148
|
+
valRetServ100: number;
|
|
149
|
+
totbasesImpReemb: number;
|
|
150
|
+
}
|
|
151
|
+
/** Sales detail for ATS */
|
|
152
|
+
export interface AtsVenta {
|
|
153
|
+
tpIdCliente: string;
|
|
154
|
+
idCliente: string;
|
|
155
|
+
tipoComprobante: string;
|
|
156
|
+
tipoEmision: 'E' | 'F';
|
|
157
|
+
numeroComprobantes: number;
|
|
158
|
+
baseNoGraIva: number;
|
|
159
|
+
baseImponible: number;
|
|
160
|
+
baseImpGrav: number;
|
|
161
|
+
montoIva: number;
|
|
162
|
+
montoIce: number;
|
|
163
|
+
valorRetIva: number;
|
|
164
|
+
valorRetRenta: number;
|
|
165
|
+
}
|
|
166
|
+
/** Input for building ATS XML */
|
|
167
|
+
export interface AtsInput {
|
|
168
|
+
tipoIDInformante: 'R' | 'C';
|
|
169
|
+
idInformante: string;
|
|
170
|
+
razonSocial: string;
|
|
171
|
+
anio: string;
|
|
172
|
+
mes: string;
|
|
173
|
+
numEstabRuc: string;
|
|
174
|
+
totalVentas: number;
|
|
175
|
+
codigoOperativo: string;
|
|
176
|
+
compras: AtsCompra[];
|
|
177
|
+
ventas: AtsVenta[];
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/xml/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/xml/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timbra-ec/sri",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/timbra-ec/timbra-app.git",
|
|
10
|
+
"directory": "packages/sri"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"lint": "echo 'no lint configured yet'"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@timbra-ec/types": "workspace:*",
|
|
28
|
+
"@xmldom/xmldom": "^0.8.11",
|
|
29
|
+
"node-forge": "^1.4.0",
|
|
30
|
+
"xml-crypto": "^6.1.2",
|
|
31
|
+
"xmlbuilder2": "^4.0.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node-forge": "^1.3.14",
|
|
35
|
+
"typescript": "^5.8.2"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { DocumentTypeCode, EmissionType, SriEnvironment } from '@timbra-ec/types'
|
|
2
|
+
import { SriValidationError } from './errors'
|
|
3
|
+
|
|
4
|
+
export interface ClaveAccesoInput {
|
|
5
|
+
/** Date in dd/MM/yyyy format */
|
|
6
|
+
fechaEmision: string
|
|
7
|
+
codDoc: DocumentTypeCode
|
|
8
|
+
ruc: string
|
|
9
|
+
ambiente: SriEnvironment
|
|
10
|
+
estab: string
|
|
11
|
+
ptoEmi: string
|
|
12
|
+
/** 9 digits, zero-padded */
|
|
13
|
+
secuencial: string
|
|
14
|
+
/** 8-digit random numeric code */
|
|
15
|
+
codigoNumerico: string
|
|
16
|
+
tipoEmision: EmissionType
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compute Modulo 11 check digit for SRI clave de acceso.
|
|
21
|
+
* Weights cycle 2,3,4,5,6,7 from right to left.
|
|
22
|
+
* Result: 11 - (sum % 11). If 11 → 0, if 10 → 1.
|
|
23
|
+
*/
|
|
24
|
+
export function computeModulo11(digits: string): number {
|
|
25
|
+
const weights = [2, 3, 4, 5, 6, 7]
|
|
26
|
+
let sum = 0
|
|
27
|
+
|
|
28
|
+
for (let i = digits.length - 1, w = 0; i >= 0; i--, w++) {
|
|
29
|
+
sum += parseInt(digits[i]!, 10) * weights[w % weights.length]!
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const remainder = sum % 11
|
|
33
|
+
const check = 11 - remainder
|
|
34
|
+
|
|
35
|
+
if (check === 11) return 0
|
|
36
|
+
if (check === 10) return 1
|
|
37
|
+
return check
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate the 49-digit clave de acceso for SRI electronic documents.
|
|
42
|
+
*
|
|
43
|
+
* Format (49 digits total):
|
|
44
|
+
* DDMMAAAA (8) + codDoc (2) + RUC (13) + ambiente (1) +
|
|
45
|
+
* estab (3) + ptoEmi (3) + secuencial (9) + codigoNumerico (8) +
|
|
46
|
+
* tipoEmision (1) + checkDigit (1)
|
|
47
|
+
*/
|
|
48
|
+
export function generateClaveAcceso(input: ClaveAccesoInput): string {
|
|
49
|
+
const {
|
|
50
|
+
fechaEmision,
|
|
51
|
+
codDoc,
|
|
52
|
+
ruc,
|
|
53
|
+
ambiente,
|
|
54
|
+
estab,
|
|
55
|
+
ptoEmi,
|
|
56
|
+
secuencial,
|
|
57
|
+
codigoNumerico,
|
|
58
|
+
tipoEmision,
|
|
59
|
+
} = input
|
|
60
|
+
|
|
61
|
+
// Validate date format dd/MM/yyyy and convert to DDMMAAAA
|
|
62
|
+
const dateParts = fechaEmision.split('/')
|
|
63
|
+
if (
|
|
64
|
+
dateParts.length !== 3 ||
|
|
65
|
+
dateParts[0]!.length !== 2 ||
|
|
66
|
+
dateParts[1]!.length !== 2 ||
|
|
67
|
+
dateParts[2]!.length !== 4
|
|
68
|
+
) {
|
|
69
|
+
throw new SriValidationError('fechaEmision must be in dd/MM/yyyy format')
|
|
70
|
+
}
|
|
71
|
+
const dateDigits = dateParts[0]! + dateParts[1]! + dateParts[2]!
|
|
72
|
+
|
|
73
|
+
if (ruc.length !== 13) {
|
|
74
|
+
throw new SriValidationError(`RUC must be 13 digits, got ${ruc.length}`)
|
|
75
|
+
}
|
|
76
|
+
if (estab.length !== 3) {
|
|
77
|
+
throw new SriValidationError(`estab must be 3 digits, got ${estab.length}`)
|
|
78
|
+
}
|
|
79
|
+
if (ptoEmi.length !== 3) {
|
|
80
|
+
throw new SriValidationError(`ptoEmi must be 3 digits, got ${ptoEmi.length}`)
|
|
81
|
+
}
|
|
82
|
+
if (secuencial.length !== 9) {
|
|
83
|
+
throw new SriValidationError(`secuencial must be 9 digits, got ${secuencial.length}`)
|
|
84
|
+
}
|
|
85
|
+
if (codigoNumerico.length !== 8) {
|
|
86
|
+
throw new SriValidationError(`codigoNumerico must be 8 digits, got ${codigoNumerico.length}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const base =
|
|
90
|
+
dateDigits +
|
|
91
|
+
codDoc +
|
|
92
|
+
ruc +
|
|
93
|
+
ambiente +
|
|
94
|
+
estab +
|
|
95
|
+
ptoEmi +
|
|
96
|
+
secuencial +
|
|
97
|
+
codigoNumerico +
|
|
98
|
+
tipoEmision
|
|
99
|
+
|
|
100
|
+
if (base.length !== 48) {
|
|
101
|
+
throw new SriValidationError(`Clave base must be 48 digits, got ${base.length}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const checkDigit = computeModulo11(base)
|
|
105
|
+
return base + checkDigit.toString()
|
|
106
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SriMensaje } from '@timbra-ec/types'
|
|
2
|
+
|
|
3
|
+
export class SriError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
message: string,
|
|
6
|
+
public readonly code?: string,
|
|
7
|
+
public readonly mensajes?: SriMensaje[],
|
|
8
|
+
) {
|
|
9
|
+
super(message)
|
|
10
|
+
this.name = 'SriError'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SriValidationError extends SriError {
|
|
15
|
+
override name = 'SriValidationError'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SriCertificateError extends SriError {
|
|
19
|
+
override name = 'SriCertificateError'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SriSigningError extends SriError {
|
|
23
|
+
override name = 'SriSigningError'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SriSoapError extends SriError {
|
|
27
|
+
override name = 'SriSoapError'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SriTimeoutError extends SriError {
|
|
31
|
+
override name = 'SriTimeoutError'
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @timbra-ec/sri — SRI SOAP integration
|
|
2
|
+
// XML generation, XAdES-BES signing, SOAP client, clave de acceso
|
|
3
|
+
|
|
4
|
+
export { SRI_URLS } from '@timbra-ec/types'
|
|
5
|
+
|
|
6
|
+
export { generateClaveAcceso } from './clave-acceso'
|
|
7
|
+
export type { ClaveAccesoInput } from './clave-acceso'
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
buildFacturaXml,
|
|
11
|
+
buildNotaCreditoXml,
|
|
12
|
+
buildNotaDebitoXml,
|
|
13
|
+
buildGuiaRemisionXml,
|
|
14
|
+
buildRetencionXml,
|
|
15
|
+
buildAtsXml,
|
|
16
|
+
} from './xml/builder'
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
InfoTributariaInput,
|
|
20
|
+
NotaCreditoInput,
|
|
21
|
+
NotaDebitoInput,
|
|
22
|
+
MotivoDebito,
|
|
23
|
+
GuiaRemisionInput,
|
|
24
|
+
Destinatario,
|
|
25
|
+
RetencionInput,
|
|
26
|
+
RetencionDetalle,
|
|
27
|
+
AtsInput,
|
|
28
|
+
AtsCompra,
|
|
29
|
+
AtsVenta,
|
|
30
|
+
} from './xml/types'
|
|
31
|
+
|
|
32
|
+
export { parseCertificate, validateCertificate } from './signing/certificate'
|
|
33
|
+
export type { ParsedCertificate, CertificateMetadata } from './signing/certificate'
|
|
34
|
+
|
|
35
|
+
export { signXml } from './signing/xades-bes'
|
|
36
|
+
|
|
37
|
+
export { submitComprobante } from './soap/recepcion'
|
|
38
|
+
|
|
39
|
+
export { queryAutorizacion } from './soap/autorizacion'
|
|
40
|
+
export type { PollOptions } from './soap/autorizacion'
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
SriError,
|
|
44
|
+
SriValidationError,
|
|
45
|
+
SriCertificateError,
|
|
46
|
+
SriSigningError,
|
|
47
|
+
SriSoapError,
|
|
48
|
+
SriTimeoutError,
|
|
49
|
+
} from './errors'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import forge from 'node-forge'
|
|
2
|
+
import { SriCertificateError } from '../errors'
|
|
3
|
+
|
|
4
|
+
export interface CertificateMetadata {
|
|
5
|
+
commonName: string
|
|
6
|
+
issuer: string
|
|
7
|
+
serialNumber: string
|
|
8
|
+
validFrom: Date
|
|
9
|
+
validTo: Date
|
|
10
|
+
/** RUC extracted from subject fields, if present */
|
|
11
|
+
ruc: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ParsedCertificate {
|
|
15
|
+
privateKey: forge.pki.rsa.PrivateKey
|
|
16
|
+
certificate: forge.pki.Certificate
|
|
17
|
+
chain: forge.pki.Certificate[]
|
|
18
|
+
metadata: CertificateMetadata
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Try to extract a 13-digit RUC from certificate subject fields */
|
|
22
|
+
function extractRuc(cert: forge.pki.Certificate): string | null {
|
|
23
|
+
const fields = cert.subject.attributes
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
const value = String(field.value ?? '')
|
|
26
|
+
const match = value.match(/\d{13}/)
|
|
27
|
+
if (match) return match[0]
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatDN(attrs: forge.pki.CertificateField[]): string {
|
|
33
|
+
return attrs.map((a) => `${a.shortName ?? a.name}=${a.value}`).join(', ')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a PKCS#12 (.p12) certificate file.
|
|
38
|
+
* Extracts private key, certificate, chain, and metadata.
|
|
39
|
+
*/
|
|
40
|
+
export function parseCertificate(p12Buffer: Buffer, password: string): ParsedCertificate {
|
|
41
|
+
let p12Asn1: forge.asn1.Asn1
|
|
42
|
+
try {
|
|
43
|
+
const p12Der = forge.util.decode64(p12Buffer.toString('base64'))
|
|
44
|
+
p12Asn1 = forge.asn1.fromDer(p12Der)
|
|
45
|
+
} catch {
|
|
46
|
+
throw new SriCertificateError('Invalid PKCS#12 file format')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let p12: forge.pkcs12.Pkcs12Pfx
|
|
50
|
+
try {
|
|
51
|
+
p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password)
|
|
52
|
+
} catch {
|
|
53
|
+
throw new SriCertificateError('Wrong certificate password or corrupted PKCS#12 file')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract private key
|
|
57
|
+
const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })
|
|
58
|
+
const keyBag = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag]
|
|
59
|
+
if (!keyBag || keyBag.length === 0 || !keyBag[0]?.key) {
|
|
60
|
+
throw new SriCertificateError('No private key found in PKCS#12 file')
|
|
61
|
+
}
|
|
62
|
+
const privateKey = keyBag[0].key as forge.pki.rsa.PrivateKey
|
|
63
|
+
|
|
64
|
+
// Extract certificates
|
|
65
|
+
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag })
|
|
66
|
+
const certs = certBags[forge.pki.oids.certBag]
|
|
67
|
+
if (!certs || certs.length === 0 || !certs[0]?.cert) {
|
|
68
|
+
throw new SriCertificateError('No certificate found in PKCS#12 file')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const certificate = certs[0].cert
|
|
72
|
+
const chain = certs
|
|
73
|
+
.slice(1)
|
|
74
|
+
.filter((b) => b.cert != null)
|
|
75
|
+
.map((b) => b.cert!)
|
|
76
|
+
|
|
77
|
+
const metadata: CertificateMetadata = {
|
|
78
|
+
commonName: (certificate.subject.getField('CN')?.value as string) ?? '',
|
|
79
|
+
issuer: formatDN(certificate.issuer.attributes),
|
|
80
|
+
serialNumber: certificate.serialNumber,
|
|
81
|
+
validFrom: certificate.validity.notBefore,
|
|
82
|
+
validTo: certificate.validity.notAfter,
|
|
83
|
+
ruc: extractRuc(certificate),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { privateKey, certificate, chain, metadata }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate that a parsed certificate is not expired.
|
|
91
|
+
* Throws SriCertificateError if the certificate has expired.
|
|
92
|
+
*/
|
|
93
|
+
export function validateCertificate(cert: ParsedCertificate): void {
|
|
94
|
+
const now = new Date()
|
|
95
|
+
if (now > cert.metadata.validTo) {
|
|
96
|
+
throw new SriCertificateError(`Certificate expired on ${cert.metadata.validTo.toISOString()}`)
|
|
97
|
+
}
|
|
98
|
+
if (now < cert.metadata.validFrom) {
|
|
99
|
+
throw new SriCertificateError(
|
|
100
|
+
`Certificate not yet valid, starts ${cert.metadata.validFrom.toISOString()}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|