@ssddo/ecf-sdk 0.1.0 → 0.2.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/README.md +76 -9
- package/dist/index.cjs +23 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +23 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -144,36 +144,103 @@ const result = await client.sendEcf(ecf, {
|
|
|
144
144
|
|
|
145
145
|
## Arquitectura Backend / Frontend
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
```mermaid
|
|
148
|
+
sequenceDiagram
|
|
149
|
+
participant C as Cliente (Browser/App)
|
|
150
|
+
participant BE as Backend
|
|
151
|
+
participant ECF as ECF API
|
|
152
|
+
|
|
153
|
+
C->>BE: POST /invoice (datos de factura)
|
|
154
|
+
Note over BE: Valida, guarda y convierte a formato ECF
|
|
155
|
+
|
|
156
|
+
BE->>ECF: POST /ecf/{tipo} (enviar ECF)
|
|
157
|
+
ECF-->>BE: { messageId }
|
|
158
|
+
BE-->>C: { messageId }
|
|
159
|
+
|
|
160
|
+
Note over C: No espera — puede continuar
|
|
161
|
+
|
|
162
|
+
alt Token en cache
|
|
163
|
+
C->>C: Usar token existente
|
|
164
|
+
else Sin token o expirado
|
|
165
|
+
C->>BE: GET /ecf-token
|
|
166
|
+
BE->>ECF: POST /apikey (solo lectura, scoped a RNC)
|
|
167
|
+
ECF-->>BE: { apiKey }
|
|
168
|
+
BE-->>C: { apiKey }
|
|
169
|
+
C->>C: Almacenar token en cache
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
loop Polling hasta completar
|
|
173
|
+
C->>ECF: GET /ecf/{rnc}/{encf} (token solo lectura)
|
|
174
|
+
ECF-->>C: { progress, codSec, ... }
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Flujo detallado
|
|
179
|
+
|
|
180
|
+
1. El **cliente** (browser/app) envía los datos de la factura al **backend** (`POST /invoice`, `/order`, `/sale`)
|
|
181
|
+
2. El **backend** valida, guarda y convierte la factura interna al formato ECF
|
|
182
|
+
3. El **backend** envía el ECF a la API de ECF SSD (`POST /ecf/{tipo}`) y recibe un `messageId`
|
|
183
|
+
4. El **backend** retorna el `messageId` al cliente — **el cliente no espera**, puede continuar
|
|
184
|
+
5. Cuando el cliente necesita consultar el estado del ECF, usa `EcfFrontendClient` que internamente:
|
|
185
|
+
- Verifica si hay un **token de solo lectura** en cache
|
|
186
|
+
- Si **no existe o expiró**: llama a `getToken()` (que el consumidor provee — típicamente un `fetch('/ecf-token')` a su backend), luego llama a `cacheToken(token)` para almacenarlo
|
|
187
|
+
- Si la API retorna **401**: automáticamente llama a `getToken()` de nuevo, actualiza el cache, y reintenta
|
|
188
|
+
6. El cliente hace **polling** directamente contra la API de ECF SSD (`GET /ecf/{rnc}/{encf}`) hasta que `progress` sea `Finished`
|
|
189
|
+
|
|
190
|
+
### Ejemplo: Backend
|
|
148
191
|
|
|
149
192
|
```typescript
|
|
150
|
-
|
|
193
|
+
import { EcfClient } from '@ssddo/ecf-sdk';
|
|
194
|
+
|
|
151
195
|
const ecfClient = new EcfClient({
|
|
152
196
|
apiKey: process.env.ECF_BACKEND_TOKEN,
|
|
153
197
|
environment: 'prod',
|
|
154
198
|
});
|
|
155
199
|
|
|
200
|
+
// Endpoint de facturación
|
|
156
201
|
app.post('/api/v1/invoices', async (req, res) => {
|
|
157
|
-
// 1. Validar y guardar tu factura interna
|
|
158
202
|
const invoice = await validateAndSave(req.body);
|
|
159
|
-
// 2. Convertir a formato ECF
|
|
160
203
|
const ecf = convertToEcf(invoice);
|
|
161
|
-
// 3. Enviar a ECF SSD
|
|
162
204
|
const { data } = await ecfClient.raw.POST('/ecf/31', { body: ecf });
|
|
163
205
|
await updateInvoice(invoice.id, { messageId: data.messageId });
|
|
164
206
|
res.json({ id: invoice.id, messageId: data.messageId });
|
|
165
207
|
});
|
|
166
208
|
|
|
167
|
-
//
|
|
209
|
+
// Generar token de solo lectura para el cliente
|
|
168
210
|
app.get('/api/v1/ecf-token', async (req, res) => {
|
|
169
211
|
const { data } = await ecfClient.createApiKey({ rnc: tenant.rnc });
|
|
170
|
-
res.json({
|
|
212
|
+
res.json({ apiKey: data.token });
|
|
171
213
|
});
|
|
172
214
|
```
|
|
173
215
|
|
|
174
|
-
|
|
216
|
+
### Ejemplo: Frontend (con `EcfFrontendClient`)
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { createFrontendClient } from '@ssddo/ecf-sdk';
|
|
175
220
|
|
|
176
|
-
|
|
221
|
+
// 1. Enviar la factura al backend
|
|
222
|
+
const invoiceRes = await fetch('/api/v1/invoices', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: JSON.stringify(invoiceData),
|
|
225
|
+
});
|
|
226
|
+
const { messageId, rnc, encf } = await invoiceRes.json();
|
|
227
|
+
// El cliente no espera — puede continuar con otras operaciones
|
|
228
|
+
|
|
229
|
+
// 2. Crear cliente de solo lectura (getToken se llama automáticamente)
|
|
230
|
+
const frontend = createFrontendClient({
|
|
231
|
+
getToken: async () => {
|
|
232
|
+
const res = await fetch('/api/v1/ecf-token');
|
|
233
|
+
const { apiKey } = await res.json();
|
|
234
|
+
return apiKey;
|
|
235
|
+
},
|
|
236
|
+
environment: 'prod',
|
|
237
|
+
// cacheToken y getCachedToken usan localStorage por defecto
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// 3. Consultar el estado del ECF
|
|
241
|
+
const { data } = await frontend.queryEcf(rnc, encf);
|
|
242
|
+
const { data: ecfs } = await frontend.searchEcfs(rnc);
|
|
243
|
+
```
|
|
177
244
|
|
|
178
245
|
## Acceso directo al cliente
|
|
179
246
|
|
package/dist/index.cjs
CHANGED
|
@@ -412,10 +412,32 @@ var EcfFrontendClient = class {
|
|
|
412
412
|
raw;
|
|
413
413
|
constructor(config) {
|
|
414
414
|
const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
|
|
415
|
+
const getToken = config.getToken;
|
|
416
|
+
const cacheToken = config.cacheToken ?? (async (token) => {
|
|
417
|
+
localStorage.setItem("ecf-token", token);
|
|
418
|
+
});
|
|
419
|
+
const getCachedToken = config.getCachedToken ?? (async () => {
|
|
420
|
+
return localStorage.getItem("ecf-token");
|
|
421
|
+
});
|
|
415
422
|
const authMiddleware = {
|
|
416
423
|
async onRequest({ request }) {
|
|
417
|
-
|
|
424
|
+
let token = await getCachedToken();
|
|
425
|
+
if (!token) {
|
|
426
|
+
token = await getToken();
|
|
427
|
+
await cacheToken(token);
|
|
428
|
+
}
|
|
429
|
+
request.headers.set("Authorization", `Bearer ${token}`);
|
|
418
430
|
return request;
|
|
431
|
+
},
|
|
432
|
+
async onResponse({ request, response }) {
|
|
433
|
+
if (response.status === 401) {
|
|
434
|
+
const token = await getToken();
|
|
435
|
+
await cacheToken(token);
|
|
436
|
+
const newRequest = request.clone();
|
|
437
|
+
newRequest.headers.set("Authorization", `Bearer ${token}`);
|
|
438
|
+
return fetch(newRequest);
|
|
439
|
+
}
|
|
440
|
+
return response;
|
|
419
441
|
}
|
|
420
442
|
};
|
|
421
443
|
this.raw = (0, import_openapi_fetch.default)({ baseUrl });
|
package/dist/index.d.cts
CHANGED
|
@@ -6851,6 +6851,18 @@ interface EcfClientConfig {
|
|
|
6851
6851
|
/** Target environment. Defaults to 'test'. */
|
|
6852
6852
|
environment?: Environment;
|
|
6853
6853
|
}
|
|
6854
|
+
interface EcfFrontendClientConfig {
|
|
6855
|
+
/** Function that fetches a fresh token (e.g. calls your backend's GET /ecf-token). */
|
|
6856
|
+
getToken: () => Promise<string>;
|
|
6857
|
+
/** Function to cache the token. Defaults to localStorage.setItem('ecf-token', token). */
|
|
6858
|
+
cacheToken?: (token: string) => Promise<void>;
|
|
6859
|
+
/** Function to retrieve a cached token. Defaults to localStorage.getItem('ecf-token'). */
|
|
6860
|
+
getCachedToken?: () => Promise<string | null>;
|
|
6861
|
+
/** Base URL override. Takes precedence over `environment`. */
|
|
6862
|
+
baseUrl?: string;
|
|
6863
|
+
/** Target environment. Defaults to 'test'. */
|
|
6864
|
+
environment?: Environment;
|
|
6865
|
+
}
|
|
6854
6866
|
interface PollingOptions {
|
|
6855
6867
|
/** Initial delay between polls in ms. Default: 1000 */
|
|
6856
6868
|
initialDelay?: number;
|
|
@@ -8860,11 +8872,15 @@ declare class EcfClient {
|
|
|
8860
8872
|
* A restricted, read-only client that only exposes GET endpoints.
|
|
8861
8873
|
* Suitable for use in frontend / browser code where write operations
|
|
8862
8874
|
* should not be available.
|
|
8875
|
+
*
|
|
8876
|
+
* Token lifecycle is handled automatically:
|
|
8877
|
+
* - On each request, checks `getCachedToken()`. If null, calls `getToken()` then `cacheToken(token)`.
|
|
8878
|
+
* - On 401 responses, calls `getToken()` again, updates the cache, and retries the request.
|
|
8863
8879
|
*/
|
|
8864
8880
|
declare class EcfFrontendClient {
|
|
8865
8881
|
/** The underlying openapi-fetch client for direct endpoint access. */
|
|
8866
8882
|
private readonly raw;
|
|
8867
|
-
constructor(config:
|
|
8883
|
+
constructor(config: EcfFrontendClientConfig);
|
|
8868
8884
|
/** Query ECFs by RNC and eNCF. */
|
|
8869
8885
|
queryEcf(rnc: string, encf: string): Promise<openapi_fetch.FetchResponse<{
|
|
8870
8886
|
parameters: {
|
|
@@ -9296,7 +9312,7 @@ declare class EcfFrontendClient {
|
|
|
9296
9312
|
* Factory that creates a restricted read-only client suitable for frontend use.
|
|
9297
9313
|
* Only GET endpoints are exposed.
|
|
9298
9314
|
*/
|
|
9299
|
-
declare function createFrontendClient(config:
|
|
9315
|
+
declare function createFrontendClient(config: EcfFrontendClientConfig): EcfFrontendClient;
|
|
9300
9316
|
|
|
9301
9317
|
declare class PollingTimeoutError extends Error {
|
|
9302
9318
|
constructor(message?: string);
|
|
@@ -9314,4 +9330,4 @@ declare class PollingMaxRetriesError extends Error {
|
|
|
9314
9330
|
*/
|
|
9315
9331
|
declare function pollUntilComplete<T>(fn: () => Promise<T>, isComplete: (result: T) => boolean, options?: PollingOptions): Promise<T>;
|
|
9316
9332
|
|
|
9317
|
-
export { EcfClient, type EcfClientConfig, EcfError, EcfFrontendClient, type Environment, PollingMaxRetriesError, type PollingOptions, PollingTimeoutError, type components, createFrontendClient, type operations, type paths, pollUntilComplete };
|
|
9333
|
+
export { EcfClient, type EcfClientConfig, EcfError, EcfFrontendClient, type EcfFrontendClientConfig, type Environment, PollingMaxRetriesError, type PollingOptions, PollingTimeoutError, type components, createFrontendClient, type operations, type paths, pollUntilComplete };
|
package/dist/index.d.ts
CHANGED
|
@@ -6851,6 +6851,18 @@ interface EcfClientConfig {
|
|
|
6851
6851
|
/** Target environment. Defaults to 'test'. */
|
|
6852
6852
|
environment?: Environment;
|
|
6853
6853
|
}
|
|
6854
|
+
interface EcfFrontendClientConfig {
|
|
6855
|
+
/** Function that fetches a fresh token (e.g. calls your backend's GET /ecf-token). */
|
|
6856
|
+
getToken: () => Promise<string>;
|
|
6857
|
+
/** Function to cache the token. Defaults to localStorage.setItem('ecf-token', token). */
|
|
6858
|
+
cacheToken?: (token: string) => Promise<void>;
|
|
6859
|
+
/** Function to retrieve a cached token. Defaults to localStorage.getItem('ecf-token'). */
|
|
6860
|
+
getCachedToken?: () => Promise<string | null>;
|
|
6861
|
+
/** Base URL override. Takes precedence over `environment`. */
|
|
6862
|
+
baseUrl?: string;
|
|
6863
|
+
/** Target environment. Defaults to 'test'. */
|
|
6864
|
+
environment?: Environment;
|
|
6865
|
+
}
|
|
6854
6866
|
interface PollingOptions {
|
|
6855
6867
|
/** Initial delay between polls in ms. Default: 1000 */
|
|
6856
6868
|
initialDelay?: number;
|
|
@@ -8860,11 +8872,15 @@ declare class EcfClient {
|
|
|
8860
8872
|
* A restricted, read-only client that only exposes GET endpoints.
|
|
8861
8873
|
* Suitable for use in frontend / browser code where write operations
|
|
8862
8874
|
* should not be available.
|
|
8875
|
+
*
|
|
8876
|
+
* Token lifecycle is handled automatically:
|
|
8877
|
+
* - On each request, checks `getCachedToken()`. If null, calls `getToken()` then `cacheToken(token)`.
|
|
8878
|
+
* - On 401 responses, calls `getToken()` again, updates the cache, and retries the request.
|
|
8863
8879
|
*/
|
|
8864
8880
|
declare class EcfFrontendClient {
|
|
8865
8881
|
/** The underlying openapi-fetch client for direct endpoint access. */
|
|
8866
8882
|
private readonly raw;
|
|
8867
|
-
constructor(config:
|
|
8883
|
+
constructor(config: EcfFrontendClientConfig);
|
|
8868
8884
|
/** Query ECFs by RNC and eNCF. */
|
|
8869
8885
|
queryEcf(rnc: string, encf: string): Promise<openapi_fetch.FetchResponse<{
|
|
8870
8886
|
parameters: {
|
|
@@ -9296,7 +9312,7 @@ declare class EcfFrontendClient {
|
|
|
9296
9312
|
* Factory that creates a restricted read-only client suitable for frontend use.
|
|
9297
9313
|
* Only GET endpoints are exposed.
|
|
9298
9314
|
*/
|
|
9299
|
-
declare function createFrontendClient(config:
|
|
9315
|
+
declare function createFrontendClient(config: EcfFrontendClientConfig): EcfFrontendClient;
|
|
9300
9316
|
|
|
9301
9317
|
declare class PollingTimeoutError extends Error {
|
|
9302
9318
|
constructor(message?: string);
|
|
@@ -9314,4 +9330,4 @@ declare class PollingMaxRetriesError extends Error {
|
|
|
9314
9330
|
*/
|
|
9315
9331
|
declare function pollUntilComplete<T>(fn: () => Promise<T>, isComplete: (result: T) => boolean, options?: PollingOptions): Promise<T>;
|
|
9316
9332
|
|
|
9317
|
-
export { EcfClient, type EcfClientConfig, EcfError, EcfFrontendClient, type Environment, PollingMaxRetriesError, type PollingOptions, PollingTimeoutError, type components, createFrontendClient, type operations, type paths, pollUntilComplete };
|
|
9333
|
+
export { EcfClient, type EcfClientConfig, EcfError, EcfFrontendClient, type EcfFrontendClientConfig, type Environment, PollingMaxRetriesError, type PollingOptions, PollingTimeoutError, type components, createFrontendClient, type operations, type paths, pollUntilComplete };
|
package/dist/index.js
CHANGED
|
@@ -370,10 +370,32 @@ var EcfFrontendClient = class {
|
|
|
370
370
|
raw;
|
|
371
371
|
constructor(config) {
|
|
372
372
|
const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
|
|
373
|
+
const getToken = config.getToken;
|
|
374
|
+
const cacheToken = config.cacheToken ?? (async (token) => {
|
|
375
|
+
localStorage.setItem("ecf-token", token);
|
|
376
|
+
});
|
|
377
|
+
const getCachedToken = config.getCachedToken ?? (async () => {
|
|
378
|
+
return localStorage.getItem("ecf-token");
|
|
379
|
+
});
|
|
373
380
|
const authMiddleware = {
|
|
374
381
|
async onRequest({ request }) {
|
|
375
|
-
|
|
382
|
+
let token = await getCachedToken();
|
|
383
|
+
if (!token) {
|
|
384
|
+
token = await getToken();
|
|
385
|
+
await cacheToken(token);
|
|
386
|
+
}
|
|
387
|
+
request.headers.set("Authorization", `Bearer ${token}`);
|
|
376
388
|
return request;
|
|
389
|
+
},
|
|
390
|
+
async onResponse({ request, response }) {
|
|
391
|
+
if (response.status === 401) {
|
|
392
|
+
const token = await getToken();
|
|
393
|
+
await cacheToken(token);
|
|
394
|
+
const newRequest = request.clone();
|
|
395
|
+
newRequest.headers.set("Authorization", `Bearer ${token}`);
|
|
396
|
+
return fetch(newRequest);
|
|
397
|
+
}
|
|
398
|
+
return response;
|
|
377
399
|
}
|
|
378
400
|
};
|
|
379
401
|
this.raw = createClient({ baseUrl });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssddo/ecf-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "SDK de TypeScript para la API de ECF DGII (comprobantes fiscales electrónicos de República Dominicana)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"generate": "openapi-typescript ../ecf_dgii/src/Apis/ECF_DGII.EcfApi/wwwroot/openapi/v1.json -o src/generated/v1.d.ts",
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
24
29
|
"dependencies": {
|
|
25
30
|
"openapi-fetch": "^0.13.0"
|
|
26
31
|
},
|
|
@@ -56,9 +61,5 @@
|
|
|
56
61
|
"openapi",
|
|
57
62
|
"ssd"
|
|
58
63
|
],
|
|
59
|
-
"license": "MIT"
|
|
60
|
-
|
|
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
|
-
}
|
|
64
|
+
"license": "MIT"
|
|
65
|
+
}
|