@voltro/plugin-billing 0.84.0 → 0.86.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.
@@ -0,0 +1,352 @@
1
+ import { BillingError as e } from "./types.js";
2
+ import { Stripe as t, makeStripeClient as n, periodOf as r, quantityOf as i, stripeCall as a, unixToDate as o } from "./stripeClient.js";
3
+ import { Effect as s } from "effect";
4
+ //#region src/stripeProvider.ts
5
+ var c = {
6
+ active: "active",
7
+ trialing: "trialing",
8
+ past_due: "pastDue",
9
+ unpaid: "pastDue",
10
+ canceled: "canceled",
11
+ incomplete: "incomplete",
12
+ incomplete_expired: "canceled",
13
+ paused: "pastDue"
14
+ }, l = (e) => {
15
+ let t = e.metadata?.tenantId;
16
+ if (typeof t == "string" && t.length > 0) return t;
17
+ let n = e.client_reference_id;
18
+ return typeof n == "string" && n.length > 0 ? n : null;
19
+ }, u = (e) => {
20
+ let t = e.items?.data ?? [];
21
+ return t.find((e) => e.price?.recurring?.usage_type !== "metered") ?? t[0];
22
+ }, d = (e) => {
23
+ let t = u(e)?.price;
24
+ return typeof t?.id == "string" ? t.id : null;
25
+ }, f = (e) => typeof e == "string" ? e : typeof e?.id == "string" ? e.id : null, p = (e, t) => {
26
+ let n = d(e), r = n !== null && t ? t(n) : null;
27
+ if (r !== null) return r;
28
+ let i = e.metadata?.plan;
29
+ return typeof i == "string" && i.length > 0 ? i : null;
30
+ }, m = [
31
+ "checkout.session.completed",
32
+ "customer.created",
33
+ "customer.deleted",
34
+ "customer.subscription.created",
35
+ "customer.subscription.deleted",
36
+ "customer.subscription.paused",
37
+ "customer.subscription.resumed",
38
+ "customer.subscription.updated",
39
+ "invoice.paid",
40
+ "invoice.payment_failed"
41
+ ], h = (e, t) => {
42
+ let n = o(e.created);
43
+ switch (e.type) {
44
+ case "customer.subscription.created":
45
+ case "customer.subscription.updated":
46
+ case "customer.subscription.resumed":
47
+ case "customer.subscription.paused": {
48
+ let a = e.data.object, s = l(a), u = p(a, t), d = c[a.status];
49
+ if (s === null || u === null || d === void 0) return null;
50
+ let f = r(a);
51
+ return {
52
+ _tag: "subscriptionUpserted",
53
+ tenantId: s,
54
+ providerSubscriptionId: a.id,
55
+ plan: u,
56
+ status: d,
57
+ quantity: i(a),
58
+ currentPeriodStart: f.start,
59
+ currentPeriodEnd: f.end,
60
+ cancelAt: o(a.cancel_at),
61
+ occurredAt: n
62
+ };
63
+ }
64
+ case "customer.subscription.deleted": {
65
+ let t = e.data.object, r = l(t);
66
+ return r === null ? null : {
67
+ _tag: "subscriptionCanceled",
68
+ tenantId: r,
69
+ providerSubscriptionId: t.id,
70
+ occurredAt: n
71
+ };
72
+ }
73
+ case "invoice.paid":
74
+ case "invoice.payment_failed": {
75
+ let t = e.data.object, r = l(t) ?? l({ metadata: t.parent?.subscription_details?.metadata ?? null });
76
+ return r === null || typeof t.id != "string" ? null : {
77
+ _tag: e.type === "invoice.paid" ? "invoicePaid" : "invoicePaymentFailed",
78
+ tenantId: r,
79
+ providerInvoiceId: t.id,
80
+ amountMinor: t.amount_due,
81
+ currency: t.currency,
82
+ occurredAt: n
83
+ };
84
+ }
85
+ case "checkout.session.completed": {
86
+ let t = e.data.object, r = l(t), i = f(t.customer);
87
+ return r === null || i === null ? null : {
88
+ _tag: "customerLinked",
89
+ tenantId: r,
90
+ providerCustomerId: i,
91
+ occurredAt: n
92
+ };
93
+ }
94
+ case "customer.deleted": {
95
+ let t = e.data.object;
96
+ return {
97
+ _tag: "customerUnlinked",
98
+ tenantId: l(t),
99
+ providerCustomerId: t.id,
100
+ occurredAt: n
101
+ };
102
+ }
103
+ case "customer.created": {
104
+ let t = e.data.object, r = l(t);
105
+ return r === null ? null : {
106
+ _tag: "customerLinked",
107
+ tenantId: r,
108
+ providerCustomerId: t.id,
109
+ occurredAt: n
110
+ };
111
+ }
112
+ default: return null;
113
+ }
114
+ }, g = {
115
+ prorate: "create_prorations",
116
+ none: "none",
117
+ always_invoice: "always_invoice"
118
+ }, _ = (d) => {
119
+ let _ = n(d), S = d.planForPriceId, C = (e) => {
120
+ let t = r(e);
121
+ return {
122
+ status: c[e.status] ?? "incomplete",
123
+ plan: p(e, S),
124
+ quantity: i(e),
125
+ currentPeriodStart: t.start,
126
+ currentPeriodEnd: t.end,
127
+ cancelAt: o(e.cancel_at)
128
+ };
129
+ }, w = (e) => ({
130
+ ...C(e),
131
+ providerSubscriptionId: e.id,
132
+ providerCustomerId: f(e.customer),
133
+ tenantId: l(e),
134
+ priceIds: (e.items?.data ?? []).map((e) => e.price?.id).filter((e) => typeof e == "string")
135
+ }), T = d.closeSettleSeconds ?? 600, E = {
136
+ periodEnd: (e) => _.subscriptions.update(e, {
137
+ cancel_at_period_end: !0,
138
+ expand: ["items.data.price"]
139
+ }),
140
+ close: (e) => _.subscriptions.update(e, {
141
+ cancel_at: Math.ceil(Date.now() / 1e3) + T,
142
+ proration_behavior: "create_prorations",
143
+ expand: ["items.data.price"]
144
+ })
145
+ }, D = (e) => _.subscriptions.retrieve(e, { expand: ["items.data.price"] }), O = (e, t) => {
146
+ let n = r(e), a = u(e)?.price;
147
+ return {
148
+ plan: p(e, S) ?? "free",
149
+ quantity: i(e),
150
+ prorationMinor: t,
151
+ currency: (a?.currency ?? "usd").toLowerCase(),
152
+ currentPeriodStart: n.start,
153
+ currentPeriodEnd: n.end
154
+ };
155
+ }, k = (e, t) => {
156
+ let n = u(e)?.id;
157
+ return {
158
+ items: [{
159
+ ...n === void 0 ? {} : { id: n },
160
+ ...t.priceId === void 0 ? {} : { price: t.priceId },
161
+ ...t.quantity === void 0 ? {} : { quantity: t.quantity }
162
+ }],
163
+ proration_behavior: g[t.proration ?? "prorate"],
164
+ ...t.priceId !== void 0 && S ? { metadata: {
165
+ ...e.metadata,
166
+ plan: S(t.priceId) ?? ""
167
+ } } : {}
168
+ };
169
+ };
170
+ return {
171
+ name: "stripe",
172
+ supportsMeteredUsage: d.supportsMeteredUsage ?? !0,
173
+ usageRequiresCustomer: !0,
174
+ createCheckoutSession: (e) => a("stripe.checkout.sessions.create", async () => {
175
+ let t = await _.checkout.sessions.create({
176
+ mode: "subscription",
177
+ line_items: [{
178
+ price: e.priceId,
179
+ quantity: e.quantity ?? 1,
180
+ ...e.adjustableQuantity ? { adjustable_quantity: {
181
+ enabled: !0,
182
+ minimum: e.adjustableQuantity.min,
183
+ maximum: e.adjustableQuantity.max
184
+ } } : {}
185
+ }, ...(e.meteredPriceIds ?? []).map((e) => ({ price: e }))],
186
+ success_url: e.successUrl,
187
+ cancel_url: e.cancelUrl,
188
+ client_reference_id: e.tenantId,
189
+ metadata: { tenantId: e.tenantId },
190
+ subscription_data: {
191
+ metadata: { tenantId: e.tenantId },
192
+ ...e.trialDays === void 0 ? {} : { trial_period_days: e.trialDays }
193
+ },
194
+ ...e.providerCustomerId === void 0 ? {} : { customer: e.providerCustomerId },
195
+ automatic_tax: { enabled: !0 },
196
+ tax_id_collection: { enabled: !0 },
197
+ ...e.providerCustomerId === void 0 ? {} : { customer_update: {
198
+ name: "auto",
199
+ address: "auto"
200
+ } },
201
+ allow_promotion_codes: !0
202
+ });
203
+ if (typeof t.url != "string") throw Error("checkout session has no url");
204
+ return { url: t.url };
205
+ }),
206
+ createPortalSession: (e) => a("stripe.billingPortal.sessions.create", async () => ({ url: (await _.billingPortal.sessions.create({
207
+ customer: e.providerCustomerId,
208
+ return_url: e.returnUrl
209
+ })).url })),
210
+ reportUsage: (t) => {
211
+ let n = t.providerCustomerId;
212
+ return n === void 0 ? s.fail(new e({
213
+ source: "stripe.billing.meterEvents.create",
214
+ message: `no Stripe customer linked for tenant '${t.tenantId}'`,
215
+ transient: !1
216
+ })) : a("stripe.billing.meterEvents.create", async () => {
217
+ try {
218
+ await _.billing.meterEvents.create({
219
+ event_name: t.entitlementKey,
220
+ identifier: t.idempotencyKey ?? `${t.tenantId}:${t.entitlementKey}:${t.periodEnd?.toISOString() ?? "current"}`,
221
+ payload: {
222
+ value: String(t.quantity),
223
+ stripe_customer_id: n
224
+ }
225
+ });
226
+ } catch (e) {
227
+ if (v(e)) return;
228
+ throw e;
229
+ }
230
+ });
231
+ },
232
+ updateSubscription: (e) => a("stripe.subscriptions.update", async () => {
233
+ let t = await D(e.providerSubscriptionId), n = await _.subscriptions.update(e.providerSubscriptionId, k(t, e)), r = await x(_, n);
234
+ return O(n, r);
235
+ }),
236
+ previewSubscriptionChange: (e) => a("stripe.invoices.createPreview", async () => {
237
+ let t = await D(e.providerSubscriptionId), n = k(t, e), r = (await _.invoices.createPreview({
238
+ subscription: e.providerSubscriptionId,
239
+ subscription_details: {
240
+ items: n.items,
241
+ ...n.proration_behavior === void 0 ? {} : { proration_behavior: n.proration_behavior }
242
+ }
243
+ })).lines.data.filter((e) => e.parent?.subscription_item_details?.proration === !0).reduce((e, t) => e + t.amount, 0);
244
+ return {
245
+ ...O(t, r),
246
+ ...e.quantity === void 0 ? {} : { quantity: e.quantity },
247
+ ...e.priceId !== void 0 && S ? { plan: S(e.priceId) ?? "free" } : {}
248
+ };
249
+ }),
250
+ fetchSubscription: (e) => a("stripe.subscriptions.retrieve", async () => {
251
+ try {
252
+ return C(await D(e));
253
+ } catch (e) {
254
+ if (e instanceof t.errors.StripeInvalidRequestError && e.statusCode === 404) return null;
255
+ throw e;
256
+ }
257
+ }),
258
+ listSubscriptions: (e) => a("stripe.subscriptions.list", async () => {
259
+ let t = [];
260
+ for await (let n of _.subscriptions.list({
261
+ limit: 100,
262
+ expand: ["data.items.data.price"],
263
+ ...e.providerCustomerId === void 0 ? {} : { customer: e.providerCustomerId }
264
+ })) t.push(w(n));
265
+ return t;
266
+ }),
267
+ cancelSubscription: (e) => a("stripe.subscriptions.cancel", async () => C(await E[e.mode](e.providerSubscriptionId))),
268
+ resumeSubscription: (e) => a("stripe.subscriptions.update", async () => C(await _.subscriptions.update(e, {
269
+ cancel_at_period_end: !1,
270
+ expand: ["items.data.price"]
271
+ }))),
272
+ ensureCustomer: (e) => a("stripe.customers.create", async () => {
273
+ let t = (await _.customers.search({
274
+ query: `metadata['tenantId']:'${e.tenantId.replace(/['\\]/g, "")}'`,
275
+ limit: 2
276
+ })).data.find((t) => t.metadata?.tenantId === e.tenantId);
277
+ return t === void 0 ? (await _.customers.create({ metadata: { tenantId: e.tenantId } }, { idempotencyKey: `voltro-billing-customer:${e.tenantId}` })).id : t.id;
278
+ }),
279
+ expireOpenCheckouts: (e) => a("stripe.checkout.sessions.expire", async () => {
280
+ let t = 0;
281
+ for await (let n of _.checkout.sessions.list({
282
+ customer: e.providerCustomerId,
283
+ status: "open",
284
+ limit: 100
285
+ })) await _.checkout.sessions.expire(n.id), t += 1;
286
+ return t;
287
+ }),
288
+ addSubscriptionPrices: (e) => a("stripe.subscriptionItems.create", async () => {
289
+ for (let t of e.priceIds) await _.subscriptionItems.create({
290
+ subscription: e.providerSubscriptionId,
291
+ price: t,
292
+ proration_behavior: "none"
293
+ });
294
+ }),
295
+ customerEmail: (e) => a("stripe.customers.retrieve", async () => {
296
+ let t = await _.customers.retrieve(e);
297
+ return t.deleted === !0 ? null : typeof t.email == "string" && t.email.length > 0 ? t.email : null;
298
+ }),
299
+ listInvoices: (e) => a("stripe.invoices.list", async () => (await _.invoices.list({
300
+ customer: e.providerCustomerId,
301
+ limit: e.limit ?? 20
302
+ })).data.map((e) => ({
303
+ providerInvoiceId: e.id ?? "",
304
+ amountMinor: e.amount_due,
305
+ currency: e.currency,
306
+ status: e.status ?? "unknown",
307
+ createdAt: o(e.created),
308
+ hostedUrl: e.hosted_invoice_url ?? null,
309
+ pdfUrl: e.invoice_pdf ?? null
310
+ }))),
311
+ verifyWebhook: (t) => s.try({
312
+ try: () => _.webhooks.constructEvent(Buffer.from(t.rawBody), t.signatureHeader, t.secret),
313
+ catch: (t) => new e({
314
+ source: "stripe.webhooks.constructEvent",
315
+ message: String(t?.message ?? t),
316
+ transient: !1
317
+ })
318
+ }),
319
+ eventContext: (e) => b(e) ? {
320
+ providerCustomerId: y(e.data.object),
321
+ createdAt: o(e.created)
322
+ } : {
323
+ providerCustomerId: null,
324
+ createdAt: null
325
+ },
326
+ listUndeliveredEvents: () => a("stripe.events.list", async () => {
327
+ let e = [];
328
+ for await (let t of _.events.list({
329
+ delivery_success: !1,
330
+ types: [...m],
331
+ limit: 100
332
+ })) e.push({
333
+ id: t.id,
334
+ providerCustomerId: y(t.data.object),
335
+ createdAt: o(t.created)
336
+ });
337
+ return e;
338
+ }),
339
+ normalizeEvent: (e) => s.succeed(b(e) ? h(e, S) : null)
340
+ };
341
+ }, v = (e) => e instanceof t.errors.StripeInvalidRequestError && typeof e.message == "string" && e.message.startsWith("An event already exists with identifier"), y = (e) => {
342
+ let t = e;
343
+ return t.object === "customer" ? typeof t.id == "string" ? t.id : null : f(t.customer ?? null);
344
+ }, b = (e) => typeof e == "object" && !!e && typeof e.type == "string" && typeof e.data == "object", x = async (e, t) => {
345
+ try {
346
+ return (await e.invoices.createPreview({ subscription: t.id })).lines.data.filter((e) => e.parent?.subscription_item_details?.proration === !0).reduce((e, t) => e + t.amount, 0);
347
+ } catch {
348
+ return 0;
349
+ }
350
+ };
351
+ //#endregion
352
+ export { m as STRIPE_WEBHOOK_EVENTS, v as isDuplicateMeterEvent, h as normalizeStripeEvent, _ as stripeProvider };
@@ -0,0 +1,52 @@
1
+ import { STRIPE_API_VERSION as e } from "./catalog.js";
2
+ //#region src/stripeRest.ts
3
+ var t = (e) => {
4
+ let t = [], n = (e, r) => {
5
+ if (r !== void 0) {
6
+ if (r === null) t.push(`${encodeURIComponent(e)}=`);
7
+ else if (Array.isArray(r)) r.forEach((t, r) => n(`${e}[${r}]`, t));
8
+ else if (typeof r == "object") for (let [t, i] of Object.entries(r)) n(`${e}[${t}]`, i);
9
+ else t.push(`${encodeURIComponent(e)}=${encodeURIComponent(String(r))}`);
10
+ }
11
+ };
12
+ for (let [t, r] of Object.entries(e)) n(t, r);
13
+ return t.join("&");
14
+ }, n = (e) => e.replace(/\b(sk|rk|rkcs|pk)_(test|live)_[A-Za-z0-9*]+/g, "$1_$2_[REDACTED]").replace(/\bwhsec_[A-Za-z0-9]+/g, "whsec_[REDACTED]"), r = class extends Error {
15
+ status;
16
+ constructor(e, t) {
17
+ super(t), this.status = e;
18
+ }
19
+ }, i = (i, a = fetch) => {
20
+ let o = async (o, s, c = {}) => {
21
+ if (!/^\/v[12]\/[A-Za-z0-9_/.-]+$/.test(s)) throw new r(0, `refusing request path ${JSON.stringify(s)}`);
22
+ let l = t(c), u = await a(`https://api.stripe.com${s}${o === "GET" && l.length > 0 ? `?${l}` : ""}`, {
23
+ method: o,
24
+ headers: {
25
+ authorization: `Bearer ${i}`,
26
+ "stripe-version": e,
27
+ ...o === "POST" ? { "content-type": "application/x-www-form-urlencoded" } : {}
28
+ },
29
+ ...o === "POST" ? { body: l } : {}
30
+ }), d = await u.json();
31
+ if (!u.ok) throw new r(u.status, n(d.error?.message ?? `HTTP ${u.status}`));
32
+ return d;
33
+ };
34
+ return {
35
+ get: (e, t) => o("GET", e, t),
36
+ post: (e, t) => o("POST", e, t),
37
+ list: async (e, t = {}) => {
38
+ let n = [], r;
39
+ for (;;) {
40
+ let i = await o("GET", e, {
41
+ limit: 100,
42
+ ...t,
43
+ ...r === void 0 ? {} : { starting_after: r }
44
+ });
45
+ if (n.push(...i.data), !i.has_more || i.data.length === 0) return n;
46
+ r = i.data[i.data.length - 1].id;
47
+ }
48
+ }
49
+ };
50
+ };
51
+ //#endregion
52
+ export { r as StripeRestError, t as formEncode, n as redactStripeKeys, i as stripeRest };
package/dist/table.js ADDED
@@ -0,0 +1,54 @@
1
+ import { id as e, integer as t, table as n, text as r, timestamp as i } from "@voltro/database";
2
+ //#region src/table.ts
3
+ var a = "_voltro_billing_customers", o = "_voltro_billing_subscriptions", s = "_voltro_billing_invoices", c = "_voltro_billing_usage", l = n(a, {
4
+ id: e({ prefix: "bcust" }),
5
+ tenantId: r(),
6
+ provider: r(),
7
+ providerCustomerId: r(),
8
+ createdAt: i(),
9
+ updatedAt: i()
10
+ }).index("byBillingCustomerTenant", ["tenantId"]).index("byBillingProviderCustomer", ["providerCustomerId"]), u = n(o, {
11
+ id: e({ prefix: "bsub" }),
12
+ tenantId: r(),
13
+ provider: r(),
14
+ providerSubscriptionId: r(),
15
+ plan: r(),
16
+ status: r(),
17
+ quantity: t().default(1),
18
+ currentPeriodStart: i().nullable(),
19
+ currentPeriodEnd: i().nullable(),
20
+ cancelAt: i().nullable(),
21
+ pastDueSince: i().nullable(),
22
+ statusEventAt: i().nullable(),
23
+ createdAt: i(),
24
+ updatedAt: i()
25
+ }).index("byBillingSubscriptionTenant", ["tenantId"]).index("byBillingProviderSubscription", ["providerSubscriptionId"]).index("byBillingSubscriptionStatus", ["status"]), d = n(s, {
26
+ id: e({ prefix: "binv" }),
27
+ tenantId: r(),
28
+ provider: r(),
29
+ providerInvoiceId: r(),
30
+ amountMinor: t(),
31
+ currency: r(),
32
+ status: r(),
33
+ statusEventAt: i().nullable(),
34
+ createdAt: i()
35
+ }).index("byBillingInvoiceTenant", ["tenantId"]).index("byBillingProviderInvoice", ["providerInvoiceId"]), f = n(c, {
36
+ id: e({ prefix: "buse" }),
37
+ tenantId: r().maxLength(191),
38
+ entitlementKey: r().maxLength(191),
39
+ period: r().maxLength(16),
40
+ used: t().default(0),
41
+ reportedToProvider: t().default(0),
42
+ updatedAt: i()
43
+ }).unique("byBillingUsageWindow", [
44
+ "tenantId",
45
+ "entitlementKey",
46
+ "period"
47
+ ]), p = () => [
48
+ l,
49
+ u,
50
+ d,
51
+ f
52
+ ];
53
+ //#endregion
54
+ export { a as BILLING_CUSTOMERS_TABLE, s as BILLING_INVOICES_TABLE, o as BILLING_SUBSCRIPTIONS_TABLE, c as BILLING_USAGE_TABLE, l as billingCustomersTable, d as billingInvoicesTable, u as billingSubscriptionsTable, p as billingTables, f as billingUsageTable };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Context } from 'effect';
2
2
  import { Effect } from 'effect';
