@ssddo/ecf-react 0.2.0 → 0.3.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 +73 -36
- package/dist/index.d.mts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +24 -1
- package/dist/index.mjs +24 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,72 +207,109 @@ Crea un cliente tipado de React Query para la API de ECF DGII.
|
|
|
207
207
|
|
|
208
208
|
### `createEcfFrontendReactClient(config)`
|
|
209
209
|
|
|
210
|
-
Crea un cliente de solo lectura restringido a endpoints GET. No expone `useMutation`. Diseñado para el
|
|
210
|
+
Crea un cliente de solo lectura restringido a endpoints GET. No expone `useMutation`. Diseñado para el cliente donde solo se consultan ECFs con un token de solo lectura. Maneja automáticamente el caching de tokens y refresh en caso de 401.
|
|
211
211
|
|
|
212
|
-
**Opciones de configuración:**
|
|
212
|
+
**Opciones de configuración:**
|
|
213
|
+
|
|
214
|
+
| Opción | Tipo | Requerido | Descripción |
|
|
215
|
+
|--------|------|-----------|-------------|
|
|
216
|
+
| `getToken` | `() => Promise<string>` | Sí | Callback para obtener un token fresco (ej. fetch a tu backend) |
|
|
217
|
+
| `cacheToken` | `(token: string) => Promise<void>` | No | Callback para almacenar el token (por defecto: `localStorage`) |
|
|
218
|
+
| `getCachedToken` | `() => Promise<string \| null>` | No | Callback para leer el token del cache (por defecto: `localStorage`) |
|
|
219
|
+
| `environment` | `'test' \| 'cert' \| 'prod'` | No | Entorno destino (por defecto: `'test'`) |
|
|
220
|
+
| `baseUrl` | `string` | No | URL base personalizada (sobreescribe `environment`) |
|
|
213
221
|
|
|
214
222
|
**Retorna:** `{ $api, fetchClient }`
|
|
215
223
|
|
|
216
224
|
- `$api` - Cliente con solo `useQuery`, `useSuspenseQuery`, y `queryOptions` (sin `useMutation`)
|
|
217
225
|
- `fetchClient` - El cliente openapi-fetch subyacente (restringido a paths GET)
|
|
218
226
|
|
|
219
|
-
|
|
220
|
-
import { createEcfFrontendReactClient } from '@ssddo/ecf-react';
|
|
221
|
-
|
|
222
|
-
const { $api } = createEcfFrontendReactClient({
|
|
223
|
-
apiKey: token,
|
|
224
|
-
environment: 'prod',
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// ✅ Funciona — endpoint GET
|
|
228
|
-
$api.useQuery('get', '/ecf/{rnc}/{encf}', { params: { path: { rnc, encf } } });
|
|
227
|
+
## Arquitectura Backend / Frontend
|
|
229
228
|
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
```mermaid
|
|
230
|
+
sequenceDiagram
|
|
231
|
+
participant C as Cliente (Browser/App)
|
|
232
|
+
participant BE as Backend
|
|
233
|
+
participant ECF as ECF API
|
|
234
|
+
|
|
235
|
+
C->>BE: POST /invoice (datos de factura)
|
|
236
|
+
Note over BE: Valida, guarda y convierte a formato ECF
|
|
237
|
+
|
|
238
|
+
BE->>ECF: POST /ecf/{tipo} (enviar ECF)
|
|
239
|
+
ECF-->>BE: { messageId }
|
|
240
|
+
BE-->>C: { messageId }
|
|
241
|
+
|
|
242
|
+
Note over C: No espera — puede continuar
|
|
243
|
+
|
|
244
|
+
alt Token en cache
|
|
245
|
+
C->>C: Usar token existente
|
|
246
|
+
else Sin token o expirado
|
|
247
|
+
C->>BE: GET /ecf-token
|
|
248
|
+
BE->>ECF: POST /apikey (solo lectura, scoped a RNC)
|
|
249
|
+
ECF-->>BE: { apiKey }
|
|
250
|
+
BE-->>C: { apiKey }
|
|
251
|
+
C->>C: Almacenar token en cache
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
loop Polling hasta completar
|
|
255
|
+
C->>ECF: GET /ecf/{rnc}/{encf} (token solo lectura)
|
|
256
|
+
ECF-->>C: { progress, codSec, ... }
|
|
257
|
+
end
|
|
232
258
|
```
|
|
233
259
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
El SDK de React está diseñado para el lado del **frontend** de la arquitectura recomendada:
|
|
260
|
+
### Flujo detallado
|
|
237
261
|
|
|
238
|
-
1.
|
|
239
|
-
2.
|
|
240
|
-
3.
|
|
262
|
+
1. El **cliente** (browser/app) envía los datos de la factura al **backend** (`POST /invoice`, `/order`, `/sale`)
|
|
263
|
+
2. El **backend** valida, guarda y convierte la factura interna al formato ECF
|
|
264
|
+
3. El **backend** envía el ECF a la API de ECF SSD (`POST /ecf/{tipo}`) y recibe un `messageId`
|
|
265
|
+
4. El **backend** retorna el `messageId` al cliente — **el cliente no espera**, puede continuar
|
|
266
|
+
5. Cuando el cliente necesita consultar el estado del ECF, usa `EcfFrontendClient` que internamente:
|
|
267
|
+
- Verifica si hay un **token de solo lectura** en cache
|
|
268
|
+
- 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
|
|
269
|
+
- Si la API retorna **401**: automáticamente llama a `getToken()` de nuevo, actualiza el cache, y reintenta
|
|
270
|
+
6. El cliente hace **polling** directamente contra la API de ECF SSD (`GET /ecf/{rnc}/{encf}`) hasta que `progress` sea `Finished`
|
|
241
271
|
|
|
242
272
|
```tsx
|
|
243
|
-
|
|
244
|
-
// Llama al endpoint /api/v1/ecf-token de tu backend, almacena el token de forma segura,
|
|
245
|
-
// y lo renueva automáticamente cuando expira o recibe un 401
|
|
246
|
-
const ecfToken = useEcfToken();
|
|
273
|
+
import { createEcfFrontendReactClient } from '@ssddo/ecf-react';
|
|
247
274
|
|
|
275
|
+
// 1. Crear cliente de solo lectura (getToken se llama automáticamente)
|
|
248
276
|
const { $api } = createEcfFrontendReactClient({
|
|
249
|
-
|
|
277
|
+
getToken: async () => {
|
|
278
|
+
const res = await fetch('/api/v1/ecf-token');
|
|
279
|
+
const { apiKey } = await res.json();
|
|
280
|
+
return apiKey;
|
|
281
|
+
},
|
|
250
282
|
environment: 'prod',
|
|
251
283
|
});
|
|
252
284
|
|
|
285
|
+
// 2. El componente que envía la factura al backend
|
|
286
|
+
function EnviarFactura() {
|
|
287
|
+
const handleSubmit = async (invoiceData) => {
|
|
288
|
+
// Enviar factura al backend — el cliente no espera por el procesamiento ECF
|
|
289
|
+
const res = await fetch('/api/v1/invoices', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
body: JSON.stringify(invoiceData),
|
|
292
|
+
});
|
|
293
|
+
const { messageId, rnc, encf } = await res.json();
|
|
294
|
+
// Navegar a la página de estado o mostrar el componente de polling
|
|
295
|
+
navigate(`/ecf-status/${rnc}/${encf}`);
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 3. El componente que consulta el estado del ECF (polling automático)
|
|
253
300
|
function EstadoEcf({ rnc, encf }: { rnc: string; encf: string }) {
|
|
254
|
-
// Consulta ECF SSD directamente — no se necesita proxy en el backend
|
|
255
301
|
const { data } = $api.useQuery('get', '/ecf/{rnc}/{encf}', {
|
|
256
302
|
params: { path: { rnc, encf } },
|
|
257
303
|
refetchInterval: 3000,
|
|
258
304
|
});
|
|
259
305
|
|
|
260
306
|
if (data?.progress === 'Finished') {
|
|
261
|
-
return
|
|
262
|
-
<div>
|
|
263
|
-
<p>Comprobante aceptado</p>
|
|
264
|
-
<p>Código seguridad: {data.codSec}</p>
|
|
265
|
-
<QRCode value={data.impresionUrl} />
|
|
266
|
-
</div>
|
|
267
|
-
);
|
|
307
|
+
return <p>Comprobante aceptado — código: {data.codSec}</p>;
|
|
268
308
|
}
|
|
269
|
-
|
|
270
309
|
return <p>Procesando... ({data?.progress})</p>;
|
|
271
310
|
}
|
|
272
311
|
```
|
|
273
312
|
|
|
274
|
-
Este patrón descarga el polling de tu backend y permite que el frontend se comunique directamente con ECF SSD usando un token restringido. Consulta el [README principal](../README.md#arquitectura-backend--frontend) para el diagrama completo y ejemplo del backend.
|
|
275
|
-
|
|
276
313
|
## Uso fuera de React
|
|
277
314
|
|
|
278
315
|
Para aplicaciones del lado del servidor o sin React, usa el SDK base de TypeScript: [`@ssddo/ecf-sdk`](https://www.npmjs.com/package/@ssddo/ecf-sdk).
|
package/dist/index.d.mts
CHANGED
|
@@ -6865,7 +6865,14 @@ type ReadOnlyPaths = {
|
|
|
6865
6865
|
patch?: never;
|
|
6866
6866
|
};
|
|
6867
6867
|
};
|
|
6868
|
-
|
|
6868
|
+
interface EcfFrontendReactClientConfig {
|
|
6869
|
+
getToken: () => Promise<string>;
|
|
6870
|
+
cacheToken?: (token: string) => Promise<void>;
|
|
6871
|
+
getCachedToken?: () => Promise<string | null>;
|
|
6872
|
+
baseUrl?: string;
|
|
6873
|
+
environment?: Environment;
|
|
6874
|
+
}
|
|
6875
|
+
declare function createEcfFrontendReactClient(config: EcfFrontendReactClientConfig): {
|
|
6869
6876
|
$api: {
|
|
6870
6877
|
useQuery: openapi_react_query.UseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
|
|
6871
6878
|
useSuspenseQuery: openapi_react_query.UseSuspenseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
|
|
@@ -6874,4 +6881,4 @@ declare function createEcfFrontendReactClient(config: EcfReactClientConfig): {
|
|
|
6874
6881
|
fetchClient: openapi_fetch.Client<ReadOnlyPaths, `${string}/${string}`>;
|
|
6875
6882
|
};
|
|
6876
6883
|
|
|
6877
|
-
export { type EcfReactClientConfig, type Environment, type components, createEcfFrontendReactClient, createEcfReactClient, type operations, type paths };
|
|
6884
|
+
export { type EcfFrontendReactClientConfig, type EcfReactClientConfig, type Environment, type components, createEcfFrontendReactClient, createEcfReactClient, type operations, type paths };
|
package/dist/index.d.ts
CHANGED
|
@@ -6865,7 +6865,14 @@ type ReadOnlyPaths = {
|
|
|
6865
6865
|
patch?: never;
|
|
6866
6866
|
};
|
|
6867
6867
|
};
|
|
6868
|
-
|
|
6868
|
+
interface EcfFrontendReactClientConfig {
|
|
6869
|
+
getToken: () => Promise<string>;
|
|
6870
|
+
cacheToken?: (token: string) => Promise<void>;
|
|
6871
|
+
getCachedToken?: () => Promise<string | null>;
|
|
6872
|
+
baseUrl?: string;
|
|
6873
|
+
environment?: Environment;
|
|
6874
|
+
}
|
|
6875
|
+
declare function createEcfFrontendReactClient(config: EcfFrontendReactClientConfig): {
|
|
6869
6876
|
$api: {
|
|
6870
6877
|
useQuery: openapi_react_query.UseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
|
|
6871
6878
|
useSuspenseQuery: openapi_react_query.UseSuspenseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
|
|
@@ -6874,4 +6881,4 @@ declare function createEcfFrontendReactClient(config: EcfReactClientConfig): {
|
|
|
6874
6881
|
fetchClient: openapi_fetch.Client<ReadOnlyPaths, `${string}/${string}`>;
|
|
6875
6882
|
};
|
|
6876
6883
|
|
|
6877
|
-
export { type EcfReactClientConfig, type Environment, type components, createEcfFrontendReactClient, createEcfReactClient, type operations, type paths };
|
|
6884
|
+
export { type EcfFrontendReactClientConfig, type EcfReactClientConfig, type Environment, type components, createEcfFrontendReactClient, createEcfReactClient, type operations, type paths };
|
package/dist/index.js
CHANGED
|
@@ -57,15 +57,38 @@ function createEcfReactClient(config) {
|
|
|
57
57
|
const $api = (0, import_openapi_react_query.default)(fetchClient);
|
|
58
58
|
return { $api, fetchClient };
|
|
59
59
|
}
|
|
60
|
+
var defaultCacheToken = async (token) => {
|
|
61
|
+
localStorage.setItem("ecf-token", token);
|
|
62
|
+
};
|
|
63
|
+
var defaultGetCachedToken = async () => {
|
|
64
|
+
return localStorage.getItem("ecf-token");
|
|
65
|
+
};
|
|
60
66
|
function createEcfFrontendReactClient(config) {
|
|
61
67
|
const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
|
|
68
|
+
const cacheToken = config.cacheToken ?? defaultCacheToken;
|
|
69
|
+
const getCachedToken = config.getCachedToken ?? defaultGetCachedToken;
|
|
62
70
|
const fetchClient = (0, import_openapi_fetch.default)({
|
|
63
71
|
baseUrl
|
|
64
72
|
});
|
|
65
73
|
fetchClient.use({
|
|
66
74
|
async onRequest({ request }) {
|
|
67
|
-
|
|
75
|
+
let token = await getCachedToken();
|
|
76
|
+
if (!token) {
|
|
77
|
+
token = await config.getToken();
|
|
78
|
+
await cacheToken(token);
|
|
79
|
+
}
|
|
80
|
+
request.headers.set("Authorization", `Bearer ${token}`);
|
|
68
81
|
return request;
|
|
82
|
+
},
|
|
83
|
+
async onResponse({ request, response }) {
|
|
84
|
+
if (response.status === 401) {
|
|
85
|
+
const token = await config.getToken();
|
|
86
|
+
await cacheToken(token);
|
|
87
|
+
const retryRequest = request.clone();
|
|
88
|
+
retryRequest.headers.set("Authorization", `Bearer ${token}`);
|
|
89
|
+
return fetch(retryRequest);
|
|
90
|
+
}
|
|
91
|
+
return response;
|
|
69
92
|
}
|
|
70
93
|
});
|
|
71
94
|
const fullApi = (0, import_openapi_react_query.default)(fetchClient);
|
package/dist/index.mjs
CHANGED
|
@@ -20,15 +20,38 @@ function createEcfReactClient(config) {
|
|
|
20
20
|
const $api = createClient(fetchClient);
|
|
21
21
|
return { $api, fetchClient };
|
|
22
22
|
}
|
|
23
|
+
var defaultCacheToken = async (token) => {
|
|
24
|
+
localStorage.setItem("ecf-token", token);
|
|
25
|
+
};
|
|
26
|
+
var defaultGetCachedToken = async () => {
|
|
27
|
+
return localStorage.getItem("ecf-token");
|
|
28
|
+
};
|
|
23
29
|
function createEcfFrontendReactClient(config) {
|
|
24
30
|
const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
|
|
31
|
+
const cacheToken = config.cacheToken ?? defaultCacheToken;
|
|
32
|
+
const getCachedToken = config.getCachedToken ?? defaultGetCachedToken;
|
|
25
33
|
const fetchClient = createFetchClient({
|
|
26
34
|
baseUrl
|
|
27
35
|
});
|
|
28
36
|
fetchClient.use({
|
|
29
37
|
async onRequest({ request }) {
|
|
30
|
-
|
|
38
|
+
let token = await getCachedToken();
|
|
39
|
+
if (!token) {
|
|
40
|
+
token = await config.getToken();
|
|
41
|
+
await cacheToken(token);
|
|
42
|
+
}
|
|
43
|
+
request.headers.set("Authorization", `Bearer ${token}`);
|
|
31
44
|
return request;
|
|
45
|
+
},
|
|
46
|
+
async onResponse({ request, response }) {
|
|
47
|
+
if (response.status === 401) {
|
|
48
|
+
const token = await config.getToken();
|
|
49
|
+
await cacheToken(token);
|
|
50
|
+
const retryRequest = request.clone();
|
|
51
|
+
retryRequest.headers.set("Authorization", `Bearer ${token}`);
|
|
52
|
+
return fetch(retryRequest);
|
|
53
|
+
}
|
|
54
|
+
return response;
|
|
32
55
|
}
|
|
33
56
|
});
|
|
34
57
|
const fullApi = createClient(fetchClient);
|
package/package.json
CHANGED