@billkit-eu/sdk 0.2.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/CHANGELOG.md +61 -2
- package/README.md +71 -4
- package/dist/index.cjs +75 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -21
- package/dist/index.d.ts +178 -21
- package/dist/index.js +75 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -2
- package/src/resources.ts +229 -21
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { BillKit } from "@billkit-eu/sdk";
|
|
8
8
|
*
|
|
9
|
-
* const client = new BillKit({ apiKey: "
|
|
9
|
+
* const client = new BillKit({ apiKey: "bk_test_..." });
|
|
10
10
|
* const customer = await client.customers.create<{ id: string }>({
|
|
11
11
|
* email: "ada@example.com",
|
|
12
12
|
* });
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* a logger to opt in:
|
|
46
46
|
*
|
|
47
47
|
* ```ts
|
|
48
|
-
* const client = new BillKit({ apiKey: "
|
|
48
|
+
* const client = new BillKit({ apiKey: "bk_test_...", logger: console });
|
|
49
49
|
* ```
|
|
50
50
|
*
|
|
51
51
|
* See {@link BillKitLogger} for what is logged and what is withheld
|
|
@@ -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
|
-
|
|
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
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
787
|
-
* rolls them
|
|
788
|
-
* `
|
|
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
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
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 {
|
|
@@ -898,7 +1106,7 @@ export class WebhookEndpoints extends BaseResource {
|
|
|
898
1106
|
return this.del<T>(`/v1/webhook_endpoints/${id}`, params);
|
|
899
1107
|
}
|
|
900
1108
|
|
|
901
|
-
/** Rotate the signing secret. The new `
|
|
1109
|
+
/** Rotate the signing secret. The new `bkwhsec_...` is returned once. */
|
|
902
1110
|
rotateSecret<T = unknown>(id: string, params: IdempotencyOptions = {}): Promise<T> {
|
|
903
1111
|
return this.postEmpty<T>(`/v1/webhook_endpoints/${id}/rotate_secret`, params);
|
|
904
1112
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.3.0";
|