@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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +5 -0
  3. package/.turbo/turbo-typecheck.log +5 -0
  4. package/dist/clave-acceso.d.ts +31 -0
  5. package/dist/clave-acceso.d.ts.map +1 -0
  6. package/dist/clave-acceso.js +70 -0
  7. package/dist/clave-acceso.js.map +1 -0
  8. package/dist/errors.d.ts +22 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +26 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/index.d.ts +13 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +11 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/signing/certificate.d.ts +27 -0
  17. package/dist/signing/certificate.d.ts.map +1 -0
  18. package/dist/signing/certificate.js +78 -0
  19. package/dist/signing/certificate.js.map +1 -0
  20. package/dist/signing/xades-bes.d.ts +10 -0
  21. package/dist/signing/xades-bes.d.ts.map +1 -0
  22. package/dist/signing/xades-bes.js +166 -0
  23. package/dist/signing/xades-bes.js.map +1 -0
  24. package/dist/soap/autorizacion.d.ts +21 -0
  25. package/dist/soap/autorizacion.d.ts.map +1 -0
  26. package/dist/soap/autorizacion.js +99 -0
  27. package/dist/soap/autorizacion.js.map +1 -0
  28. package/dist/soap/client.d.ts +6 -0
  29. package/dist/soap/client.d.ts.map +1 -0
  30. package/dist/soap/client.js +37 -0
  31. package/dist/soap/client.js.map +1 -0
  32. package/dist/soap/recepcion.d.ts +9 -0
  33. package/dist/soap/recepcion.d.ts.map +1 -0
  34. package/dist/soap/recepcion.js +60 -0
  35. package/dist/soap/recepcion.js.map +1 -0
  36. package/dist/xml/builder.d.ts +31 -0
  37. package/dist/xml/builder.d.ts.map +1 -0
  38. package/dist/xml/builder.js +287 -0
  39. package/dist/xml/builder.js.map +1 -0
  40. package/dist/xml/shared.d.ts +21 -0
  41. package/dist/xml/shared.d.ts.map +1 -0
  42. package/dist/xml/shared.js +93 -0
  43. package/dist/xml/shared.js.map +1 -0
  44. package/dist/xml/types.d.ts +179 -0
  45. package/dist/xml/types.d.ts.map +1 -0
  46. package/dist/xml/types.js +2 -0
  47. package/dist/xml/types.js.map +1 -0
  48. package/package.json +37 -0
  49. package/src/clave-acceso.ts +106 -0
  50. package/src/errors.ts +32 -0
  51. package/src/index.ts +49 -0
  52. package/src/signing/certificate.ts +103 -0
  53. package/src/signing/xades-bes.ts +188 -0
  54. package/src/soap/autorizacion.ts +139 -0
  55. package/src/soap/client.ts +45 -0
  56. package/src/soap/recepcion.ts +83 -0
  57. package/src/xml/builder.ts +344 -0
  58. package/src/xml/shared.ts +110 -0
  59. package/src/xml/types.ts +195 -0
  60. package/tsconfig.json +8 -0