3
3
  import { Schema } from 'effect';
4
+ import { YieldableError } from 'effect/Cause';
4
5
 
5
6
  /**
6
7
  * What a billing call would do to what the tenant pays — the unit an app's
@@ -20,15 +21,13 @@ export declare type BillingAction = 'checkout' | 'change' | 'resume' | 'portal'
20
21
  export declare class BillingError extends BillingError_base {
21
22
  }
22
23
 
23
- declare const BillingError_base: Schema.TaggedErrorClass<BillingError, "BillingError", {
24
- readonly _tag: Schema.tag<"BillingError">;
25
- } & {
24
+ declare const BillingError_base: Schema.Class<BillingError, Schema.TaggedStruct<"BillingError", {
26
25
  /** Where the failure originated — provider name or an internal stage. */
27
- source: typeof Schema.String;
28
- message: typeof Schema.String;
26
+ readonly source: Schema.String;
27
+ readonly message: Schema.String;
29
28
  /** Retryable? The service retries `transient` failures on a Schedule. */
30
- transient: typeof Schema.Boolean;
31
- }>;
29
+ readonly transient: Schema.Boolean;
30
+ }>, YieldableError>;
32
31
 
33
32
  /**
34
33
  * The normalized event union a `BillingProvider.normalizeEvent` produces
@@ -229,7 +228,7 @@ export declare interface BillingProvider {
229
228
  export declare class BillingService extends BillingService_base {
230
229
  }
231
230
 
232
- declare const BillingService_base: Context.TagClass<BillingService, "@voltro/plugin-billing/BillingService", BillingServiceShape>;
231
+ declare const BillingService_base: Context.ServiceClass<BillingService, "@voltro/plugin-billing/BillingService", BillingServiceShape>;
233
232
 
234
233
  export declare interface BillingServiceShape {
235
234
  /** Read the tenant's subscription row; null when none exists. */
