@ssddo/ecf-react 0.1.5 → 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
@@ -205,48 +205,111 @@ Crea un cliente tipado de React Query para la API de ECF DGII.
205
205
  - `$api` - El cliente openapi-react-query con `useQuery`, `useMutation`, `useSuspenseQuery`, etc.
206
206
  - `fetchClient` - El cliente openapi-fetch subyacente para uso fuera de React.
207
207
 
208
+ ### `createEcfFrontendReactClient(config)`
209
+
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
+
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`) |
221
+
222
+ **Retorna:** `{ $api, fetchClient }`
223
+
224
+ - `$api` - Cliente con solo `useQuery`, `useSuspenseQuery`, y `queryOptions` (sin `useMutation`)
225
+ - `fetchClient` - El cliente openapi-fetch subyacente (restringido a paths GET)
226
+
208
227
  ## Arquitectura Backend / Frontend
209
228
 
210
- El SDK de React está diseñado para el lado del **frontend** de la arquitectura recomendada:
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
258
+ ```
211
259
 
212
- 1. Tu **backend** valida, guarda y convierte tu factura interna al formato ECF, luego la envía a ECF SSD usando su token principal
213
- 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
214
- 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
260
+ ### Flujo detallado
215
261
 
216
- ```tsx
217
- // Gestión de token tu hook personalizado
218
- // Llama al endpoint /api/v1/ecf-token de tu backend, almacena el token de forma segura,
219
- // y lo renueva automáticamente cuando expira o recibe un 401
220
- const ecfToken = useEcfToken();
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`
221
271
 
