@pimia/sdk 0.3.0 → 0.4.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/dist/api.d.ts +145 -14
- package/dist/client.d.ts +4 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +81 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/webhooks.d.ts +40 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -233,6 +233,21 @@ export interface paths {
|
|
|
233
233
|
/**
|
|
234
234
|
* Handle the incoming request
|
|
235
235
|
* @description **No disponible para integradores.** Exige `settings:write`, que el Authorization Server de Pimia no emite: la acción la realiza el dueño desde su panel y un token de partner recibe `403`. Aparece en el contrato para que el hueco sea explícito, no para que se llame.
|
|
236
|
+
*
|
|
237
|
+
* Asistente de arranque: fija el tipo de cambio de cada moneda en uso y
|
|
238
|
+
* RECALCULA con él los importes base de todas las facturas, presupuestos y
|
|
239
|
+
* pagos de esa moneda — documentos ya emitidos incluidos. Corre una sola vez
|
|
240
|
+
* por empresa (mientras `bulk_exchange_rate_configured` sea 'NO').
|
|
241
|
+
*
|
|
242
|
+
* Reescribir en masa los importes de documentos emitidos es configuración de
|
|
243
|
+
* empresa, no una operación de uso diario: va con el mismo gate de owner que
|
|
244
|
+
* el resto de lo que escribe en Settings. Hasta 2026-08-10 no tenía ninguno
|
|
245
|
+
* —el FormRequest autoriza a todo el mundo— y para un token de API la única
|
|
246
|
+
* barrera era que `settings:write` no esté en el catálogo OAuth.
|
|
247
|
+
*
|
|
248
|
+
* OJO al frontend: el modal lo abre `LayoutBasic` en cuanto entra CUALQUIER
|
|
249
|
+
* usuario con la marca a 'NO'. Sin la condición de owner que se añadió allí,
|
|
250
|
+
* este gate deja a un empleado atrapado en un modal que no puede completar.
|
|
236
251
|
*/
|
|
237
252
|
post: operations["general.bulkExchangeRate"];
|
|
238
253
|
delete?: never;
|
|
@@ -384,7 +399,14 @@ export interface paths {
|
|
|
384
399
|
};
|
|
385
400
|
get?: never;
|
|
386
401
|
put?: never;
|
|
387
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* Convertir un presupuesto en factura borrador
|
|
404
|
+
* @description Acepta `external_ref` para etiquetar la factura resultante **en el mismo
|
|
405
|
+
* paso**. Etiquetarla después, con `PUT /invoices/{id}`, llega tarde:
|
|
406
|
+
* `invoice.created` ya ha salido con la referencia nula, y entre las dos
|
|
407
|
+
* llamadas hay una ventana en la que la factura existe y no la encuentras
|
|
408
|
+
* por tu referencia.
|
|
409
|
+
*/
|
|
388
410
|
post: operations["estimate.convertEstimate"];
|
|
389
411
|
delete?: never;
|
|
390
412
|
options?: never;
|
|
@@ -881,6 +903,34 @@ export interface paths {
|
|
|
881
903
|
/**
|
|
882
904
|
* Toggle lock state for a quarter
|
|
883
905
|
* @description **No disponible para integradores.** Exige `reports:write`, que el Authorization Server de Pimia no emite: la acción la realiza el dueño desde su panel y un token de partner recibe `403`. Aparece en el contrato para que el hueco sea explícito, no para que se llame.
|
|
906
|
+
*
|
|
907
|
+
* Cerrar un trimestre corta el alta y la edición de facturas emitidas y
|
|
908
|
+
* recibidas con fecha en ese periodo ({@see FiscalQuarter::isLocked}); esta
|
|
909
|
+
* misma acción lo REABRE. Es cierre contable, así que va con el gate de
|
|
910
|
+
* owner que ya gobierna la configuración sensible del tenant.
|
|
911
|
+
*
|
|
912
|
+
* Hasta 2026-08-10 no tenía ninguno: el `index` y el `toggle` colgaban de la
|
|
913
|
+
* ability de LECTURA de informes que gatea la pantalla (VIEW_FINANCIAL_REPORT
|
|
914
|
+
* en el router del panel), de modo que cualquiera que pudiera ver el informe
|
|
915
|
+
* contable podía reabrir un ejercicio cerrado. Para un token de API la única
|
|
916
|
+
* barrera era que `reports:write` no esté en el catálogo OAuth — una ausencia
|
|
917
|
+
* que protegía por accidente y que este gate deja de necesitar.
|
|
918
|
+
*
|
|
919
|
+
* El `index` sigue sin gate a propósito: leer qué trimestres están cerrados
|
|
920
|
+
* es lo que pinta el panel y no cambia nada.
|
|
921
|
+
*
|
|
922
|
+
* `locked` (opcional) es el ESTADO DESEADO, y es lo que deberían mandar los
|
|
923
|
+
* clientes: con él la operación es idempotente y dice lo que quiere en vez
|
|
924
|
+
* de depender de lo que había. Sin él se conserva el alternado histórico.
|
|
925
|
+
*
|
|
926
|
+
* El alternado ciego tiene una carrera que no es teórica: el cliente decide
|
|
927
|
+
* qué va a pasar leyendo un estado que ya puede haber cambiado. El panel
|
|
928
|
+
* pinta «Cerrar T1» porque lo leyó abierto, otro usuario lo cierra, y el
|
|
929
|
+
* clic acaba REABRIENDO el trimestre que su botón prometía cerrar. Por la
|
|
930
|
+
* vía del MCP era peor: `toggle_fiscal_quarter_lock` declaraba este mismo
|
|
931
|
+
* parámetro y el servidor lo ignoraba, así que un agente que pidiera cerrar
|
|
932
|
+
* un trimestre ya cerrado lo abría — en una operación que el propio MCP
|
|
933
|
+
* anuncia como PELIGROSA.
|
|
884
934
|
*/
|
|
885
935
|
post: operations["fiscalQuarter.toggle"];
|
|
886
936
|
delete?: never;
|
|
@@ -2976,6 +3026,22 @@ export interface components {
|
|
|
2976
3026
|
created_at: string;
|
|
2977
3027
|
updated_at: string;
|
|
2978
3028
|
};
|
|
3029
|
+
/**
|
|
3030
|
+
* ConvertEstimateRequest
|
|
3031
|
+
* @description Cuerpo —opcional— de la conversión de un presupuesto en factura.
|
|
3032
|
+
*/
|
|
3033
|
+
ConvertEstimateRequest: {
|
|
3034
|
+
/**
|
|
3035
|
+
* @description Referencia externa: TU identificador para la factura que sale de
|
|
3036
|
+
* esta conversión (el id del pedido o de la venta cerrada en tu
|
|
3037
|
+
* sistema). Sin él la factura nace sin etiquetar y los webhooks
|
|
3038
|
+
* `invoice.created` e `invoice.paid` llegan con `external_ref`
|
|
3039
|
+
* nulo. El alcance es tu client OAuth, igual que en el alta
|
|
3040
|
+
* directa de facturas. Duplicada dentro de tu namespace → 422 con
|
|
3041
|
+
* `existing_id`, y la conversión entera se deshace.
|
|
3042
|
+
*/
|
|
3043
|
+
external_ref?: string | null;
|
|
3044
|
+
};
|
|
2979
3045
|
/** CountryResource */
|
|
2980
3046
|
CountryResource: {
|
|
2981
3047
|
id: number;
|
|
@@ -3081,6 +3147,17 @@ export interface components {
|
|
|
3081
3147
|
prefix?: string | null;
|
|
3082
3148
|
tax_id?: string | null;
|
|
3083
3149
|
notes?: string | null;
|
|
3150
|
+
/**
|
|
3151
|
+
* @description Referencia externa: TU identificador para este cliente (el id
|
|
3152
|
+
* del deal, del contacto o del pedido en tu sistema). El alcance
|
|
3153
|
+
* es tu client OAuth, así que dos integradores pueden usar la
|
|
3154
|
+
* misma cadena sin pisarse y ninguno ve la del otro. Se consulta
|
|
3155
|
+
* con `GET /customers?external_ref=…`, vuelve en el recurso y
|
|
3156
|
+
* viaja en los webhooks `customer.*`. Si ya la lleva otro cliente
|
|
3157
|
+
* tuyo, la respuesta es un 422 con `existing_id` — que es el
|
|
3158
|
+
* find-or-create sin mantener mapeo local. `null` la desvincula.
|
|
3159
|
+
*/
|
|
3160
|
+
external_ref?: string | null;
|
|
3084
3161
|
currency_id?: string | null;
|
|
3085
3162
|
payment_method_id?: number | null;
|
|
3086
3163
|
billing?: {
|
|
@@ -3139,6 +3216,13 @@ export interface components {
|
|
|
3139
3216
|
prefix: string;
|
|
3140
3217
|
tax_id: string;
|
|
3141
3218
|
notes: string;
|
|
3219
|
+
/**
|
|
3220
|
+
* @description Referencia externa DE QUIEN PREGUNTA: la resuelve el client OAuth
|
|
3221
|
+
* del token, así que dos integradores ven cada uno la suya y el
|
|
3222
|
+
* panel (sin client) ve la del namespace de la company. Siempre
|
|
3223
|
+
* presente aunque sea null, para que el tipo del SDK no alterne.
|
|
3224
|
+
*/
|
|
3225
|
+
external_ref: string | null;
|
|
3142
3226
|
iban: string;
|
|
3143
3227
|
bic: string;
|
|
3144
3228
|
sepa_mandate_id: string;
|
|
@@ -3280,6 +3364,12 @@ export interface components {
|
|
|
3280
3364
|
template_name: string;
|
|
3281
3365
|
customer_id: string;
|
|
3282
3366
|
lead_id: string;
|
|
3367
|
+
/**
|
|
3368
|
+
* @description Referencia externa del client OAuth del token (null si no hay).
|
|
3369
|
+
* Es el asidero que `lead_id` no podía ser: aquel es un entero de
|
|
3370
|
+
* Pimia y no admite el cuid o el uuid de un CRM de fuera.
|
|
3371
|
+
*/
|
|
3372
|
+
external_ref: string | null;
|
|
3283
3373
|
exchange_rate: string;
|
|
3284
3374
|
base_discount_val: string;
|
|
3285
3375
|
base_sub_total: string;
|
|
@@ -3313,6 +3403,17 @@ export interface components {
|
|
|
3313
3403
|
expiry_date?: string | null;
|
|
3314
3404
|
customer_id?: number | null;
|
|
3315
3405
|
lead_id?: number | null;
|
|
3406
|
+
/**
|
|
3407
|
+
* @description Referencia externa: TU identificador para este presupuesto (el
|
|
3408
|
+
* id de la oportunidad en tu CRM). Es el asidero que `lead_id` no
|
|
3409
|
+
* podía ser: aquel es un entero de Pimia y no admite un cuid ni un
|
|
3410
|
+
* uuid. El alcance es tu client OAuth. Se consulta con
|
|
3411
|
+
* `GET /estimates?external_ref=…` y viaja en el payload de
|
|
3412
|
+
* `estimate.accepted`, así que recuperas tu oportunidad en el
|
|
3413
|
+
* evento sin mantener ningún mapeo. Duplicada dentro de tu
|
|
3414
|
+
* namespace → 422 con `existing_id`. `null` la desvincula.
|
|
3415
|
+
*/
|
|
3416
|
+
external_ref?: string | null;
|
|
3316
3417
|
/**
|
|
3317
3418
|
* @description Opcional en el alta: si no llega, lo genera el servidor con el mismo
|
|
3318
3419
|
* SerialNumberFormatter que alimenta a GET /next-number, que es de donde
|
|
@@ -3533,6 +3634,11 @@ export interface components {
|
|
|
3533
3634
|
template_name: string;
|
|
3534
3635
|
invoice_series_id: string;
|
|
3535
3636
|
customer_id: string;
|
|
3637
|
+
/**
|
|
3638
|
+
* @description Referencia externa del client OAuth del token (null si no hay):
|
|
3639
|
+
* cada integrador ve la suya y solo la suya.
|
|
3640
|
+
*/
|
|
3641
|
+
external_ref: string | null;
|
|
3536
3642
|
payment_method_id: string;
|
|
3537
3643
|
recurring_invoice_id: string;
|
|
3538
3644
|
sequence_number: string;
|
|
@@ -3684,6 +3790,15 @@ export interface components {
|
|
|
3684
3790
|
due_date?: string | null;
|
|
3685
3791
|
customer_id: number;
|
|
3686
3792
|
invoice_number?: string | null;
|
|
3793
|
+
/**
|
|
3794
|
+
* @description Referencia externa: TU identificador para esta factura (el id
|
|
3795
|
+
* del pedido o de la suscripción en tu sistema). El alcance es tu
|
|
3796
|
+
* client OAuth. Se consulta con `GET /invoices?external_ref=…` y
|
|
3797
|
+
* viaja en los payloads de `invoice.created` e `invoice.paid`.
|
|
3798
|
+
* Duplicada dentro de tu namespace → 422 con `existing_id`.
|
|
3799
|
+
* `null` la desvincula.
|
|
3800
|
+
*/
|
|
3801
|
+
external_ref?: string | null;
|
|
3687
3802
|
exchange_rate?: string | null;
|
|
3688
3803
|
/**
|
|
3689
3804
|
* @description Descuento global: opcional en el contrato y rellenado a 0 en
|
|
@@ -4720,31 +4835,31 @@ export interface components {
|
|
|
4720
4835
|
};
|
|
4721
4836
|
};
|
|
4722
4837
|
responses: {
|
|
4723
|
-
/** @description
|
|
4724
|
-
|
|
4838
|
+
/** @description Authorization error */
|
|
4839
|
+
AuthorizationException: {
|
|
4725
4840
|
headers: {
|
|
4726
4841
|
[name: string]: unknown;
|
|
4727
4842
|
};
|
|
4728
4843
|
content: {
|
|
4729
4844
|
"application/json": {
|
|
4730
|
-
/** @description
|
|
4845
|
+
/** @description Error overview. */
|
|
4731
4846
|
message: string;
|
|
4732
|
-
/** @description A detailed description of each field that failed validation. */
|
|
4733
|
-
errors: {
|
|
4734
|
-
[key: string]: string[];
|
|
4735
|
-
};
|
|
4736
4847
|
};
|
|
4737
4848
|
};
|
|
4738
4849
|
};
|
|
4739
|
-
/** @description
|
|
4740
|
-
|
|
4850
|
+
/** @description Validation error */
|
|
4851
|
+
ValidationException: {
|
|
4741
4852
|
headers: {
|
|
4742
4853
|
[name: string]: unknown;
|
|
4743
4854
|
};
|
|
4744
4855
|
content: {
|
|
4745
4856
|
"application/json": {
|
|
4746
|
-
/** @description
|
|
4857
|
+
/** @description Errors overview. */
|
|
4747
4858
|
message: string;
|
|
4859
|
+
/** @description A detailed description of each field that failed validation. */
|
|
4860
|
+
errors: {
|
|
4861
|
+
[key: string]: string[];
|
|
4862
|
+
};
|
|
4748
4863
|
};
|
|
4749
4864
|
};
|
|
4750
4865
|
};
|
|
@@ -5436,6 +5551,7 @@ export interface operations {
|
|
|
5436
5551
|
"application/json": Record<string, never>;
|
|
5437
5552
|
};
|
|
5438
5553
|
};
|
|
5554
|
+
403: components["responses"]["AuthorizationException"];
|
|
5439
5555
|
422: components["responses"]["ValidationException"];
|
|
5440
5556
|
};
|
|
5441
5557
|
};
|
|
@@ -5759,7 +5875,11 @@ export interface operations {
|
|
|
5759
5875
|
};
|
|
5760
5876
|
cookie?: never;
|
|
5761
5877
|
};
|
|
5762
|
-
requestBody?:
|
|
5878
|
+
requestBody?: {
|
|
5879
|
+
content: {
|
|
5880
|
+
"application/json": components["schemas"]["ConvertEstimateRequest"];
|
|
5881
|
+
};
|
|
5882
|
+
};
|
|
5763
5883
|
responses: {
|
|
5764
5884
|
200: {
|
|
5765
5885
|
headers: {
|
|
@@ -5771,6 +5891,7 @@ export interface operations {
|
|
|
5771
5891
|
};
|
|
5772
5892
|
403: components["responses"]["AuthorizationException"];
|
|
5773
5893
|
404: components["responses"]["ModelNotFoundException"];
|
|
5894
|
+
422: components["responses"]["ValidationException"];
|
|
5774
5895
|
};
|
|
5775
5896
|
};
|
|
5776
5897
|
"general.countries": {
|
|
@@ -6074,7 +6195,10 @@ export interface operations {
|
|
|
6074
6195
|
};
|
|
6075
6196
|
"customers.index": {
|
|
6076
6197
|
parameters: {
|
|
6077
|
-
query?:
|
|
6198
|
+
query?: {
|
|
6199
|
+
/** @description Devuelve solo el recurso que lleve esta referencia externa. El alcance es el client OAuth del token: cada integrador consulta las suyas y nunca ve las de otro. Combinado con la escritura de `external_ref`, es el find-or-create sin mantener ningún mapeo local. */
|
|
6200
|
+
external_ref?: string;
|
|
6201
|
+
};
|
|
6078
6202
|
header?: never;
|
|
6079
6203
|
path?: never;
|
|
6080
6204
|
cookie?: never;
|
|
@@ -6632,7 +6756,10 @@ export interface operations {
|
|
|
6632
6756
|
};
|
|
6633
6757
|
"estimates.index": {
|
|
6634
6758
|
parameters: {
|
|
6635
|
-
query?:
|
|
6759
|
+
query?: {
|
|
6760
|
+
/** @description Devuelve solo el recurso que lleve esta referencia externa. El alcance es el client OAuth del token: cada integrador consulta las suyas y nunca ve las de otro. Combinado con la escritura de `external_ref`, es el find-or-create sin mantener ningún mapeo local. */
|
|
6761
|
+
external_ref?: string;
|
|
6762
|
+
};
|
|
6636
6763
|
header?: never;
|
|
6637
6764
|
path?: never;
|
|
6638
6765
|
cookie?: never;
|
|
@@ -7094,6 +7221,7 @@ export interface operations {
|
|
|
7094
7221
|
"application/json": {
|
|
7095
7222
|
year: number;
|
|
7096
7223
|
quarter: number;
|
|
7224
|
+
locked?: boolean | null;
|
|
7097
7225
|
};
|
|
7098
7226
|
};
|
|
7099
7227
|
};
|
|
@@ -7115,6 +7243,7 @@ export interface operations {
|
|
|
7115
7243
|
};
|
|
7116
7244
|
};
|
|
7117
7245
|
};
|
|
7246
|
+
403: components["responses"]["AuthorizationException"];
|
|
7118
7247
|
422: components["responses"]["ValidationException"];
|
|
7119
7248
|
};
|
|
7120
7249
|
};
|
|
@@ -8013,6 +8142,8 @@ export interface operations {
|
|
|
8013
8142
|
parameters: {
|
|
8014
8143
|
query?: {
|
|
8015
8144
|
limit?: string;
|
|
8145
|
+
/** @description Devuelve solo el recurso que lleve esta referencia externa. El alcance es el client OAuth del token: cada integrador consulta las suyas y nunca ve las de otro. Combinado con la escritura de `external_ref`, es el find-or-create sin mantener ningún mapeo local. */
|
|
8146
|
+
external_ref?: string;
|
|
8016
8147
|
};
|
|
8017
8148
|
header?: never;
|
|
8018
8149
|
path?: never;
|
package/dist/client.d.ts
CHANGED
|
@@ -168,6 +168,7 @@ export declare class PimiaClient {
|
|
|
168
168
|
template_name: string;
|
|
169
169
|
invoice_series_id: string;
|
|
170
170
|
customer_id: string;
|
|
171
|
+
external_ref: string | null;
|
|
171
172
|
payment_method_id: string;
|
|
172
173
|
recurring_invoice_id: string;
|
|
173
174
|
sequence_number: string;
|
|
@@ -253,6 +254,7 @@ export declare class PimiaClient {
|
|
|
253
254
|
template_name: string;
|
|
254
255
|
invoice_series_id: string;
|
|
255
256
|
customer_id: string;
|
|
257
|
+
external_ref: string | null;
|
|
256
258
|
payment_method_id: string;
|
|
257
259
|
recurring_invoice_id: string;
|
|
258
260
|
sequence_number: string;
|
|
@@ -350,6 +352,7 @@ export declare class PimiaClient {
|
|
|
350
352
|
prefix: string;
|
|
351
353
|
tax_id: string;
|
|
352
354
|
notes: string;
|
|
355
|
+
external_ref: string | null;
|
|
353
356
|
iban: string;
|
|
354
357
|
bic: string;
|
|
355
358
|
sepa_mandate_id: string;
|
|
@@ -422,6 +425,7 @@ export declare class PimiaClient {
|
|
|
422
425
|
template_name: string;
|
|
423
426
|
invoice_series_id: string;
|
|
424
427
|
customer_id: string;
|
|
428
|
+
external_ref: string | null;
|
|
425
429
|
payment_method_id: string;
|
|
426
430
|
recurring_invoice_id: string;
|
|
427
431
|
sequence_number: string;
|
package/dist/errors.d.ts
CHANGED
|
@@ -38,6 +38,49 @@ export declare class NotFoundError extends PimiaApiError {
|
|
|
38
38
|
export declare class ValidationError extends PimiaApiError {
|
|
39
39
|
get errors(): Record<string, string[]>;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* 422 `external_ref_already_used`: la referencia externa que intentaste colgar
|
|
43
|
+
* ya la lleva otro recurso del mismo tipo dentro de tu namespace (company +
|
|
44
|
+
* client OAuth).
|
|
45
|
+
*
|
|
46
|
+
* **Lo normal es que no sea un error tuyo, sino tu reintento**: «crea el cliente
|
|
47
|
+
* del deal 42» ejecutado dos veces porque el proceso se cayó entre el POST y el
|
|
48
|
+
* guardado de tu mapeo. Por eso el error trae {@link existingId}, el recurso que
|
|
49
|
+
* ya lleva esa referencia — que es lo que convierte el choque en un
|
|
50
|
+
* find-or-create sin mantener ningún mapeo local:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* async function clienteDelDeal(dealId: string, name: string): Promise<number> {
|
|
54
|
+
* try {
|
|
55
|
+
* const { id } = await crearCliente({ name, external_ref: `deal_${dealId}` })
|
|
56
|
+
* return id
|
|
57
|
+
* } catch (error) {
|
|
58
|
+
* // Ya existía: el propio error dice cuál es.
|
|
59
|
+
* if (error instanceof DuplicateExternalRefError) return error.existingId
|
|
60
|
+
* throw error
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Hereda de {@link ValidationError} a propósito: el cuerpo trae también el
|
|
66
|
+
* `errors` de siempre, así que el código que ya trataba los 422 por ese camino
|
|
67
|
+
* sigue funcionando sin ramas nuevas.
|
|
68
|
+
*/
|
|
69
|
+
export declare class DuplicateExternalRefError extends ValidationError {
|
|
70
|
+
/** Id del recurso que YA lleva esa referencia. Tu find-or-create acaba aquí. */
|
|
71
|
+
readonly existingId: number;
|
|
72
|
+
/** La referencia que chocó, tal y como la mandaste. */
|
|
73
|
+
readonly externalRef: string;
|
|
74
|
+
/** Tipo del recurso en el core (`customer`, `estimate`, `invoice`). */
|
|
75
|
+
readonly entityType: string;
|
|
76
|
+
constructor(
|
|
77
|
+
/** Id del recurso que YA lleva esa referencia. Tu find-or-create acaba aquí. */
|
|
78
|
+
existingId: number,
|
|
79
|
+
/** La referencia que chocó, tal y como la mandaste. */
|
|
80
|
+
externalRef: string,
|
|
81
|
+
/** Tipo del recurso en el core (`customer`, `estimate`, `invoice`). */
|
|
82
|
+
entityType: string, status: number, message: string, body: unknown, requestId?: string);
|
|
83
|
+
}
|
|
41
84
|
/** 429: pasado el rate limit. `retryAfter` en segundos si la API lo dijo. */
|
|
42
85
|
export declare class RateLimitError extends PimiaApiError {
|
|
43
86
|
readonly retryAfter: number | undefined;
|
package/dist/errors.js
CHANGED
|
@@ -30,8 +30,13 @@ export class PimiaApiError extends PimiaError {
|
|
|
30
30
|
return new MissingScopeError(scope, status, message, body, requestId);
|
|
31
31
|
return new ForbiddenError(status, message, body, requestId);
|
|
32
32
|
}
|
|
33
|
-
if (status === 422)
|
|
33
|
+
if (status === 422) {
|
|
34
|
+
const duplicate = duplicateExternalRefFrom(body);
|
|
35
|
+
if (duplicate) {
|
|
36
|
+
return new DuplicateExternalRefError(duplicate.existingId, duplicate.externalRef, duplicate.entityType, status, message, body, requestId);
|
|
37
|
+
}
|
|
34
38
|
return new ValidationError(status, message, body, requestId);
|
|
39
|
+
}
|
|
35
40
|
if (status === 404)
|
|
36
41
|
return new NotFoundError(status, message, body, requestId);
|
|
37
42
|
return new PimiaApiError(status, message, body, requestId);
|
|
@@ -67,6 +72,51 @@ export class ValidationError extends PimiaApiError {
|
|
|
67
72
|
return body?.errors ?? {};
|
|
68
73
|
}
|
|
69
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 422 `external_ref_already_used`: la referencia externa que intentaste colgar
|
|
77
|
+
* ya la lleva otro recurso del mismo tipo dentro de tu namespace (company +
|
|
78
|
+
* client OAuth).
|
|
79
|
+
*
|
|
80
|
+
* **Lo normal es que no sea un error tuyo, sino tu reintento**: «crea el cliente
|
|
81
|
+
* del deal 42» ejecutado dos veces porque el proceso se cayó entre el POST y el
|
|
82
|
+
* guardado de tu mapeo. Por eso el error trae {@link existingId}, el recurso que
|
|
83
|
+
* ya lleva esa referencia — que es lo que convierte el choque en un
|
|
84
|
+
* find-or-create sin mantener ningún mapeo local:
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* async function clienteDelDeal(dealId: string, name: string): Promise<number> {
|
|
88
|
+
* try {
|
|
89
|
+
* const { id } = await crearCliente({ name, external_ref: `deal_${dealId}` })
|
|
90
|
+
* return id
|
|
91
|
+
* } catch (error) {
|
|
92
|
+
* // Ya existía: el propio error dice cuál es.
|
|
93
|
+
* if (error instanceof DuplicateExternalRefError) return error.existingId
|
|
94
|
+
* throw error
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* Hereda de {@link ValidationError} a propósito: el cuerpo trae también el
|
|
100
|
+
* `errors` de siempre, así que el código que ya trataba los 422 por ese camino
|
|
101
|
+
* sigue funcionando sin ramas nuevas.
|
|
102
|
+
*/
|
|
103
|
+
export class DuplicateExternalRefError extends ValidationError {
|
|
104
|
+
existingId;
|
|
105
|
+
externalRef;
|
|
106
|
+
entityType;
|
|
107
|
+
constructor(
|
|
108
|
+
/** Id del recurso que YA lleva esa referencia. Tu find-or-create acaba aquí. */
|
|
109
|
+
existingId,
|
|
110
|
+
/** La referencia que chocó, tal y como la mandaste. */
|
|
111
|
+
externalRef,
|
|
112
|
+
/** Tipo del recurso en el core (`customer`, `estimate`, `invoice`). */
|
|
113
|
+
entityType, status, message, body, requestId) {
|
|
114
|
+
super(status, message, body, requestId);
|
|
115
|
+
this.existingId = existingId;
|
|
116
|
+
this.externalRef = externalRef;
|
|
117
|
+
this.entityType = entityType;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
70
120
|
/** 429: pasado el rate limit. `retryAfter` en segundos si la API lo dijo. */
|
|
71
121
|
export class RateLimitError extends PimiaApiError {
|
|
72
122
|
retryAfter;
|
|
@@ -103,6 +153,36 @@ function messageFrom(body) {
|
|
|
103
153
|
}
|
|
104
154
|
return undefined;
|
|
105
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Reconoce el 422 de referencia duplicada por su campo `error`, no por la prosa
|
|
158
|
+
* del mensaje —que está en castellano y puede cambiar—. Sin `existing_id` usable
|
|
159
|
+
* no se promueve el error: sin ese id no hay find-or-create que hacer, y un
|
|
160
|
+
* {@link ValidationError} normal describe mejor lo que pasó.
|
|
161
|
+
*/
|
|
162
|
+
function duplicateExternalRefFrom(body) {
|
|
163
|
+
if (!body || typeof body !== 'object')
|
|
164
|
+
return undefined;
|
|
165
|
+
const record = body;
|
|
166
|
+
if (record.error !== 'external_ref_already_used')
|
|
167
|
+
return undefined;
|
|
168
|
+
// El core lo manda como entero; se normaliza igualmente porque en este
|
|
169
|
+
// contrato hay enteros que llegan como cadena según el driver. Ojo con
|
|
170
|
+
// `Number(null)`, que es 0 y no NaN: sin descartar antes los no-numéricos, un
|
|
171
|
+
// `existing_id: null` se colaría como el id 0.
|
|
172
|
+
const raw = record.existing_id;
|
|
173
|
+
const existingId = typeof raw === 'number'
|
|
174
|
+
? raw
|
|
175
|
+
: typeof raw === 'string' && raw.trim() !== ''
|
|
176
|
+
? Number(raw)
|
|
177
|
+
: Number.NaN;
|
|
178
|
+
if (!Number.isInteger(existingId))
|
|
179
|
+
return undefined;
|
|
180
|
+
return {
|
|
181
|
+
existingId,
|
|
182
|
+
externalRef: typeof record.external_ref === 'string' ? record.external_ref : '',
|
|
183
|
+
entityType: typeof record.entity_type === 'string' ? record.entity_type : '',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
106
186
|
/** «Token lacks the invoices:write scope» → `invoices:write`. */
|
|
107
187
|
function scopeFrom(message) {
|
|
108
188
|
return /Token lacks the (\S+) scope/.exec(message)?.[1];
|
package/dist/index.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export { OAuth, createPkceChallenge, createState } from './oauth.js';
|
|
|
11
11
|
export type { AuthorizationServerMetadata, AuthorizeUrlOptions, OAuthConfig, PkceChallenge, } from './oauth.js';
|
|
12
12
|
export { MemoryTokenStore, isExpired, tokenSetFromResponse } from './tokens.js';
|
|
13
13
|
export type { TokenSet, TokenStore } from './tokens.js';
|
|
14
|
-
export { ForbiddenError, MissingScopeError, NotAuthenticatedError, NotFoundError, OAuthError, PimiaApiError, PimiaError, RateLimitError, UnauthorizedError, ValidationError, } from './errors.js';
|
|
14
|
+
export { DuplicateExternalRefError, ForbiddenError, MissingScopeError, NotAuthenticatedError, NotFoundError, OAuthError, PimiaApiError, PimiaError, RateLimitError, UnauthorizedError, ValidationError, } from './errors.js';
|
|
15
15
|
export { WEBHOOK_DEFAULT_TOLERANCE_SECONDS, WEBHOOK_EVENTS, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_VERSION, WebhookVerificationError, isWebhookEvent, signWebhook, verifyWebhook, } from './webhooks.js';
|
|
16
|
-
export type { ApprovalDecidedPayload, AppRevokedPayload, CustomerPayload, EstimateAcceptedPayload, InvoiceCreatedPayload, InvoicePaidPayload, InvoiceReceivedPayload, IsoDateTime, KnownWebhook, PimiaWebhook, SignWebhookOptions, UnknownWebhook, VerifyWebhookOptions, WebhookBodyInput, WebhookEvent, WebhookHeadersInput, WebhookPayloads, WebhookVerificationReason, } from './webhooks.js';
|
|
16
|
+
export type { ApprovalDecidedPayload, AppRevokedPayload, CustomerPayload, EstimateAcceptedPayload, ExternalRef, InvoiceCreatedPayload, InvoicePaidPayload, InvoiceReceivedPayload, IsoDateTime, KnownWebhook, PimiaWebhook, SignWebhookOptions, UnknownWebhook, VerifyWebhookOptions, WebhookBodyInput, WebhookEvent, WebhookHeadersInput, WebhookPayloads, WebhookVerificationReason, } from './webhooks.js';
|
|
17
17
|
/** Scopes granulares del catálogo de Pimia (paso 4). Pide siempre lo mínimo. */
|
|
18
18
|
export declare const SCOPES: {
|
|
19
19
|
readonly invoicesRead: "invoices:read";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export { PimiaClient } from './client.js';
|
|
9
9
|
export { OAuth, createPkceChallenge, createState } from './oauth.js';
|
|
10
10
|
export { MemoryTokenStore, isExpired, tokenSetFromResponse } from './tokens.js';
|
|
11
|
-
export { ForbiddenError, MissingScopeError, NotAuthenticatedError, NotFoundError, OAuthError, PimiaApiError, PimiaError, RateLimitError, UnauthorizedError, ValidationError, } from './errors.js';
|
|
11
|
+
export { DuplicateExternalRefError, ForbiddenError, MissingScopeError, NotAuthenticatedError, NotFoundError, OAuthError, PimiaApiError, PimiaError, RateLimitError, UnauthorizedError, ValidationError, } from './errors.js';
|
|
12
12
|
export { WEBHOOK_DEFAULT_TOLERANCE_SECONDS, WEBHOOK_EVENTS, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_VERSION, WebhookVerificationError, isWebhookEvent, signWebhook, verifyWebhook, } from './webhooks.js';
|
|
13
13
|
/** Scopes granulares del catálogo de Pimia (paso 4). Pide siempre lo mínimo. */
|
|
14
14
|
export const SCOPES = {
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -67,6 +67,26 @@ export declare function isWebhookEvent(value: string): value is WebhookEvent;
|
|
|
67
67
|
export type IsoDateTime = string;
|
|
68
68
|
/** Une un enum abierto: autocompleta los valores conocidos sin cerrar el tipo. */
|
|
69
69
|
type OpenEnum<T extends string> = T | (string & {});
|
|
70
|
+
/**
|
|
71
|
+
* Referencia externa **de quien recibe esta entrega**: la que TU app puso sobre
|
|
72
|
+
* el recurso del evento, nunca la de otro integrador.
|
|
73
|
+
*
|
|
74
|
+
* No se calcula una vez y se reparte. El core la resuelve endpoint por endpoint
|
|
75
|
+
* contra el `client_id` del destinatario, porque un mismo evento se entrega a
|
|
76
|
+
* todos los endpoints suscritos de la company y esos pueden pertenecer a
|
|
77
|
+
* integradores distintos: con un valor común en el payload, la referencia del
|
|
78
|
+
* integrador A habría llegado al endpoint de B. Una referencia escrita sin
|
|
79
|
+
* client OAuth (panel, token personal) no sale nunca por este canal.
|
|
80
|
+
*
|
|
81
|
+
* Es `string | null` y **no** opcional a propósito: la clave viaja siempre, con
|
|
82
|
+
* `null` cuando no hay referencia. Un payload cuya forma cambia según el dato es
|
|
83
|
+
* un payload que el receptor no puede tipar.
|
|
84
|
+
*
|
|
85
|
+
* Solo la llevan los cinco eventos de recurso (`customer.*`, `invoice.created`,
|
|
86
|
+
* `invoice.paid`, `estimate.accepted`). `approval.decided`, `invoice.received` y
|
|
87
|
+
* `app.revoked` no van sobre un recurso etiquetable y no la incluyen.
|
|
88
|
+
*/
|
|
89
|
+
export type ExternalRef = string | null;
|
|
70
90
|
/**
|
|
71
91
|
* Una decisión de aprobación delegada se resolvió.
|
|
72
92
|
*
|
|
@@ -129,6 +149,8 @@ export interface CustomerPayload {
|
|
|
129
149
|
company_id: number;
|
|
130
150
|
created_at: IsoDateTime | null;
|
|
131
151
|
updated_at: IsoDateTime | null;
|
|
152
|
+
/** Tu referencia para este cliente. Ver {@link ExternalRef}. */
|
|
153
|
+
external_ref: ExternalRef;
|
|
132
154
|
}
|
|
133
155
|
/** Alta de factura. Importes en céntimos. */
|
|
134
156
|
export interface InvoiceCreatedPayload {
|
|
@@ -147,6 +169,14 @@ export interface InvoiceCreatedPayload {
|
|
|
147
169
|
due_amount: number;
|
|
148
170
|
currency_id: number | null;
|
|
149
171
|
created_at: IsoDateTime | null;
|
|
172
|
+
/**
|
|
173
|
+
* Tu referencia para esta factura. Ver {@link ExternalRef}.
|
|
174
|
+
*
|
|
175
|
+
* Llega poblada también cuando la factura nace de
|
|
176
|
+
* `POST /estimates/{id}/convert-to-invoice` con `external_ref` en el cuerpo:
|
|
177
|
+
* sellar en la conversión existe justo para que este evento no salga nulo.
|
|
178
|
+
*/
|
|
179
|
+
external_ref: ExternalRef;
|
|
150
180
|
}
|
|
151
181
|
/**
|
|
152
182
|
* El CLIENTE FINAL aceptó el presupuesto. Es una transición, no un estado: solo
|
|
@@ -166,6 +196,14 @@ export interface EstimateAcceptedPayload {
|
|
|
166
196
|
total: number;
|
|
167
197
|
currency_id: number | null;
|
|
168
198
|
accepted_at: IsoDateTime | null;
|
|
199
|
+
/**
|
|
200
|
+
* Tu referencia para este presupuesto. Ver {@link ExternalRef}.
|
|
201
|
+
*
|
|
202
|
+
* Es el asidero que `lead_id` no podía ser: aquel es un entero de Pimia y no
|
|
203
|
+
* admite el cuid ni el uuid de un CRM de fuera, así que recuperas tu
|
|
204
|
+
* oportunidad desde el propio evento sin mantener ninguna tabla de mapeo.
|
|
205
|
+
*/
|
|
206
|
+
external_ref: ExternalRef;
|
|
169
207
|
}
|
|
170
208
|
/**
|
|
171
209
|
* La factura quedó cobrada del todo. `PARTIALLY_PAID` no emite: es una
|
|
@@ -185,6 +223,8 @@ export interface InvoicePaidPayload {
|
|
|
185
223
|
due_amount: number;
|
|
186
224
|
currency_id: number | null;
|
|
187
225
|
paid_at: IsoDateTime | null;
|
|
226
|
+
/** Tu referencia para esta factura. Ver {@link ExternalRef}. */
|
|
227
|
+
external_ref: ExternalRef;
|
|
188
228
|
}
|
|
189
229
|
/** Payload de cada evento del catálogo, por nombre. */
|
|
190
230
|
export interface WebhookPayloads {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pimia/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Cliente TypeScript de la API de Pimia para apps de partner: OAuth con PKCE, rotación de refresh persistida, reintentos de rate limit y tipos generados del OpenAPI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Pimia (https://pimia.es)",
|