@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/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
- /** Patch mutable Product fields. */
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
- return paginate((p) => this.get("/v1/subscriptions", p), { pageSize: options.pageSize });
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 fallbackClass(status) {
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
- let cls = TYPE_TO_CLASS[type] ?? fallbackClass(status);
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
- function shouldRetry(status, attempt, policy, retryAfterMs) {
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.1.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
- async function parseJson(response) {
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 parseJson(response);
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;