@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.
- package/CHANGELOG.md +1215 -0
- package/README.md +7 -0
- package/THIRD-PARTY-NOTICES.md +118 -600
- package/dist/catalog.js +471 -0
- package/dist/dunning.js +209 -0
- package/dist/entitlement.js +41 -0
- package/dist/flushClaim.js +20 -0
- package/dist/index.d.ts +19 -24
- package/dist/index.js +158 -2776
- package/dist/metering.js +74 -0
- package/dist/plans.js +24 -0
- package/dist/providers.js +133 -0
- package/dist/provision.js +153 -0
- package/dist/rpc.d.ts +72 -73
- package/dist/service.js +615 -0
- package/dist/stores.js +425 -0
- package/dist/stripeClient.js +53 -0
- package/dist/stripeProvider.js +352 -0
- package/dist/stripeRest.js +52 -0
- package/dist/table.js +54 -0
- package/dist/types.d.ts +19 -24
- package/dist/types.js +1 -1
- package/dist/webhook.js +45 -0
- package/package.json +7 -7
|
@@ -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.
|
|
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:
|
|
28
|
-
message:
|
|
26
|
+
readonly source: Schema.String;
|
|
27
|
+
readonly message: Schema.String;
|
|
29
28
|
/** Retryable? The service retries `transient` failures on a Schedule. */
|
|
30
|
-
transient:
|
|
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.
|
|
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.
|
|
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:
|
|
432
|
+
readonly entitlement: Schema.String;
|
|
436
433
|
/** The static limit for the caller's plan. */
|
|
437
|
-
limit:
|
|
434
|
+
readonly limit: Schema.Number;
|
|
438
435
|
/** How much was already used in the current window. */
|
|
439
|
-
used:
|
|
436
|
+
readonly used: Schema.Number;
|
|
440
437
|
/** The cost the rejected call would have charged. */
|
|
441
|
-
cost:
|
|
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.
|
|
664
|
-
readonly
|
|
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:
|
|
663
|
+
readonly status: Schema.String;
|
|
669
664
|
/** ISO timestamp at which the grace period expired. */
|
|
670
|
-
lockedSince:
|
|
665
|
+
readonly lockedSince: Schema.String;
|
|
671
666
|
/** `'hard'` (limits fell to the free plan) or `'soft'` (report only). */
|
|
672
|
-
lockout:
|
|
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.
|
|
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 };
|
package/dist/webhook.js
ADDED
|
@@ -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.
|
|
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.
|
|
52
|
-
"@voltro/env": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/plugin-webhooks": "0.
|
|
55
|
-
"@voltro/protocol": "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": "
|
|
58
|
+
"effect": "4.0.1",
|
|
59
59
|
"react": "^19.0.0",
|
|
60
60
|
"stripe": "^22.6.1"
|
|
61
61
|
},
|