@billkit-eu/sdk 0.2.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@billkit-eu/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Official Node.js SDK for BillKit: a Stripe-Billing-shape multi-tenant SaaS API on Mollie.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/src/index.ts CHANGED
@@ -84,6 +84,7 @@ export type {
84
84
  EventsListParams,
85
85
  IdempotencyOptions,
86
86
  ListParams,
87
+ PriceTier,
87
88
  PricesListParams,
88
89
  RotateProviderCredentialParams,
89
90
  SetPortalBrandingParams,
package/src/resources.ts CHANGED
@@ -109,6 +109,15 @@ export interface UpdateCustomerParams extends IdempotencyOptions {
109
109
  metadata?: Record<string, string>;
110
110
  }
111
111
 
112
+ /** Query parameters accepted by `GET /v1/customers`. */
113
+ export interface CustomerListParams extends BaseListParams {
114
+ /**
115
+ * `false` for customers who have paid, `true` for abandoned checkouts,
116
+ * omitted for both.
117
+ */
118
+ provisional?: boolean;
119
+ }
120
+
112
121
  /**
113
122
  * Body for `POST /v1/customers/{id}/vat_number`. The VAT number is
114
123
  * sent through VIES server-side; the response carries
@@ -138,6 +147,16 @@ export interface CreateProductParams extends IdempotencyOptions {
138
147
  marketing_features?: string[];
139
148
  /** Small string metadata map echoed back on the Product object. */
140
149
  metadata?: Record<string, string>;
150
+ /**
151
+ * Let a *buyer* type a coupon code at the embedded checkout for this
152
+ * product. Defaults to `false`. A coupon you apply yourself by passing
153
+ * `coupon_code` when you create a Checkout Session is unaffected — that
154
+ * is you discounting your own sale, and it has never needed this flag.
155
+ *
156
+ * The code is redeemed only once the payment settles, so a shopper who
157
+ * tries a single-use code and abandons the checkout does not use it up.
158
+ */
159
+ allow_promotion_codes?: boolean;
141
160
  }
142
161
 