222
- const { $api } = createEcfReactClient({
223
- apiKey: ecfToken, // solo lectura, con alcance al tenant/RNC
272
+ ```tsx
273
+ import { createEcfFrontendReactClient } from '@ssddo/ecf-react';
274
+
275
+ // 1. Crear cliente de solo lectura (getToken se llama automáticamente)
276
+ const { $api } = createEcfFrontendReactClient({
277
+ getToken: async () => {
278
+ const res = await fetch('/api/v1/ecf-token');
279
+ const { apiKey } = await res.json();
280
+ return apiKey;
281
+ },
224
282
  environment: 'prod',
225
283
  });
226
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)
227
300
  function EstadoEcf({ rnc, encf }: { rnc: string; encf: string }) {
228
- // Consulta ECF SSD directamente — no se necesita proxy en el backend
229
301
  const { data } = $api.useQuery('get', '/ecf/{rnc}/{encf}', {
230
302
  params: { path: { rnc, encf } },
231
303
  refetchInterval: 3000,
232
304
  });
233
305
 
234
306
  if (data?.progress === 'Finished') {
235
- return (
236
- <div>
237
- <p>Comprobante aceptado</p>
238
- <p>Código seguridad: {data.codSec}</p>
239
- <QRCode value={data.impresionUrl} />
240
- </div>
241
- );
307
+ return <p>Comprobante aceptado — código: {data.codSec}</p>;
242
308
  }
243
-
244
309
  return <p>Procesando... ({data?.progress})</p>;
245
310
  }
246
311
  ```
247
312
 
248
- 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.
249
-
250
313
  ## Uso fuera de React
251
314
 
252
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
@@ -6852,5 +6852,33 @@ declare function createEcfReactClient(config: EcfReactClientConfig): {
6852
6852
  $api: openapi_react_query.OpenapiQueryClient<paths, `${string}/${string}`>;
6853
6853
  fetchClient: openapi_fetch.Client<paths, `${string}/${string}`>;
6854
6854
  };
6855
+ type PathsWithGet = {
6856
+ [K in keyof paths as paths[K] extends {
6857
+ get: unknown;
6858
+ } ? K : never]: paths[K];
6859
+ };
6860
+ type ReadOnlyPaths = {
6861
+ [K in keyof PathsWithGet]: Pick<PathsWithGet[K], 'get' | 'parameters'> & {
6862
+ put?: never;
6863
+ post?: never;
6864
+ delete?: never;
6865
+ patch?: never;
6866
+ };
6867
+ };
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): {
6876
+ $api: {
6877
+ useQuery: openapi_react_query.UseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
6878
+ useSuspenseQuery: openapi_react_query.UseSuspenseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
6879
+ queryOptions: openapi_react_query.QueryOptionsFunction<ReadOnlyPaths, `${string}/${string}`>;
6880
+ };
6881
+ fetchClient: openapi_fetch.Client<ReadOnlyPaths, `${string}/${string}`>;
6882
+ };
6855
6883
 
6856
- export { type EcfReactClientConfig, type Environment, type components, 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
@@ -6852,5 +6852,33 @@ declare function createEcfReactClient(config: EcfReactClientConfig): {
6852
6852
  $api: openapi_react_query.OpenapiQueryClient<paths, `${string}/${string}`>;
6853
6853
  fetchClient: openapi_fetch.Client<paths, `${string}/${string}`>;
6854
6854
  };
6855
+ type PathsWithGet = {
6856
+ [K in keyof paths as paths[K] extends {
6857
+ get: unknown;
6858
+ } ? K : never]: paths[K];
6859
+ };
6860
+ type ReadOnlyPaths = {
6861
+ [K in keyof PathsWithGet]: Pick<PathsWithGet[K], 'get' | 'parameters'> & {
6862
+ put?: never;
6863
+ post?: never;
6864
+ delete?: never;
6865
+ patch?: never;
6866
+ };
6867
+ };
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): {
6876
+ $api: {
6877
+ useQuery: openapi_react_query.UseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
6878
+ useSuspenseQuery: openapi_react_query.UseSuspenseQueryMethod<ReadOnlyPaths, `${string}/${string}`>;
6879
+ queryOptions: openapi_react_query.QueryOptionsFunction<ReadOnlyPaths, `${string}/${string}`>;
6880
+ };
6881
+ fetchClient: openapi_fetch.Client<ReadOnlyPaths, `${string}/${string}`>;
6882
+ };
6855
6883
 
6856
- export { type EcfReactClientConfig, type Environment, type components, 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
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ createEcfFrontendReactClient: () => createEcfFrontendReactClient,
33
34
  createEcfReactClient: () => createEcfReactClient
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
@@ -56,7 +57,50 @@ function createEcfReactClient(config) {
56
57
  const $api = (0, import_openapi_react_query.default)(fetchClient);
57
58
  return { $api, fetchClient };
58
59
  }
60
+ var defaultCacheToken = async (token) => {
61
+ localStorage.setItem("ecf-token", token);
62
+ };
63
+ var defaultGetCachedToken = async () => {
64
+ return localStorage.getItem("ecf-token");
65
+ };
66
+ function createEcfFrontendReactClient(config) {
67
+ const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
68
+ const cacheToken = config.cacheToken ?? defaultCacheToken;
69
+ const getCachedToken = config.getCachedToken ?? defaultGetCachedToken;
70
+ const fetchClient = (0, import_openapi_fetch.default)({
71
+ baseUrl
72
+ });
73
+ fetchClient.use({
74
+ async onRequest({ request }) {
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}`);
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;
92
+ }
93
+ });
94
+ const fullApi = (0, import_openapi_react_query.default)(fetchClient);
95
+ const $api = {
96
+ useQuery: fullApi.useQuery,
97
+ useSuspenseQuery: fullApi.useSuspenseQuery,
98
+ queryOptions: fullApi.queryOptions
99
+ };
100
+ return { $api, fetchClient };
101
+ }
59
102
  // Annotate the CommonJS export names for ESM import in node:
60
103
  0 && (module.exports = {
104
+ createEcfFrontendReactClient,
61
105
  createEcfReactClient
62
106
  });
package/dist/index.mjs CHANGED
@@ -20,6 +20,49 @@ 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
+ };
29
+ function createEcfFrontendReactClient(config) {
30
+ const baseUrl = config.baseUrl ?? ENVIRONMENT_URLS[config.environment ?? "test"];
31
+ const cacheToken = config.cacheToken ?? defaultCacheToken;
32
+ const getCachedToken = config.getCachedToken ?? defaultGetCachedToken;
33
+ const fetchClient = createFetchClient({
34
+ baseUrl
35
+ });
36
+ fetchClient.use({
37
+ async onRequest({ request }) {
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}`);
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;
55
+ }
56
+ });
57
+ const fullApi = createClient(fetchClient);
58
+ const $api = {
59
+ useQuery: fullApi.useQuery,
60
+ useSuspenseQuery: fullApi.useSuspenseQuery,
61
+ queryOptions: fullApi.queryOptions
62
+ };
63
+ return { $api, fetchClient };
64
+ }
23
65
  export {
66
+ createEcfFrontendReactClient,
24
67
  createEcfReactClient
25
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssddo/ecf-react",
3
- "version": "0.1.5",
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",