@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 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 frontend donde solo se consultan ECFs con un token de solo lectura.
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:** mismas que `createEcfReactClient`.
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
- ```tsx
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
- // ❌ Error de tipo — useMutation no está disponible
231
- // $api.useMutation('post', '/ecf/31');
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
- ## Arquitectura Backend / Frontend
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. Tu **backend** valida, guarda y convierte tu factura interna al formato ECF, luego la envía a ECF SSD usando su token principal
239
- 2. Tu **backend** expone un endpoint (ej. `GET /api/v1/ecf-token`) que genera un **API key de solo lectura** con alcance al tenant/RNC a través del endpoint `/apikey` de ECF SSD
240
- 3. Tu **frontend** almacena este token de forma segura, lo renueva ante `401` o expiración, y lo usa con `@ssddo/ecf-react` para consultar el estado de los ECF directamente
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
- // Gestión de token tu hook personalizado
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
- apiKey: ecfToken, // solo lectura, con alcance al tenant/RNC
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
- declare function createEcfFrontendReactClient(config: EcfReactClientConfig): {
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
- declare function createEcfFrontendReactClient(config: EcfReactClientConfig): {
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
- request.headers.set("Authorization", `Bearer ${config.apiKey}`);
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
- request.headers.set("Authorization", `Bearer ${config.apiKey}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssddo/ecf-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Hooks de React Query para la API de ECF DGII (comprobantes fiscales electrónicos de República Dominicana)",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",