@@ -428,18 +427,16 @@ export declare interface EntitlementDecision {
428
427
  export declare class EntitlementExceeded extends EntitlementExceeded_base {
429
428
  }
430
429
 
431
- declare const EntitlementExceeded_base: Schema.TaggedErrorClass<EntitlementExceeded, "EntitlementExceeded", {
432
- readonly _tag: Schema.tag<"EntitlementExceeded">;
433
- } & {
430
+ declare const EntitlementExceeded_base: Schema.Class<EntitlementExceeded, Schema.TaggedStruct<"EntitlementExceeded", {
434
431
  /** The entitlement key that was exhausted (`'aiCalls'`). */
435
- entitlement: typeof Schema.String;
432
+ readonly entitlement: Schema.String;
436
433
  /** The static limit for the caller's plan. */
437
- limit: typeof Schema.Number;
434
+ readonly limit: Schema.Number;
438
435
  /** How much was already used in the current window. */
439
- used: typeof Schema.Number;
436
+ readonly used: Schema.Number;
440
437
  /** The cost the rejected call would have charged. */
441
- cost: typeof Schema.Number;
442
- }>;
438
+ readonly cost: Schema.Number;
439
+ }>, YieldableError>;
443
440
 
444
441
  /** An entitlement limit: a finite quota or the unbounded `'unlimited'`. */
445
442
  export declare type EntitlementLimit = number | 'unlimited';
@@ -660,17 +657,15 @@ export declare interface SubscriptionChangeResult {
660
657
  export declare class SubscriptionLocked extends SubscriptionLocked_base {
661
658
  }
662
659
 
663
- declare const SubscriptionLocked_base: Schema.TaggedErrorClass<SubscriptionLocked, "SubscriptionLocked", {
664
- readonly _tag: Schema.tag<"SubscriptionLocked">;
665
- } & {
666
- tenantId: typeof Schema.String;
660
+ declare const SubscriptionLocked_base: Schema.Class<SubscriptionLocked, Schema.TaggedStruct<"SubscriptionLocked", {
661
+ readonly tenantId: Schema.String;
667
662
  /** The subscription status at the time of the refusal (`'pastDue'`). */
668
- status: typeof Schema.String;
663
+ readonly status: Schema.String;
669
664
  /** ISO timestamp at which the grace period expired. */
670
- lockedSince: typeof Schema.String;
665
+ readonly lockedSince: Schema.String;
671
666
  /** `'hard'` (limits fell to the free plan) or `'soft'` (report only). */
672
- lockout: typeof Schema.String;
673
- }>;
667
+ readonly lockout: Schema.String;
668
+ }>, YieldableError>;
674
669
 
675
670
  export declare type SubscriptionStatus = 'active' | 'trialing' | 'pastDue' | 'canceled' | 'incomplete';
676
671
 
package/dist/types.js CHANGED
@@ -14,6 +14,6 @@ var n = class extends t.TaggedError()("BillingError", {
14
14
  limit: t.Number,
15
15
  used: t.Number,
16
16
  cost: t.Number
17
- }) {}, a = "free", o = class extends e.Tag("@voltro/plugin-billing/BillingService")() {};
17
+ }) {}, a = "free", o = class extends e.Service()("@voltro/plugin-billing/BillingService") {};
18
18
  //#endregion
19
19
  export { n as BillingError, o as BillingService, a as DEFAULT_PLAN, i as EntitlementExceeded, r as SubscriptionLocked };
@@ -0,0 +1,45 @@
1
+ import { Effect as e, Schema as t } from "effect";
2
+ import { createLogger as n } from "@voltro/logger";
3
+ import { defineIncomingWebhook as r, mountIncomingWebhook as i } from "@voltro/plugin-webhooks";
4
+ import { stripeProvider as a } from "@voltro/plugin-webhooks/providers";
5
+ //#region src/webhook.ts
6
+ var o = n({ scope: "@voltro/plugin-billing" }), s = "billing", c = "/billing/webhook", l = 3e5, u = (n) => r({
7
+ id: s,
8
+ path: c,
9
+ provider: a(),
10
+ payload: t.Unknown,
11
+ handler: async (t) => {
12
+ let r = n.provider.verifyWebhook !== void 0 && n.webhookSecret !== null && n.webhookSecret !== void 0 ? await e.runPromise(n.provider.verifyWebhook({
13
+ rawBody: t.rawBody,
14
+ signatureHeader: t.headers["stripe-signature"] ?? "",
15
+ secret: n.webhookSecret ?? ""
16
+ })) : t.body, i = await e.runPromise(n.provider.normalizeEvent(r)), a = n.provider.eventContext?.(r) ?? {
17
+ providerCustomerId: null,
18
+ createdAt: null
19
+ };
20
+ if (i === null && a.providerCustomerId === null) return;
21
+ let s = i?._tag === "subscriptionUpserted" || i?._tag === "subscriptionCanceled", c = a.providerCustomerId !== null && i?._tag !== "customerUnlinked", l = [];
22
+ if (i !== null && !(s && c) && (await e.runPromise(n.service.applyEvent(i)), l.push(i)), c) {
23
+ let t = await e.runPromise(n.service.syncCustomer(a.providerCustomerId, i !== null && "tenantId" in i && typeof i.tenantId == "string" ? i.tenantId : void 0));
24
+ l.push(...t.events);
25
+ }
26
+ a.createdAt !== null && Date.now() - a.createdAt.getTime() > 3e5 && n.onLateDelivery?.();
27
+ for (let t of l) {
28
+ let r = n.onEvent?.[t._tag];
29
+ r !== void 0 && await e.runPromise(r(t).pipe(e.tapCause((n) => e.sync(() => {
30
+ o.error("billing onEvent side effect failed — answering 500 so the provider redelivers", {
31
+ event: t._tag,
32
+ cause: String(n)
33
+ });
34
+ }))));
35
+ }
36
+ }
37
+ }), d = (e) => i(u(e), {
38
+ resolveSecret: () => Promise.resolve(e.webhookSecret),
39
+ ...e.idempotencyCache === void 0 ? {} : { idempotencyCache: e.idempotencyCache },
40
+ log: (e) => {
41
+ e.status >= 400 && o.warn("billing webhook rejected", { ...e });
42
+ }
43
+ });
44
+ //#endregion
45
+ export { s as BILLING_WEBHOOK_ID, c as BILLING_WEBHOOK_PATH, l as REDELIVERY_AGE_MS, u as billingWebhookDescriptor, d as mountBillingWebhook };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-billing",
3
- "version": "0.84.0",
3
+ "version": "0.86.0",
4
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",
@@ -48,14 +48,14 @@
48
48
  "node": ">=24.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@voltro/database": "0.84.0",
52
- "@voltro/env": "0.84.0",
53
- "@voltro/logger": "0.84.0",
54
- "@voltro/plugin-webhooks": "0.84.0",
55
- "@voltro/protocol": "0.84.0"
51
+ "@voltro/database": "0.86.0",
52
+ "@voltro/env": "0.86.0",
53
+ "@voltro/logger": "0.86.0",
54
+ "@voltro/plugin-webhooks": "0.86.0",
55
+ "@voltro/protocol": "0.86.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "effect": "^3.22.2",
58
+ "effect": "4.0.1",
59
59
  "react": "^19.0.0",
60
60
  "stripe": "^22.6.1"
61
61
  },