@basaltkit/subscriptions 1.2.0 → 2.0.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.d.ts CHANGED
@@ -19,7 +19,12 @@ declare function meter(limit: number): Meter;
19
19
  type FeatureValue = boolean | number | Meter;
20
20
  type BillingPeriod = 'monthly' | 'yearly';
21
21
  interface PlanDefinition {
22
- /** 0 = free · number = same price both periods · object = per period · 'custom' = sales-led */
22
+ /**
23
+ * Price in the currency's minor unit (integer; cents — `2900` = $29.00).
24
+ * 0 = free · number = same price both periods · object = per period ·
25
+ * 'custom' = sales-led. Only gated (`> 0`) and displayed here; the actual
26
+ * Stripe charge uses a pre-created price id on the gateway.
27
+ */
23
28
  price: number | {
24
29
  monthly: number;
25
30
  yearly: number;
@@ -268,12 +273,16 @@ declare class FakeBillingGateway implements BillingGateway {
268
273
  * `BillingGateway` (card subscriptions). Recurring billing is modelled by
269
274
  * creating one payment per period (invoice → reference → webhook confirms → the
270
275
  * period is activated).
276
+ *
277
+ * **All amounts are integers in the currency's minor unit** (cents; `100 = 1.00`)
278
+ * — the Stripe convention. Use the `money` helpers (`toMinor`/`formatMoney`) at
279
+ * the human boundary. Drivers translate to each provider's expected format.
271
280
  */
272
281
  /** A one-off payment request handed to a `PaymentGateway`. */
273
282
  interface PaymentRequest {
274
283
  /** Who is paying — a tenant/user/customer id you reconcile against. */
275
284
  billableId: string;
276
- /** Amount in the currency's major unit (e.g. 5000 = 5000,00 Kz). */
285
+ /** Amount in the currency's minor unit (integer; `500000` = 5.000,00 Kz). */
277
286
  amount: number;
278
287
  /** ISO 4217. Defaults to the gateway's own (AOA for Angolan gateways). */
279
288
  currency?: string;
@@ -464,7 +473,7 @@ interface RecurringSubscription {
464
473
  /** The customer/tenant this subscription bills. One per billableId. */
465
474
  billableId: string;
466
475
  plan: string;
467
- /** Price per period, in the currency's major unit. */
476
+ /** Price per period, in the currency's minor unit (integer; cents). */
468
477
  amount: number;
469
478
  interval: RecurringInterval;
470
479
  status: RecurringStatus;
@@ -558,6 +567,38 @@ declare class RecurringReferenceBilling {
558
567
  cancel(billableId: string): Promise<void>;
559
568
  }
560
569
 
570
+ /**
571
+ * Money handling for the subscriptions ecosystem. **All amounts in the public
572
+ * API are integers in the currency's minor unit** — cents, `100 = 1.00`. This
573
+ * is the Stripe/Adyen convention: exact, no floating-point rounding, and no
574
+ * ambiguity about units. Use these helpers to convert at the human boundary
575
+ * (input forms, display) and to validate.
576
+ *
577
+ * ```ts
578
+ * toMinor(5000, 'AOA') // 500000 (5.000,00 Kz)
579
+ * toMajor(500000, 'AOA') // 5000
580
+ * formatMoney(500000, 'AOA', 'pt-AO') // "5.000,00 AOA"
581
+ * assertMinorUnits(2999) // ok ($29.99)
582
+ * assertMinorUnits(29.99) // throws
583
+ * ```
584
+ */
585
+ /** Number of minor-unit decimal places for a currency (default 2). */
586
+ declare function currencyDecimals(currency: string): number;
587
+ /** Convert a major-unit amount (e.g. `5000` Kz) to minor units (`500000`). */
588
+ declare function toMinor(major: number, currency: string): number;
589
+ /** Convert minor units (`500000`) back to a major-unit amount (`5000` Kz). */
590
+ declare function toMajor(minor: number, currency: string): number;
591
+ /** Format a minor-unit amount for display. Falls back to a plain string if Intl lacks the currency. */
592
+ declare function formatMoney(minor: number, currency: string, locale?: string): string;
593
+ /** True when `amount` is a valid minor-unit value (a non-negative integer). */
594
+ declare function isMinorUnits(amount: number): boolean;
595
+ /**
596
+ * Throw unless `amount` is a valid minor-unit value. Drivers call this so a
597
+ * major-unit slip (e.g. `29.99` instead of `2999`) fails fast instead of
598
+ * silently under/over-charging.
599
+ */
600
+ declare function assertMinorUnits(amount: number, label?: string): void;
601
+
561
602
  declare class StripeRequestError extends BasaltError {
562
603
  readonly httpStatus: number;
563
604
  constructor(httpStatus: number, message: string);
@@ -770,4 +811,4 @@ declare function billingRoutes(options: BillingRoutesOptions): BasaltRoute[];
770
811
  */
771
812
  declare function billingWebhookRoute(gateway: BillingGateway): BasaltRoute;
772
813
 
773
- export { type BillingGateway, type BillingPeriod, type BillingRoutesOptions, type CheckoutInput, type CreateSubscriptionInput, FakeBillingGateway, FakePaymentGateway, FeatureUnavailableError, type FeatureValue, GatewayUnsupportedError, type HandleEventResult, MemoryPaymentStore, MemoryRecurringStore, MemorySubscriptionStore, MemoryUsageStore, MemoryWebhookStore, type Meter, type NewPayment, NotSubscribedError, type PaymentApplyResult, type PaymentEvent, type PaymentGateway, type PaymentInstruction, PaymentLedger, type PaymentLedgerOptions, type PaymentRecord, type PaymentRecordStatus, type PaymentRequest, type PaymentStore, type PlanDefinition, type Plans, type PortalInput, QuotaExceededError, type RecurringBillingOptions, type RecurringInterval, RecurringReferenceBilling, type RecurringStatus, type RecurringStore, type RecurringSubscription, type RedisLike, RedisUsageStore, type RedisUsageStoreOptions, type RedisWebhookClient, RedisWebhookStore, type RedisWebhookStoreOptions, SUBSCRIPTIONS, StripeBillingGateway, type StripeGatewayOptions, StripeRequestError, type SubscribeInput, type SubscriptionRecord, type SubscriptionStatus, type SubscriptionStore, Subscriptions, type SubscriptionsOptions, type SubscriptionsPluginOptions, type SwapInput, UnknownPlanError, type UsageConsumeResult, type UsageStore, type WebhookEvent, WebhookInvalidError, type WebhookStore, addInterval, billingRoutes, billingWebhookRoute, definePlans, featureLimit, isMeter, meter, planPrice, subscriptionsPlugin };
814
+ export { type BillingGateway, type BillingPeriod, type BillingRoutesOptions, type CheckoutInput, type CreateSubscriptionInput, FakeBillingGateway, FakePaymentGateway, FeatureUnavailableError, type FeatureValue, GatewayUnsupportedError, type HandleEventResult, MemoryPaymentStore, MemoryRecurringStore, MemorySubscriptionStore, MemoryUsageStore, MemoryWebhookStore, type Meter, type NewPayment, NotSubscribedError, type PaymentApplyResult, type PaymentEvent, type PaymentGateway, type PaymentInstruction, PaymentLedger, type PaymentLedgerOptions, type PaymentRecord, type PaymentRecordStatus, type PaymentRequest, type PaymentStore, type PlanDefinition, type Plans, type PortalInput, QuotaExceededError, type RecurringBillingOptions, type RecurringInterval, RecurringReferenceBilling, type RecurringStatus, type RecurringStore, type RecurringSubscription, type RedisLike, RedisUsageStore, type RedisUsageStoreOptions, type RedisWebhookClient, RedisWebhookStore, type RedisWebhookStoreOptions, SUBSCRIPTIONS, StripeBillingGateway, type StripeGatewayOptions, StripeRequestError, type SubscribeInput, type SubscriptionRecord, type SubscriptionStatus, type SubscriptionStore, Subscriptions, type SubscriptionsOptions, type SubscriptionsPluginOptions, type SwapInput, UnknownPlanError, type UsageConsumeResult, type UsageStore, type WebhookEvent, WebhookInvalidError, type WebhookStore, addInterval, assertMinorUnits, billingRoutes, billingWebhookRoute, currencyDecimals, definePlans, featureLimit, formatMoney, isMeter, isMinorUnits, meter, planPrice, subscriptionsPlugin, toMajor, toMinor };
package/dist/index.js CHANGED
@@ -419,6 +419,52 @@ var RecurringReferenceBilling = class {
419
419
  }
420
420
  };
421
421
 
422
+ // src/money.ts
423
+ var CURRENCY_DECIMALS = {
424
+ AOA: 2,
425
+ USD: 2,
426
+ EUR: 2,
427
+ GBP: 2,
428
+ BRL: 2,
429
+ ZAR: 2,
430
+ MZN: 2,
431
+ CVE: 2,
432
+ NGN: 2,
433
+ KES: 2,
434
+ JPY: 0,
435
+ XOF: 0,
436
+ XAF: 0,
437
+ CLP: 0
438
+ };
439
+ var DEFAULT_DECIMALS = 2;
440
+ function currencyDecimals(currency) {
441
+ return CURRENCY_DECIMALS[currency.toUpperCase()] ?? DEFAULT_DECIMALS;
442
+ }
443
+ function toMinor(major, currency) {
444
+ return Math.round(major * 10 ** currencyDecimals(currency));
445
+ }
446
+ function toMajor(minor, currency) {
447
+ return minor / 10 ** currencyDecimals(currency);
448
+ }
449
+ function formatMoney(minor, currency, locale = "en-US") {
450
+ const major = toMajor(minor, currency);
451
+ try {
452
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(major);
453
+ } catch {
454
+ return `${major.toFixed(currencyDecimals(currency))} ${currency.toUpperCase()}`;
455
+ }
456
+ }
457
+ function isMinorUnits(amount) {
458
+ return Number.isInteger(amount) && amount >= 0;
459
+ }
460
+ function assertMinorUnits(amount, label = "amount") {
461
+ if (!isMinorUnits(amount)) {
462
+ throw new RangeError(
463
+ `${label} must be a non-negative integer in minor units (e.g. cents), got ${amount}`
464
+ );
465
+ }
466
+ }
467
+
422
468
  // src/drivers/stripe.ts
423
469
  import { createHmac, timingSafeEqual } from "crypto";
424
470
  import { BasaltError as BasaltError3 } from "@basaltkit/core";
@@ -939,12 +985,18 @@ export {
939
985
  UnknownPlanError,
940
986
  WebhookInvalidError,
941
987
  addInterval,
988
+ assertMinorUnits,
942
989
  billingRoutes,
943
990
  billingWebhookRoute,
991
+ currencyDecimals,
944
992
  definePlans,
945
993
  featureLimit,
994
+ formatMoney,
946
995
  isMeter,
996
+ isMinorUnits,
947
997
  meter,
948
998
  planPrice,
949
- subscriptionsPlugin
999
+ subscriptionsPlugin,
1000
+ toMajor,
1001
+ toMinor
950
1002
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/subscriptions",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Billing for Basalt, Cashier/Soulbscription-style: declarative plans, subscriptions with trials, feature flags, usage limits, gateway drivers and idempotent webhooks.",
5
5
  "license": "MIT",
6
6
  "type": "module",