@voltro/plugin-billing 0.5.0 → 0.7.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
@@ -1,6 +1,7 @@
1
1
  import { ColumnDefinition } from '@voltro/database';
2
2
  import { Context } from 'effect';
3
3
  import { DataStore } from '@voltro/database';
4
+ import { default as default_2 } from 'stripe';
4
5
  import { Effect } from 'effect';
5
6
  import { IncomingRequest } from '@voltro/plugin-webhooks';
6
7
  import { IncomingResponse } from '@voltro/plugin-webhooks';
@@ -15,18 +16,8 @@ import { TableIndex } from '@voltro/database';
15
16
  import { TableLike } from '@voltro/database';
16
17
  import { VoltroPlugin } from '@voltro/protocol';
17
18
 
18
- /**
19
- * Advance one due dunning record: if there is a NEXT offset in the schedule,
20
- * move to it (CAS on `attempt`); otherwise exhaust — cancel the subscription
21
- * and close the record. Returns which happened, or `null` if another tick won
22
- * the advance first (the CAS lost).
23
- */
24
- export declare const advanceDunning: (opts: DunningEngineOptions, record: DunningState) => Effect.Effect<"retried" | "canceled" | null, BillingError>;
25
-
26
19
  export declare const BILLING_CUSTOMERS_TABLE = "_voltro_billing_customers";
27
20
 
28
- export declare const BILLING_DUNNING_TABLE = "_voltro_billing_dunning";
29
-
30
21
  export declare const BILLING_FLUSH_CLAIMS_TABLE = "_voltro_billing_flush_claims";
31
22
 
32
23
  export declare const BILLING_INVOICES_TABLE = "_voltro_billing_invoices";
