@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/rpc.d.ts CHANGED
@@ -47,12 +47,40 @@ export declare const changeSeatsDescriptor: ActionProcedureDescriptor<"billing.c
47
47
  currency: typeof Schema.String;
48
48
  }>, typeof BillingError>;
49
49
 
50
+ /** The provider's invoice history. `hostedUrl` / `pdfUrl` are the provider's
51
+ * own pages — we never render an invoice ourselves. */
52
+ export declare const invoicesDescriptor: QueryProcedureDescriptor<"billing.invoices", Schema.Struct<{}>, Schema.Struct<{
53
+ invoices: Schema.Array$<Schema.Struct<{
54
+ providerInvoiceId: typeof Schema.String;
55
+ amountMinor: typeof Schema.Number;
56
+ currency: typeof Schema.String;
57
+ status: typeof Schema.String;
58
+ createdAt: Schema.NullOr<typeof Schema.String>;
59
+ hostedUrl: Schema.NullOr<typeof Schema.String>;
60
+ pdfUrl: Schema.NullOr<typeof Schema.String>;
61
+ }>>;
62
+ }>, typeof BillingError>;
63
+
50
64
  export declare const portalUrlDescriptor: ActionProcedureDescriptor<"billing.portalUrl", Schema.Struct<{
51
65
  returnUrl: typeof Schema.String;
52
66
  }>, Schema.Struct<{
53
67
  url: typeof Schema.String;
54
68
  }>, typeof BillingError>;
55
69
 
70
+ /** What a plan/seat change would cost, from the provider's invoice preview.
71
+ * Quote THIS in a confirmation dialog — a locally-estimated figure differs
72
+ * from the charge by the provider's rounding, tax and credit balance. */
73
+ export declare const previewChangeDescriptor: ActionProcedureDescriptor<"billing.previewChange", Schema.Struct<{
74
+ plan: Schema.optional<typeof Schema.String>;
75
+ quantity: Schema.optional<typeof Schema.Number>;
76
+ }>, Schema.Struct<{
77
+ plan: typeof Schema.String;
78
+ quantity: typeof Schema.Number;
79
+ /** Integer minor-unit settlement: > 0 charge, < 0 credit, 0 at boundary. */
80
+ prorationMinor: typeof Schema.Number;
81
+ currency: typeof Schema.String;
82
+ }>, typeof BillingError>;
83
+
56
84
  export declare const reportUsageDescriptor: ActionProcedureDescriptor<"billing.reportUsage", Schema.Struct<{
57
85
  key: typeof Schema.String;
58
86
  quantity: typeof Schema.Number;
@@ -64,6 +92,8 @@ export declare const startCheckoutDescriptor: ActionProcedureDescriptor<"billing
64
92
  plan: typeof Schema.String;
65
93
  successUrl: typeof Schema.String;
66
94
  cancelUrl: typeof Schema.String;
95
+ /** Seats to buy up front. Omit for 1. */
96
+ quantity: Schema.optional<typeof Schema.Number>;
67
97
  }>, Schema.Struct<{
68
98
  url: typeof Schema.String;
69
99
  }>, typeof BillingError>;
