@basaltkit/subscriptions 2.0.0 → 2.1.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
@@ -412,6 +412,26 @@ interface PaymentApplyResult {
412
412
  fresh: boolean;
413
413
  record?: PaymentRecord;
414
414
  }
415
+ /** Lifecycle events emitted by the ledger — subscribe with `ledger.on(...)`. */
416
+ interface PaymentLedgerEvents {
417
+ /** A payment was recorded as pending (on `created`). */
418
+ recorded: {
419
+ record: PaymentRecord | undefined;
420
+ payment: NewPayment;
421
+ };
422
+ /** A payment was confirmed paid (fresh `apply` of a `payment.succeeded`). */
423
+ confirmed: {
424
+ record: PaymentRecord | undefined;
425
+ event: PaymentEvent;
426
+ };
427
+ /** A payment failed (fresh `apply` of a `payment.failed`). */
428
+ failed: {
429
+ record: PaymentRecord | undefined;
430
+ event: PaymentEvent;
431
+ };
432
+ }
433
+ type PaymentLedgerEvent = keyof PaymentLedgerEvents;
434
+ type PaymentLedgerListener<K extends PaymentLedgerEvent> = (payload: PaymentLedgerEvents[K]) => void | Promise<void>;
415
435
  interface PaymentLedgerOptions {
416
436
  /** Where payments are stored. Default: in-memory. */
417
437
  store?: PaymentStore;
@@ -420,6 +440,12 @@ interface PaymentLedgerOptions {
420
440
  * Redis) across the app so a retried callback is applied exactly once.
421
441
  */
422
442
  webhooks?: WebhookStore;
443
+ /**
444
+ * Called when a lifecycle listener throws. Listeners are best-effort side
445
+ * effects (notifications, analytics) that never roll back a payment — a
446
+ * throwing one is reported here instead. Default: swallow.
447
+ */
448
+ onListenerError?: (error: unknown, event: PaymentLedgerEvent) => void;
423
449
  }
424
450
  /**
425
451
  * Ties a `PaymentStore` to webhook idempotency so a retried callback is applied
@@ -441,7 +467,17 @@ interface PaymentLedgerOptions {
441
467
  declare class PaymentLedger {
442
468
  private readonly store;
443
469
  private readonly webhooks;
470
+ private readonly onListenerError;
471
+ private readonly listeners;
444
472
  constructor(options?: PaymentLedgerOptions);
473
+ /**
474
+ * Subscribe to a lifecycle event (`recorded`/`confirmed`/`failed`). Listeners
475
+ * are best-effort: they run after the payment is safely persisted and a
476
+ * throwing one never rolls it back (it's reported via `onListenerError`).
477
+ * Returns an unsubscribe function.
478
+ */
479
+ on<K extends PaymentLedgerEvent>(event: K, listener: PaymentLedgerListener<K>): () => void;
480
+ private emit;
445
481
  /** Record a just-created payment as pending. Call after `createPayment`. */
446
482
  created(instruction: PaymentInstruction, request: PaymentRequest): Promise<void>;
447
483
  /**
@@ -811,4 +847,4 @@ declare function billingRoutes(options: BillingRoutesOptions): BasaltRoute[];
811
847
  */
812
848
  declare function billingWebhookRoute(gateway: BillingGateway): BasaltRoute;
813
849
 
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 };
850
+ 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 PaymentLedgerEvent, type PaymentLedgerEvents, type PaymentLedgerListener, 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
@@ -246,19 +246,46 @@ var MemoryPaymentStore = class {
246
246
  var PaymentLedger = class {
247
247
  store;
248
248
  webhooks;
249
+ onListenerError;
250
+ listeners = { recorded: /* @__PURE__ */ new Set(), confirmed: /* @__PURE__ */ new Set(), failed: /* @__PURE__ */ new Set() };
249
251
  constructor(options = {}) {
250
252
  this.store = options.store ?? new MemoryPaymentStore();
251
253
  this.webhooks = options.webhooks ?? new MemoryWebhookStore();
254
+ this.onListenerError = options.onListenerError ?? (() => {
255
+ });
256
+ }
257
+ /**
258
+ * Subscribe to a lifecycle event (`recorded`/`confirmed`/`failed`). Listeners
259
+ * are best-effort: they run after the payment is safely persisted and a
260
+ * throwing one never rolls it back (it's reported via `onListenerError`).
261
+ * Returns an unsubscribe function.
262
+ */
263
+ on(event, listener) {
264
+ this.listeners[event].add(listener);
265
+ return () => {
266
+ this.listeners[event].delete(listener);
267
+ };
268
+ }
269
+ async emit(event, payload) {
270
+ for (const listener of this.listeners[event]) {
271
+ try {
272
+ await listener(payload);
273
+ } catch (error) {
274
+ this.onListenerError(error, event);
275
+ }
276
+ }
252
277
  }
253
278
  /** Record a just-created payment as pending. Call after `createPayment`. */
254
279
  async created(instruction, request) {
255
- await this.store.create({
280
+ const payment = {
256
281
  id: instruction.id,
257
282
  amount: request.amount,
258
283
  ...request.billableId ? { billableId: request.billableId } : {},
259
284
  ...request.reference ? { reference: request.reference } : {},
260
285
  ...instruction.raw !== void 0 ? { raw: instruction.raw } : {}
261
- });
286
+ };
287
+ await this.store.create(payment);
288
+ await this.emit("recorded", { record: await this.store.get(instruction.id), payment });
262
289
  }
263
290
  /**
264
291
  * Apply a verified `PaymentEvent` idempotently. Dedupes by `event.id`; on a
@@ -273,19 +300,21 @@ var PaymentLedger = class {
273
300
  async apply(event, onFresh) {
274
301
  const fresh = await this.webhooks.markProcessed(event.id);
275
302
  if (!fresh) return { fresh: false };
303
+ let record;
276
304
  try {
277
305
  const status = event.type === "payment.succeeded" ? "paid" : "failed";
278
306
  await this.store.setStatus(event.paymentId, status, {
279
307
  amount: event.amount,
280
308
  ...event.raw !== void 0 ? { raw: event.raw } : {}
281
309
  });
282
- const record = await this.store.get(event.paymentId);
310
+ record = await this.store.get(event.paymentId);
283
311
  if (onFresh) await onFresh(record, event);
284
- return { fresh: true, ...record ? { record } : {} };
285
312
  } catch (error) {
286
313
  await this.webhooks.release(event.id);
287
314
  throw error;
288
315
  }
316
+ await this.emit(event.type === "payment.succeeded" ? "confirmed" : "failed", { record, event });
317
+ return { fresh: true, ...record ? { record } : {} };
289
318
  }
290
319
  get(id) {
291
320
  return this.store.get(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/subscriptions",
3
- "version": "2.0.0",
3
+ "version": "2.1.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",
@@ -14,8 +14,8 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@basaltkit/core": "^1.0.0",
18
- "@basaltkit/fastify": "^1.0.0"
17
+ "@basaltkit/fastify": "^1.0.0",
18
+ "@basaltkit/core": "^1.0.0"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "zod": "^3.24.0 || ^4.0.0"