@pimia/sdk 0.2.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/README.md +57 -0
- package/dist/api.d.ts +295 -268
- package/dist/client.d.ts +386 -11
- package/dist/client.js +27 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +81 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/webhooks.d.ts +345 -0
- package/dist/webhooks.js +230 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -13,8 +13,42 @@
|
|
|
13
13
|
* - **429**: respeta `Retry-After` y reintenta con espera acotada;
|
|
14
14
|
* - **errores tipados**: MissingScopeError trae el scope exacto que falta.
|
|
15
15
|
*/
|
|
16
|
+
import type { components } from './api.js';
|
|
16
17
|
import { OAuth, type OAuthConfig } from './oauth.js';
|
|
17
18
|
import { type TokenStore } from './tokens.js';
|
|
19
|
+
type Schemas = components['schemas'];
|
|
20
|
+
/** Cliente tal y como lo devuelve la API. */
|
|
21
|
+
export type CustomerResource = Schemas['CustomerResource'];
|
|
22
|
+
/** Factura tal y como la devuelve la API. */
|
|
23
|
+
export type InvoiceResource = Schemas['InvoiceResource'];
|
|
24
|
+
/** Presupuesto tal y como lo devuelve la API. */
|
|
25
|
+
export type EstimateResource = Schemas['EstimateResource'];
|
|
26
|
+
/** Cuerpo de alta/edición de cliente. Incluye `customFields`. */
|
|
27
|
+
export type CustomerRequest = Schemas['CustomerRequest'];
|
|
28
|
+
/** Cuerpo de alta/edición de factura. Incluye `customFields`. */
|
|
29
|
+
export type InvoicesRequest = Schemas['InvoicesRequest'];
|
|
30
|
+
/** Cuerpo de alta/edición de presupuesto. Incluye `customFields`. */
|
|
31
|
+
export type EstimatesRequest = Schemas['EstimatesRequest'];
|
|
32
|
+
/**
|
|
33
|
+
* El sobre `{ data: … }` de Laravel para las escrituras que el spec **no
|
|
34
|
+
* tipa**.
|
|
35
|
+
*
|
|
36
|
+
* Hay 17 operaciones cuyo `200` sale del generador como objeto vacío, y entre
|
|
37
|
+
* ellas están `POST /invoices`, `PUT /invoices/{id}`, `PUT /customers/{id}` y
|
|
38
|
+
* `POST /estimates/{id}/convert-to-invoice`. Usar ahí el tipo generado sería
|
|
39
|
+
* peor que no tipar: `Record<string, never>` afirma que la respuesta **no
|
|
40
|
+
* tiene propiedades**, y el `data` real desaparecería del autocompletado.
|
|
41
|
+
*
|
|
42
|
+
* Así que el sobre se declara aquí y el recurso de dentro sí sale del spec.
|
|
43
|
+
* Está verificado contra los controladores del core, no supuesto: los cuatro
|
|
44
|
+
* devuelven `new XResource($modelo)` con el envoltorio `data` de Laravel
|
|
45
|
+
* activo. La causa del hueco es del generador —un `@return JsonResponse`
|
|
46
|
+
* heredado que le gana a la inferencia—, no del contrato; cuando se arregle
|
|
47
|
+
* en el core, estos tipos pasarán a salir de `Ok<…>` como los demás.
|
|
48
|
+
*/
|
|
49
|
+
export interface ResourceEnvelope<T> {
|
|
50
|
+
data: T;
|
|
51
|
+
}
|
|
18
52
|
export interface PimiaClientOptions extends OAuthConfig {
|
|
19
53
|
tokens: TokenStore;
|
|
20
54
|
/** Segundos de margen para refrescar antes de que caduque (default 60). */
|
|
@@ -94,21 +128,361 @@ export declare class PimiaClient {
|
|
|
94
128
|
/** Cabeceras `X-RateLimit-*` de la última respuesta. */
|
|
95
129
|
get rateLimit(): RateLimit;
|
|
96
130
|
get invoices(): {
|
|
97
|
-
list: (query?: RequestOptions["query"]) => Promise<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
131
|
+
list: (query?: RequestOptions["query"]) => Promise<{
|
|
132
|
+
data: components["schemas"]["InvoiceResource"][];
|
|
133
|
+
meta: {
|
|
134
|
+
invoice_total_count: number;
|
|
135
|
+
};
|
|
136
|
+
}>;
|
|
137
|
+
get: (id: number | string) => Promise<{
|
|
138
|
+
data: components["schemas"]["InvoiceResource"] & Record<string, never>;
|
|
139
|
+
}>;
|
|
140
|
+
/**
|
|
141
|
+
* Devuelve `{ data: InvoiceResource }`. El tipo NO sale del spec: el
|
|
142
|
+
* `200` de `invoices.store` está vacío ahí (ver {@link ResourceEnvelope}).
|
|
143
|
+
*/
|
|
144
|
+
create: (body: InvoicesRequest, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
145
|
+
id: string;
|
|
146
|
+
invoice_date: string;
|
|
147
|
+
due_date: string;
|
|
148
|
+
invoice_number: string;
|
|
149
|
+
reference_number: string;
|
|
150
|
+
status: string;
|
|
151
|
+
paid_status: string;
|
|
152
|
+
tax_per_item: string;
|
|
153
|
+
tax_included: string;
|
|
154
|
+
discount_per_item: string;
|
|
155
|
+
notes: string;
|
|
156
|
+
discount_type: string;
|
|
157
|
+
discount: string;
|
|
158
|
+
discount_val: string;
|
|
159
|
+
sub_total: string;
|
|
160
|
+
total: string;
|
|
161
|
+
effective_total: string;
|
|
162
|
+
tax: string;
|
|
163
|
+
due_amount: string;
|
|
164
|
+
effective_due_amount: string;
|
|
165
|
+
sent: string;
|
|
166
|
+
viewed: string;
|
|
167
|
+
unique_hash: string;
|
|
168
|
+
template_name: string;
|
|
169
|
+
invoice_series_id: string;
|
|
170
|
+
customer_id: string;
|
|
171
|
+
external_ref: string | null;
|
|
172
|
+
payment_method_id: string;
|
|
173
|
+
recurring_invoice_id: string;
|
|
174
|
+
sequence_number: string;
|
|
175
|
+
exchange_rate: string;
|
|
176
|
+
base_discount_val: string;
|
|
177
|
+
base_sub_total: string;
|
|
178
|
+
base_total: string;
|
|
179
|
+
creator_id: string;
|
|
180
|
+
base_tax: string;
|
|
181
|
+
base_due_amount: string;
|
|
182
|
+
effective_base_total: string;
|
|
183
|
+
effective_base_due_amount: string;
|
|
184
|
+
credited_total: string;
|
|
185
|
+
credited_base_total: string;
|
|
186
|
+
currency_id: string;
|
|
187
|
+
formatted_created_at: string;
|
|
188
|
+
invoice_pdf_url: string;
|
|
189
|
+
formatted_invoice_date: string;
|
|
190
|
+
formatted_due_date: string;
|
|
191
|
+
allow_edit: string;
|
|
192
|
+
payment_module_enabled: string;
|
|
193
|
+
sales_tax_type: string;
|
|
194
|
+
sales_tax_address_type: string;
|
|
195
|
+
overdue: string;
|
|
196
|
+
effective_paid_status: string;
|
|
197
|
+
effective_overdue: string;
|
|
198
|
+
aeat_status: string;
|
|
199
|
+
qr_data: string;
|
|
200
|
+
hash: string;
|
|
201
|
+
aeat_csv: string;
|
|
202
|
+
is_credit_note: string | boolean;
|
|
203
|
+
rectified_invoice_id: string;
|
|
204
|
+
rectified_invoice_number?: string | null;
|
|
205
|
+
rectified_invoice?: {
|
|
206
|
+
id: string;
|
|
207
|
+
invoice_number: string;
|
|
208
|
+
tax_per_item: string;
|
|
209
|
+
tax_included: string;
|
|
210
|
+
sub_total: string;
|
|
211
|
+
discount_val: string;
|
|
212
|
+
tax: string;
|
|
213
|
+
total: string;
|
|
214
|
+
items: components["schemas"]["InvoiceItemResource"][];
|
|
215
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
216
|
+
};
|
|
217
|
+
credit_notes_count: number;
|
|
218
|
+
items?: components["schemas"]["InvoiceItemResource"][];
|
|
219
|
+
payments?: components["schemas"]["PaymentResource"][];
|
|
220
|
+
customer?: components["schemas"]["CustomerResource"];
|
|
221
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
222
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
223
|
+
creator?: components["schemas"]["UserResource"];
|
|
224
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
225
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
226
|
+
company?: components["schemas"]["CompanyResource"];
|
|
227
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
228
|
+
}>>;
|
|
229
|
+
/** Mismo caso que `create`: el `200` de `invoices.update` no está tipado en el spec. */
|
|
230
|
+
update: (id: number | string, body: InvoicesRequest, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
231
|
+
id: string;
|
|
232
|
+
invoice_date: string;
|
|
233
|
+
due_date: string;
|
|
234
|
+
invoice_number: string;
|
|
235
|
+
reference_number: string;
|
|
236
|
+
status: string;
|
|
237
|
+
paid_status: string;
|
|
238
|
+
tax_per_item: string;
|
|
239
|
+
tax_included: string;
|
|
240
|
+
discount_per_item: string;
|
|
241
|
+
notes: string;
|
|
242
|
+
discount_type: string;
|
|
243
|
+
discount: string;
|
|
244
|
+
discount_val: string;
|
|
245
|
+
sub_total: string;
|
|
246
|
+
total: string;
|
|
247
|
+
effective_total: string;
|
|
248
|
+
tax: string;
|
|
249
|
+
due_amount: string;
|
|
250
|
+
effective_due_amount: string;
|
|
251
|
+
sent: string;
|
|
252
|
+
viewed: string;
|
|
253
|
+
unique_hash: string;
|
|
254
|
+
template_name: string;
|
|
255
|
+
invoice_series_id: string;
|
|
256
|
+
customer_id: string;
|
|
257
|
+
external_ref: string | null;
|
|
258
|
+
payment_method_id: string;
|
|
259
|
+
recurring_invoice_id: string;
|
|
260
|
+
sequence_number: string;
|
|
261
|
+
exchange_rate: string;
|
|
262
|
+
base_discount_val: string;
|
|
263
|
+
base_sub_total: string;
|
|
264
|
+
base_total: string;
|
|
265
|
+
creator_id: string;
|
|
266
|
+
base_tax: string;
|
|
267
|
+
base_due_amount: string;
|
|
268
|
+
effective_base_total: string;
|
|
269
|
+
effective_base_due_amount: string;
|
|
270
|
+
credited_total: string;
|
|
271
|
+
credited_base_total: string;
|
|
272
|
+
currency_id: string;
|
|
273
|
+
formatted_created_at: string;
|
|
274
|
+
invoice_pdf_url: string;
|
|
275
|
+
formatted_invoice_date: string;
|
|
276
|
+
formatted_due_date: string;
|
|
277
|
+
allow_edit: string;
|
|
278
|
+
payment_module_enabled: string;
|
|
279
|
+
sales_tax_type: string;
|
|
280
|
+
sales_tax_address_type: string;
|
|
281
|
+
overdue: string;
|
|
282
|
+
effective_paid_status: string;
|
|
283
|
+
effective_overdue: string;
|
|
284
|
+
aeat_status: string;
|
|
285
|
+
qr_data: string;
|
|
286
|
+
hash: string;
|
|
287
|
+
aeat_csv: string;
|
|
288
|
+
is_credit_note: string | boolean;
|
|
289
|
+
rectified_invoice_id: string;
|
|
290
|
+
rectified_invoice_number?: string | null;
|
|
291
|
+
rectified_invoice?: {
|
|
292
|
+
id: string;
|
|
293
|
+
invoice_number: string;
|
|
294
|
+
tax_per_item: string;
|
|
295
|
+
tax_included: string;
|
|
296
|
+
sub_total: string;
|
|
297
|
+
discount_val: string;
|
|
298
|
+
tax: string;
|
|
299
|
+
total: string;
|
|
300
|
+
items: components["schemas"]["InvoiceItemResource"][];
|
|
301
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
302
|
+
};
|
|
303
|
+
credit_notes_count: number;
|
|
304
|
+
items?: components["schemas"]["InvoiceItemResource"][];
|
|
305
|
+
payments?: components["schemas"]["PaymentResource"][];
|
|
306
|
+
customer?: components["schemas"]["CustomerResource"];
|
|
307
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
308
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
309
|
+
creator?: components["schemas"]["UserResource"];
|
|
310
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
311
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
312
|
+
company?: components["schemas"]["CompanyResource"];
|
|
313
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
314
|
+
}>>;
|
|
101
315
|
};
|
|
102
316
|
get customers(): {
|
|
103
|
-
list: (query?: RequestOptions["query"]) => Promise<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
317
|
+
list: (query?: RequestOptions["query"]) => Promise<{
|
|
318
|
+
data: components["schemas"]["CustomerResource"][];
|
|
319
|
+
meta: {
|
|
320
|
+
customer_total_count: number;
|
|
321
|
+
};
|
|
322
|
+
}>;
|
|
323
|
+
get: (id: number | string) => Promise<{
|
|
324
|
+
data: components["schemas"]["CustomerResource"];
|
|
325
|
+
}>;
|
|
326
|
+
create: (body: CustomerRequest, options?: WriteOptions) => Promise<{
|
|
327
|
+
data: components["schemas"]["CustomerResource"];
|
|
328
|
+
}>;
|
|
329
|
+
/** El `200` de `customers.update` no está tipado en el spec. */
|
|
330
|
+
update: (id: number | string, body: CustomerRequest, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
331
|
+
id: string;
|
|
332
|
+
name: string;
|
|
333
|
+
email: string;
|
|
334
|
+
phone: string;
|
|
335
|
+
contact_name: string;
|
|
336
|
+
company_name: string;
|
|
337
|
+
website: string;
|
|
338
|
+
enable_portal: string;
|
|
339
|
+
password_added: boolean;
|
|
340
|
+
currency_id: string;
|
|
341
|
+
payment_method_id: string;
|
|
342
|
+
company_id: string;
|
|
343
|
+
facebook_id: string;
|
|
344
|
+
google_id: string;
|
|
345
|
+
github_id: string;
|
|
346
|
+
created_at: string;
|
|
347
|
+
formatted_created_at: string;
|
|
348
|
+
updated_at: string;
|
|
349
|
+
avatar: string;
|
|
350
|
+
due_amount: string;
|
|
351
|
+
base_due_amount: string;
|
|
352
|
+
prefix: string;
|
|
353
|
+
tax_id: string;
|
|
354
|
+
notes: string;
|
|
355
|
+
external_ref: string | null;
|
|
356
|
+
iban: string;
|
|
357
|
+
bic: string;
|
|
358
|
+
sepa_mandate_id: string;
|
|
359
|
+
sepa_mandate_date: string;
|
|
360
|
+
billing?: components["schemas"]["AddressResource"];
|
|
361
|
+
shipping?: components["schemas"]["AddressResource"];
|
|
362
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
363
|
+
company?: components["schemas"]["CompanyResource"];
|
|
364
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
365
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
366
|
+
}>>;
|
|
107
367
|
};
|
|
108
368
|
get estimates(): {
|
|
109
|
-
list: (query?: RequestOptions["query"]) => Promise<
|
|
110
|
-
|
|
111
|
-
|
|
369
|
+
list: (query?: RequestOptions["query"]) => Promise<{
|
|
370
|
+
data: components["schemas"]["EstimateResource"][];
|
|
371
|
+
meta: {
|
|
372
|
+
estimate_total_count: number;
|
|
373
|
+
};
|
|
374
|
+
}>;
|
|
375
|
+
get: (id: number | string) => Promise<{
|
|
376
|
+
data: components["schemas"]["EstimateResource"];
|
|
377
|
+
}>;
|
|
378
|
+
create: (body: EstimatesRequest, options?: WriteOptions) => Promise<{
|
|
379
|
+
data: components["schemas"]["EstimateResource"];
|
|
380
|
+
}>;
|
|
381
|
+
/**
|
|
382
|
+
* Convierte un presupuesto aceptado en factura.
|
|
383
|
+
*
|
|
384
|
+
* El helper existe porque es el cierre natural del bucle
|
|
385
|
+
* `estimate.accepted` → facturar, y sin él hay que ir por ruta cruda y
|
|
386
|
+
* adivinar la forma de la respuesta.
|
|
387
|
+
*
|
|
388
|
+
* Dos cosas que conviene saber y que el spec no dice:
|
|
389
|
+
*
|
|
390
|
+
* - **la factura nace BORRADOR y sin numerar**: `data.invoice_number` es
|
|
391
|
+
* `null` hasta que la publiques cambiando su estado. No es un fallo;
|
|
392
|
+
* - el id de la factura nueva está en `data.id`. El `r?.data?.id ?? r?.id`
|
|
393
|
+
* defensivo que se ve por ahí sobra: la segunda rama nunca ocurre.
|
|
394
|
+
*
|
|
395
|
+
* Manda `idempotencyKey` —una clave estable por presupuesto, del estilo
|
|
396
|
+
* `estimate:{id}:invoice`— y el reintento tras un timeout no te creará
|
|
397
|
+
* una segunda factura.
|
|
398
|
+
*
|
|
399
|
+
* Exige `estimates:write` **e** `invoices:write`.
|
|
400
|
+
*/
|
|
401
|
+
convertToInvoice: (id: number | string, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
402
|
+
id: string;
|
|
403
|
+
invoice_date: string;
|
|
404
|
+
due_date: string;
|
|
405
|
+
invoice_number: string;
|
|
406
|
+
reference_number: string;
|
|
407
|
+
status: string;
|
|
408
|
+
paid_status: string;
|
|
409
|
+
tax_per_item: string;
|
|
410
|
+
tax_included: string;
|
|
411
|
+
discount_per_item: string;
|
|
412
|
+
notes: string;
|
|
413
|
+
discount_type: string;
|
|
414
|
+
discount: string;
|
|
415
|
+
discount_val: string;
|
|
416
|
+
sub_total: string;
|
|
417
|
+
total: string;
|
|
418
|
+
effective_total: string;
|
|
419
|
+
tax: string;
|
|
420
|
+
due_amount: string;
|
|
421
|
+
effective_due_amount: string;
|
|
422
|
+
sent: string;
|
|
423
|
+
viewed: string;
|
|
424
|
+
unique_hash: string;
|
|
425
|
+
template_name: string;
|
|
426
|
+
invoice_series_id: string;
|
|
427
|
+
customer_id: string;
|
|
428
|
+
external_ref: string | null;
|
|
429
|
+
payment_method_id: string;
|
|
430
|
+
recurring_invoice_id: string;
|
|
431
|
+
sequence_number: string;
|
|
432
|
+
exchange_rate: string;
|
|
433
|
+
base_discount_val: string;
|
|
434
|
+
base_sub_total: string;
|
|
435
|
+
base_total: string;
|
|
436
|
+
creator_id: string;
|
|
437
|
+
base_tax: string;
|
|
438
|
+
base_due_amount: string;
|
|
439
|
+
effective_base_total: string;
|
|
440
|
+
effective_base_due_amount: string;
|
|
441
|
+
credited_total: string;
|
|
442
|
+
credited_base_total: string;
|
|
443
|
+
currency_id: string;
|
|
444
|
+
formatted_created_at: string;
|
|
445
|
+
invoice_pdf_url: string;
|
|
446
|
+
formatted_invoice_date: string;
|
|
447
|
+
formatted_due_date: string;
|
|
448
|
+
allow_edit: string;
|
|
449
|
+
payment_module_enabled: string;
|
|
450
|
+
sales_tax_type: string;
|
|
451
|
+
sales_tax_address_type: string;
|
|
452
|
+
overdue: string;
|
|
453
|
+
effective_paid_status: string;
|
|
454
|
+
effective_overdue: string;
|
|
455
|
+
aeat_status: string;
|
|
456
|
+
qr_data: string;
|
|
457
|
+
hash: string;
|
|
458
|
+
aeat_csv: string;
|
|
459
|
+
is_credit_note: string | boolean;
|
|
460
|
+
rectified_invoice_id: string;
|
|
461
|
+
rectified_invoice_number?: string | null;
|
|
462
|
+
rectified_invoice?: {
|
|
463
|
+
id: string;
|
|
464
|
+
invoice_number: string;
|
|
465
|
+
tax_per_item: string;
|
|
466
|
+
tax_included: string;
|
|
467
|
+
sub_total: string;
|
|
468
|
+
discount_val: string;
|
|
469
|
+
tax: string;
|
|
470
|
+
total: string;
|
|
471
|
+
items: components["schemas"]["InvoiceItemResource"][];
|
|
472
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
473
|
+
};
|
|
474
|
+
credit_notes_count: number;
|
|
475
|
+
items?: components["schemas"]["InvoiceItemResource"][];
|
|
476
|
+
payments?: components["schemas"]["PaymentResource"][];
|
|
477
|
+
customer?: components["schemas"]["CustomerResource"];
|
|
478
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
479
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
480
|
+
creator?: components["schemas"]["UserResource"];
|
|
481
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
482
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
483
|
+
company?: components["schemas"]["CompanyResource"];
|
|
484
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
485
|
+
}>>;
|
|
112
486
|
};
|
|
113
487
|
get<T = unknown>(path: string, query?: RequestOptions['query']): Promise<T>;
|
|
114
488
|
post<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
@@ -149,3 +523,4 @@ export declare class PimiaClient {
|
|
|
149
523
|
private captureRateLimit;
|
|
150
524
|
private retryDelay;
|
|
151
525
|
}
|
|
526
|
+
export {};
|
package/dist/client.js
CHANGED
|
@@ -46,7 +46,12 @@ export class PimiaClient {
|
|
|
46
46
|
return {
|
|
47
47
|
list: (query) => this.get('/invoices', query),
|
|
48
48
|
get: (id) => this.get(`/invoices/${id}`),
|
|
49
|
+
/**
|
|
50
|
+
* Devuelve `{ data: InvoiceResource }`. El tipo NO sale del spec: el
|
|
51
|
+
* `200` de `invoices.store` está vacío ahí (ver {@link ResourceEnvelope}).
|
|
52
|
+
*/
|
|
49
53
|
create: (body, options) => this.post('/invoices', body, options),
|
|
54
|
+
/** Mismo caso que `create`: el `200` de `invoices.update` no está tipado en el spec. */
|
|
50
55
|
update: (id, body, options) => this.put(`/invoices/${id}`, body, options),
|
|
51
56
|
};
|
|
52
57
|
}
|
|
@@ -55,6 +60,7 @@ export class PimiaClient {
|
|
|
55
60
|
list: (query) => this.get('/customers', query),
|
|
56
61
|
get: (id) => this.get(`/customers/${id}`),
|
|
57
62
|
create: (body, options) => this.post('/customers', body, options),
|
|
63
|
+
/** El `200` de `customers.update` no está tipado en el spec. */
|
|
58
64
|
update: (id, body, options) => this.put(`/customers/${id}`, body, options),
|
|
59
65
|
};
|
|
60
66
|
}
|
|
@@ -63,6 +69,27 @@ export class PimiaClient {
|
|
|
63
69
|
list: (query) => this.get('/estimates', query),
|
|
64
70
|
get: (id) => this.get(`/estimates/${id}`),
|
|
65
71
|
create: (body, options) => this.post('/estimates', body, options),
|
|
72
|
+
/**
|
|
73
|
+
* Convierte un presupuesto aceptado en factura.
|
|
74
|
+
*
|
|
75
|
+
* El helper existe porque es el cierre natural del bucle
|
|
76
|
+
* `estimate.accepted` → facturar, y sin él hay que ir por ruta cruda y
|
|
77
|
+
* adivinar la forma de la respuesta.
|
|
78
|
+
*
|
|
79
|
+
* Dos cosas que conviene saber y que el spec no dice:
|
|
80
|
+
*
|
|
81
|
+
* - **la factura nace BORRADOR y sin numerar**: `data.invoice_number` es
|
|
82
|
+
* `null` hasta que la publiques cambiando su estado. No es un fallo;
|
|
83
|
+
* - el id de la factura nueva está en `data.id`. El `r?.data?.id ?? r?.id`
|
|
84
|
+
* defensivo que se ve por ahí sobra: la segunda rama nunca ocurre.
|
|
85
|
+
*
|
|
86
|
+
* Manda `idempotencyKey` —una clave estable por presupuesto, del estilo
|
|
87
|
+
* `estimate:{id}:invoice`— y el reintento tras un timeout no te creará
|
|
88
|
+
* una segunda factura.
|
|
89
|
+
*
|
|
90
|
+
* Exige `estimates:write` **e** `invoices:write`.
|
|
91
|
+
*/
|
|
92
|
+
convertToInvoice: (id, options) => this.post(`/estimates/${id}/convert-to-invoice`, {}, options),
|
|
66
93
|
};
|
|
67
94
|
}
|
|
68
95
|
get(path, query) {
|
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
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* salen los tipos de `./api`.
|
|
7
7
|
*/
|
|
8
8
|
export { PimiaClient } from './client.js';
|
|
9
|
-
export type { PimiaClientOptions, RateLimit, RequestOptions, ResponseMeta, ResponseWithMeta, WriteOptions, } from './client.js';
|
|
9
|
+
export type { CustomerRequest, CustomerResource, EstimateResource, EstimatesRequest, InvoiceResource, InvoicesRequest, PimiaClientOptions, RateLimit, RequestOptions, ResourceEnvelope, ResponseMeta, ResponseWithMeta, WriteOptions, } from './client.js';
|
|
10
10
|
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
|
+
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, ExternalRef, InvoiceCreatedPayload, InvoicePaidPayload, InvoiceReceivedPayload, IsoDateTime, KnownWebhook, PimiaWebhook, SignWebhookOptions, UnknownWebhook, VerifyWebhookOptions, WebhookBodyInput, WebhookEvent, WebhookHeadersInput, WebhookPayloads, WebhookVerificationReason, } from './webhooks.js';
|
|
15
17
|
/** Scopes granulares del catálogo de Pimia (paso 4). Pide siempre lo mínimo. */
|
|
16
18
|
export declare const SCOPES: {
|
|
17
19
|
readonly invoicesRead: "invoices:read";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
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
|
+
export { WEBHOOK_DEFAULT_TOLERANCE_SECONDS, WEBHOOK_EVENTS, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_VERSION, WebhookVerificationError, isWebhookEvent, signWebhook, verifyWebhook, } from './webhooks.js';
|
|
12
13
|
/** Scopes granulares del catálogo de Pimia (paso 4). Pide siempre lo mínimo. */
|
|
13
14
|
export const SCOPES = {
|
|
14
15
|
invoicesRead: 'invoices:read',
|