package/dist/rpc.js CHANGED
@@ -7,7 +7,8 @@ var i = n({
7
7
  input: t.Struct({
8
8
  plan: t.String,
9
9
  successUrl: t.String,
10
- cancelUrl: t.String
10
+ cancelUrl: t.String,
11
+ quantity: t.optional(t.Number)
11
12
  }),
12
13
  output: t.Struct({ url: t.String }),
13
14
  error: e
@@ -41,16 +42,37 @@ var i = n({
41
42
  prorationMinor: t.Number,
42
43
  currency: t.String
43
44
  }), l = n({
45
+ name: "billing.previewChange",
46
+ input: t.Struct({
47
+ plan: t.optional(t.String),
48
+ quantity: t.optional(t.Number)
49
+ }),
50
+ output: c,
51
+ error: e
52
+ }), u = r({
53
+ name: "billing.invoices",
54
+ input: t.Struct({}),
55
+ output: t.Struct({ invoices: t.Array(t.Struct({
56
+ providerInvoiceId: t.String,
57
+ amountMinor: t.Number,
58
+ currency: t.String,
59
+ status: t.String,
60
+ createdAt: t.NullOr(t.String),
61
+ hostedUrl: t.NullOr(t.String),
62
+ pdfUrl: t.NullOr(t.String)
63
+ })) }),
64
+ error: e
65
+ }), d = n({
44
66
  name: "billing.changePlan",
45
67
  input: t.Struct({ plan: t.String }),
46
68
  output: c,
47
69
  error: e
48
- }), u = n({
70
+ }), f = n({
49
71
  name: "billing.changeSeats",
50
72
  input: t.Struct({ quantity: t.Number }),
51
73
  output: c,
52
74
  error: e
53
- }), d = [
75
+ }), p = [
54
76
  {
55
77
  tag: "billing.startCheckout",
56
78
  kind: "action",
@@ -98,7 +120,23 @@ var i = n({
98
120
  module: "@voltro/plugin-billing/rpc",
99
121
  name: "changeSeatsDescriptor"
100
122
  }
123
+ },
124
+ {
125
+ tag: "billing.previewChange",
126
+ kind: "action",
127
+ import: {
128
+ module: "@voltro/plugin-billing/rpc",
129
+ name: "previewChangeDescriptor"
130
+ }
131
+ },
132
+ {
133
+ tag: "billing.invoices",
134
+ kind: "query",
135
+ import: {
136
+ module: "@voltro/plugin-billing/rpc",
137
+ name: "invoicesDescriptor"
138
+ }
101
139
  }
102
140
  ];
103
141
  //#endregion
104
- export { d as billingRpcClientImports, l as changePlanDescriptor, u as changeSeatsDescriptor, a as portalUrlDescriptor, s as reportUsageDescriptor, i as startCheckoutDescriptor, o as subscriptionDescriptor };
142
+ export { p as billingRpcClientImports, d as changePlanDescriptor, f as changeSeatsDescriptor, u as invoicesDescriptor, a as portalUrlDescriptor, l as previewChangeDescriptor, s as reportUsageDescriptor, i as startCheckoutDescriptor, o as subscriptionDescriptor };
package/dist/types.d.ts CHANGED
@@ -88,6 +88,39 @@ export declare interface BillingProvider {
88
88
  url: string;
89
89
  }, BillingError>;
90
90
  readonly reportUsage: (input: UsagePush) => Effect.Effect<void, BillingError>;
91
+ /**
92
+ * Apply a plan / seat change to the LIVE subscription.
93
+ *
94
+ * This is the call whose absence made `changeSeats` a lie: without it the
95
+ * service patched its own table and the provider went on billing the old
96
+ * quantity, so a customer could be granted seats nobody was charging for.
97
+ * The provider — not us — computes and bills the proration.
98
+ */
99
+ readonly updateSubscription: (input: SubscriptionUpdateInput) => Effect.Effect<SubscriptionChangeResult, BillingError>;
100
+ /**
101
+ * What WOULD a change cost, without applying it? Backed by the provider's
102
+ * own invoice preview, so the number quoted to the customer is the number
103
+ * they are charged — a locally-computed estimate can differ from the
104
+ * provider's rounding, its tax, and its credit balance, and any difference
105
+ * is a support ticket.
106
+ */
107
+ readonly previewSubscriptionChange: (input: SubscriptionUpdateInput) => Effect.Effect<SubscriptionChangeResult, BillingError>;
108
+ /** The provider's invoice history. We mirror it for listing but never
109
+ * render an invoice ourselves — `hostedUrl` / `pdfUrl` are theirs. */
110
+ readonly listInvoices: (input: {
111
+ readonly providerCustomerId: string;
112
+ readonly limit?: number;
113
+ }) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
114
+ /**
115
+ * Verify a webhook signature and decode the payload, using the provider's
116
+ * OWN verification. Returns `null` when this provider does not do its own
117
+ * verification (the mount's generic check then stands alone).
118
+ */
119
+ readonly verifyWebhook?: (input: {
120
+ readonly rawBody: Uint8Array;
121
+ readonly signatureHeader: string;
122
+ readonly secret: string;
123
+ }) => Effect.Effect<unknown, BillingError>;
91
124
  /** Map a verified, decoded webhook payload to a `BillingEvent`, or
92
125
  * `null` for an event type this provider doesn't model (handler
93
126
  * no-ops, still 200). */
@@ -113,6 +146,15 @@ export declare interface BillingServiceShape {
113
146
  readonly reportUsage: (tenantId: string, key: string, qty: number) => Effect.Effect<void, BillingError>;
114
147
  /** Flush all pending local usage counters to the provider. */
115
148
  readonly flushUsage: () => Effect.Effect<void, BillingError>;
149
+ /** What a plan/seat change WOULD cost, from the provider's own invoice
150
+ * preview. Quote this, not a local estimate — the two differ by the
151
+ * provider's rounding, tax and credit balance. */
152
+ readonly previewChange: (tenantId: string, next: {
153
+ readonly plan?: PlanId;
154
+ readonly quantity?: number;
155
+ }) => Effect.Effect<SubscriptionChange, BillingError>;
156
+ /** The provider's invoice history for this tenant. */
157
+ readonly invoices: (tenantId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
116
158
  /** Mint a provider-hosted checkout URL for `plan`. */
117
159
  readonly startCheckout: (input: {
118
160
  readonly tenantId: string;
@@ -144,26 +186,6 @@ export declare interface BillingServiceShape {
144
186
  * positive integer. Fails `BillingError` on no subscription / bad quantity.
145
187
  */
146
188
  readonly changeSeats: (tenantId: string, quantity: number, changeAt?: Date) => Effect.Effect<SubscriptionChange, BillingError>;
147
- /**
148
- * Record a failed payment: schedule the first dunning retry (per the
149
- * configured schedule) and transition the subscription `active → pastDue`.
150
- * Idempotent — a replayed failure for a subscription already in dunning does
151
- * not re-arm or double-schedule.
152
- */
153
- readonly recordPaymentFailure: (tenantId: string, at?: Date) => Effect.Effect<DunningState | null, BillingError>;
154
- /**
155
- * Record a recovered payment: clear any open dunning and transition the
156
- * subscription back to `active`. Idempotent / a no-op when not in dunning.
157
- */
158
- readonly recordPaymentSuccess: (tenantId: string, at?: Date) => Effect.Effect<void, BillingError>;
159
- /**
160
- * Advance every due dunning entry: subscriptions whose next retry is due move
161
- * to the next offset in the schedule; those that have exhausted the schedule
162
- * transition `pastDue → canceled`. Idempotent per due-time (a claim gate
163
- * makes a re-run over the same window a no-op). Driven by the self-scheduled
164
- * interval, or call it from your own `*.cron.tsx`.
165
- */
166
- readonly runDunningCycle: (now?: Date) => Effect.Effect<DunningCycleResult, BillingError>;
167
189
  }
168
190
 
169
191
  export declare interface CheckoutInput {
@@ -174,31 +196,21 @@ export declare interface CheckoutInput {
174
196
  readonly cancelUrl: string;
175
197
  /** Optional existing provider customer id to attach the checkout to. */
176
198
  readonly providerCustomerId?: string;
199
+ /** Seats to buy up front. Defaults to 1. */
200
+ readonly quantity?: number;
201
+ /** Let the buyer change the seat count on the checkout page itself.
202
+ * Stripe renders the stepper; we neither build nor validate it. */
203
+ readonly adjustableQuantity?: {
204
+ readonly min: number;
205
+ readonly max: number;
206
+ };
207
+ /** Days of free trial. Stripe runs the trial and emits `trialing`. */
208
+ readonly trialDays?: number;
177
209
  }
178
210
 
179
211
  /** The free/fallback plan id every app implicitly has. */
180
212
  export declare const DEFAULT_PLAN: PlanId;
181
213
 
182
- /** What one `runDunningCycle` did. */
183
- export declare interface DunningCycleResult {
184
- /** Subscriptions advanced to the next retry offset. */
185
- readonly retried: number;
186
- /** Subscriptions that exhausted the schedule and were canceled. */
187
- readonly canceled: number;
188
- }
189
-
190
- /** A tenant's live dunning record — a failed payment being retried. */
191
- export declare interface DunningState {
192
- readonly tenantId: string;
193
- readonly providerSubscriptionId: string;
194
- /** 0-based index into the configured retry-offset schedule. */
195
- readonly attempt: number;
196
- /** When the current retry is due. */
197
- readonly nextRetryAt: Date;
198
- /** When dunning was first opened (the original failure). */
199
- readonly startedAt: Date;
200
- }
201
-
202
214
  /** The result of a pure (non-mutating) entitlement check. */
203
215
  export declare interface EntitlementDecision {
204
216
  /** Whether the call would be allowed. */
@@ -239,6 +251,17 @@ declare const EntitlementExceeded_base: Schema.TaggedErrorClass<EntitlementExcee
239
251
  /** An entitlement limit: a finite quota or the unbounded `'unlimited'`. */
240
252
  export declare type EntitlementLimit = number | 'unlimited';
241
253
 
254
+ export declare interface InvoiceRecord {
255
+ readonly providerInvoiceId: string;
256
+ readonly amountMinor: number;
257
+ readonly currency: string;
258
+ readonly status: string;
259
+ readonly createdAt: Date | null;
260
+ /** Provider-hosted invoice page — we never render one ourselves. */
261
+ readonly hostedUrl: string | null;
262
+ readonly pdfUrl: string | null;
263
+ }
264
+
242
265
  /** A monetary amount: integer minor units + ISO-4217 currency. */
243
266
  export declare interface Money {
244
267
  /** Integer minor units (cents). Never a float. */
@@ -302,8 +325,40 @@ export declare interface SubscriptionChange {
302
325
  readonly currency: string;
303
326
  }
304
327
 
328
+ /** What a provider reports back after (or before) a subscription change. */
329
+ export declare interface SubscriptionChangeResult {
330
+ readonly plan: PlanId;
331
+ readonly quantity: number;
332
+ /** The proration the PROVIDER computed — and, unless `proration: 'none'`,
333
+ * will actually bill. Positive = owed, negative = credited. */
334
+ readonly prorationMinor: number;
335
+ readonly currency: string;
336
+ readonly currentPeriodStart: Date | null;
337
+ readonly currentPeriodEnd: Date | null;
338
+ }
339
+
305
340
  export declare type SubscriptionStatus = 'active' | 'trialing' | 'pastDue' | 'canceled' | 'incomplete';
306
341
 
342
+ /** Apply a plan / seat change to the LIVE subscription at the provider. */
343
+ export declare interface SubscriptionUpdateInput {
344
+ readonly providerSubscriptionId: string;
345
+ /** New price to move to. Omit to keep the current one. */
346
+ readonly priceId?: string;
347
+ /** New seat quantity. Omit to keep the current one. */
348
+ readonly quantity?: number;
349
+ /**
350
+ * How the provider should handle the mid-period money.
351
+ *
352
+ * - `'prorate'` — bill or credit the difference for the remaining period
353
+ * (Stripe's `create_prorations`). The default, and the only option that
354
+ * charges what the customer actually used.
355
+ * - `'none'` — change takes effect with no adjustment.
356
+ * - `'always_invoice'` — prorate AND invoice immediately rather than at
357
+ * the next cycle.
358
+ */
359
+ readonly proration?: 'prorate' | 'none' | 'always_invoice';
360
+ }
361
+
307
362
  export declare interface UsagePush {
308
363
  readonly tenantId: string;
309
364
  readonly entitlementKey: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@voltro/plugin-billing",
3
- "version": "0.5.0",
4
- "description": "Billing — subscriptions, plans, entitlements + usage metering on top of a pluggable BillingProvider (Stripe + mock). Ships billingPlugin() (provider-event webhook via @voltro/plugin-webhooks, the entitlement interceptor, typed BillingError + EntitlementExceeded errors, the _voltro_billing_* tables), the BillingService Context.Tag, the requireEntitlement() in-handler guard, and a browser-safe useStartCheckout() hook. Money is integer minor units end-to-end.",
3
+ "version": "0.7.0",
4
+ "description": "Billing on the official Stripe SDK — subscriptions, plans, entitlements and usage metering. Proration, failed-payment retries, tax, promotion codes, trials and the checkout seat stepper are Stripe's own; the plugin adds the entitlement engine, the enforce interceptor, typed BillingError/EntitlementExceeded, the /billing/webhook mount with Stripe's signature verification, the BillingService Tag, requireEntitlement(), and a browser-safe useStartCheckout() hook. Money is integer minor units end-to-end.",
5
5
  "keywords": [
6
6
  "voltro",
7
7
  "typescript",
@@ -47,11 +47,12 @@
47
47
  "node": ">=24.0.0"
48
48
  },
49
49
  "dependencies": {
50
- "@voltro/database": "0.5.0",
51
- "@voltro/env": "0.5.0",
52
- "@voltro/logger": "0.5.0",
53
- "@voltro/plugin-webhooks": "0.5.0",
54
- "@voltro/protocol": "0.5.0"
50
+ "@voltro/database": "0.7.0",
51
+ "@voltro/env": "0.7.0",
52
+ "@voltro/logger": "0.7.0",
53
+ "@voltro/plugin-webhooks": "0.7.0",
54
+ "@voltro/protocol": "0.7.0",
55
+ "stripe": "22.3.2"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "effect": "^3.21.4",