@@ -0,0 +1,344 @@
1
+ import { create } from 'xmlbuilder2'
2
+ import type { Invoice, InvoiceItem, Organization, PaymentDetail } from '@timbra-ec/types'
3
+ import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'
4
+ import type {
5
+ NotaCreditoInput,
6
+ NotaDebitoInput,
7
+ GuiaRemisionInput,
8
+ RetencionInput,
9
+ AtsInput,
10
+ } from './types'
11
+ import {
12
+ money,
13
+ toSriDate,
14
+ aggregateTaxTotals,
15
+ buildInfoTributaria,
16
+ buildInfoAdicional,
17
+ buildDetalles,
18
+ } from './shared'
19
+
20
+ /** Build pagos section */
21
+ function buildPagos(parent: XMLBuilder, pagos: PaymentDetail[]): void {
22
+ const pagosEle = parent.ele('pagos')
23
+ for (const pago of pagos) {
24
+ const pagoEle = pagosEle.ele('pago')
25
+ pagoEle.ele('formaPago').txt(pago.formaPago)
26
+ pagoEle.ele('total').txt(money(pago.total))
27
+ if (pago.plazo !== undefined) {
28
+ pagoEle.ele('plazo').txt(pago.plazo.toString())
29
+ pagoEle.ele('unidadTiempo').txt(pago.unidadTiempo ?? 'dias')
30
+ }
31
+ }
32
+ }
33
+
34
+ /** Build totalConImpuestos section from aggregated tax totals */
35
+ function buildTotalConImpuestos(parent: XMLBuilder, items: InvoiceItem[]): void {
36
+ const taxTotals = aggregateTaxTotals(items)
37
+ const totalConImpuestos = parent.ele('totalConImpuestos')
38
+ for (const tax of taxTotals) {
39
+ const totalImpuesto = totalConImpuestos.ele('totalImpuesto')
40
+ totalImpuesto.ele('codigo').txt(tax.codigo)
41
+ totalImpuesto.ele('codigoPorcentaje').txt(tax.codigoPorcentaje)
42
+ totalImpuesto.ele('baseImponible').txt(money(tax.baseImponible))
43
+ totalImpuesto.ele('valor').txt(money(tax.valor))
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Build SRI-compliant factura XML (codDoc=01, version 2.1.0).
49
+ *
50
+ * The returned XML has `id="comprobante"` on the root element,
51
+ * which is required for XAdES-BES signing to reference.
52
+ */
53
+ export function buildFacturaXml(invoice: Invoice, org: Organization): string {
54
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
55
+ const factura = doc.ele('factura', { id: 'comprobante', version: '2.1.0' })
56
+
57
+ // --- infoTributaria ---
58
+ buildInfoTributaria(factura, {
59
+ ambiente: invoice.ambiente,
60
+ tipoEmision: invoice.tipoEmision,
61
+ ruc: org.ruc,
62
+ claveAcceso: invoice.claveAcceso,
63
+ codDoc: invoice.codDoc,
64
+ estab: invoice.estab,
65
+ ptoEmi: invoice.ptoEmi,
66
+ secuencial: invoice.secuencial,
67
+ razonSocial: org.razonSocial,
68
+ nombreComercial: org.nombreComercial,
69
+ direccionMatriz: org.direccionMatriz,
70
+ regimenRimpe: org.regimenRimpe,
71
+ })
72
+
73
+ // --- infoFactura ---
74
+ const infoFact = factura.ele('infoFactura')
75
+ infoFact.ele('fechaEmision').txt(toSriDate(invoice.fechaEmision))
76
+ infoFact.ele('obligadoContabilidad').txt(org.obligadoContabilidad ? 'SI' : 'NO')
77
+ infoFact.ele('tipoIdentificacionComprador').txt(invoice.tipoIdentificacionComprador)
78
+ infoFact.ele('razonSocialComprador').txt(invoice.razonSocialComprador)
79
+ infoFact.ele('identificacionComprador').txt(invoice.identificacionComprador)
80
+ infoFact.ele('totalSinImpuestos').txt(money(invoice.totalSinImpuestos))
81
+ infoFact.ele('totalDescuento').txt(money(invoice.totalDescuento))
82
+ buildTotalConImpuestos(infoFact, invoice.items)
83
+ infoFact.ele('propina').txt(money(invoice.propina ?? 0))
84
+ infoFact.ele('importeTotal').txt(money(invoice.importeTotal))
85
+ infoFact.ele('moneda').txt(invoice.moneda)
86
+ buildPagos(infoFact, invoice.pagos)
87
+
88
+ // --- detalles ---
89
+ buildDetalles(factura, invoice.items)
90
+
91
+ // --- infoAdicional ---
92
+ buildInfoAdicional(factura, invoice.infoAdicional)
93
+
94
+ return doc.end({ prettyPrint: false })
95
+ }
96
+
97
+ /**
98
+ * Build SRI-compliant Nota de Credito XML (codDoc=04, version 1.1.0).
99
+ */
100
+ export function buildNotaCreditoXml(input: NotaCreditoInput): string {
101
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
102
+ const root = doc.ele('notaCredito', { id: 'comprobante', version: '1.1.0' })
103
+
104
+ buildInfoTributaria(root, input.infoTributaria)
105
+
106
+ const info = root.ele('infoNotaCredito')
107
+ info.ele('fechaEmision').txt(toSriDate(input.fechaEmision))
108
+ info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
109
+ info.ele('tipoIdentificacionComprador').txt(input.tipoIdentificacionComprador)
110
+ info.ele('razonSocialComprador').txt(input.razonSocialComprador)
111
+ info.ele('identificacionComprador').txt(input.identificacionComprador)
112
+ info.ele('codDocModificado').txt(input.codDocModificado)
113
+ info.ele('numDocModificado').txt(input.numDocModificado)
114
+ info.ele('fechaEmisionDocSustento').txt(toSriDate(input.fechaEmisionDocSustento))
115
+ info.ele('totalSinImpuestos').txt(money(input.totalSinImpuestos))
116
+ buildTotalConImpuestos(info, input.items)
117
+ info.ele('motivo').txt(input.motivo)
118
+ info.ele('importeTotal').txt(money(input.importeTotal))
119
+ buildPagos(info, input.pagos)
120
+
121
+ buildDetalles(root, input.items)
122
+ buildInfoAdicional(root, input.infoAdicional)
123
+
124
+ return doc.end({ prettyPrint: false })
125
+ }
126
+
127
+ /**
128
+ * Build SRI-compliant Nota de Debito XML (codDoc=05, version 1.0.0).
129
+ */
130
+ export function buildNotaDebitoXml(input: NotaDebitoInput): string {
131
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
132
+ const root = doc.ele('notaDebito', { id: 'comprobante', version: '1.0.0' })
133
+
134
+ buildInfoTributaria(root, input.infoTributaria)
135
+
136
+ const info = root.ele('infoNotaDebito')
137
+ info.ele('fechaEmision').txt(toSriDate(input.fechaEmision))
138
+ info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
139
+ info.ele('tipoIdentificacionComprador').txt(input.tipoIdentificacionComprador)
140
+ info.ele('razonSocialComprador').txt(input.razonSocialComprador)
141
+ info.ele('identificacionComprador').txt(input.identificacionComprador)
142
+ info.ele('codDocModificado').txt(input.codDocModificado)
143
+ info.ele('numDocModificado').txt(input.numDocModificado)
144
+ info.ele('fechaEmisionDocSustento').txt(toSriDate(input.fechaEmisionDocSustento))
145
+ info.ele('totalSinImpuestos').txt(money(input.totalSinImpuestos))
146
+
147
+ const impuestosEle = info.ele('impuestos')
148
+ for (const imp of input.impuestos) {
149
+ const impuesto = impuestosEle.ele('impuesto')
150
+ impuesto.ele('codigo').txt(imp.codigo)
151
+ impuesto.ele('codigoPorcentaje').txt(imp.codigoPorcentaje)
152
+ impuesto.ele('tarifa').txt(money(imp.tarifa))
153
+ impuesto.ele('baseImponible').txt(money(imp.baseImponible))
154
+ impuesto.ele('valor').txt(money(imp.valor))
155
+ }
156
+
157
+ info.ele('importeTotal').txt(money(input.importeTotal))
158
+ buildPagos(info, input.pagos)
159
+
160
+ const motivos = root.ele('motivos')
161
+ for (const m of input.motivos) {
162
+ const motivo = motivos.ele('motivo')
163
+ motivo.ele('razon').txt(m.razon)
164
+ motivo.ele('valor').txt(money(m.valor))
165
+ }
166
+
167
+ buildInfoAdicional(root, input.infoAdicional)
168
+
169
+ return doc.end({ prettyPrint: false })
170
+ }
171
+
172
+ /**
173
+ * Build SRI-compliant Guia de Remision XML (codDoc=06, version 1.1.0).
174
+ */
175
+ export function buildGuiaRemisionXml(input: GuiaRemisionInput): string {
176
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
177
+ const root = doc.ele('guiaRemision', { id: 'comprobante', version: '1.1.0' })
178
+
179
+ buildInfoTributaria(root, input.infoTributaria)
180
+
181
+ const info = root.ele('infoGuiaRemision')
182
+ info.ele('dirEstablecimiento').txt(input.infoTributaria.direccionMatriz)
183
+ info.ele('dirPartida').txt(input.dirPartida)
184
+ info.ele('razonSocialTransportista').txt(input.razonSocialTransportista)
185
+ info.ele('tipoIdentificacionTransportista').txt(input.tipoIdentificacionTransportista)
186
+ info.ele('rucTransportista').txt(input.rucTransportista)
187
+ info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
188
+ info.ele('fechaIniTransporte').txt(toSriDate(input.fechaIniTransporte))
189
+ info.ele('fechaFinTransporte').txt(toSriDate(input.fechaFinTransporte))
190
+ info.ele('placa').txt(input.placa)
191
+
192
+ const destinatarios = root.ele('destinatarios')
193
+ for (const dest of input.destinatarios) {
194
+ const destinatario = destinatarios.ele('destinatario')
195
+ destinatario.ele('identificacionDestinatario').txt(dest.identificacionDestinatario)
196
+ destinatario.ele('razonSocialDestinatario').txt(dest.razonSocialDestinatario)
197
+ destinatario.ele('dirDestinatario').txt(dest.dirDestinatario)
198
+ destinatario.ele('motivoTraslado').txt(dest.motivoTraslado)
199
+
200
+ if (dest.codDocSustento) {
201
+ destinatario.ele('codDocSustento').txt(dest.codDocSustento)
202
+ }
203
+ if (dest.numDocSustento) {
204
+ destinatario.ele('numDocSustento').txt(dest.numDocSustento)
205
+ }
206
+ if (dest.numAutDocSustento) {
207
+ destinatario.ele('numAutDocSustento').txt(dest.numAutDocSustento)
208
+ }
209
+ if (dest.fechaEmisionDocSustento) {
210
+ destinatario.ele('fechaEmisionDocSustento').txt(toSriDate(dest.fechaEmisionDocSustento))
211
+ }
212
+
213
+ const detalles = destinatario.ele('detalles')
214
+ for (const item of dest.detalles) {
215
+ const detalle = detalles.ele('detalle')
216
+ detalle.ele('codigoInterno').txt(item.codigoInterno)
217
+ detalle.ele('descripcion').txt(item.descripcion)
218
+ detalle.ele('cantidad').txt(money(item.cantidad))
219
+ }
220
+ }
221
+
222
+ buildInfoAdicional(root, input.infoAdicional)
223
+
224
+ return doc.end({ prettyPrint: false })
225
+ }
226
+
227
+ /**
228
+ * Build SRI-compliant Comprobante de Retencion XML (codDoc=07, Version ATS format).
229
+ */
230
+ export function buildRetencionXml(input: RetencionInput): string {
231
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
232
+ const root = doc.ele('comprobanteRetencion', { id: 'comprobante', version: '2.0.0' })
233
+
234
+ buildInfoTributaria(root, input.infoTributaria)
235
+
236
+ const info = root.ele('infoCompRetencion')
237
+ info.ele('fechaEmision').txt(toSriDate(input.fechaEmision))
238
+ info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
239
+ info.ele('tipoIdentificacionSujetoRetenido').txt(input.tipoIdentificacionSujetoRetenido)
240
+ info.ele('razonSocialSujetoRetenido').txt(input.razonSocialSujetoRetenido)
241
+ info.ele('identificacionSujetoRetenido').txt(input.identificacionSujetoRetenido)
242
+ info.ele('periodoFiscal').txt(input.periodoFiscal)
243
+
244
+ const docsSustento = root.ele('docsSustento')
245
+ // Group retention details by sustaining document
246
+ const byDoc = new Map<string, typeof input.impuestos>()
247
+ for (const imp of input.impuestos) {
248
+ const key = `${imp.codDocSustento}-${imp.numDocSustento}-${imp.fechaEmisionDocSustento}`
249
+ const group = byDoc.get(key) ?? []
250
+ group.push(imp)
251
+ byDoc.set(key, group)
252
+ }
253
+
254
+ for (const [, retenciones] of byDoc) {
255
+ const first = retenciones[0]!
256
+ const docSustento = docsSustento.ele('docSustento')
257
+ docSustento.ele('codSustento').txt('01')
258
+ docSustento.ele('codDocSustento').txt(first.codDocSustento)
259
+ docSustento.ele('numDocSustento').txt(first.numDocSustento)
260
+ docSustento.ele('fechaEmisionDocSustento').txt(toSriDate(first.fechaEmisionDocSustento))
261
+
262
+ const retencionesEle = docSustento.ele('retenciones')
263
+ for (const ret of retenciones) {
264
+ const retencion = retencionesEle.ele('retencion')
265
+ retencion.ele('codigo').txt(ret.codigo)
266
+ retencion.ele('codigoRetencion').txt(ret.codigoRetencion)
267
+ retencion.ele('baseImponible').txt(money(ret.baseImponible))
268
+ retencion.ele('porcentajeRetener').txt(money(ret.porcentajeRetener))
269
+ retencion.ele('valorRetenido').txt(money(ret.valorRetenido))
270
+ }
271
+ }
272
+
273
+ buildInfoAdicional(root, input.infoAdicional)
274
+
275
+ return doc.end({ prettyPrint: false })
276
+ }
277
+
278
+ /**
279
+ * Build ATS (Anexo Transaccional Simplificado) XML for monthly filing.
280
+ * This is NOT an electronic document — it's a report XML uploaded to SRI portal.
281
+ */
282
+ export function buildAtsXml(input: AtsInput): string {
283
+ const doc = create({ version: '1.0', encoding: 'UTF-8' })
284
+ const iva = doc.ele('iva')
285
+
286
+ iva.ele('TipoIDInformante').txt(input.tipoIDInformante)
287
+ iva.ele('IdInformante').txt(input.idInformante)
288
+ iva.ele('razonSocial').txt(input.razonSocial)
289
+ iva.ele('Anio').txt(input.anio)
290
+ iva.ele('Mes').txt(input.mes)
291
+ iva.ele('numEstabRuc').txt(input.numEstabRuc)
292
+ iva.ele('totalVentas').txt(money(input.totalVentas))
293
+ iva.ele('codigoOperativo').txt(input.codigoOperativo)
294
+
295
+ if (input.compras.length > 0) {
296
+ const compras = iva.ele('compras')
297
+ for (const c of input.compras) {
298
+ const detalle = compras.ele('detalleCompras')
299
+ detalle.ele('codSustento').txt(c.codSustento)
300
+ detalle.ele('tpIdProv').txt(c.tpIdProv)
301
+ detalle.ele('idProv').txt(c.idProv)
302
+ detalle.ele('tipoComprobante').txt(c.tipoComprobante)
303
+ detalle.ele('parteRel').txt(c.parteRel)
304
+ detalle.ele('fechaRegistro').txt(c.fechaRegistro)
305
+ detalle.ele('establecimiento').txt(c.establecimiento)
306
+ detalle.ele('puntoEmision').txt(c.puntoEmision)
307
+ detalle.ele('secuencial').txt(c.secuencial)
308
+ detalle.ele('fechaEmision').txt(c.fechaEmision)
309
+ detalle.ele('autorizacion').txt(c.autorizacion)
310
+ detalle.ele('baseNoGraIva').txt(money(c.baseNoGraIva))
311
+ detalle.ele('baseImponible').txt(money(c.baseImponible))
312
+ detalle.ele('baseImpGrav').txt(money(c.baseImpGrav))
313
+ detalle.ele('montoIva').txt(money(c.montoIva))
314
+ detalle.ele('valRetBien10').txt(money(c.valRetBien10))
315
+ detalle.ele('valRetServ20').txt(money(c.valRetServ20))
316
+ detalle.ele('valorRetBienes').txt(money(c.valorRetBienes))
317
+ detalle.ele('valRetServ50').txt(money(c.valRetServ50))
318
+ detalle.ele('valorRetServicios').txt(money(c.valorRetServicios))
319
+ detalle.ele('valRetServ100').txt(money(c.valRetServ100))
320
+ detalle.ele('totbasesImpReemb').txt(money(c.totbasesImpReemb))
321
+ }
322
+ }
323
+
324
+ if (input.ventas.length > 0) {
325
+ const ventas = iva.ele('ventas')
326
+ for (const v of input.ventas) {
327
+ const detalle = ventas.ele('detalleVentas')
328
+ detalle.ele('tpIdCliente').txt(v.tpIdCliente)
329
+ detalle.ele('idCliente').txt(v.idCliente)
330
+ detalle.ele('tipoComprobante').txt(v.tipoComprobante)
331
+ detalle.ele('tipoEmision').txt(v.tipoEmision)
332
+ detalle.ele('numeroComprobantes').txt(v.numeroComprobantes.toString())
333
+ detalle.ele('baseNoGraIva').txt(money(v.baseNoGraIva))
334
+ detalle.ele('baseImponible').txt(money(v.baseImponible))
335
+ detalle.ele('baseImpGrav').txt(money(v.baseImpGrav))
336
+ detalle.ele('montoIva').txt(money(v.montoIva))
337
+ detalle.ele('montoIce').txt(money(v.montoIce))
338
+ detalle.ele('valorRetIva').txt(money(v.valorRetIva))
339
+ detalle.ele('valorRetRenta').txt(money(v.valorRetRenta))
340
+ }
341
+ }
342
+
343
+ return doc.end({ prettyPrint: false })
344
+ }
@@ -0,0 +1,110 @@
1
+ import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'
2
+ import type { InvoiceItem } from '@timbra-ec/types'
3
+ import type { InfoTributariaInput } from './types'
4
+
5
+ /** Format a number to exactly 2 decimal places */
6
+ export function money(value: number): string {
7
+ return value.toFixed(2)
8
+ }
9
+
10
+ /** Convert ISO date (YYYY-MM-DD) to SRI format (dd/MM/yyyy) */
11
+ export function toSriDate(isoDate: string): string {
12
+ const [year, month, day] = isoDate.split('-')
13
+ return `${day}/${month}/${year}`
14
+ }
15
+
16
+ /** Aggregate tax totals by codigoPorcentaje across all items */
17
+ export function aggregateTaxTotals(items: InvoiceItem[]) {
18
+ const totals = new Map<
19
+ string,
20
+ { codigo: string; codigoPorcentaje: string; baseImponible: number; valor: number }
21
+ >()
22
+
23
+ for (const item of items) {
24
+ for (const imp of item.impuestos) {
25
+ const key = `${imp.codigo}-${imp.codigoPorcentaje}`
26
+ const existing = totals.get(key)
27
+ if (existing) {
28
+ existing.baseImponible += imp.baseImponible
29
+ existing.valor += imp.valor
30
+ } else {
31
+ totals.set(key, {
32
+ codigo: imp.codigo,
33
+ codigoPorcentaje: imp.codigoPorcentaje,
34
+ baseImponible: imp.baseImponible,
35
+ valor: imp.valor,
36
+ })
37
+ }
38
+ }
39
+ }
40
+
41
+ return Array.from(totals.values())
42
+ }
43
+
44
+ /** Build the infoTributaria section (common to all SRI documents) */
45
+ export function buildInfoTributaria(parent: XMLBuilder, input: InfoTributariaInput): void {
46
+ const infoTrib = parent.ele('infoTributaria')
47
+ infoTrib.ele('ambiente').txt(input.ambiente)
48
+ infoTrib.ele('tipoEmision').txt(input.tipoEmision)
49
+ infoTrib.ele('razonSocial').txt(input.razonSocial)
50
+ if (input.nombreComercial) {
51
+ infoTrib.ele('nombreComercial').txt(input.nombreComercial)
52
+ }
53
+ infoTrib.ele('ruc').txt(input.ruc)
54
+ infoTrib.ele('claveAcceso').txt(input.claveAcceso)
55
+ infoTrib.ele('codDoc').txt(input.codDoc)
56
+ infoTrib.ele('estab').txt(input.estab)
57
+ infoTrib.ele('ptoEmi').txt(input.ptoEmi)
58
+ infoTrib.ele('secuencial').txt(input.secuencial)
59
+ infoTrib.ele('dirMatriz').txt(input.direccionMatriz)
60
+
61
+ if (input.regimenRimpe === 'emprendedor') {
62
+ infoTrib.ele('contribuyenteRimpe').txt('CONTRIBUYENTE RÉGIMEN RIMPE')
63
+ } else if (input.regimenRimpe === 'negocio_popular') {
64
+ infoTrib.ele('contribuyenteRimpe').txt('CONTRIBUYENTE NEGOCIO POPULAR - RÉGIMEN RIMPE')
65
+ }
66
+ }
67
+
68
+ /** Build infoAdicional section */
69
+ export function buildInfoAdicional(
70
+ parent: XMLBuilder,
71
+ infoAdicional?: Record<string, string>,
72
+ ): void {
73
+ if (infoAdicional && Object.keys(infoAdicional).length > 0) {
74
+ const section = parent.ele('infoAdicional')
75
+ for (const [nombre, valor] of Object.entries(infoAdicional)) {
76
+ section.ele('campoAdicional', { nombre }).txt(valor)
77
+ }
78
+ }
79
+ }
80
+
81
+ /** Build detalles section from InvoiceItems */
82
+ export function buildDetalles(parent: XMLBuilder, items: InvoiceItem[]): void {
83
+ const detalles = parent.ele('detalles')
84
+ for (const item of items) {
85
+ const detalle = detalles.ele('detalle')
86
+ detalle.ele('codigoPrincipal').txt(item.codigoPrincipal)
87
+ detalle.ele('descripcion').txt(item.descripcion)
88
+ detalle.ele('cantidad').txt(money(item.cantidad))
89
+ detalle.ele('precioUnitario').txt(money(item.precioUnitario))
90
+ detalle.ele('descuento').txt(money(item.descuento))
91
+ detalle.ele('precioTotalSinImpuesto').txt(money(item.precioTotalSinImpuesto))
92
+
93
+ const impuestos = detalle.ele('impuestos')
94
+ for (const imp of item.impuestos) {
95
+ const impuesto = impuestos.ele('impuesto')
96
+ impuesto.ele('codigo').txt(imp.codigo)
97
+ impuesto.ele('codigoPorcentaje').txt(imp.codigoPorcentaje)
98
+ impuesto.ele('tarifa').txt(money(imp.tarifa))
99
+ impuesto.ele('baseImponible').txt(money(imp.baseImponible))
100
+ impuesto.ele('valor').txt(money(imp.valor))
101
+ }
102
+
103
+ if (item.detallesAdicionales && item.detallesAdicionales.length > 0) {
104
+ const detAdicionales = detalle.ele('detallesAdicionales')
105
+ for (const da of item.detallesAdicionales) {
106
+ detAdicionales.ele('detAdicional', { nombre: da.nombre }).txt(da.valor)
107
+ }
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,195 @@
1
+ import type {
2
+ DocumentTypeCode,
3
+ EmissionType,
4
+ InvoiceItem,
5
+ PaymentDetail,
6
+ SriEnvironment,
7
+ } from '@timbra-ec/types'
8
+
9
+ /** Common fields for all SRI documents (used in infoTributaria) */
10
+ export interface InfoTributariaInput {
11
+ ambiente: SriEnvironment
12
+ tipoEmision: EmissionType
13
+ ruc: string
14
+ claveAcceso: string
15
+ codDoc: DocumentTypeCode
16
+ estab: string
17
+ ptoEmi: string
18
+ secuencial: string
19
+ razonSocial: string
20
+ nombreComercial?: string
21
+ direccionMatriz: string
22
+ regimenRimpe?: 'emprendedor' | 'negocio_popular' | null
23
+ }
24
+
25
+ /** Input for building a Nota de Credito XML (codDoc=04) */
26
+ export interface NotaCreditoInput {
27
+ infoTributaria: InfoTributariaInput
28
+ /** Date in YYYY-MM-DD format */
29
+ fechaEmision: string
30
+ obligadoContabilidad: boolean
31
+ tipoIdentificacionComprador: string
32
+ identificacionComprador: string
33
+ razonSocialComprador: string
34
+ /** codDoc of the modified document (e.g. '01' for factura) */
35
+ codDocModificado: DocumentTypeCode
36
+ /** Document number being modified: estab-ptoEmi-secuencial (e.g. '001-001-000000001') */
37
+ numDocModificado: string
38
+ /** Emission date of the modified document in YYYY-MM-DD */
39
+ fechaEmisionDocSustento: string
40
+ /** Reason for the credit note */
41
+ motivo: string
42
+ totalSinImpuestos: number
43
+ importeTotal: number
44
+ items: InvoiceItem[]
45
+ pagos: PaymentDetail[]
46
+ infoAdicional?: Record<string, string>
47
+ }
48
+
49
+ /** Motivo entry for a Nota de Debito */
50
+ export interface MotivoDebito {
51
+ razon: string
52
+ valor: number
53
+ }
54
+
55
+ /** Input for building a Nota de Debito XML (codDoc=05) */
56
+ export interface NotaDebitoInput {
57
+ infoTributaria: InfoTributariaInput
58
+ fechaEmision: string
59
+ obligadoContabilidad: boolean
60
+ tipoIdentificacionComprador: string
61
+ identificacionComprador: string
62
+ razonSocialComprador: string
63
+ codDocModificado: DocumentTypeCode
64
+ numDocModificado: string
65
+ fechaEmisionDocSustento: string
66
+ totalSinImpuestos: number
67
+ importeTotal: number
68
+ impuestos: {
69
+ codigo: string
70
+ codigoPorcentaje: string
71
+ tarifa: number
72
+ baseImponible: number
73
+ valor: number
74
+ }[]
75
+ motivos: MotivoDebito[]
76
+ pagos: PaymentDetail[]
77
+ infoAdicional?: Record<string, string>
78
+ }
79
+
80
+ /** Destinatario for a Guia de Remision */
81
+ export interface Destinatario {
82
+ identificacionDestinatario: string
83
+ razonSocialDestinatario: string
84
+ dirDestinatario: string
85
+ motivoTraslado: string
86
+ /** Optional: referencing a factura or liquidacion */
87
+ codDocSustento?: DocumentTypeCode
88
+ numDocSustento?: string
89
+ numAutDocSustento?: string
90
+ fechaEmisionDocSustento?: string
91
+ detalles: {
92
+ codigoInterno: string
93
+ descripcion: string
94
+ cantidad: number
95
+ }[]
96
+ }
97
+
98
+ /** Input for building a Guia de Remision XML (codDoc=06) */
99
+ export interface GuiaRemisionInput {
100
+ infoTributaria: InfoTributariaInput
101
+ fechaEmision: string
102
+ obligadoContabilidad: boolean
103
+ dirPartida: string
104
+ razonSocialTransportista: string
105
+ tipoIdentificacionTransportista: string
106
+ rucTransportista: string
107
+ placa: string
108
+ fechaIniTransporte: string
109
+ fechaFinTransporte: string
110
+ destinatarios: Destinatario[]
111
+ infoAdicional?: Record<string, string>
112
+ }
113
+
114
+ /** Retention line item */
115
+ export interface RetencionDetalle {
116
+ /** '1' = renta, '2' = IVA, '6' = ISD */
117
+ codigo: string
118
+ codigoRetencion: string
119
+ baseImponible: number
120
+ porcentajeRetener: number
121
+ valorRetenido: number
122
+ /** codDoc of the sustaining document */
123
+ codDocSustento: DocumentTypeCode
124
+ numDocSustento: string
125
+ fechaEmisionDocSustento: string
126
+ }
127
+
128
+ /** Input for building a Comprobante de Retencion XML (codDoc=07, Version ATS) */
129
+ export interface RetencionInput {
130
+ infoTributaria: InfoTributariaInput
131
+ fechaEmision: string
132
+ obligadoContabilidad: boolean
133
+ tipoIdentificacionSujetoRetenido: string
134
+ identificacionSujetoRetenido: string
135
+ razonSocialSujetoRetenido: string
136
+ periodoFiscal: string
137
+ impuestos: RetencionDetalle[]
138
+ infoAdicional?: Record<string, string>
139
+ }
140
+
141
+ /** Purchase detail for ATS */
142
+ export interface AtsCompra {
143
+ codSustento: string
144
+ tpIdProv: string
145
+ idProv: string
146
+ tipoComprobante: string
147
+ parteRel: 'SI' | 'NO'
148
+ fechaRegistro: string
149
+ establecimiento: string
150
+ puntoEmision: string
151
+ secuencial: string
152
+ fechaEmision: string
153
+ autorizacion: string
154
+ baseNoGraIva: number
155
+ baseImponible: number
156
+ baseImpGrav: number
157
+ montoIva: number
158
+ valRetBien10: number
159
+ valRetServ20: number
160
+ valorRetBienes: number
161
+ valRetServ50: number
162
+ valorRetServicios: number
163
+ valRetServ100: number
164
+ totbasesImpReemb: number
165
+ }
166
+
167
+ /** Sales detail for ATS */
168
+ export interface AtsVenta {
169
+ tpIdCliente: string
170
+ idCliente: string
171
+ tipoComprobante: string
172
+ tipoEmision: 'E' | 'F'
173
+ numeroComprobantes: number
174
+ baseNoGraIva: number
175
+ baseImponible: number
176
+ baseImpGrav: number
177
+ montoIva: number
178
+ montoIce: number
179
+ valorRetIva: number
180
+ valorRetRenta: number
181
+ }
182
+
183
+ /** Input for building ATS XML */
184
+ export interface AtsInput {
185
+ tipoIDInformante: 'R' | 'C'
186
+ idInformante: string
187
+ razonSocial: string
188
+ anio: string
189
+ mes: string
190
+ numEstabRuc: string
191
+ totalVentas: number
192
+ codigoOperativo: string
193
+ compras: AtsCompra[]
194
+ ventas: AtsVenta[]
195
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@timbra-ec/config-ts/library.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }