@voltro/plugin-billing 0.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/rpc.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { ActionProcedureDescriptor } from '@voltro/protocol';
2
+ import { PluginRpcClientDescriptor } from '@voltro/protocol';
3
+ import { QueryProcedureDescriptor } from '@voltro/protocol';
4
+ import { Schema } from 'effect';
5
+
6
+ /**
7
+ * A billing failure. `transient: true` marks failures the service retries
8
+ * (network blip talking to the provider); non-transient failures (a 4xx
9
+ * from the provider, a misconfigured plan) are surfaced immediately. The
10
+ * `billingPlugin` registers this via `errorSchemas` so it decodes TYPED on
11
+ * the client rather than crossing as an untyped defect.
12
+ */
13
+ declare class BillingError extends BillingError_base {
14
+ }
15
+
16
+ declare const BillingError_base: Schema.TaggedErrorClass<BillingError, "BillingError", {
17
+ readonly _tag: Schema.tag<"BillingError">;
18
+ } & {
19
+ /** Where the failure originated — provider name or an internal stage. */
20
+ source: typeof Schema.String;
21
+ message: typeof Schema.String;
22
+ /** Retryable? The service retries `transient` failures on a Schedule. */
23
+ transient: typeof Schema.Boolean;
24
+ }>;
25
+
26
+ /** Declared for the codegen (`VoltroPlugin.rpcClientDescriptors`) so each tag is
27
+ * emitted into the generated client group. Kept in lockstep with the exports. */
28
+ export declare const billingRpcClientImports: ReadonlyArray<PluginRpcClientDescriptor>;
29
+
30
+ export declare const changePlanDescriptor: ActionProcedureDescriptor<"billing.changePlan", Schema.Struct<{
31
+ plan: typeof Schema.String;
32
+ }>, Schema.Struct<{
33
+ plan: typeof Schema.String;
34
+ quantity: typeof Schema.Number;
35
+ /** Integer minor-unit settlement: > 0 charge, < 0 credit, 0 at boundary. */
36
+ prorationMinor: typeof Schema.Number;
37
+ currency: typeof Schema.String;
38
+ }>, typeof BillingError>;
39
+
40
+ export declare const changeSeatsDescriptor: ActionProcedureDescriptor<"billing.changeSeats", Schema.Struct<{
41
+ quantity: typeof Schema.Number;
42
+ }>, Schema.Struct<{
43
+ plan: typeof Schema.String;
44
+ quantity: typeof Schema.Number;
45
+ /** Integer minor-unit settlement: > 0 charge, < 0 credit, 0 at boundary. */
46
+ prorationMinor: typeof Schema.Number;
47
+ currency: typeof Schema.String;
48
+ }>, typeof BillingError>;
49
+
50
+ export declare const portalUrlDescriptor: ActionProcedureDescriptor<"billing.portalUrl", Schema.Struct<{
51
+ returnUrl: typeof Schema.String;
52
+ }>, Schema.Struct<{
53
+ url: typeof Schema.String;
54
+ }>, typeof BillingError>;
55
+
56
+ export declare const reportUsageDescriptor: ActionProcedureDescriptor<"billing.reportUsage", Schema.Struct<{
57
+ key: typeof Schema.String;
58
+ quantity: typeof Schema.Number;
59
+ }>, Schema.Struct<{
60
+ ok: typeof Schema.Boolean;
61
+ }>, typeof BillingError>;
62
+
63
+ export declare const startCheckoutDescriptor: ActionProcedureDescriptor<"billing.startCheckout", Schema.Struct<{
64
+ plan: typeof Schema.String;
65
+ successUrl: typeof Schema.String;
66
+ cancelUrl: typeof Schema.String;
67
+ }>, Schema.Struct<{
68
+ url: typeof Schema.String;
69
+ }>, typeof BillingError>;
70
+
71
+ export declare const subscriptionDescriptor: QueryProcedureDescriptor<"billing.subscription", Schema.Struct<{}>, Schema.Struct<{
72
+ subscription: Schema.NullOr<Schema.Struct<{
73
+ plan: typeof Schema.String;
74
+ status: typeof Schema.String;
75
+ quantity: typeof Schema.Number;
76
+ currentPeriodEnd: Schema.NullOr<typeof Schema.String>;
77
+ cancelAt: Schema.NullOr<typeof Schema.String>;
78
+ }>>;
79
+ }>, typeof BillingError>;
80
+
81
+ export { }
package/dist/rpc.js ADDED
@@ -0,0 +1,104 @@
1
+ import { BillingError as e } from "./types.js";
2
+ import { Schema as t } from "effect";
3
+ import { defineAction as n, defineQuery as r } from "@voltro/protocol";
4
+ //#region src/rpc.ts
5
+ var i = n({
6
+ name: "billing.startCheckout",
7
+ input: t.Struct({
8
+ plan: t.String,
9
+ successUrl: t.String,
10
+ cancelUrl: t.String
11
+ }),
12
+ output: t.Struct({ url: t.String }),
13
+ error: e
14
+ }), a = n({
15
+ name: "billing.portalUrl",
16
+ input: t.Struct({ returnUrl: t.String }),
17
+ output: t.Struct({ url: t.String }),
18
+ error: e
19
+ }), o = r({
20
+ name: "billing.subscription",
21
+ input: t.Struct({}),
22
+ output: t.Struct({ subscription: t.NullOr(t.Struct({
23
+ plan: t.String,
24
+ status: t.String,
25
+ quantity: t.Number,
26
+ currentPeriodEnd: t.NullOr(t.String),
27
+ cancelAt: t.NullOr(t.String)
28
+ })) }),
29
+ error: e
30
+ }), s = n({
31
+ name: "billing.reportUsage",
32
+ input: t.Struct({
33
+ key: t.String,
34
+ quantity: t.Number
35
+ }),
36
+ output: t.Struct({ ok: t.Boolean }),
37
+ error: e
38
+ }), c = t.Struct({
39
+ plan: t.String,
40
+ quantity: t.Number,
41
+ prorationMinor: t.Number,
42
+ currency: t.String
43
+ }), l = n({
44
+ name: "billing.changePlan",
45
+ input: t.Struct({ plan: t.String }),
46
+ output: c,
47
+ error: e
48
+ }), u = n({
49
+ name: "billing.changeSeats",
50
+ input: t.Struct({ quantity: t.Number }),
51
+ output: c,
52
+ error: e
53
+ }), d = [
54
+ {
55
+ tag: "billing.startCheckout",
56
+ kind: "action",
57
+ import: {
58
+ module: "@voltro/plugin-billing/rpc",
59
+ name: "startCheckoutDescriptor"
60
+ }
61
+ },
62
+ {
63
+ tag: "billing.portalUrl",
64
+ kind: "action",
65
+ import: {
66
+ module: "@voltro/plugin-billing/rpc",
67
+ name: "portalUrlDescriptor"
68
+ }
69
+ },
70
+ {
71
+ tag: "billing.subscription",
72
+ kind: "query",
73
+ import: {
74
+ module: "@voltro/plugin-billing/rpc",
75
+ name: "subscriptionDescriptor"
76
+ }
77
+ },
78
+ {
79
+ tag: "billing.reportUsage",
80
+ kind: "action",
81
+ import: {
82
+ module: "@voltro/plugin-billing/rpc",
83
+ name: "reportUsageDescriptor"
84
+ }
85
+ },
86
+ {
87
+ tag: "billing.changePlan",
88
+ kind: "action",
89
+ import: {
90
+ module: "@voltro/plugin-billing/rpc",
91
+ name: "changePlanDescriptor"
92
+ }
93
+ },
94
+ {
95
+ tag: "billing.changeSeats",
96
+ kind: "action",
97
+ import: {
98
+ module: "@voltro/plugin-billing/rpc",
99
+ name: "changeSeatsDescriptor"
100
+ }
101
+ }
102
+ ];
103
+ //#endregion
104
+ export { d as billingRpcClientImports, l as changePlanDescriptor, u as changeSeatsDescriptor, a as portalUrlDescriptor, s as reportUsageDescriptor, i as startCheckoutDescriptor, o as subscriptionDescriptor };
@@ -0,0 +1,316 @@
1
+ import { Context } from 'effect';
2
+ import { Effect } from 'effect';
3
+ import { Schema } from 'effect';
4
+
5
+ /**
6
+ * A billing failure. `transient: true` marks failures the service retries
7
+ * (network blip talking to the provider); non-transient failures (a 4xx
8
+ * from the provider, a misconfigured plan) are surfaced immediately. The
9
+ * `billingPlugin` registers this via `errorSchemas` so it decodes TYPED on
10
+ * the client rather than crossing as an untyped defect.
11
+ */
12
+ export declare class BillingError extends BillingError_base {
13
+ }
14
+
15
+ declare const BillingError_base: Schema.TaggedErrorClass<BillingError, "BillingError", {
16
+ readonly _tag: Schema.tag<"BillingError">;
17
+ } & {
18
+ /** Where the failure originated — provider name or an internal stage. */
19
+ source: typeof Schema.String;
20
+ message: typeof Schema.String;
21
+ /** Retryable? The service retries `transient` failures on a Schedule. */
22
+ transient: typeof Schema.Boolean;
23
+ }>;
24
+
25
+ /**
26
+ * The normalized event union a `BillingProvider.normalizeEvent` produces
27
+ * from a verified, decoded provider webhook payload. `BillingService`
28
+ * applies these to the DB rows — switching provider swaps the adapter;
29
+ * the service + rows + entitlement engine are unchanged.
30
+ */
31
+ export declare type BillingEvent = {
32
+ readonly _tag: 'subscriptionUpserted';
33
+ readonly tenantId: string;
34
+ readonly providerSubscriptionId: string;
35
+ readonly plan: PlanId;
36
+ readonly status: SubscriptionStatus;
37
+ /** Seat quantity from the provider event; defaults to 1 when absent. */
38
+ readonly quantity?: number;
39
+ readonly currentPeriodStart?: Date | null;
40
+ readonly currentPeriodEnd: Date | null;
41
+ readonly cancelAt: Date | null;
42
+ } | {
43
+ readonly _tag: 'subscriptionCanceled';
44
+ readonly tenantId: string;
45
+ readonly providerSubscriptionId: string;
46
+ } | {
47
+ readonly _tag: 'invoicePaid';
48
+ readonly tenantId: string;
49
+ readonly providerInvoiceId: string;
50
+ readonly amountMinor: number;
51
+ readonly currency: string;
52
+ } | {
53
+ readonly _tag: 'invoicePaymentFailed';
54
+ readonly tenantId: string;
55
+ readonly providerInvoiceId: string;
56
+ readonly amountMinor: number;
57
+ readonly currency: string;
58
+ } | {
59
+ readonly _tag: 'customerLinked';
60
+ readonly tenantId: string;
61
+ readonly providerCustomerId: string;
62
+ };
63
+
64
+ /** The event tag union — handy for `onEvent` keys. */
65
+ export declare type BillingEventTag = BillingEvent['_tag'];
66
+
67
+ /**
68
+ * Dumb adapter over a billing provider. Holds NO policy — checkout/portal
69
+ * URL minting, usage push, and pure payload→event mapping only. The
70
+ * `BillingService` wraps it with the entitlement engine, the DB rows, and
71
+ * transient retry.
72
+ *
73
+ * `normalizeEvent` receives a payload whose signature was ALREADY verified
74
+ * by @voltro/plugin-webhooks' mounter — this is pure mapping, never a
75
+ * second signature check.
76
+ */
77
+ export declare interface BillingProvider {
78
+ readonly name: string;
79
+ /** Whether the provider supports server-pushed metered usage. Stripe
80
+ * does (subscription-item usage records); a flat-plan provider may not.
81
+ * When false, `BillingService.reportUsage` records locally but never
82
+ * calls `provider.reportUsage`. */
83
+ readonly supportsMeteredUsage: boolean;
84
+ readonly createCheckoutSession: (input: CheckoutInput) => Effect.Effect<{
85
+ url: string;
86
+ }, BillingError>;
87
+ readonly createPortalSession: (input: PortalInput) => Effect.Effect<{
88
+ url: string;
89
+ }, BillingError>;
90
+ readonly reportUsage: (input: UsagePush) => Effect.Effect<void, BillingError>;
91
+ /** Map a verified, decoded webhook payload to a `BillingEvent`, or
92
+ * `null` for an event type this provider doesn't model (handler
93
+ * no-ops, still 200). */
94
+ readonly normalizeEvent: (raw: unknown) => Effect.Effect<BillingEvent | null, BillingError>;
95
+ }
96
+
97
+ /** The billing service Tag — `const billing = yield* BillingService`. */
98
+ export declare class BillingService extends BillingService_base {
99
+ }
100
+
101
+ declare const BillingService_base: Context.TagClass<BillingService, "@voltro/plugin-billing/BillingService", BillingServiceShape>;
102
+
103
+ export declare interface BillingServiceShape {
104
+ /** Read the tenant's subscription row; null when none exists. */
105
+ readonly subscription: (tenantId: string) => Effect.Effect<Subscription | null, BillingError>;
106
+ /** Resolve the tenant's plan id; defaults to `'free'` when no row. */
107
+ readonly plan: (tenantId: string) => Effect.Effect<PlanId, BillingError>;
108
+ /** Pure read — would `cost` units of `key` be allowed? No mutation. */
109
+ readonly checkEntitlement: (tenantId: string, key: string, cost: number) => Effect.Effect<EntitlementDecision, BillingError>;
110
+ /** Check + decrement, atomically. Fails `EntitlementExceeded` over-limit. */
111
+ readonly consumeEntitlement: (tenantId: string, key: string, cost: number) => Effect.Effect<void, BillingError | EntitlementExceeded>;
112
+ /** Record metered usage locally; flushed to the provider in batches. */
113
+ readonly reportUsage: (tenantId: string, key: string, qty: number) => Effect.Effect<void, BillingError>;
114
+ /** Flush all pending local usage counters to the provider. */
115
+ readonly flushUsage: () => Effect.Effect<void, BillingError>;
116
+ /** Mint a provider-hosted checkout URL for `plan`. */
117
+ readonly startCheckout: (input: {
118
+ readonly tenantId: string;
119
+ readonly plan: PlanId;
120
+ readonly successUrl: string;
121
+ readonly cancelUrl: string;
122
+ }) => Effect.Effect<{
123
+ url: string;
124
+ }, BillingError>;
125
+ /** Mint a provider-hosted billing-portal URL. */
126
+ readonly portalUrl: (tenantId: string, returnUrl: string) => Effect.Effect<{
127
+ url: string;
128
+ }, BillingError>;
129
+ /** Apply a normalized provider event to the DB rows (idempotent upsert). */
130
+ readonly applyEvent: (event: BillingEvent) => Effect.Effect<void, BillingError>;
131
+ /**
132
+ * Change the tenant's plan mid-cycle. Computes a TIME-BASED prorated
133
+ * settlement on the amount difference for the unused remainder of the
134
+ * current period (positive = charge on upgrade, negative = credit on
135
+ * downgrade), persists the new plan, and returns the settlement. A same-plan
136
+ * call is a no-op with a zero delta. Fails `BillingError` when the tenant has
137
+ * no subscription or the target plan is unknown.
138
+ */
139
+ readonly changePlan: (tenantId: string, newPlan: PlanId, changeAt?: Date) => Effect.Effect<SubscriptionChange, BillingError>;
140
+ /**
141
+ * Set the tenant's seat quantity mid-cycle. Prorates the amount delta
142
+ * (`unitAmountMinor × Δquantity`) over the unused remainder of the period,
143
+ * persists the new quantity, and returns the settlement. `quantity` must be a
144
+ * positive integer. Fails `BillingError` on no subscription / bad quantity.
145
+ */
146
+ 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
+ }
168
+
169
+ export declare interface CheckoutInput {
170
+ readonly tenantId: string;
171
+ /** Provider price id to subscribe to. */
172
+ readonly priceId: string;
173
+ readonly successUrl: string;
174
+ readonly cancelUrl: string;
175
+ /** Optional existing provider customer id to attach the checkout to. */
176
+ readonly providerCustomerId?: string;
177
+ }
178
+
179
+ /** The free/fallback plan id every app implicitly has. */
180
+ export declare const DEFAULT_PLAN: PlanId;
181
+
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
+ /** The result of a pure (non-mutating) entitlement check. */
203
+ export declare interface EntitlementDecision {
204
+ /** Whether the call would be allowed. */
205
+ readonly allowed: boolean;
206
+ /** The entitlement key checked. */
207
+ readonly entitlement: string;
208
+ /** The static limit for the caller's plan (`Infinity` for `'unlimited'`). */
209
+ readonly limit: number;
210
+ /** How much is already used in the current window. */
211
+ readonly used: number;
212
+ /** The cost the call would charge. */
213
+ readonly cost: number;
214
+ }
215
+
216
+ /**
217
+ * Raised when a caller has exhausted an entitlement (quota). Distinct from
218
+ * the scope/permission `Forbidden` / `ScopeError` — those gate "may you
219
+ * call this proc"; this gates "do you have quota left". Both axes compose.
220
+ * `billingPlugin` registers it via `errorSchemas` so callers can
221
+ * pattern-match on `_tag === 'EntitlementExceeded'`.
222
+ */
223
+ export declare class EntitlementExceeded extends EntitlementExceeded_base {
224
+ }
225
+
226
+ declare const EntitlementExceeded_base: Schema.TaggedErrorClass<EntitlementExceeded, "EntitlementExceeded", {
227
+ readonly _tag: Schema.tag<"EntitlementExceeded">;
228
+ } & {
229
+ /** The entitlement key that was exhausted (`'aiCalls'`). */
230
+ entitlement: typeof Schema.String;
231
+ /** The static limit for the caller's plan. */
232
+ limit: typeof Schema.Number;
233
+ /** How much was already used in the current window. */
234
+ used: typeof Schema.Number;
235
+ /** The cost the rejected call would have charged. */
236
+ cost: typeof Schema.Number;
237
+ }>;
238
+
239
+ /** An entitlement limit: a finite quota or the unbounded `'unlimited'`. */
240
+ export declare type EntitlementLimit = number | 'unlimited';
241
+
242
+ /** A monetary amount: integer minor units + ISO-4217 currency. */
243
+ export declare interface Money {
244
+ /** Integer minor units (cents). Never a float. */
245
+ readonly amountMinor: number;
246
+ /** ISO-4217 currency code, lowercased (`'usd'`, `'eur'`). */
247
+ readonly currency: string;
248
+ }
249
+
250
+ /** One plan/tier: its entitlement limits + (for paid plans) a provider price id. */
251
+ export declare interface PlanConfig {
252
+ /** Provider price id (Stripe `price_…`). Absent for free/zero-cost plans. */
253
+ readonly priceId?: string;
254
+ /** Per-entitlement-key limits. Values are `number | 'unlimited'`. */
255
+ readonly entitlements: Readonly<Record<string, EntitlementLimit>>;
256
+ /** The plan's per-period price in INTEGER minor units (cents) — the unit
257
+ * amount for ONE seat. Multiplied by the subscription's seat `quantity` to
258
+ * get the billed amount, and drives mid-cycle proration. Absent for free
259
+ * plans (treated as 0). Never a float. */
260
+ readonly unitAmountMinor?: number;
261
+ /** ISO-4217 currency for `unitAmountMinor`, lowercased (`'usd'`). Defaults
262
+ * to `'usd'` when a `unitAmountMinor` is set without one. */
263
+ readonly currency?: string;
264
+ }
265
+
266
+ /** Plan id (the key in `billingPlugin({ plans })`). */
267
+ export declare type PlanId = string;
268
+
269
+ export declare interface PortalInput {
270
+ readonly tenantId: string;
271
+ /** Provider customer id whose billing portal to open. */
272
+ readonly providerCustomerId: string;
273
+ readonly returnUrl: string;
274
+ }
275
+
276
+ /** A tenant's subscription row, normalized across providers. */
277
+ export declare interface Subscription {
278
+ readonly tenantId: string;
279
+ readonly provider: string;
280
+ readonly providerSubscriptionId: string;
281
+ readonly plan: PlanId;
282
+ readonly status: SubscriptionStatus;
283
+ /** Per-seat quantity — the billed amount is `plan.unitAmountMinor × quantity`.
284
+ * Defaults to 1 for a single-seat subscription. */
285
+ readonly quantity: number;
286
+ /** Start of the current paid period; null for plans without a period.
287
+ * Anchors mid-cycle proration (the "used" side of the period). */
288
+ readonly currentPeriodStart: Date | null;
289
+ /** When the current paid period ends; null for plans without a period. */
290
+ readonly currentPeriodEnd: Date | null;
291
+ /** When the subscription is scheduled to cancel; null if not scheduled. */
292
+ readonly cancelAt: Date | null;
293
+ }
294
+
295
+ /** The outcome of a mid-cycle plan/seat change — the prorated settlement. */
296
+ export declare interface SubscriptionChange {
297
+ readonly tenantId: string;
298
+ readonly plan: PlanId;
299
+ readonly quantity: number;
300
+ /** Integer minor-unit settlement: > 0 charge, < 0 credit, 0 at boundary. */
301
+ readonly prorationMinor: number;
302
+ readonly currency: string;
303
+ }
304
+
305
+ export declare type SubscriptionStatus = 'active' | 'trialing' | 'pastDue' | 'canceled' | 'incomplete';
306
+
307
+ export declare interface UsagePush {
308
+ readonly tenantId: string;
309
+ readonly entitlementKey: string;
310
+ /** Aggregate quantity to report for the current period. */
311
+ readonly quantity: number;
312
+ /** End of the period the usage falls in (provider timestamp anchor). */
313
+ readonly periodEnd?: Date;
314
+ }
315
+
316
+ export { }
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ import { Context as e, Schema as t } from "effect";
2
+ //#region src/types.ts
3
+ var n = class extends t.TaggedError()("BillingError", {
4
+ source: t.String,
5
+ message: t.String,
6
+ transient: t.Boolean
7
+ }) {}, r = class extends t.TaggedError()("EntitlementExceeded", {
8
+ entitlement: t.String,
9
+ limit: t.Number,
10
+ used: t.Number,
11
+ cost: t.Number
12
+ }) {}, i = "free", a = class extends e.Tag("@voltro/plugin-billing/BillingService")() {};
13
+ //#endregion
14
+ export { n as BillingError, a as BillingService, i as DEFAULT_PLAN, r as EntitlementExceeded };
@@ -0,0 +1,41 @@
1
+ /** The plan id + redirect URLs a checkout needs. */
2
+ export declare interface StartCheckoutInput {
3
+ readonly plan: string;
4
+ readonly successUrl: string;
5
+ readonly cancelUrl: string;
6
+ }
7
+
8
+ /**
9
+ * The caller injects the rpc binding — the generated client's
10
+ * `billing.startCheckout` action. Kept as a plain function type so the web
11
+ * subpath has zero dependency on the rpc client package; the app wires its
12
+ * generated binding in.
13
+ */
14
+ export declare type StartCheckoutRpc = (input: StartCheckoutInput) => Promise<{
15
+ readonly url: string;
16
+ }>;
17
+
18
+ /**
19
+ * React hook that drives provider-hosted checkout. Pass the generated
20
+ * `billing.startCheckout` rpc binding; the hook tracks pending/error and
21
+ * performs the redirect on success.
22
+ *
23
+ * ```tsx
24
+ * const { startCheckout, pending } = useStartCheckout(rpc.billing.startCheckout)
25
+ * <button disabled={pending} onClick={() => startCheckout({
26
+ * plan: 'pro', successUrl: location.href, cancelUrl: location.href,
27
+ * })}>Upgrade</button>
28
+ * ```
29
+ */
30
+ export declare const useStartCheckout: (rpc: StartCheckoutRpc) => UseStartCheckoutResult;
31
+
32
+ export declare interface UseStartCheckoutResult {
33
+ /** Kick off checkout: calls the rpc, then redirects to the provider URL. */
34
+ readonly startCheckout: (input: StartCheckoutInput) => Promise<void>;
35
+ /** True while the rpc is in flight. */
36
+ readonly pending: boolean;
37
+ /** The last error, or null. */
38
+ readonly error: Error | null;
39
+ }
40
+
41
+ export { }
@@ -0,0 +1,22 @@
1
+ import { useCallback as e, useState as t } from "react";
2
+ //#region src/web/index.ts
3
+ var n = () => globalThis.window, r = (r) => {
4
+ let [i, a] = t(!1), [o, s] = t(null);
5
+ return {
6
+ startCheckout: e(async (e) => {
7
+ a(!0), s(null);
8
+ try {
9
+ let { url: t } = await r(e);
10
+ n()?.location.assign(t);
11
+ } catch (e) {
12
+ throw s(e instanceof Error ? e : Error(String(e))), e;
13
+ } finally {
14
+ a(!1);
15
+ }
16
+ }, [r]),
17
+ pending: i,
18
+ error: o
19
+ };
20
+ };
21
+ //#endregion
22
+ export { r as useStartCheckout };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@voltro/plugin-billing",
3
+ "version": "0.1.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.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./web": {
27
+ "types": "./dist/web/index.d.ts",
28
+ "import": "./dist/web/index.js",
29
+ "default": "./dist/web/index.js"
30
+ },
31
+ "./errors": {
32
+ "types": "./dist/types.d.ts",
33
+ "import": "./dist/types.js",
34
+ "default": "./dist/types.js"
35
+ },
36
+ "./rpc": {
37
+ "types": "./dist/rpc.d.ts",
38
+ "import": "./dist/rpc.js",
39
+ "default": "./dist/rpc.js"
40
+ }
41
+ },
42
+ "main": "./dist/index.js",
43
+ "module": "./dist/index.js",
44
+ "types": "./dist/index.d.ts",
45
+ "sideEffects": false,
46
+ "engines": {
47
+ "node": ">=24.0.0"
48
+ },
49
+ "dependencies": {
50
+ "@voltro/database": "0.1.0",
51
+ "@voltro/env": "0.1.0",
52
+ "@voltro/logger": "0.1.0",
53
+ "@voltro/plugin-webhooks": "0.1.0",
54
+ "@voltro/protocol": "0.1.0"
55
+ },
56
+ "peerDependencies": {
57
+ "effect": "^3.21.4",
58
+ "react": "^19.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "react": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ }
68
+ }