@pimia/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -7
- package/dist/api.d.ts +1217 -313
- package/dist/client.d.ts +440 -14
- package/dist/client.js +75 -12
- package/dist/index.d.ts +10 -1
- package/dist/index.js +8 -0
- package/dist/webhooks.d.ts +305 -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). */
|
|
@@ -34,12 +68,50 @@ export interface RequestOptions {
|
|
|
34
68
|
body?: unknown;
|
|
35
69
|
headers?: Record<string, string>;
|
|
36
70
|
signal?: AbortSignal;
|
|
71
|
+
/**
|
|
72
|
+
* Clave de idempotencia para este `POST`. Manda una única por operación —un
|
|
73
|
+
* UUID nuevo— y reúsala SOLO en los reintentos de esa misma operación:
|
|
74
|
+
* Pimia ejecuta la escritura una vez y reproduce la respuesta original en
|
|
75
|
+
* los reintentos. La misma clave con otro cuerpo responde 422.
|
|
76
|
+
*
|
|
77
|
+
* Para saber si lo que recibiste es un eco y no una escritura nueva, usa
|
|
78
|
+
* {@link PimiaClient.requestWithMeta} y mira `meta.idempotentReplay`.
|
|
79
|
+
*/
|
|
80
|
+
idempotencyKey?: string;
|
|
37
81
|
}
|
|
38
82
|
/** Cabeceras de rate limit que devuelve la API en cada respuesta. */
|
|
39
83
|
export interface RateLimit {
|
|
40
84
|
limit?: number;
|
|
41
85
|
remaining?: number;
|
|
42
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Lo que la respuesta dice ADEMÁS del cuerpo.
|
|
89
|
+
*
|
|
90
|
+
* Va por petición y no como estado del cliente —al contrario que
|
|
91
|
+
* {@link PimiaClient.rateLimit}— a propósito: `idempotentReplay` solo
|
|
92
|
+
* significa algo referido a UNA llamada concreta, y justo se consulta cuando
|
|
93
|
+
* hay reintentos, que es cuando puede haber varias en vuelo. Un campo
|
|
94
|
+
* compartido en el cliente daría la respuesta de otra.
|
|
95
|
+
*/
|
|
96
|
+
export interface ResponseMeta {
|
|
97
|
+
status: number;
|
|
98
|
+
/**
|
|
99
|
+
* `true` si Pimia reprodujo la respuesta de una petición anterior con la
|
|
100
|
+
* misma `Idempotency-Key` en vez de volver a escribir. Es la diferencia
|
|
101
|
+
* entre «he creado la factura» y «ya estaba creada»: sin esto, un partner
|
|
102
|
+
* no puede distinguirlas en sus propios registros.
|
|
103
|
+
*/
|
|
104
|
+
idempotentReplay: boolean;
|
|
105
|
+
requestId?: string;
|
|
106
|
+
rateLimit: RateLimit;
|
|
107
|
+
}
|
|
108
|
+
/** Cuerpo y metadatos de una misma respuesta. */
|
|
109
|
+
export interface ResponseWithMeta<T> {
|
|
110
|
+
data: T;
|
|
111
|
+
meta: ResponseMeta;
|
|
112
|
+
}
|
|
113
|
+
/** Lo que se puede afinar en una escritura (`post`/`put`/`patch`). */
|
|
114
|
+
export type WriteOptions = Pick<RequestOptions, 'headers' | 'query' | 'signal' | 'idempotencyKey'>;
|
|
43
115
|
export declare class PimiaClient {
|
|
44
116
|
readonly oauth: OAuth;
|
|
45
117
|
private readonly baseUrl;
|
|
@@ -56,32 +128,385 @@ export declare class PimiaClient {
|
|
|
56
128
|
/** Cabeceras `X-RateLimit-*` de la última respuesta. */
|
|
57
129
|
get rateLimit(): RateLimit;
|
|
58
130
|
get invoices(): {
|
|
59
|
-
list: (query?: RequestOptions["query"]) => Promise<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
payment_method_id: string;
|
|
172
|
+
recurring_invoice_id: string;
|
|
173
|
+
sequence_number: string;
|
|
174
|
+
exchange_rate: string;
|
|
175
|
+
base_discount_val: string;
|
|
176
|
+
base_sub_total: string;
|
|
177
|
+
base_total: string;
|
|
178
|
+
creator_id: string;
|
|
179
|
+
base_tax: string;
|
|
180
|
+
base_due_amount: string;
|
|
181
|
+
effective_base_total: string;
|
|
182
|
+
effective_base_due_amount: string;
|
|
183
|
+
credited_total: string;
|
|
184
|
+
credited_base_total: string;
|
|
185
|
+
currency_id: string;
|
|
186
|
+
formatted_created_at: string;
|
|
187
|
+
invoice_pdf_url: string;
|
|
188
|
+
formatted_invoice_date: string;
|
|
189
|
+
formatted_due_date: string;
|
|
190
|
+
allow_edit: string;
|
|
191
|
+
payment_module_enabled: string;
|
|
192
|
+
sales_tax_type: string;
|
|
193
|
+
sales_tax_address_type: string;
|
|
194
|
+
overdue: string;
|
|
195
|
+
effective_paid_status: string;
|
|
196
|
+
effective_overdue: string;
|
|
197
|
+
aeat_status: string;
|
|
198
|
+
qr_data: string;
|
|
199
|
+
hash: string;
|
|
200
|
+
aeat_csv: string;
|
|
201
|
+
is_credit_note: string | boolean;
|
|
202
|
+
rectified_invoice_id: string;
|
|
203
|
+
rectified_invoice_number?: string | null;
|
|
204
|
+
rectified_invoice?: {
|
|
205
|
+
id: string;
|
|
206
|
+
invoice_number: string;
|
|
207
|
+
tax_per_item: string;
|
|
208
|
+
tax_included: string;
|
|
209
|
+
sub_total: string;
|
|
210
|
+
discount_val: string;
|
|
211
|
+
tax: string;
|
|
212
|
+
total: string;
|
|
213
|
+
items: components["schemas"]["InvoiceItemResource"][];
|
|
214
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
215
|
+
};
|
|
216
|
+
credit_notes_count: number;
|
|
217
|
+
items?: components["schemas"]["InvoiceItemResource"][];
|
|
218
|
+
payments?: components["schemas"]["PaymentResource"][];
|
|
219
|
+
customer?: components["schemas"]["CustomerResource"];
|
|
220
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
221
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
222
|
+
creator?: components["schemas"]["UserResource"];
|
|
223
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
224
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
225
|
+
company?: components["schemas"]["CompanyResource"];
|
|
226
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
227
|
+
}>>;
|
|
228
|
+
/** Mismo caso que `create`: el `200` de `invoices.update` no está tipado en el spec. */
|
|
229
|
+
update: (id: number | string, body: InvoicesRequest, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
230
|
+
id: string;
|
|
231
|
+
invoice_date: string;
|
|
232
|
+
due_date: string;
|
|
233
|
+
invoice_number: string;
|
|
234
|
+
reference_number: string;
|
|
235
|
+
status: string;
|
|
236
|
+
paid_status: string;
|
|
237
|
+
tax_per_item: string;
|
|
238
|
+
tax_included: string;
|
|
239
|
+
discount_per_item: string;
|
|
240
|
+
notes: string;
|
|
241
|
+
discount_type: string;
|
|
242
|
+
discount: string;
|
|
243
|
+
discount_val: string;
|
|
244
|
+
sub_total: string;
|
|
245
|
+
total: string;
|
|
246
|
+
effective_total: string;
|
|
247
|
+
tax: string;
|
|
248
|
+
due_amount: string;
|
|
249
|
+
effective_due_amount: string;
|
|
250
|
+
sent: string;
|
|
251
|
+
viewed: string;
|
|
252
|
+
unique_hash: string;
|
|
253
|
+
template_name: string;
|
|
254
|
+
invoice_series_id: string;
|
|
255
|
+
customer_id: string;
|
|
256
|
+
payment_method_id: string;
|
|
257
|
+
recurring_invoice_id: string;
|
|
258
|
+
sequence_number: string;
|
|
259
|
+
exchange_rate: string;
|
|
260
|
+
base_discount_val: string;
|
|
261
|
+
base_sub_total: string;
|
|
262
|
+
base_total: string;
|
|
263
|
+
creator_id: string;
|
|
264
|
+
base_tax: string;
|
|
265
|
+
base_due_amount: string;
|
|
266
|
+
effective_base_total: string;
|
|
267
|
+
effective_base_due_amount: string;
|
|
268
|
+
credited_total: string;
|
|
269
|
+
credited_base_total: string;
|
|
270
|
+
currency_id: string;
|
|
271
|
+
formatted_created_at: string;
|
|
272
|
+
invoice_pdf_url: string;
|
|
273
|
+
formatted_invoice_date: string;
|
|
274
|
+
formatted_due_date: string;
|
|
275
|
+
allow_edit: string;
|
|
276
|
+
payment_module_enabled: string;
|
|
277
|
+
sales_tax_type: string;
|
|
278
|
+
sales_tax_address_type: string;
|
|
279
|
+
overdue: string;
|
|
280
|
+
effective_paid_status: string;
|
|
281
|
+
effective_overdue: string;
|
|
282
|
+
aeat_status: string;
|
|
283
|
+
qr_data: string;
|
|
284
|
+
hash: string;
|
|
285
|
+
aeat_csv: string;
|
|
286
|
+
is_credit_note: string | boolean;
|
|
287
|
+
rectified_invoice_id: string;
|
|
288
|
+
rectified_invoice_number?: string | null;
|
|
289
|
+
rectified_invoice?: {
|
|
290
|
+
id: string;
|
|
291
|
+
invoice_number: string;
|
|
292
|
+
tax_per_item: string;
|
|
293
|
+
tax_included: string;
|
|
294
|
+
sub_total: string;
|
|
295
|
+
discount_val: string;
|
|
296
|
+
tax: string;
|
|
297
|
+
total: string;
|
|
298
|
+
items: components["schemas"]["InvoiceItemResource"][];
|
|
299
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
300
|
+
};
|
|
301
|
+
credit_notes_count: number;
|
|
302
|
+
items?: components["schemas"]["InvoiceItemResource"][];
|
|
303
|
+
payments?: components["schemas"]["PaymentResource"][];
|
|
304
|
+
customer?: components["schemas"]["CustomerResource"];
|
|
305
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
306
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
307
|
+
creator?: components["schemas"]["UserResource"];
|
|
308
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
309
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
310
|
+
company?: components["schemas"]["CompanyResource"];
|
|
311
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
312
|
+
}>>;
|
|
63
313
|
};
|
|
64
314
|
get customers(): {
|
|
65
|
-
list: (query?: RequestOptions["query"]) => Promise<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
315
|
+
list: (query?: RequestOptions["query"]) => Promise<{
|
|
316
|
+
data: components["schemas"]["CustomerResource"][];
|
|
317
|
+
meta: {
|
|
318
|
+
customer_total_count: number;
|
|
319
|
+
};
|
|
320
|
+
}>;
|
|
321
|
+
get: (id: number | string) => Promise<{
|
|
322
|
+
data: components["schemas"]["CustomerResource"];
|
|
323
|
+
}>;
|
|
324
|
+
create: (body: CustomerRequest, options?: WriteOptions) => Promise<{
|
|
325
|
+
data: components["schemas"]["CustomerResource"];
|
|
326
|
+
}>;
|
|
327
|
+
/** El `200` de `customers.update` no está tipado en el spec. */
|
|
328
|
+
update: (id: number | string, body: CustomerRequest, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
329
|
+
id: string;
|
|
330
|
+
name: string;
|
|
331
|
+
email: string;
|
|
332
|
+
phone: string;
|
|
333
|
+
contact_name: string;
|
|
334
|
+
company_name: string;
|
|
335
|
+
website: string;
|
|
336
|
+
enable_portal: string;
|
|
337
|
+
password_added: boolean;
|
|
338
|
+
currency_id: string;
|
|
339
|
+
payment_method_id: string;
|
|
340
|
+
company_id: string;
|
|
341
|
+
facebook_id: string;
|
|
342
|
+
google_id: string;
|
|
343
|
+
github_id: string;
|
|
344
|
+
created_at: string;
|
|
345
|
+
formatted_created_at: string;
|
|
346
|
+
updated_at: string;
|
|
347
|
+
avatar: string;
|
|
348
|
+
due_amount: string;
|
|
349
|
+
base_due_amount: string;
|
|
350
|
+
prefix: string;
|
|
351
|
+
tax_id: string;
|
|
352
|
+
notes: string;
|
|
353
|
+
iban: string;
|
|
354
|
+
bic: string;
|
|
355
|
+
sepa_mandate_id: string;
|
|
356
|
+
sepa_mandate_date: string;
|
|
357
|
+
billing?: components["schemas"]["AddressResource"];
|
|
358
|
+
shipping?: components["schemas"]["AddressResource"];
|
|
359
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
360
|
+
company?: components["schemas"]["CompanyResource"];
|
|
361
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
362
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
363
|
+
}>>;
|
|
69
364
|
};
|
|
70
365
|
get estimates(): {
|
|
71
|
-
list: (query?: RequestOptions["query"]) => Promise<
|
|
72
|
-
|
|
73
|
-
|
|
366
|
+
list: (query?: RequestOptions["query"]) => Promise<{
|
|
367
|
+
data: components["schemas"]["EstimateResource"][];
|
|
368
|
+
meta: {
|
|
369
|
+
estimate_total_count: number;
|
|
370
|
+
};
|
|
371
|
+
}>;
|
|
372
|
+
get: (id: number | string) => Promise<{
|
|
373
|
+
data: components["schemas"]["EstimateResource"];
|
|
374
|
+
}>;
|
|
375
|
+
create: (body: EstimatesRequest, options?: WriteOptions) => Promise<{
|
|
376
|
+
data: components["schemas"]["EstimateResource"];
|
|
377
|
+
}>;
|
|
378
|
+
/**
|
|
379
|
+
* Convierte un presupuesto aceptado en factura.
|
|
380
|
+
*
|
|
381
|
+
* El helper existe porque es el cierre natural del bucle
|
|
382
|
+
* `estimate.accepted` → facturar, y sin él hay que ir por ruta cruda y
|
|
383
|
+
* adivinar la forma de la respuesta.
|
|
384
|
+
*
|
|
385
|
+
* Dos cosas que conviene saber y que el spec no dice:
|
|
386
|
+
*
|
|
387
|
+
* - **la factura nace BORRADOR y sin numerar**: `data.invoice_number` es
|
|
388
|
+
* `null` hasta que la publiques cambiando su estado. No es un fallo;
|
|
389
|
+
* - el id de la factura nueva está en `data.id`. El `r?.data?.id ?? r?.id`
|
|
390
|
+
* defensivo que se ve por ahí sobra: la segunda rama nunca ocurre.
|
|
391
|
+
*
|
|
392
|
+
* Manda `idempotencyKey` —una clave estable por presupuesto, del estilo
|
|
393
|
+
* `estimate:{id}:invoice`— y el reintento tras un timeout no te creará
|
|
394
|
+
* una segunda factura.
|
|
395
|
+
*
|
|
396
|
+
* Exige `estimates:write` **e** `invoices:write`.
|
|
397
|
+
*/
|
|
398
|
+
convertToInvoice: (id: number | string, options?: WriteOptions) => Promise<ResourceEnvelope<{
|
|
399
|
+
id: string;
|
|
400
|
+
invoice_date: string;
|
|
401
|
+
due_date: string;
|
|
402
|
+
invoice_number: string;
|
|
403
|
+
reference_number: string;
|
|
404
|
+
status: string;
|
|
405
|
+
paid_status: string;
|
|
406
|
+
tax_per_item: string;
|
|
407
|
+
tax_included: string;
|
|
408
|
+
discount_per_item: string;
|
|
409
|
+
notes: string;
|
|
410
|
+
discount_type: string;
|
|
411
|
+
discount: string;
|
|
412
|
+
discount_val: string;
|
|
413
|
+
sub_total: string;
|
|
414
|
+
total: string;
|
|
415
|
+
effective_total: string;
|
|
416
|
+
tax: string;
|
|
417
|
+
due_amount: string;
|
|
418
|
+
effective_due_amount: string;
|
|
419
|
+
sent: string;
|
|
420
|
+
viewed: string;
|
|
421
|
+
unique_hash: string;
|
|
422
|
+
template_name: string;
|
|
423
|
+
invoice_series_id: string;
|
|
424
|
+
customer_id: string;
|
|
425
|
+
payment_method_id: string;
|
|
426
|
+
recurring_invoice_id: string;
|
|
427
|
+
sequence_number: string;
|
|
428
|
+
exchange_rate: string;
|
|
429
|
+
base_discount_val: string;
|
|
430
|
+
base_sub_total: string;
|
|
431
|
+
base_total: string;
|
|
432
|
+
creator_id: string;
|
|
433
|
+
base_tax: string;
|
|
434
|
+
base_due_amount: string;
|
|
435
|
+
effective_base_total: string;
|
|
436
|
+
effective_base_due_amount: string;
|
|
437
|
+
credited_total: string;
|
|
438
|
+
credited_base_total: string;
|
|
439
|
+
currency_id: string;
|
|
440
|
+
formatted_created_at: string;
|
|
441
|
+
invoice_pdf_url: string;
|
|
442
|
+
formatted_invoice_date: string;
|
|
443
|
+
formatted_due_date: string;
|
|
444
|
+
allow_edit: string;
|
|
445
|
+
payment_module_enabled: string;
|
|
446
|
+
sales_tax_type: string;
|
|
447
|
+
sales_tax_address_type: string;
|
|
448
|
+
overdue: string;
|
|
449
|
+
effective_paid_status: string;
|
|
450
|
+
effective_overdue: string;
|
|
451
|
+
aeat_status: string;
|
|
452
|
+
qr_data: string;
|
|
453
|
+
hash: string;
|
|
454
|
+
aeat_csv: string;
|
|
455
|
+
is_credit_note: string | boolean;
|
|
456
|
+
rectified_invoice_id: string;
|
|
457
|
+
rectified_invoice_number?: string | null;
|
|
458
|
+
rectified_invoice?: {
|
|
459
|
+
id: string;
|
|
460
|
+
invoice_number: string;
|
|
461
|
+
tax_per_item: string;
|
|
462
|
+
tax_included: string;
|
|
463
|
+
sub_total: string;
|
|
464
|
+
discount_val: string;
|
|
465
|
+
tax: string;
|
|
466
|
+
total: string;
|
|
467
|
+
items: components["schemas"]["InvoiceItemResource"][];
|
|
468
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
469
|
+
};
|
|
470
|
+
credit_notes_count: number;
|
|
471
|
+
items?: components["schemas"]["InvoiceItemResource"][];
|
|
472
|
+
payments?: components["schemas"]["PaymentResource"][];
|
|
473
|
+
customer?: components["schemas"]["CustomerResource"];
|
|
474
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
475
|
+
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
476
|
+
creator?: components["schemas"]["UserResource"];
|
|
477
|
+
taxes: components["schemas"]["TaxResource"][];
|
|
478
|
+
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
479
|
+
company?: components["schemas"]["CompanyResource"];
|
|
480
|
+
currency?: components["schemas"]["CurrencyResource"];
|
|
481
|
+
}>>;
|
|
74
482
|
};
|
|
75
483
|
get<T = unknown>(path: string, query?: RequestOptions['query']): Promise<T>;
|
|
76
|
-
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
77
|
-
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
78
|
-
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
484
|
+
post<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
485
|
+
put<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
486
|
+
patch<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
79
487
|
delete<T = unknown>(path: string): Promise<T>;
|
|
80
488
|
/**
|
|
81
489
|
* Petición cruda contra `/api/v1`. `path` puede llevar el prefijo o no:
|
|
82
490
|
* `/invoices` y `/api/v1/invoices` son lo mismo.
|
|
83
491
|
*/
|
|
84
492
|
request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
493
|
+
/**
|
|
494
|
+
* Lo mismo que {@link request}, pero devuelve también los metadatos de la
|
|
495
|
+
* respuesta.
|
|
496
|
+
*
|
|
497
|
+
* Existe por la idempotencia: tras un reintento, el cuerpo es idéntico al de
|
|
498
|
+
* la primera llamada —ese es justo el contrato—, así que el cuerpo solo no
|
|
499
|
+
* dice si Pimia escribió o se limitó a repetirse. `meta.idempotentReplay` sí.
|
|
500
|
+
*
|
|
501
|
+
* ```ts
|
|
502
|
+
* const clave = crypto.randomUUID()
|
|
503
|
+
* const { data, meta } = await client.requestWithMeta('/estimates', {
|
|
504
|
+
* method: 'POST', body, idempotencyKey: clave,
|
|
505
|
+
* })
|
|
506
|
+
* if (meta.idempotentReplay) log('el presupuesto ya existía; no se duplicó')
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
requestWithMeta<T = unknown>(path: string, options?: RequestOptions): Promise<ResponseWithMeta<T>>;
|
|
85
510
|
private currentTokens;
|
|
86
511
|
/**
|
|
87
512
|
* Refresca UNA sola vez aunque lo pidan N peticiones en paralelo, y persiste
|
|
@@ -94,3 +519,4 @@ export declare class PimiaClient {
|
|
|
94
519
|
private captureRateLimit;
|
|
95
520
|
private retryDelay;
|
|
96
521
|
}
|
|
522
|
+
export {};
|
package/dist/client.js
CHANGED
|
@@ -46,36 +46,63 @@ export class PimiaClient {
|
|
|
46
46
|
return {
|
|
47
47
|
list: (query) => this.get('/invoices', query),
|
|
48
48
|
get: (id) => this.get(`/invoices/${id}`),
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
*/
|
|
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. */
|
|
55
|
+
update: (id, body, options) => this.put(`/invoices/${id}`, body, options),
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
get customers() {
|
|
54
59
|
return {
|
|
55
60
|
list: (query) => this.get('/customers', query),
|
|
56
61
|
get: (id) => this.get(`/customers/${id}`),
|
|
57
|
-
create: (body) => this.post('/customers', body),
|
|
58
|
-
update
|
|
62
|
+
create: (body, options) => this.post('/customers', body, options),
|
|
63
|
+
/** El `200` de `customers.update` no está tipado en el spec. */
|
|
64
|
+
update: (id, body, options) => this.put(`/customers/${id}`, body, options),
|
|
59
65
|
};
|
|
60
66
|
}
|
|
61
67
|
get estimates() {
|
|
62
68
|
return {
|
|
63
69
|
list: (query) => this.get('/estimates', query),
|
|
64
70
|
get: (id) => this.get(`/estimates/${id}`),
|
|
65
|
-
create: (body) => this.post('/estimates', body),
|
|
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) {
|
|
69
96
|
return this.request(path, { method: 'GET', query });
|
|
70
97
|
}
|
|
71
|
-
post(path, body) {
|
|
72
|
-
return this.request(path, { method: 'POST', body });
|
|
98
|
+
post(path, body, options) {
|
|
99
|
+
return this.request(path, { ...options, method: 'POST', body });
|
|
73
100
|
}
|
|
74
|
-
put(path, body) {
|
|
75
|
-
return this.request(path, { method: 'PUT', body });
|
|
101
|
+
put(path, body, options) {
|
|
102
|
+
return this.request(path, { ...options, method: 'PUT', body });
|
|
76
103
|
}
|
|
77
|
-
patch(path, body) {
|
|
78
|
-
return this.request(path, { method: 'PATCH', body });
|
|
104
|
+
patch(path, body, options) {
|
|
105
|
+
return this.request(path, { ...options, method: 'PATCH', body });
|
|
79
106
|
}
|
|
80
107
|
delete(path) {
|
|
81
108
|
return this.request(path, { method: 'DELETE' });
|
|
@@ -85,6 +112,26 @@ export class PimiaClient {
|
|
|
85
112
|
* `/invoices` y `/api/v1/invoices` son lo mismo.
|
|
86
113
|
*/
|
|
87
114
|
async request(path, options = {}) {
|
|
115
|
+
const { data } = await this.requestWithMeta(path, options);
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Lo mismo que {@link request}, pero devuelve también los metadatos de la
|
|
120
|
+
* respuesta.
|
|
121
|
+
*
|
|
122
|
+
* Existe por la idempotencia: tras un reintento, el cuerpo es idéntico al de
|
|
123
|
+
* la primera llamada —ese es justo el contrato—, así que el cuerpo solo no
|
|
124
|
+
* dice si Pimia escribió o se limitó a repetirse. `meta.idempotentReplay` sí.
|
|
125
|
+
*
|
|
126
|
+
* ```ts
|
|
127
|
+
* const clave = crypto.randomUUID()
|
|
128
|
+
* const { data, meta } = await client.requestWithMeta('/estimates', {
|
|
129
|
+
* method: 'POST', body, idempotencyKey: clave,
|
|
130
|
+
* })
|
|
131
|
+
* if (meta.idempotentReplay) log('el presupuesto ya existía; no se duplicó')
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
async requestWithMeta(path, options = {}) {
|
|
88
135
|
let tokens = await this.currentTokens();
|
|
89
136
|
if (isExpired(tokens, this.skew)) {
|
|
90
137
|
tokens = await this.refreshTokens(tokens);
|
|
@@ -99,6 +146,12 @@ export class PimiaClient {
|
|
|
99
146
|
...(options.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
100
147
|
...this.extraHeaders,
|
|
101
148
|
...options.headers,
|
|
149
|
+
// Después de `options.headers` para que la opción con nombre mande
|
|
150
|
+
// sobre una cabecera puesta a mano: si alguien usa las dos, la
|
|
151
|
+
// explícita del API es la que quiso de verdad.
|
|
152
|
+
...(options.idempotencyKey === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { 'idempotency-key': options.idempotencyKey }),
|
|
102
155
|
authorization: `Bearer ${tokens.accessToken}`,
|
|
103
156
|
},
|
|
104
157
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
@@ -106,7 +159,17 @@ export class PimiaClient {
|
|
|
106
159
|
});
|
|
107
160
|
this.captureRateLimit(response);
|
|
108
161
|
if (response.ok) {
|
|
109
|
-
return
|
|
162
|
+
return {
|
|
163
|
+
data: (await parseBody(response)),
|
|
164
|
+
meta: {
|
|
165
|
+
status: response.status,
|
|
166
|
+
// Presente solo cuando Pimia reproduce; su ausencia significa
|
|
167
|
+
// «esta escritura ocurrió de verdad».
|
|
168
|
+
idempotentReplay: response.headers.get('idempotency-replayed') === 'true',
|
|
169
|
+
requestId: response.headers.get('x-request-id') ?? undefined,
|
|
170
|
+
rateLimit: this.lastRateLimit,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
110
173
|
}
|
|
111
174
|
const body = await parseBody(response);
|
|
112
175
|
const requestId = response.headers.get('x-request-id') ?? undefined;
|
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 } 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
14
|
export { 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, 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";
|
|
@@ -33,5 +35,12 @@ export declare const SCOPES: {
|
|
|
33
35
|
readonly agendaRead: "agenda:read";
|
|
34
36
|
readonly agendaWrite: "agenda:write";
|
|
35
37
|
readonly reportsRead: "reports:read";
|
|
38
|
+
/**
|
|
39
|
+
* Proponer cambios que el dueño del tenant aprueba antes de aplicarse.
|
|
40
|
+
* `approvalsSubmit` es un alias del mismo permiso, aceptado por el
|
|
41
|
+
* Authorization Server.
|
|
42
|
+
*/
|
|
43
|
+
readonly approvalsWrite: "approvals:write";
|
|
44
|
+
readonly approvalsSubmit: "approvals:submit";
|
|
36
45
|
};
|
|
37
46
|
export type Scope = (typeof SCOPES)[keyof typeof SCOPES];
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { PimiaClient } from './client.js';
|
|
|
9
9
|
export { OAuth, createPkceChallenge, createState } from './oauth.js';
|
|
10
10
|
export { MemoryTokenStore, isExpired, tokenSetFromResponse } from './tokens.js';
|
|
11
11
|
export { 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',
|
|
@@ -30,4 +31,11 @@ export const SCOPES = {
|
|
|
30
31
|
agendaRead: 'agenda:read',
|
|
31
32
|
agendaWrite: 'agenda:write',
|
|
32
33
|
reportsRead: 'reports:read',
|
|
34
|
+
/**
|
|
35
|
+
* Proponer cambios que el dueño del tenant aprueba antes de aplicarse.
|
|
36
|
+
* `approvalsSubmit` es un alias del mismo permiso, aceptado por el
|
|
37
|
+
* Authorization Server.
|
|
38
|
+
*/
|
|
39
|
+
approvalsWrite: 'approvals:write',
|
|
40
|
+
approvalsSubmit: 'approvals:submit',
|
|
33
41
|
};
|