@ssddo/ecf-sdk 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/dist/index.js ADDED
@@ -0,0 +1,420 @@
1
+ // src/client.ts
2
+ import createClient from "openapi-fetch";
3
+
4
+ // src/polling.ts
5
+ var PollingTimeoutError = class extends Error {
6
+ constructor(message = "Polling timed out") {
7
+ super(message);
8
+ this.name = "PollingTimeoutError";
9
+ }
10
+ };
11
+ var PollingMaxRetriesError = class extends Error {
12
+ constructor(retries) {
13
+ super(`Polling exceeded maximum retries (${retries})`);
14
+ this.name = "PollingMaxRetriesError";
15
+ }
16
+ };
17
+ async function pollUntilComplete(fn, isComplete, options) {
18
+ const {
19
+ initialDelay = 1e3,
20
+ maxDelay = 3e4,
21
+ maxRetries = 60,
22
+ backoffMultiplier = 2,
23
+ timeout,
24
+ signal
25
+ } = options ?? {};
26
+ const startTime = Date.now();
27
+ let delay = initialDelay;
28
+ let retries = 0;
29
+ while (true) {
30
+ if (signal?.aborted) {
31
+ throw new DOMException("Polling was aborted", "AbortError");
32
+ }
33
+ const result = await fn();
34
+ if (isComplete(result)) {
35
+ return result;
36
+ }
37
+ retries++;
38
+ if (retries >= maxRetries) {
39
+ throw new PollingMaxRetriesError(maxRetries);
40
+ }
41
+ if (timeout !== void 0 && Date.now() - startTime >= timeout) {
42
+ throw new PollingTimeoutError();
43
+ }
44
+ await sleep(delay, signal);
45
+ delay = Math.min(delay * backoffMultiplier, maxDelay);
46
+ }
47
+ }
48
+ function sleep(ms, signal) {
49
+ return new Promise((resolve, reject) => {
50
+ if (signal?.aborted) {
51
+ reject(new DOMException("Polling was aborted", "AbortError"));
52
+ return;
53
+ }
54
+ const timer = setTimeout(resolve, ms);
55
+ signal?.addEventListener(
56
+ "abort",
57
+ () => {
58
+ clearTimeout(timer);
59
+ reject(new DOMException("Polling was aborted", "AbortError"));
60
+ },
61
+ { once: true }
62
+ );
63
+ });
64
+ }
65
+
66
+ // src/client.ts
67
+ var ENVIRONMENT_URLS = {
68
+ test: "https://api.test.ecfx.ssd.com.do",
69
+ cert: "https://api.cert.ecfx.ssd.com.do",
70
+ prod: "https://api.prod.ecfx.ssd.com.do"
71
+ };
72
+ var ECF_TYPE_ROUTE_MAP = {
73
+ FacturaDeCreditoFiscalElectronica: "31",
74
+ FacturaDeConsumoElectronica: "32",
75
+ NotaDeDebitoElectronica: "33",
76
+ NotaDeCreditoElectronica: "34",
77
+ ComprasElectronico: "41",
78
+ GastosMenoresElectronico: "43",
79
+ RegimenesEspecialesElectronico: "44",
80
+ GubernamentalElectronico: "45",
81
+ ComprobanteDeExportacionesElectronico: "46",
82
+ ComprobanteParaPagosAlExteriorElectronico: "47"
83
+ };
84
+ var EcfError = class extends Error {
85
+ response;
86
+ constructor(message, response) {
87
+ super(message);
88
+ this.name = "EcfError";
89
+ this.response = response;
90
+ }
91
+ };
92
+ var EcfClient = class {
93
+ /** The underlying openapi-fetch client for direct endpoint access. */
94
+ raw;
95
+ constructor(config) {
96
+ const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
97
+ const authMiddleware = {
98
+ async onRequest({ request }) {
99
+ request.headers.set("Authorization", `Bearer ${config.apiKey}`);
100
+ return request;
101
+ }
102
+ };
103
+ this.raw = createClient({ baseUrl });
104
+ this.raw.use(authMiddleware);
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // ECF send + poll
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Send an ECF and poll until processing completes.
111
+ *
112
+ * Determines the correct endpoint from `ecf.encabezado.idDoc.tipoeCF`,
113
+ * posts the ECF, then polls until `progress` is `Finished` or `Error`.
114
+ */
115
+ async sendEcf(ecf, pollingOptions) {
116
+ const tipoeCF = ecf.encabezado?.idDoc?.tipoeCF;
117
+ if (!tipoeCF) {
118
+ throw new Error("ECF must have encabezado.idDoc.tipoeCF");
119
+ }
120
+ const route = ECF_TYPE_ROUTE_MAP[tipoeCF];
121
+ if (!route) {
122
+ throw new Error(`Unknown tipoeCF: ${tipoeCF}`);
123
+ }
124
+ const rnc = ecf.encabezado?.emisor?.rncEmisor;
125
+ if (!rnc) {
126
+ throw new Error("ECF must have encabezado.emisor.rncEmisor");
127
+ }
128
+ const encf = ecf.encabezado?.idDoc?.encf;
129
+ if (!encf) {
130
+ throw new Error("ECF must have encabezado.idDoc.encf");
131
+ }
132
+ const response = await this.postEcf(route, ecf);
133
+ const result = await pollUntilComplete(
134
+ async () => {
135
+ const { data: queryData, error: queryError } = await this.raw.GET("/ecf/{rnc}/{encf}", {
136
+ params: { path: { rnc, encf } }
137
+ });
138
+ if (queryError) {
139
+ throw new Error(`Failed to query ECF status: ${JSON.stringify(queryError)}`);
140
+ }
141
+ const results = queryData;
142
+ const match = results.find((r) => r.messageId === response.messageId) ?? results[0];
143
+ if (!match) {
144
+ throw new Error("No ECF response found for the given rnc/encf");
145
+ }
146
+ return match;
147
+ },
148
+ (r) => r.progress === "Finished" || r.progress === "Error",
149
+ pollingOptions
150
+ );
151
+ if (result.progress === "Error") {
152
+ throw new EcfError(
153
+ result.errors ?? result.mensaje ?? "ECF processing failed",
154
+ result
155
+ );
156
+ }
157
+ return result;
158
+ }
159
+ async postEcf(route, body) {
160
+ const path = `/ecf/${route}`;
161
+ const { data, error } = await this.raw.POST(path, { body });
162
+ if (error) {
163
+ throw new Error(`Failed to send ECF: ${JSON.stringify(error)}`);
164
+ }
165
+ return data;
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Company operations
169
+ // ---------------------------------------------------------------------------
170
+ /** List companies with optional filters. */
171
+ async getCompanies(params) {
172
+ return this.raw.GET("/company", { params: { query: params } });
173
+ }
174
+ /** Get a company by RNC. */
175
+ async getCompanyByRnc(rnc) {
176
+ return this.raw.GET("/company/{rnc}", { params: { path: { rnc } } });
177
+ }
178
+ /** Create or update a company. */
179
+ async upsertCompany(body) {
180
+ return this.raw.PUT("/company", { body });
181
+ }
182
+ /** Delete a company by RNC. */
183
+ async deleteCompany(rnc) {
184
+ return this.raw.DELETE("/company/{rnc}", { params: { path: { rnc } } });
185
+ }
186
+ // ---------------------------------------------------------------------------
187
+ // Certificate operations
188
+ // ---------------------------------------------------------------------------
189
+ /** Get the current certificate for a company. */
190
+ async getCertificate(rnc) {
191
+ return this.raw.GET("/company/{rnc}/certificate", { params: { path: { rnc } } });
192
+ }
193
+ /** Update a company's certificate. Pass a FormData with 'certificate' file and 'password' field. */
194
+ async updateCertificate(rnc, body) {
195
+ const formData = new FormData();
196
+ formData.append("certificate", body.certificate);
197
+ formData.append("password", body.password);
198
+ return this.raw.PUT("/company/{rnc}/certificate", {
199
+ params: { path: { rnc } },
200
+ body: formData,
201
+ bodySerializer: (b) => b
202
+ });
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // ECF query operations
206
+ // ---------------------------------------------------------------------------
207
+ /** Query ECFs by RNC and eNCF. */
208
+ async queryEcf(rnc, encf) {
209
+ return this.raw.GET("/ecf/{rnc}/{encf}", { params: { path: { rnc, encf } } });
210
+ }
211
+ /** Search ECFs for a specific RNC. */
212
+ async searchEcfs(rnc, params) {
213
+ return this.raw.GET("/ecf/{rnc}", {
214
+ params: { path: { rnc }, query: params }
215
+ });
216
+ }
217
+ /** Search all ECFs across all companies. */
218
+ async searchAllEcfs(params) {
219
+ return this.raw.GET("/ecf", { params: { query: params } });
220
+ }
221
+ /** Get a specific ECF by message ID. */
222
+ async getEcfById(rnc, id) {
223
+ return this.raw.GET("/ecf/{rnc}/message/{id}", { params: { path: { rnc, id } } });
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Aprobacion comercial
227
+ // ---------------------------------------------------------------------------
228
+ /** Send aprobacion comercial for an ECF. */
229
+ async aprobacionComercial(rnc, encf, body) {
230
+ return this.raw.POST("/ecf/aprobacioncomercial/{rnc}/{encf}", {
231
+ params: { path: { rnc, encf } },
232
+ body
233
+ });
234
+ }
235
+ // ---------------------------------------------------------------------------
236
+ // Anulacion rangos
237
+ // ---------------------------------------------------------------------------
238
+ /** Request range annulment. */
239
+ async anulacionRangos(rnc, body) {
240
+ return this.raw.POST("/ecf/anularrango/{rnc}", {
241
+ params: { path: { rnc } },
242
+ body
243
+ });
244
+ }
245
+ /** List annulments. */
246
+ async listAnulaciones(params) {
247
+ return this.raw.GET("/ecf/anulaciones", { params: { query: params } });
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // Firmar semilla
251
+ // ---------------------------------------------------------------------------
252
+ /** Sign a seed for a company. */
253
+ async firmarSemilla(rnc, body) {
254
+ const formData = new FormData();
255
+ formData.append("xml", body.xml);
256
+ return this.raw.POST("/ecf/FirmarSemilla/{rnc}", {
257
+ params: { path: { rnc } },
258
+ body: formData,
259
+ bodySerializer: (b) => b
260
+ });
261
+ }
262
+ // ---------------------------------------------------------------------------
263
+ // Recepcion operations
264
+ // ---------------------------------------------------------------------------
265
+ /** Search ECF reception requests. */
266
+ async searchEcfReceptionRequests(params) {
267
+ return this.raw.GET("/recepcion/ecf", { params: { query: params } });
268
+ }
269
+ /** Search ACECF reception requests. */
270
+ async searchAcecfReceptionRequests(params) {
271
+ return this.raw.GET("/recepcion/acecf", { params: { query: params } });
272
+ }
273
+ /** Search ECF reception requests by RNC. */
274
+ async searchEcfReceptionRequestsByRnc(rnc, params) {
275
+ return this.raw.GET("/recepcion/{rnc}/ecf", {
276
+ params: { path: { rnc }, query: params }
277
+ });
278
+ }
279
+ /** Get a specific ECF reception request. */
280
+ async getEcfReceptionRequest(rnc, messageId) {
281
+ return this.raw.GET("/recepcion/{rnc}/ecf/{messageId}", {
282
+ params: { path: { rnc, messageId } }
283
+ });
284
+ }
285
+ /** Search ACECF reception requests by RNC. */
286
+ async searchAcecfReceptionRequestsByRnc(rnc, params) {
287
+ return this.raw.GET("/recepcion/{rnc}/acecf", {
288
+ params: { path: { rnc }, query: params }
289
+ });
290
+ }
291
+ /** Get a specific ACECF reception request. */
292
+ async getAcecfReceptionRequest(rnc, messageId) {
293
+ return this.raw.GET("/recepcion/{rnc}/acecf/{messageId}", {
294
+ params: { path: { rnc, messageId } }
295
+ });
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // DGII operations
299
+ // ---------------------------------------------------------------------------
300
+ /** Consulta directorio - listado. */
301
+ async consultaDirectorioListado(rnc) {
302
+ return this.raw.GET("/dgii/{rnc}/consultadirectorio/listado", {
303
+ params: { path: { rnc } }
304
+ });
305
+ }
306
+ /** Consulta directorio - obtener directorio por RNC. */
307
+ async consultaDirectorioPorRnc(rnc, query) {
308
+ return this.raw.GET("/dgii/{rnc}/consultadirectorio/obtener-directorio-por-rnc", {
309
+ params: { path: { rnc }, query }
310
+ });
311
+ }
312
+ /** Consulta estado. */
313
+ async consultaEstado(rnc, query) {
314
+ return this.raw.GET("/dgii/{rnc}/consultaestado/estado", {
315
+ params: { path: { rnc }, query }
316
+ });
317
+ }
318
+ /** Consulta resultado. */
319
+ async consultaResultado(rnc, query) {
320
+ return this.raw.GET("/dgii/{rnc}/consultaresultado/estado", {
321
+ params: { path: { rnc }, query }
322
+ });
323
+ }
324
+ /** Consulta RFCE. */
325
+ async consultaRFCE(rnc, query) {
326
+ return this.raw.GET("/dgii/{rnc}/consultarfce/consulta", {
327
+ params: { path: { rnc }, query }
328
+ });
329
+ }
330
+ /** Consulta timbre. */
331
+ async consultaTimbre(rnc, query) {
332
+ return this.raw.GET("/dgii/{rnc}/consultatimbre", {
333
+ params: { path: { rnc }, query }
334
+ });
335
+ }
336
+ /** Consulta timbre FC. */
337
+ async consultaTimbreFC(rnc, query) {
338
+ return this.raw.GET("/dgii/{rnc}/consultatimbrefc", {
339
+ params: { path: { rnc }, query }
340
+ });
341
+ }
342
+ /** Consulta track IDs. */
343
+ async consultaTrackId(rnc, query) {
344
+ return this.raw.GET("/dgii/{rnc}/consultatrackids/consulta", {
345
+ params: { path: { rnc }, query }
346
+ });
347
+ }
348
+ /** Estatus servicios - obtener estatus. */
349
+ async estatusServicios(rnc) {
350
+ return this.raw.GET("/dgii/{rnc}/estatusservicios/obtener-estatus", {
351
+ params: { path: { rnc } }
352
+ });
353
+ }
354
+ /** Estatus servicios - obtener ventanas de mantenimiento. */
355
+ async ventanasMantenimiento(rnc) {
356
+ return this.raw.GET("/dgii/{rnc}/estatusservicios/obtener-ventanas-mantenimiento", {
357
+ params: { path: { rnc } }
358
+ });
359
+ }
360
+ // ---------------------------------------------------------------------------
361
+ // ApiKey operations
362
+ // ---------------------------------------------------------------------------
363
+ /** Create a new API key. */
364
+ async createApiKey(body) {
365
+ return this.raw.POST("/apiKey", { body });
366
+ }
367
+ };
368
+ var EcfFrontendClient = class {
369
+ /** The underlying openapi-fetch client for direct endpoint access. */
370
+ raw;
371
+ constructor(config) {
372
+ const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
373
+ const authMiddleware = {
374
+ async onRequest({ request }) {
375
+ request.headers.set("Authorization", `Bearer ${config.apiKey}`);
376
+ return request;
377
+ }
378
+ };
379
+ this.raw = createClient({ baseUrl });
380
+ this.raw.use(authMiddleware);
381
+ }
382
+ /** Query ECFs by RNC and eNCF. */
383
+ async queryEcf(rnc, encf) {
384
+ return this.raw.GET("/ecf/{rnc}/{encf}", { params: { path: { rnc, encf } } });
385
+ }
386
+ /** Search ECFs for a specific RNC. */
387
+ async searchEcfs(rnc, params) {
388
+ return this.raw.GET("/ecf/{rnc}", {
389
+ params: { path: { rnc }, query: params }
390
+ });
391
+ }
392
+ /** Search all ECFs across all companies. */
393
+ async searchAllEcfs(params) {
394
+ return this.raw.GET("/ecf", { params: { query: params } });
395
+ }
396
+ /** Get a specific ECF by message ID. */
397
+ async getEcfById(rnc, id) {
398
+ return this.raw.GET("/ecf/{rnc}/message/{id}", { params: { path: { rnc, id } } });
399
+ }
400
+ /** List companies with optional filters. */
401
+ async getCompanies(params) {
402
+ return this.raw.GET("/company", { params: { query: params } });
403
+ }
404
+ /** Get a company by RNC. */
405
+ async getCompanyByRnc(rnc) {
406
+ return this.raw.GET("/company/{rnc}", { params: { path: { rnc } } });
407
+ }
408
+ };
409
+ function createFrontendClient(config) {
410
+ return new EcfFrontendClient(config);
411
+ }
412
+ export {
413
+ EcfClient,
414
+ EcfError,
415
+ EcfFrontendClient,
416
+ PollingMaxRetriesError,
417
+ PollingTimeoutError,
418
+ createFrontendClient,
419
+ pollUntilComplete
420
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@ssddo/ecf-sdk",
3
+ "version": "0.1.0",
4
+ "description": "SDK de TypeScript para la API de ECF DGII (comprobantes fiscales electrónicos de República Dominicana)",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "dependencies": {
25
+ "openapi-fetch": "^0.13.0"
26
+ },
27
+ "devDependencies": {
28
+ "openapi-typescript": "^7.6.1",
29
+ "tsup": "^8.3.0",
30
+ "typescript": "^5.7.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "author": {
36
+ "name": "SSD Smart Software Development SRL",
37
+ "email": "contacto@ssd.com.do",
38
+ "url": "https://ssd.com.do"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/SSD-Smart-Software-Development-SRL/ecf_dgii",
43
+ "directory": "typescript"
44
+ },
45
+ "homepage": "https://github.com/SSD-Smart-Software-Development-SRL/ecf_dgii/tree/main/typescript#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/SSD-Smart-Software-Development-SRL/ecf_dgii/issues"
48
+ },
49
+ "keywords": [
50
+ "ecf",
51
+ "dgii",
52
+ "factura-electronica",
53
+ "comprobante-fiscal",
54
+ "republica-dominicana",
55
+ "typescript",
56
+ "openapi",
57
+ "ssd"
58
+ ],
59
+ "license": "MIT",
60
+ "scripts": {
61
+ "generate": "openapi-typescript ../ecf_dgii/src/Apis/ECF_DGII.EcfApi/wwwroot/openapi/v1.json -o src/generated/v1.d.ts",
62
+ "build": "tsup"
63
+ }
64
+ }