143
162
  export interface UpdateProductParams extends IdempotencyOptions {
@@ -147,6 +166,8 @@ export interface UpdateProductParams extends IdempotencyOptions {
147
166
  metadata?: Record<string, string>;
148
167
  /** Set false to stop selling a product without deleting history. */
149
168
  active?: boolean;
169
+ /** See {@link CreateProductParams.allow_promotion_codes}. */
170
+ allow_promotion_codes?: boolean;
150
171
  }
151
172
 
152
173
  /**
@@ -159,16 +180,94 @@ export interface UpdatePriceParams extends IdempotencyOptions {
159
180
  active: boolean;
160
181
  }
161
182
 
183
+ /**
184
+ * One band of a tiered price.
185
+ *
186
+ * `up_to` is inclusive, and the **last band must be `"inf"`** because a
187
+ * bounded top band cannot price the usage above it. Bands must strictly
188
+ * increase.
189
+ *
190
+ * A band names a unit rate (`unit_amount` in whole minor units, or
191
+ * `unit_amount_decimal` for a finer one), a `flat_amount` charged once for
192
+ * reaching the band, or both. Write a free band as `unit_amount: 0` rather
193
+ * than by omitting the rate, so "free" is something the price says instead
194
+ * of something it forgot.
195
+ */
196
+ export interface PriceTier {
197
+ up_to: number | "inf";
198
+ /** Whole minor units per unit in this band. */
199
+ unit_amount?: number;
200
+ /**
201
+ * A rate finer than one minor unit, **as a string** — see
202
+ * {@link CreatePriceParams.unit_amount_decimal} for why it is never a
203
+ * `number`.
204
+ */
205
+ unit_amount_decimal?: string;
206
+ /** Charged once when the usage reaches this band. Whole minor units. */
207
+ flat_amount?: number;
208
+ }
209
+
162
210
  export interface CreatePriceParams extends IdempotencyOptions {
163
211
  /** Existing Product id returned from `client.products.create`. */
164
212
  product_id: string;
165
- amount_cents: number;
213
+ /**
214
+ * Whole minor units per period (licensed) or per unit (metered).
215
+ *
216
+ * Optional because a metered price can be priced by
217
+ * {@link CreatePriceParams.unit_amount_decimal} or by
218
+ * {@link CreatePriceParams.tiers} instead. Exactly one of the three; a
219
+ * price with none of them is refused server-side.
220
+ */
221
+ amount_cents?: number;
222
+ /**
223
+ * A per-unit rate smaller than one minor unit, in **minor units**, to 12
224
+ * decimal places. `"0.02"` is 0.02 cents, i.e. EUR 0.0002 per unit, which
225
+ * is the canonical per-API-call price and not expressible as an integer.
226
+ * Metered prices only.
227
+ *
228
+ * **It is a `string`, and that is load-bearing.** A JS `number` is an
229
+ * IEEE-754 double and cannot hold 0.0002 exactly, so the rate would be
230
+ * corrupted before it was ever multiplied by a quantity. The type forbids
231
+ * a number at compile time, and the SDK throws a `TypeError` if an
232
+ * untyped JavaScript caller passes one anyway.
233
+ *
234
+ * The period's whole quantity is multiplied by the rate and rounded
235
+ * **once**, at the invoice.
236
+ */
237
+ unit_amount_decimal?: string;
238
+ /**
239
+ * `"per_unit"` (the default) multiplies one rate by the quantity.
240
+ * `"tiered"` prices by bands and requires
241
+ * {@link CreatePriceParams.tiers} and
242
+ * {@link CreatePriceParams.tiers_mode}. Metered prices only.
243
+ */
244
+ billing_scheme?: "per_unit" | "tiered";
245
+ /**
246
+ * How a tier table is read, and there is **no default** because the same
247
+ * table means two different bills. `"graduated"` prices the units inside
248
+ * each band; `"volume"` lets the period total pick one band which then
249
+ * prices every unit. 1,500 units against "first 1,000 at EUR 0.01, then
250
+ * EUR 0.005" is EUR 12.50 graduated and EUR 7.50 by volume.
251
+ */
252
+ tiers_mode?: "graduated" | "volume";
253
+ /** The band table. Required when `billing_scheme` is `"tiered"`, refused otherwise. */
254
+ tiers?: PriceTier[];
166
255
  currency: string;
167
256
  interval: "month" | "year" | (string & {});
168
257
  metadata?: Record<string, string>;
169
258
  trial_days?: number;
170
259
  trial_verification_cents?: number;
171
- payment_methods?: Array<"creditcard" | "directdebit" | (string & {})>;
260
+ payment_methods?: Array<"creditcard" | "directdebit" | "ideal" | "applepay" | (string & {})>;
261
+ /**
262
+ * What a cancellation refunds without being asked. `"none"` (the default)
263
+ * nothing; `"full"` the whole last charge; `"prorated"` the unused part
264
+ * of the current period. Both non-none modes also end access
265
+ * immediately, and both stay bounded by the refund window below.
266
+ *
267
+ * Metered prices must leave this at `"none"`: ending access mid-period
268
+ * would strand usage that has not been billed yet.
269
+ */
270
+ refund_on_cancel?: "none" | "full" | "prorated";
172
271
  /**
173
272
  * Per-Price refund-window override (`POST /v1/prices`). `undefined`
174
273
  * inherits the default policy table (7d / 30d initial, 3d renewal);
@@ -189,12 +288,15 @@ export interface CreatePriceParams extends IdempotencyOptions {
189
288
  tax_behavior?: "inclusive" | "exclusive" | "unspecified";
190
289
  /**
191
290
  * `"licensed"` (the default when omitted) bills `amount_cents` per
192
- * period regardless of consumption. `"metered"` bills
193
- * `amount_cents` **per reported unit**: post consumption with
194
- * `subscriptions.createUsageRecord` and the renewal invoice charges
195
- * `amount_cents × sum(quantity)` for the period. Metered prices
196
- * must be `interval: "month"`, carry `amount_cents > 0`, and cannot
197
- * have `trial_days`.
291
+ * period regardless of consumption. `"metered"` bills **per reported
292
+ * unit**: post consumption with `subscriptions.createUsageRecord`, and
293
+ * at each period close BillKit invoices the period's total and charges
294
+ * the stored mandate.
295
+ *
296
+ * A metered unit is priced by `amount_cents`, by `unit_amount_decimal`,
297
+ * or by `tiers` — exactly one. Metered prices must be
298
+ * `interval: "month"`, cannot have `trial_days`, and cannot set
299
+ * `refund_on_cancel`.
198
300
  */
199
301
  usage_type?: "licensed" | "metered";
200
302
  }
@@ -213,6 +315,22 @@ export interface CreateUsageRecordParams extends IdempotencyOptions {
213
315
  * the record must land in the period the usage occurred.
214
316
  */
215
317
  occurred_at?: number;
318
+ /**
319
+ * Your own id for the event being metered, unique within this
320
+ * subscription. This is the dedupe an `Idempotency-Key` cannot do.
321
+ *
322
+ * The key covers a retry of *one HTTP request*, including the SDK's own
323
+ * internal retries. `identifier` covers a retry of *your* call — a job
324
+ * runner replaying a task, a queue delivering twice, your code
325
+ * re-invoking after its own timeout — which arrives at the API as a
326
+ * genuinely new request with a new key. A second report of the same
327
+ * identifier returns the first record unchanged instead of billing
328
+ * twice.
329
+ *
330
+ * If your reporting pipeline is at-least-once, this is the one that
331
+ * matters.
332
+ */
333
+ identifier?: string;
216
334
  /** Small string metadata map echoed back on the record. */
217
335
  metadata?: Record<string, string>;
218
336
  }
@@ -254,7 +372,7 @@ export interface CreateCheckoutSessionParams extends IdempotencyOptions {
254
372
  * the customer's available methods; when set, must be in the price's
255
373
  * `payment_methods` allowlist.
256
374
  */
257
- method?: "creditcard" | "directdebit" | (string & {});
375
+ method?: "creditcard" | "directdebit" | "ideal" | "applepay" | (string & {});
258
376
  /** Optional coupon code applied at checkout; atomically claimed. */
259
377
  coupon_code?: string;
260
378
  /**
@@ -321,7 +439,14 @@ export interface CreateOneShotPaymentParams extends IdempotencyOptions {
321
439
  * *request* type the server validates, so an SDK that lags a newly-added
322
440
  * method should not be the thing that blocks the call.
323
441
  */
324
- method: "creditcard" | "directdebit" | "ideal" | "bancontact" | "eps" | (string & {});
442
+ method:
443
+ | "creditcard"
444
+ | "directdebit"
445
+ | "ideal"
446
+ | "bancontact"
447
+ | "eps"
448
+ | "applepay"
449
+ | (string & {});
325
450
  /** Where Mollie returns the payer after the hosted checkout. */
326
451
  success_url: string;
327
452
  /** Optional page for an abandoned/cancelled payment. */
@@ -468,6 +593,40 @@ function splitIdempotency<P extends IdempotencyOptions>(
468
593
  return { body: dropUndefined(rest as Record<string, unknown>), idempotencyKey };
469
594
  }
470
595
 
596
+ /**
597
+ * Refuse a sub-minor-unit rate that arrived as a `number`.
598
+ *
599
+ * The type already forbids it, so this exists for the callers the type
600
+ * system cannot reach: plain JavaScript, a value that came through `any`,
601
+ * a body parsed from JSON. A double cannot hold 0.0002 exactly, so
602
+ * accepting one would work for the rates that happen to round-trip and
603
+ * silently mis-price the ones that do not — the worst of the three
604
+ * available behaviours, and the reason the field is a string in the first
605
+ * place.
606
+ */
607
+ function assertDecimalRateIsString(value: unknown, field: string): void {
608
+ if (value === undefined || value === null || typeof value === "string") return;
609
+ throw new TypeError(
610
+ `${field} must be a string, not a ${typeof value}. A JavaScript number cannot ` +
611
+ "hold a rate like 0.0002 exactly, so it would be corrupted before it was ever " +
612
+ `multiplied by a quantity. Pass it as a string: "${String(value)}".`,
613
+ );
614
+ }
615
+
616
+ /** Same check at the price level and inside every band of a tier table. */
617
+ function assertPriceRatesAreStrings(params: CreatePriceParams): CreatePriceParams {
618
+ assertDecimalRateIsString(params.unit_amount_decimal, "unit_amount_decimal");
619
+ // Inside a tier is where a rate is most likely to be typed as a bare
620
+ // literal, so the guard has to reach in there too.
621
+ (params.tiers ?? []).forEach((tier, index) => {
622
+ assertDecimalRateIsString(
623
+ (tier as PriceTier | undefined)?.unit_amount_decimal,
624
+ `tiers[${index}].unit_amount_decimal`,
625
+ );
626
+ });
627
+ return params;
628
+ }
629
+
471
630
  /**
472
631
  * Shared transport wrapper. Resources subclass this so each method
473
632
  * reads as a single line, "verb to path with params", instead of
@@ -550,7 +709,17 @@ export class Customers extends BaseResource {
550
709
  return this.del<T>(`/v1/customers/${id}`, params);
551
710
  }
552
711
 
553
- list<T = unknown>(params: BaseListParams = {}): Promise<ListResponseEnvelope<T>> {
712
+ /**
713
+ * List customers, newest first.
714
+ *
715
+ * `provisional` filters on whether the customer ever completed a
716
+ * payment. A checkout that captures an email commits its Customer
717
+ * before the charge, so a checkout nobody finished leaves a row behind:
718
+ * pass `false` for real customers only, `true` for the abandoned ones
719
+ * (the cart-recovery worklist), or omit for both. Abandoned rows are
720
+ * swept after the tenant's retention window.
721
+ */
722
+ list<T = unknown>(params: CustomerListParams = {}): Promise<ListResponseEnvelope<T>> {
554
723
  return this.get<ListResponseEnvelope<T>>("/v1/customers", params);
555
724
  }
556
725
 
@@ -615,9 +784,23 @@ export class Products extends BaseResource {
615
784
  }
616
785
 
617
786
  export class Prices extends BaseResource {
618
- /** Create immutable billing terms for an existing Product. */
619
- create<T = unknown>(params: CreatePriceParams): Promise<T> {
620
- return this.post<T, CreatePriceParams>("/v1/prices", params);
787
+ /**
788
+ * Create immutable billing terms for an existing Product.
789
+ *
790
+ * A licensed price sends `amount_cents`. A metered price sends one of
791
+ * `amount_cents`, `unit_amount_decimal` (a rate finer than one minor
792
+ * unit, as a string) or `billing_scheme: "tiered"` with `tiers` and
793
+ * `tiers_mode`. Throws `TypeError` before any HTTP call if a decimal
794
+ * rate arrives as a number — see
795
+ * {@link CreatePriceParams.unit_amount_decimal}.
796
+ */
797
+ // `async` on purpose. The rate guard throws, and a synchronous throw out
798
+ // of a method typed `Promise<T>` escapes `.catch()` entirely — the caller
799
+ // would have to wrap the call site in try/catch as well, which nobody
800
+ // does for a promise-returning API. Marking it async turns the throw into
801
+ // a rejection, so one error path handles both.
802
+ async create<T = unknown>(params: CreatePriceParams): Promise<T> {
803
+ return this.post<T, CreatePriceParams>("/v1/prices", assertPriceRatesAreStrings(params));
621
804
  }
622
805
 
623
806
  retrieve<T = unknown>(id: string): Promise<T> {
@@ -783,13 +966,16 @@ export class Subscriptions extends BaseResource {
783
966
  *
784
967
  * Only valid when the subscription's price is `usage_type:
785
968
  * "metered"`; a licensed subscription is rejected with `400
786
- * parameter_invalid`. Records accumulate until the renewal invoice
787
- * rolls them up (`amount_cents × sum(quantity)`); the record's
788
- * `invoice_id` stays `null` until then.
969
+ * parameter_invalid`. Records accumulate until the next period close
970
+ * rolls them into one invoice line; the record's `invoice_id` stays
971
+ * `null` until then. Records are immutable once written — they are the
972
+ * audit trail behind that line — so there is no update or delete.
789
973
  *
790
- * Supports `Idempotency-Key` replay: retrying with the same key
791
- * returns the same record instead of double-counting the usage,
792
- * which is what makes at-least-once reporting pipelines safe.
974
+ * Two dedupe mechanisms, covering different failures. The
975
+ * `Idempotency-Key` the SDK sends covers a retry of this HTTP request,
976
+ * including its own internal retries. `params.identifier` covers a
977
+ * retry of *your* call, which arrives as a new request with a new key.
978
+ * See {@link CreateUsageRecordParams.identifier}.
793
979
  */
794
980
  createUsageRecord<T = unknown>(id: string, params: CreateUsageRecordParams): Promise<T> {
795
981
  return this.post<T, CreateUsageRecordParams>(`/v1/subscriptions/${id}/usage_records`, params);
@@ -819,6 +1005,28 @@ export class Subscriptions extends BaseResource {
819
1005
  filters: { invoice_id: options.invoice_id },
820
1006
  });
821
1007
  }
1008
+
1009
+ /**
1010
+ * Price the pending usage, before the period close bills it.
1011
+ *
1012
+ * `listUsageRecords({ invoice_id: "pending" })` gives the quantity; this
1013
+ * gives the money. `net_cents` / `tax_cents` / `gross_cents` are
1014
+ * computed through the same rate or tier table and the same VAT
1015
+ * resolution the close itself uses, so it is a forecast of the real
1016
+ * invoice rather than an estimate.
1017
+ *
1018
+ * **Read `will_charge` before promising a customer an amount.** A period
1019
+ * whose total is under `minimum_charge_cents` (EUR 1.00) is not charged,
1020
+ * because the payment provider would refuse it. The usage is not lost:
1021
+ * it stays pending and rolls into the next period, which is then billed
1022
+ * for both.
1023
+ *
1024
+ * `open_invoice_id` names an earlier cycle that is invoiced and still
1025
+ * unsettled; while one is open, this period cannot be charged.
1026
+ */
1027
+ retrieveUsageSummary<T = unknown>(id: string): Promise<T> {
1028
+ return this.get<T>(`/v1/subscriptions/${id}/usage_summary`);
1029
+ }
822
1030
  }
823
1031
 
824
1032
  export class Refunds extends BaseResource {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.2.1";
1
+ export const VERSION = "0.3.0";