@@ -54,15 +45,6 @@ export declare interface BillingContext {
54
45
  /** tenant ↔ provider customer link. */
55
46
  export declare const billingCustomersTable: BillingTable;
56
47
 
57
- /**
58
- * Dunning ledger — one row per subscription with a failed payment being
59
- * retried. `providerSubscriptionId` is UNIQUE so the create-path `insertIgnore`
60
- * gate makes opening dunning idempotent under a replayed payment-failed
61
- * webhook. Deleted on recovery / cancellation, so the table is bounded by the
62
- * count of subscriptions CURRENTLY in dunning (small).
63
- */
64
- export declare const billingDunningTable: BillingTable;
65
-
66
48
  /**
67
49
  * A billing failure. `transient: true` marks failures the service retries
68
50
  * (network blip talking to the provider); non-transient failures (a 4xx
@@ -155,15 +137,25 @@ export declare interface BillingPluginOptions {
155
137
  * self-scheduled flush — call `billing.flushUsage()` from your own
156
138
  * `*.cron.tsx` instead (e.g. for cluster-coordinated flushing). */
157
139
  readonly flushIntervalMs?: number;
158
- /** Dunning retry schedule — day-offsets from the original payment failure at
159
- * which to retry before canceling. Default `[1, 3, 5, 7]`. A failed payment
160
- * transitions the subscription `active → pastDue`; exhausting the schedule
161
- * transitions it to `canceled`; a recovered payment returns it to `active`. */
162
- readonly dunningSchedule?: DunningSchedule;
163
140
  /** Typed per-event side effects, run AFTER the row is updated. */
164
141
  readonly onEvent?: OnEventMap;
165
142
  /** Transient-failure retries on provider calls. Default 3. */
166
143
  readonly attempts?: number;
144
+ /**
145
+ * Checkout behaviour, handed straight to the provider.
146
+ *
147
+ * `adjustableQuantity` renders STRIPE's seat stepper on STRIPE's checkout
148
+ * page — set it for a per-seat plan so a customer can buy the number of
149
+ * seats they want in one go. `trialDays` makes Stripe run the trial and
150
+ * report `trialing`; we neither count trial days nor expire them.
151
+ */
152
+ readonly checkout?: {
153
+ readonly adjustableQuantity?: {
154
+ readonly min: number;
155
+ readonly max: number;
156
+ };
157
+ readonly trialDays?: number;
158
+ };
167
159
  /**
168
160
  * Per-tenant entitlement-limit override — the seam for a cloud-issued license
169
161
  * snapshot. Pass `@voltro/plugin-licensing`'s `entitlementResolver` here to let
@@ -199,6 +191,39 @@ export declare interface BillingProvider {
199
191
  url: string;
200
192
  }, BillingError>;
201
193
  readonly reportUsage: (input: UsagePush) => Effect.Effect<void, BillingError>;
194
+ /**
195
+ * Apply a plan / seat change to the LIVE subscription.
196
+ *
197
+ * This is the call whose absence made `changeSeats` a lie: without it the
198
+ * service patched its own table and the provider went on billing the old
199
+ * quantity, so a customer could be granted seats nobody was charging for.
200
+ * The provider — not us — computes and bills the proration.
201
+ */
202
+ readonly updateSubscription: (input: SubscriptionUpdateInput) => Effect.Effect<SubscriptionChangeResult, BillingError>;
203
+ /**
204
+ * What WOULD a change cost, without applying it? Backed by the provider's
205
+ * own invoice preview, so the number quoted to the customer is the number
206
+ * they are charged — a locally-computed estimate can differ from the
207
+ * provider's rounding, its tax, and its credit balance, and any difference
208
+ * is a support ticket.
209
+ */
210
+ readonly previewSubscriptionChange: (input: SubscriptionUpdateInput) => Effect.Effect<SubscriptionChangeResult, BillingError>;
211
+ /** The provider's invoice history. We mirror it for listing but never
212
+ * render an invoice ourselves — `hostedUrl` / `pdfUrl` are theirs. */
213
+ readonly listInvoices: (input: {
214
+ readonly providerCustomerId: string;
215
+ readonly limit?: number;
216
+ }) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
217
+ /**
218
+ * Verify a webhook signature and decode the payload, using the provider's
219
+ * OWN verification. Returns `null` when this provider does not do its own
220
+ * verification (the mount's generic check then stands alone).
221
+ */
222
+ readonly verifyWebhook?: (input: {
223
+ readonly rawBody: Uint8Array;
224
+ readonly signatureHeader: string;
225
+ readonly secret: string;
226
+ }) => Effect.Effect<unknown, BillingError>;
202
227
  /** Map a verified, decoded webhook payload to a `BillingEvent`, or
203
228
  * `null` for an event type this provider doesn't model (handler
204
229
  * no-ops, still 200). */
@@ -223,11 +248,19 @@ export declare interface BillingServiceOptions {
223
248
  readonly stores?: BillingStores;
224
249
  /** Transient-failure retries on provider calls. Default 3. */
225
250
  readonly attempts?: number;
251
+ /** Checkout behaviour handed straight to the provider — we render none of
252
+ * it ourselves. */
253
+ readonly checkout?: {
254
+ /** Seat stepper bounds on the provider's checkout page. */
255
+ readonly adjustableQuantity?: {
256
+ readonly min: number;
257
+ readonly max: number;
258
+ };
259
+ /** Free-trial length. The provider runs the trial and reports `trialing`. */
260
+ readonly trialDays?: number;
261
+ };
226
262
  /** Clock injection for deterministic period windowing in tests. */
227
263
  readonly now?: () => Date;
228
- /** Dunning retry schedule (day-offsets from the original failure). Default
229
- * `[1, 3, 5, 7]`. */
230
- readonly dunningSchedule?: DunningSchedule;
231
264
  /**
232
265
  * Optional per-tenant entitlement-limit override — the seam for a
233
266
  * cloud-issued license snapshot (e.g. `@voltro/plugin-licensing`'s
@@ -251,6 +284,15 @@ export declare interface BillingServiceShape {
251
284
  readonly reportUsage: (tenantId: string, key: string, qty: number) => Effect.Effect<void, BillingError>;
252
285
  /** Flush all pending local usage counters to the provider. */
253
286
  readonly flushUsage: () => Effect.Effect<void, BillingError>;
287
+ /** What a plan/seat change WOULD cost, from the provider's own invoice
288
+ * preview. Quote this, not a local estimate — the two differ by the
289
+ * provider's rounding, tax and credit balance. */
290
+ readonly previewChange: (tenantId: string, next: {
291
+ readonly plan?: PlanId;
292
+ readonly quantity?: number;
293
+ }) => Effect.Effect<SubscriptionChange, BillingError>;
294
+ /** The provider's invoice history for this tenant. */
295
+ readonly invoices: (tenantId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
254
296
  /** Mint a provider-hosted checkout URL for `plan`. */
255
297
  readonly startCheckout: (input: {
256
298
  readonly tenantId: string;
@@ -282,35 +324,13 @@ export declare interface BillingServiceShape {
282
324
  * positive integer. Fails `BillingError` on no subscription / bad quantity.
283
325
  */
284
326
  readonly changeSeats: (tenantId: string, quantity: number, changeAt?: Date) => Effect.Effect<SubscriptionChange, BillingError>;
285
- /**
286
- * Record a failed payment: schedule the first dunning retry (per the
287
- * configured schedule) and transition the subscription `active → pastDue`.
288
- * Idempotent — a replayed failure for a subscription already in dunning does
289
- * not re-arm or double-schedule.
290
- */
291
- readonly recordPaymentFailure: (tenantId: string, at?: Date) => Effect.Effect<DunningState | null, BillingError>;
292
- /**
293
- * Record a recovered payment: clear any open dunning and transition the
294
- * subscription back to `active`. Idempotent / a no-op when not in dunning.
295
- */
296
- readonly recordPaymentSuccess: (tenantId: string, at?: Date) => Effect.Effect<void, BillingError>;
297
- /**
298
- * Advance every due dunning entry: subscriptions whose next retry is due move
299
- * to the next offset in the schedule; those that have exhausted the schedule
300
- * transition `pastDue → canceled`. Idempotent per due-time (a claim gate
301
- * makes a re-run over the same window a no-op). Driven by the self-scheduled
302
- * interval, or call it from your own `*.cron.tsx`.
303
- */
304
- readonly runDunningCycle: (now?: Date) => Effect.Effect<DunningCycleResult, BillingError>;
305
327
  }
306
328
 
307
- /** The five billing stores bundled — swapped wholesale by `bindDataStore`. */
308
329
  export declare interface BillingStores {
309
330
  readonly customers: CustomerStore;
310
331
  readonly subscriptions: SubscriptionStore;
311
332
  readonly invoices: InvoiceStore;
312
333
  readonly usage: UsageStore;
313
- readonly dunning: DunningStore;
314
334
  }
315
335
 
316
336
  /** One active subscription per tenant. */
@@ -327,7 +347,6 @@ export declare interface BillingTable extends TableLike {
327
347
  readonly appliedIndexes: ReadonlyArray<TableIndex>;
328
348
  }
329
349
 
330
- /** All billing tables, ready to spread into `extendSchema.tables`. */
331
350
  export declare const billingTables: () => ReadonlyArray<BillingTable>;
332
351
 
333
352
  /** Per-tenant metered counters driving the entitlement engine. */
@@ -344,6 +363,9 @@ declare interface BillingWebhookOptions {
344
363
  readonly provider: BillingProvider;
345
364
  readonly service: BillingServiceShape;
346
365
  readonly onEvent?: OnEventMap;
366
+ /** The signing secret, so the provider can run its OWN verification on the
367
+ * raw bytes. Null → only the mount's generic check applies. */
368
+ readonly webhookSecret?: string | null;
347
369
  }
348
370
 
349
371
  /**
@@ -379,6 +401,16 @@ export declare interface CheckoutInput {
379
401
  readonly cancelUrl: string;
380
402
  /** Optional existing provider customer id to attach the checkout to. */
381
403
  readonly providerCustomerId?: string;
404
+ /** Seats to buy up front. Defaults to 1. */
405
+ readonly quantity?: number;
406
+ /** Let the buyer change the seat count on the checkout page itself.
407
+ * Stripe renders the stepper; we neither build nor validate it. */
408
+ readonly adjustableQuantity?: {
409
+ readonly min: number;
410
+ readonly max: number;
411
+ };
412
+ /** Days of free trial. Stripe runs the trial and emits `trialing`. */
413
+ readonly trialDays?: number;
382
414
  }
383
415
 
384
416
  /**
@@ -390,16 +422,6 @@ export declare interface CheckoutInput {
390
422
  */
391
423
  export declare const claimFlushWindow: (store: DataStore, windowKey: string, replicaId: string, nowMs: number) => Promise<boolean>;
392
424
 
393
- /**
394
- * Compute the prorated settlement for a mid-cycle amount change.
395
- *
396
- * Fails `BillingError` (non-transient) when the period is degenerate
397
- * (`periodEnd <= periodStart`) — a caller must supply a real paid period; a
398
- * plan with no period (`currentPeriodEnd === null`) is not prorated at all
399
- * (the service short-circuits before calling this).
400
- */
401
- export declare const computeProration: (input: ProrationInput) => ProrationResult;
402
-
403
425
  declare interface CustomerRecord {
404
426
  readonly tenantId: string;
405
427
  readonly provider: string;
@@ -413,62 +435,9 @@ export declare interface CustomerStore {
413
435
 
414
436
  export declare const dataStoreStores: (store: DataStore) => BillingStores;
415
437
 
416
- export declare const DEFAULT_DUNNING_SCHEDULE: DunningSchedule;
417
-
418
438
  /** The free/fallback plan id every app implicitly has. */
419
439
  export declare const DEFAULT_PLAN: PlanId;
420
440
 
421
- /** What one `runDunningCycle` did. */
422
- export declare interface DunningCycleResult {
423
- /** Subscriptions advanced to the next retry offset. */
424
- readonly retried: number;
425
- /** Subscriptions that exhausted the schedule and were canceled. */
426
- readonly canceled: number;
427
- }
428
-
429
- export declare interface DunningEngineOptions {
430
- readonly subscriptions: SubscriptionStore;
431
- readonly dunning: DunningStore;
432
- readonly schedule: DunningSchedule;
433
- }
434
-
435
- /** The dunning retry schedule: day-offsets from the original failure at which
436
- * to retry. Default `[1, 3, 5, 7]` (four retries over a week, then cancel). */
437
- export declare type DunningSchedule = ReadonlyArray<number>;
438
-
439
- /** A tenant's live dunning record — a failed payment being retried. */
440
- export declare interface DunningState {
441
- readonly tenantId: string;
442
- readonly providerSubscriptionId: string;
443
- /** 0-based index into the configured retry-offset schedule. */
444
- readonly attempt: number;
445
- /** When the current retry is due. */
446
- readonly nextRetryAt: Date;
447
- /** When dunning was first opened (the original failure). */
448
- readonly startedAt: Date;
449
- }
450
-
451
- export declare interface DunningStore {
452
- /**
453
- * Open a dunning record for a subscription (attempt 0, due at `nextRetryAt`),
454
- * IDEMPOTENTLY — if one already exists it is left untouched and `false` is
455
- * returned. Returns `true` only when THIS call opened it. The create-path
456
- * `insertIgnore` over the `providerSubscriptionId` UNIQUE makes a replayed
457
- * payment-failed webhook a no-op across instances.
458
- */
459
- readonly open: (tenantId: string, providerSubscriptionId: string, nextRetryAt: Date, at: Date) => Effect.Effect<boolean, BillingError>;
460
- readonly getByTenant: (tenantId: string) => Effect.Effect<DunningState | null, BillingError>;
461
- /** All dunning records due at/before `at` (`nextRetryAt <= at`). */
462
- readonly due: (at: Date) => Effect.Effect<ReadonlyArray<DunningState>, BillingError>;
463
- /** Advance a record to the next attempt/retry time, CONDITIONALLY on the
464
- * record still being at `fromAttempt` (a CAS so two ticks can't both advance
465
- * the same record). Returns `true` iff this call won the advance. */
466
- readonly advance: (providerSubscriptionId: string, fromAttempt: number, toAttempt: number, nextRetryAt: Date) => Effect.Effect<boolean, BillingError>;
467
- /** Close (delete) the dunning record for a subscription — on recovery or on
468
- * exhaustion→cancel. No-op if absent. */
469
- readonly close: (providerSubscriptionId: string) => Effect.Effect<void, BillingError>;
470
- }
471
-
472
441
  /** A registry with a single zero-quota `free` plan — the zero-config default. */
473
442
  export declare const emptyPlanRegistry: () => PlanRegistry;
474
443
 
@@ -534,6 +503,17 @@ export declare const evaluateEntitlement: (entitlement: string, limit: number, u
534
503
  export declare const flushWindowKey: (nowMs: number, intervalMs: number) => string;
535
504
 
536
505
  declare interface InvoiceRecord {
506
+ readonly providerInvoiceId: string;
507
+ readonly amountMinor: number;
508
+ readonly currency: string;
509
+ readonly status: string;
510
+ readonly createdAt: Date | null;
511
+ /** Provider-hosted invoice page — we never render one ourselves. */
512
+ readonly hostedUrl: string | null;
513
+ readonly pdfUrl: string | null;
514
+ }
515
+
516
+ declare interface InvoiceRecord_2 {
537
517
  readonly tenantId: string;
538
518
  readonly provider: string;
539
519
  readonly providerInvoiceId: string;
@@ -545,8 +525,8 @@ declare interface InvoiceRecord {
545
525
 
546
526
  export declare interface InvoiceStore {
547
527
  /** Upsert by `providerInvoiceId` (idempotent under webhook replay). */
548
- readonly upsert: (invoice: InvoiceRecord) => Effect.Effect<void, BillingError>;
549
- readonly listByTenant: (tenantId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
528
+ readonly upsert: (invoice: InvoiceRecord_2) => Effect.Effect<void, BillingError>;
529
+ readonly listByTenant: (tenantId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord_2>, BillingError>;
550
530
  }
551
531
 
552
532
  export declare const memoryStores: () => BillingStores;
@@ -587,6 +567,10 @@ declare type MeterSource =
587
567
  export declare interface MockProvider extends BillingProvider {
588
568
  /** Recorded usage pushes — assertion surface for tests. */
589
569
  readonly pushed: ReadonlyArray<UsagePush>;
570
+ /** Recorded subscription updates. The point of asserting on these is that
571
+ * a change which never reaches the provider is a change the customer is
572
+ * not billed for — the exact defect this contract was widened to fix. */
573
+ readonly updates: ReadonlyArray<SubscriptionUpdateInput>;
590
574
  }
591
575
 
592
576
  export declare const mockProvider: (options?: {
@@ -617,23 +601,15 @@ declare interface MountBillingWebhookOptions extends BillingWebhookOptions {
617
601
  }
618
602
 
619
603
  /**
620
- * Map a verified, decoded Stripe webhook event (`{ type, data: { object } }`)
621
- * to a `BillingEvent`, or null for unhandled types. Pure the signature
622
- * was already checked by the webhook mounter.
604
+ * Map a verified Stripe event to a `BillingEvent`, or null for a type we do
605
+ * not model (the handler no-ops and still answers 200, which is what stops
606
+ * Stripe retrying an event we will never care about).
623
607
  */
624
- export declare const normalizeStripeEvent: (raw: unknown) => BillingEvent | null;
608
+ export declare const normalizeStripeEvent: (event: default_2.Event, resolvePrice?: (priceId: string) => string | null) => BillingEvent | null;
625
609
 
626
610
  /** Per-event side effect, run AFTER the row is updated. */
627
611
  export declare type OnEventMap = Readonly<Record<string, (event: BillingEvent) => Effect.Effect<void, unknown>>>;
628
612
 
629
- /**
630
- * Open dunning for a subscription on a payment failure: schedule the first
631
- * retry (offset[0]) and transition `active → pastDue`. Idempotent — a replayed
632
- * failure whose record already exists is a no-op (returns `null`). Returns the
633
- * opened `DunningState` when this call opened it.
634
- */
635
- export declare const openDunning: (opts: DunningEngineOptions, tenantId: string, providerSubscriptionId: string, at: Date) => Effect.Effect<DunningState | null, BillingError>;
636
-
637
613
  /** Calendar-month window key (`'YYYY-MM'`) for metered counters. */
638
614
  export declare const periodKey: (now?: Date) => string;
639
615
 
@@ -687,35 +663,6 @@ export declare interface PortalInput {
687
663
  readonly returnUrl: string;
688
664
  }
689
665
 
690
- /** The inputs to one proration computation. All amounts are integer minor units. */
691
- export declare interface ProrationInput {
692
- /** The amount the subscription was billing for the current period (minor units). */
693
- readonly oldAmountMinor: number;
694
- /** The amount it will bill after the change (minor units). */
695
- readonly newAmountMinor: number;
696
- /** Start of the current paid period. */
697
- readonly periodStart: Date;
698
- /** End of the current paid period (`currentPeriodEnd`). */
699
- readonly periodEnd: Date;
700
- /** When the change takes effect (usually "now"). */
701
- readonly changeAt: Date;
702
- }
703
-
704
- /** The result of a proration: the settlement delta + the fraction it was based on. */
705
- export declare interface ProrationResult {
706
- /** Integer minor-unit settlement: > 0 charge (upgrade), < 0 credit
707
- * (downgrade), 0 at the period boundary or when the amount is unchanged. */
708
- readonly deltaMinor: number;
709
- /** The unused fraction of the period the delta was prorated over, in [0, 1]. */
710
- readonly unusedFraction: number;
711
- }
712
-
713
- /**
714
- * Recover a subscription: close any open dunning record and set it back to
715
- * `active`. Idempotent / no-op when not in dunning.
716
- */
717
- export declare const recoverDunning: (opts: DunningEngineOptions, providerSubscriptionId: string) => Effect.Effect<void, BillingError>;
718
-
719
666
  /**
720
667
  * In-handler entitlement guard. Resolves `ctx.request.subject.tenantId`,
721
668
  * consumes `cost` units of `key`, and fails with the typed
@@ -745,29 +692,10 @@ declare interface ResolveProviderInput {
745
692
  readonly provider?: BillingProviderName | BillingProvider;
746
693
  readonly apiKey?: string;
747
694
  readonly env: NodeJS.ProcessEnv;
695
+ /** Price id → plan id, from the plugin's plan registry. */
696
+ readonly planForPriceId?: (priceId: string) => string | null;
748
697
  }
749
698
 
750
- /**
751
- * The retry time for attempt `index` (0-based) given the original failure
752
- * `startedAt` and the schedule of day-offsets. Returns `null` when `index` is
753
- * past the end of the schedule — the signal to CANCEL rather than reschedule.
754
- */
755
- export declare const retryTimeFor: (startedAt: Date, index: number, schedule: DunningSchedule) => Date | null;
756
-
757
- /** Round half away from zero — symmetric for charges (+) and credits (−) so
758
- * proration doesn't systematically favour either party. `Math.round` rounds
759
- * half toward +∞ (−0.5 → 0, +0.5 → 1), which WOULD bias credits; this
760
- * corrects the negative side. Operates on a float intermediate and returns an
761
- * integer. */
762
- export declare const roundHalfAwayFromZero: (value: number) => number;
763
-
764
- /**
765
- * Run one dunning cycle: for every record due at `now`, advance it (retry to
766
- * the next offset, or cancel if exhausted). Idempotent per due-time — the CAS
767
- * on `attempt` means a re-run over an already-advanced record is a no-op.
768
- */
769
- export declare const runDunningCycle: (opts: DunningEngineOptions, now: Date) => Effect.Effect<DunningCycleResult, BillingError>;
770
-
771
699
  /**
772
700
  * A mutable holder so `bindDataStore` can swap the stores AFTER the service
773
701
  * layer is built. The service reads `holder.stores` on every call rather
@@ -784,6 +712,18 @@ export declare interface StripeProviderOptions {
784
712
  readonly apiKey: string;
785
713
  /** Override metered-usage support (Stripe supports it by default). */
786
714
  readonly supportsMeteredUsage?: boolean;
715
+ /**
716
+ * Map a Stripe price id to one of our plan ids.
717
+ *
718
+ * Without it, a subscription is only recognised when it carries
719
+ * `metadata.plan` — which our own checkout sets, but a subscription created
720
+ * in the Stripe Dashboard, by a sales-led flow, or by a migration does NOT.
721
+ * Those events were previously dropped on the floor and the customer stayed
722
+ * on `free` while paying. The plugin passes its plan registry here.
723
+ */
724
+ readonly planForPriceId?: (priceId: string) => string | null;
725
+ /** Injected in tests. */
726
+ readonly client?: default_2;
787
727
  }
788
728
 
789
729
  /** A tenant's subscription row, normalized across providers. */
@@ -815,6 +755,18 @@ export declare interface SubscriptionChange {
815
755
  readonly currency: string;
816
756
  }
817
757
 
758
+ /** What a provider reports back after (or before) a subscription change. */
759
+ declare interface SubscriptionChangeResult {
760
+ readonly plan: PlanId;
761
+ readonly quantity: number;
762
+ /** The proration the PROVIDER computed — and, unless `proration: 'none'`,
763
+ * will actually bill. Positive = owed, negative = credited. */
764
+ readonly prorationMinor: number;
765
+ readonly currency: string;
766
+ readonly currentPeriodStart: Date | null;
767
+ readonly currentPeriodEnd: Date | null;
768
+ }
769
+
818
770
  export declare type SubscriptionStatus = 'active' | 'trialing' | 'pastDue' | 'canceled' | 'incomplete';
819
771
 
820
772
  export declare interface SubscriptionStore {
@@ -835,6 +787,26 @@ export declare interface SubscriptionStore {
835
787
  readonly setStatus: (providerSubscriptionId: string, status: SubscriptionStatus) => Effect.Effect<void, BillingError>;
836
788
  }
837
789
 
790
+ /** Apply a plan / seat change to the LIVE subscription at the provider. */
791
+ declare interface SubscriptionUpdateInput {
792
+ readonly providerSubscriptionId: string;
793
+ /** New price to move to. Omit to keep the current one. */
794
+ readonly priceId?: string;
795
+ /** New seat quantity. Omit to keep the current one. */
796
+ readonly quantity?: number;
797
+ /**
798
+ * How the provider should handle the mid-period money.
799
+ *
800
+ * - `'prorate'` — bill or credit the difference for the remaining period
801
+ * (Stripe's `create_prorations`). The default, and the only option that
802
+ * charges what the customer actually used.
803
+ * - `'none'` — change takes effect with no adjustment.
804
+ * - `'always_invoice'` — prorate AND invoice immediately rather than at
805
+ * the next cycle.
806
+ */
807
+ readonly proration?: 'prorate' | 'none' | 'always_invoice';
808
+ }
809
+
838
810
  export declare interface UsagePush {
839
811
  readonly tenantId: string;
840
812
  readonly entitlementKey: string;
@@ -875,13 +847,6 @@ export declare interface UsageStore {
875
847
  readonly markReported: (tenantId: string, key: string, period: string, reported: number) => Effect.Effect<void, BillingError>;
876
848
  }
877
849
 
878
- /**
879
- * Validate a dunning schedule at boot — fails LOUD on a bad config rather than
880
- * silently never retrying. Offsets must be a non-empty list of strictly
881
- * increasing positive numbers (days).
882
- */
883
- export declare const validateDunningSchedule: (schedule: DunningSchedule) => void;
884
-
885
850
  export { VoltroPlugin }
886
851
 
887
852
  export { }