@billkit-eu/sdk 0.1.0 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +82 -8
- package/dist/index.cjs +181 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -15
- package/dist/index.d.ts +243 -15
- package/dist/index.js +181 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +28 -22
- package/src/index.ts +3 -0
- package/src/resources.ts +254 -21
- package/src/retry.ts +31 -3
- package/src/transport.ts +58 -4
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -85,6 +85,16 @@ var Customers = class extends BaseResource {
|
|
|
85
85
|
update(id, params = {}) {
|
|
86
86
|
return this.post(`/v1/customers/${id}`, params);
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Delete a customer. Resolves to `{ id, object: "customer", deleted:
|
|
90
|
+
* true }`, not the customer.
|
|
91
|
+
*
|
|
92
|
+
* The customer leaves the API: `retrieve()` 404s and they drop out of
|
|
93
|
+
* `list()`. Their payments, invoices and refunds are untouched, and so
|
|
94
|
+
* is their personal data — use {@link Customers.purge} for a GDPR
|
|
95
|
+
* erasure. Refused while they hold a subscription that can still
|
|
96
|
+
* charge them.
|
|
97
|
+
*/
|
|
88
98
|
delete(id, params = {}) {
|
|
89
99
|
return this.del(`/v1/customers/${id}`, params);
|
|
90
100
|
}
|
|
@@ -124,14 +134,18 @@ var Products = class extends BaseResource {
|
|
|
124
134
|
retrieve(id) {
|
|
125
135
|
return this.get(`/v1/products/${id}`);
|
|
126
136
|
}
|
|
127
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Patch mutable Product fields, or archive it with `active: false`.
|
|
139
|
+
*
|
|
140
|
+
* Archiving is how you stop offering something. The product keeps its
|
|
141
|
+
* id and still comes back from `retrieve()` and `list()`, because what
|
|
142
|
+
* was sold under it has to stay readable, so there is no `delete()`.
|
|
143
|
+
* A checkout against any of its prices is refused from then on, and
|
|
144
|
+
* `active: true` un-archives.
|
|
145
|
+
*/
|
|
128
146
|
update(id, params) {
|
|
129
147
|
return this.post(`/v1/products/${id}`, params);
|
|
130
148
|
}
|
|
131
|
-
/** Archive a Product. */
|
|
132
|
-
delete(id, params = {}) {
|
|
133
|
-
return this.del(`/v1/products/${id}`, params);
|
|
134
|
-
}
|
|
135
149
|
list(params = {}) {
|
|
136
150
|
return this.get("/v1/products", params);
|
|
137
151
|
}
|
|
@@ -147,6 +161,29 @@ var Prices = class extends BaseResource {
|
|
|
147
161
|
retrieve(id) {
|
|
148
162
|
return this.get(`/v1/prices/${id}`);
|
|
149
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Archive a Price so it stops selling, or put it back on sale.
|
|
166
|
+
*
|
|
167
|
+
* `update(id, { active: false })` archives. The price keeps its id and
|
|
168
|
+
* is still returned by `retrieve()` and by `list()`, because
|
|
169
|
+
* subscriptions renew against it by id and what they are charged has to
|
|
170
|
+
* stay readable. Subscriptions already on it keep renewing at it. What
|
|
171
|
+
* stops is new business: a checkout session against the price is
|
|
172
|
+
* refused and it is no longer offered as a plan change.
|
|
173
|
+
*
|
|
174
|
+
* `{ active: true }` undoes that. `active` is the only field because
|
|
175
|
+
* `amount_cents`, `currency` and `interval` are fixed at creation, and
|
|
176
|
+
* since none of them move here neither direction can change what a past
|
|
177
|
+
* charge was made under. To charge something different, create a new
|
|
178
|
+
* price.
|
|
179
|
+
*
|
|
180
|
+
* Sending the value a price already has returns it unchanged and emits
|
|
181
|
+
* no second event, so a retry is safe. Archiving emits
|
|
182
|
+
* `price.archived`; putting one back emits `price.updated`.
|
|
183
|
+
*/
|
|
184
|
+
update(id, params) {
|
|
185
|
+
return this.post(`/v1/prices/${id}`, params);
|
|
186
|
+
}
|
|
150
187
|
list(params = {}) {
|
|
151
188
|
return this.get("/v1/prices", params);
|
|
152
189
|
}
|
|
@@ -178,11 +215,23 @@ var Subscriptions = class extends BaseResource {
|
|
|
178
215
|
retrieve(id) {
|
|
179
216
|
return this.get(`/v1/subscriptions/${id}`);
|
|
180
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* List subscriptions, newest first, optionally filtered.
|
|
220
|
+
*
|
|
221
|
+
* Reach for `renewal_state: "paused"` rather than `status: "paused"`
|
|
222
|
+
* to find paused subscriptions; see `SubscriptionsListParams`.
|
|
223
|
+
*/
|
|
181
224
|
list(params = {}) {
|
|
182
225
|
return this.get("/v1/subscriptions", params);
|
|
183
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Walk every page of `list()`. Filters are carried onto each page
|
|
229
|
+
* request, so a filtered walk narrows server-side instead of paging
|
|
230
|
+
* the whole history and discarding rows client-side.
|
|
231
|
+
*/
|
|
184
232
|
iter(options = {}) {
|
|
185
|
-
|
|
233
|
+
const { pageSize, ...filter } = options;
|
|
234
|
+
return paginate((p) => this.get("/v1/subscriptions", { ...filter, ...p }), { pageSize });
|
|
186
235
|
}
|
|
187
236
|
cancel(id, params = {}) {
|
|
188
237
|
return this.postEmpty(`/v1/subscriptions/${id}/cancel`, params);
|
|
@@ -223,6 +272,39 @@ var Subscriptions = class extends BaseResource {
|
|
|
223
272
|
{ idempotencyKey: params.idempotencyKey }
|
|
224
273
|
);
|
|
225
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Report consumption against a metered subscription.
|
|
277
|
+
*
|
|
278
|
+
* Only valid when the subscription's price is `usage_type:
|
|
279
|
+
* "metered"`; a licensed subscription is rejected with `400
|
|
280
|
+
* parameter_invalid`. Records accumulate until the renewal invoice
|
|
281
|
+
* rolls them up (`amount_cents × sum(quantity)`); the record's
|
|
282
|
+
* `invoice_id` stays `null` until then.
|
|
283
|
+
*
|
|
284
|
+
* Supports `Idempotency-Key` replay: retrying with the same key
|
|
285
|
+
* returns the same record instead of double-counting the usage,
|
|
286
|
+
* which is what makes at-least-once reporting pipelines safe.
|
|
287
|
+
*/
|
|
288
|
+
createUsageRecord(id, params) {
|
|
289
|
+
return this.post(`/v1/subscriptions/${id}/usage_records`, params);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* List usage records for one subscription.
|
|
293
|
+
*
|
|
294
|
+
* Pass `invoice_id: "pending"` to reconcile what has been reported
|
|
295
|
+
* but not yet billed, or a concrete invoice id to see what that
|
|
296
|
+
* invoice charged for.
|
|
297
|
+
*/
|
|
298
|
+
listUsageRecords(id, params = {}) {
|
|
299
|
+
return this.get(`/v1/subscriptions/${id}/usage_records`, params);
|
|
300
|
+
}
|
|
301
|
+
/** Walk every page of `listUsageRecords()` for one subscription. */
|
|
302
|
+
iterUsageRecords(id, options = {}) {
|
|
303
|
+
return paginate((p) => this.get(`/v1/subscriptions/${id}/usage_records`, p), {
|
|
304
|
+
pageSize: options.pageSize,
|
|
305
|
+
filters: { invoice_id: options.invoice_id }
|
|
306
|
+
});
|
|
307
|
+
}
|
|
226
308
|
};
|
|
227
309
|
var Refunds = class extends BaseResource {
|
|
228
310
|
create(params) {
|
|
@@ -256,9 +338,28 @@ var WebhookEndpoints = class extends BaseResource {
|
|
|
256
338
|
retrieve(id) {
|
|
257
339
|
return this.get(`/v1/webhook_endpoints/${id}`);
|
|
258
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Update an endpoint, or stop delivery with `status: "disabled"`.
|
|
343
|
+
*
|
|
344
|
+
* Disabling keeps the endpoint, its signing secret and its delivery
|
|
345
|
+
* history, and `status: "enabled"` resumes. Use {@link
|
|
346
|
+
* WebhookEndpoints.delete} when the endpoint should not exist at all:
|
|
347
|
+
* disabling is reversible and deleting is not.
|
|
348
|
+
*/
|
|
259
349
|
update(id, params) {
|
|
260
350
|
return this.post(`/v1/webhook_endpoints/${id}`, params);
|
|
261
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Delete an endpoint. Resolves to `{ id, object: "webhook_endpoint",
|
|
354
|
+
* deleted: true }`, not the endpoint.
|
|
355
|
+
*
|
|
356
|
+
* A URL registered by mistake should not be a permanent fixture of the
|
|
357
|
+
* account, so this removes it: `retrieve()` 404s afterwards and it is
|
|
358
|
+
* gone from `list()`. Its delivery attempts go with it, because they
|
|
359
|
+
* are readable only through the endpoint that owns them. The events
|
|
360
|
+
* themselves are untouched and still in `client.events`, so what you
|
|
361
|
+
* were sent stays on record.
|
|
362
|
+
*/
|
|
262
363
|
delete(id, params = {}) {
|
|
263
364
|
return this.del(`/v1/webhook_endpoints/${id}`, params);
|
|
264
365
|
}
|
|
@@ -367,12 +468,17 @@ var Coupons = class extends BaseResource {
|
|
|
367
468
|
retrieve(id) {
|
|
368
469
|
return this.get(`/v1/coupons/${id}`);
|
|
369
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Update a coupon's limits, or withdraw it with `active: false`.
|
|
473
|
+
*
|
|
474
|
+
* A withdrawn code is refused at checkout while the coupon stays
|
|
475
|
+
* readable and discounts already applied keep working out, so there is
|
|
476
|
+
* no `delete()`: a coupon that has been redeemed is part of what a
|
|
477
|
+
* customer was charged. `active: true` brings the campaign back.
|
|
478
|
+
*/
|
|
370
479
|
update(id, params) {
|
|
371
480
|
return this.post(`/v1/coupons/${id}`, params);
|
|
372
481
|
}
|
|
373
|
-
delete(id, params = {}) {
|
|
374
|
-
return this.del(`/v1/coupons/${id}`, params);
|
|
375
|
-
}
|
|
376
482
|
/**
|
|
377
483
|
* Server-side dry-run of a coupon redemption.
|
|
378
484
|
*
|
|
@@ -399,12 +505,17 @@ var TaxRates = class extends BaseResource {
|
|
|
399
505
|
retrieve(id) {
|
|
400
506
|
return this.get(`/v1/tax_rates/${id}`);
|
|
401
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Correct a rate, retire it with `active: false`, or bring one back.
|
|
510
|
+
*
|
|
511
|
+
* Retiring is how you stop charging VAT in a country. The rate stays
|
|
512
|
+
* readable, because an invoice records the percentage it charged and
|
|
513
|
+
* you have to be able to point at the rate that produced it, so there
|
|
514
|
+
* is no `delete()`.
|
|
515
|
+
*/
|
|
402
516
|
update(id, params) {
|
|
403
517
|
return this.post(`/v1/tax_rates/${id}`, params);
|
|
404
518
|
}
|
|
405
|
-
delete(id, params = {}) {
|
|
406
|
-
return this.del(`/v1/tax_rates/${id}`, params);
|
|
407
|
-
}
|
|
408
519
|
list(params = {}) {
|
|
409
520
|
return this.get("/v1/tax_rates", params);
|
|
410
521
|
}
|
|
@@ -416,6 +527,27 @@ var Invoices = class extends BaseResource {
|
|
|
416
527
|
retrieve(id) {
|
|
417
528
|
return this.get(`/v1/invoices/${id}`);
|
|
418
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Download the rendered invoice PDF as raw bytes.
|
|
532
|
+
*
|
|
533
|
+
* ```ts
|
|
534
|
+
* const pdf = await client.invoices.retrievePdf("inv_123");
|
|
535
|
+
* await writeFile("invoice.pdf", Buffer.from(pdf));
|
|
536
|
+
* ```
|
|
537
|
+
*
|
|
538
|
+
* Blob-backed deployments stream the bytes inline; S3-backed ones
|
|
539
|
+
* answer `302` to a presigned URL, which `fetch` follows for us under
|
|
540
|
+
* the SDK's own timeout and retry policy — so both storage adapters
|
|
541
|
+
* look identical from here.
|
|
542
|
+
*
|
|
543
|
+
* Deployments with `INVOICE_PDF_ENABLED=false` never render one and
|
|
544
|
+
* answer `501 rendering_pending`, which surfaces as a `ServerError`
|
|
545
|
+
* whose `code` is `"rendering_pending"`; `retrieve()` still returns the
|
|
546
|
+
* structured invoice for tenants who render their own.
|
|
547
|
+
*/
|
|
548
|
+
retrievePdf(id) {
|
|
549
|
+
return this.t.requestBinary({ method: "GET", path: `/v1/invoices/${id}/pdf` });
|
|
550
|
+
}
|
|
419
551
|
list(params = {}) {
|
|
420
552
|
return this.get("/v1/invoices", params);
|
|
421
553
|
}
|
|
@@ -521,19 +653,6 @@ var RateLimitError = class extends BillKitError {
|
|
|
521
653
|
this.retryAfter = options.retryAfter;
|
|
522
654
|
}
|
|
523
655
|
};
|
|
524
|
-
var TYPE_TO_CLASS = {
|
|
525
|
-
api_connection_error: APIConnectionError,
|
|
526
|
-
// ``api_error`` is the Stripe-convention type for 5xx, so surface it as
|
|
527
|
-
// ServerError (a subclass of APIError) so `catch (e instanceof
|
|
528
|
-
// ServerError)` works without false negatives.
|
|
529
|
-
api_error: ServerError,
|
|
530
|
-
authentication_error: AuthenticationError,
|
|
531
|
-
permission_error: PermissionError,
|
|
532
|
-
invalid_request_error: InvalidRequestError,
|
|
533
|
-
idempotency_error: ConflictError,
|
|
534
|
-
conflict: ConflictError,
|
|
535
|
-
rate_limit_error: RateLimitError
|
|
536
|
-
};
|
|
537
656
|
function fallbackType(status) {
|
|
538
657
|
if (status === 401) return "authentication_error";
|
|
539
658
|
if (status === 403) return "permission_error";
|
|
@@ -543,13 +662,13 @@ function fallbackType(status) {
|
|
|
543
662
|
if (status >= 500) return "api_error";
|
|
544
663
|
return "invalid_request_error";
|
|
545
664
|
}
|
|
546
|
-
function
|
|
665
|
+
function classForStatus(status) {
|
|
666
|
+
if (status >= 500) return ServerError;
|
|
547
667
|
if (status === 401) return AuthenticationError;
|
|
548
668
|
if (status === 403) return PermissionError;
|
|
549
669
|
if (status === 404) return ResourceMissingError;
|
|
550
670
|
if (status === 409) return ConflictError;
|
|
551
671
|
if (status === 429) return RateLimitError;
|
|
552
|
-
if (status >= 500) return ServerError;
|
|
553
672
|
return InvalidRequestError;
|
|
554
673
|
}
|
|
555
674
|
function errorFromResponse(args) {
|
|
@@ -557,9 +676,7 @@ function errorFromResponse(args) {
|
|
|
557
676
|
const envelope = typeof body === "object" && body !== null && "error" in body ? body.error ?? {} : {};
|
|
558
677
|
const type = envelope.type ?? fallbackType(status);
|
|
559
678
|
const message = envelope.message ?? `BillKit API returned HTTP ${status} with no error body.`;
|
|
560
|
-
|
|
561
|
-
if (status === 404 && cls === InvalidRequestError) cls = ResourceMissingError;
|
|
562
|
-
if (status === 409 && cls === InvalidRequestError) cls = ConflictError;
|
|
679
|
+
const cls = classForStatus(status);
|
|
563
680
|
const options = {
|
|
564
681
|
type,
|
|
565
682
|
code: envelope.code,
|
|
@@ -598,9 +715,11 @@ function backoffForMs(attempt, policy) {
|
|
|
598
715
|
const jittered = capped + (Math.random() * 2 - 1) * jitterRange;
|
|
599
716
|
return Math.max(0, jittered);
|
|
600
717
|
}
|
|
601
|
-
|
|
718
|
+
var IN_PROGRESS_CODE = "idempotency_in_progress";
|
|
719
|
+
function shouldRetry(status, attempt, policy, retryAfterMs, errorCode) {
|
|
602
720
|
if (attempt >= policy.maxAttempts) return false;
|
|
603
721
|
if (status === null) return true;
|
|
722
|
+
if (status === 409) return errorCode === IN_PROGRESS_CODE;
|
|
604
723
|
if (status === 429) {
|
|
605
724
|
if (retryAfterMs === void 0 || retryAfterMs < 0) return false;
|
|
606
725
|
return policy.maxRetryAfterMs === void 0 || retryAfterMs <= policy.maxRetryAfterMs;
|
|
@@ -612,7 +731,7 @@ function sleep(ms) {
|
|
|
612
731
|
}
|
|
613
732
|
|
|
614
733
|
// src/version.ts
|
|
615
|
-
var VERSION = "0.
|
|
734
|
+
var VERSION = "0.2.0";
|
|
616
735
|
|
|
617
736
|
// src/transport.ts
|
|
618
737
|
var DEFAULT_BASE_URL = "https://api.billkit.eu";
|
|
@@ -662,8 +781,7 @@ function buildHeaders(apiKey, hasBody, idempotencyKey, extra) {
|
|
|
662
781
|
}
|
|
663
782
|
return headers;
|
|
664
783
|
}
|
|
665
|
-
|
|
666
|
-
const text = await response.text();
|
|
784
|
+
function parseJsonText(text) {
|
|
667
785
|
if (!text) return null;
|
|
668
786
|
try {
|
|
669
787
|
return JSON.parse(text);
|
|
@@ -671,6 +789,17 @@ async function parseJson(response) {
|
|
|
671
789
|
return null;
|
|
672
790
|
}
|
|
673
791
|
}
|
|
792
|
+
async function parseJson(response) {
|
|
793
|
+
return parseJsonText(await response.text());
|
|
794
|
+
}
|
|
795
|
+
async function readBody(response, responseType) {
|
|
796
|
+
if (responseType !== "binary") {
|
|
797
|
+
return { parsed: await parseJson(response), binary: void 0 };
|
|
798
|
+
}
|
|
799
|
+
const buffer = await response.arrayBuffer();
|
|
800
|
+
if (response.ok) return { parsed: null, binary: buffer };
|
|
801
|
+
return { parsed: parseJsonText(new TextDecoder().decode(buffer)), binary: void 0 };
|
|
802
|
+
}
|
|
674
803
|
function parseRetryAfterMs(header) {
|
|
675
804
|
if (!header) return void 0;
|
|
676
805
|
const n = Number.parseFloat(header);
|
|
@@ -714,7 +843,21 @@ var Transport = class {
|
|
|
714
843
|
}
|
|
715
844
|
this.fetchFn = fetchFn.bind(globalThis);
|
|
716
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Fetch a binary document (currently only the invoice PDF).
|
|
848
|
+
*
|
|
849
|
+
* Same retry policy, same timeout, same typed errors as
|
|
850
|
+
* {@link Transport.request}; only the success-path decoding differs.
|
|
851
|
+
* `fetch` follows the storage adapter's `302` to the signed URL by
|
|
852
|
+
* itself, and the WHATWG spec drops the `Authorization` header on that
|
|
853
|
+
* cross-origin hop — which is correct, since a presigned URL carries
|
|
854
|
+
* its own credential and must not be handed BillKit's API key.
|
|
855
|
+
*/
|
|
856
|
+
requestBinary(options) {
|
|
857
|
+
return this.request({ ...options, responseType: "binary" });
|
|
858
|
+
}
|
|
717
859
|
async request(options) {
|
|
860
|
+
const responseType = options.responseType ?? "json";
|
|
718
861
|
const idempotencyKey = autoIdempotencyKey(options.method, options.idempotencyKey);
|
|
719
862
|
const url = buildUrl(this.baseUrl, options.path, options.query);
|
|
720
863
|
const headers = buildHeaders(
|
|
@@ -736,6 +879,7 @@ var Transport = class {
|
|
|
736
879
|
const startedAt = Date.now();
|
|
737
880
|
let response;
|
|
738
881
|
let parsedBody;
|
|
882
|
+
let binaryBody;
|
|
739
883
|
try {
|
|
740
884
|
const init = {
|
|
741
885
|
method: options.method,
|
|
@@ -744,7 +888,7 @@ var Transport = class {
|
|
|
744
888
|
};
|
|
745
889
|
if (body !== void 0) init.body = body;
|
|
746
890
|
response = await this.fetchFn(url, init);
|
|
747
|
-
parsedBody = await
|
|
891
|
+
({ parsed: parsedBody, binary: binaryBody } = await readBody(response, responseType));
|
|
748
892
|
} catch (err) {
|
|
749
893
|
lastError = connectionError(err, this.timeoutMs);
|
|
750
894
|
if (!shouldRetry(null, attempt, this.retryPolicy)) throw lastError;
|
|
@@ -768,6 +912,7 @@ var Transport = class {
|
|
|
768
912
|
requestId: requestId ?? null
|
|
769
913
|
});
|
|
770
914
|
if (response.ok) {
|
|
915
|
+
if (responseType === "binary") return binaryBody;
|
|
771
916
|
return parsedBody ?? void 0;
|
|
772
917
|
}
|
|
773
918
|
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
@@ -777,7 +922,7 @@ var Transport = class {
|
|
|
777
922
|
requestId,
|
|
778
923
|
retryAfter: retryAfterMs === void 0 ? void 0 : retryAfterMs / 1e3
|
|
779
924
|
});
|
|
780
|
-
if (!shouldRetry(response.status, attempt, this.retryPolicy, retryAfterMs)) {
|
|
925
|
+
if (!shouldRetry(response.status, attempt, this.retryPolicy, retryAfterMs, error.code)) {
|
|
781
926
|
throw error;
|
|
782
927
|
}
|
|
783
928
|
lastError = error;
|