@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 +85 -22
- package/dist/index.d.mts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +44 -0
- package/dist/index.mjs +43 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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