@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/dist/stores.js ADDED
@@ -0,0 +1,425 @@
1
+ import { BillingError as e } from "./types.js";
2
+ import { BILLING_CUSTOMERS_TABLE as t, BILLING_INVOICES_TABLE as n, BILLING_SUBSCRIPTIONS_TABLE as r, BILLING_USAGE_TABLE as i } from "./table.js";
3
+ import { dataStoreDunningNoticeStore as a, memoryDunningNoticeStore as o } from "./dunning.js";
4
+ import { Effect as s } from "effect";
5
+ import { and as c, eq as l } from "@voltro/database";
6
+ import { createLogger as u } from "@voltro/logger";
7
+ //#region src/stores.ts
8
+ var d = (e) => `${e}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, f = u({ scope: "@voltro/plugin-billing" }), p = (t, n) => s.tryPromise({
9
+ try: t,
10
+ catch: (t) => (f.warn("billing store operation failed", {
11
+ source: n,
12
+ detail: String(t?.message ?? t)
13
+ }), new e({
14
+ source: n,
15
+ message: `${n} failed`,
16
+ transient: !0
17
+ }))
18
+ }), m = (e) => ({
19
+ table: e.table,
20
+ predicate: e.predicate,
21
+ order: e.order ?? [],
22
+ take: e.take,
23
+ skip: void 0,
24
+ projection: void 0
25
+ }), h = () => {
26
+ let e = /* @__PURE__ */ new Map();
27
+ return {
28
+ upsert: (t) => s.sync(() => {
29
+ e.set(t.tenantId, t);
30
+ }),
31
+ getByTenant: (t) => s.sync(() => e.get(t) ?? null),
32
+ getByProviderCustomerId: (t) => s.sync(() => [...e.values()].find((e) => e.providerCustomerId === t) ?? null),
33
+ list: () => s.sync(() => [...e.values()]),
34
+ removeByProviderCustomerId: (t) => s.sync(() => {
35
+ for (let [n, r] of e) if (r.providerCustomerId === t) return e.delete(n), n;
36
+ return null;
37
+ })
38
+ };
39
+ }, g = (e) => ({
40
+ tenantId: String(e.tenantId),
41
+ provider: String(e.provider),
42
+ providerCustomerId: String(e.providerCustomerId)
43
+ }), _ = (e) => ({
44
+ upsert: (n) => p(async () => {
45
+ let r = await e.query(m({
46
+ table: t,
47
+ predicate: l("tenantId", n.tenantId),
48
+ take: 1
49
+ })), i = /* @__PURE__ */ new Date();
50
+ r[0] ? await e.update(t, String(r[0].id), {
51
+ provider: n.provider,
52
+ providerCustomerId: n.providerCustomerId,
53
+ updatedAt: i
54
+ }) : await e.insert(t, {
55
+ id: d("bcust"),
56
+ ...n,
57
+ createdAt: i,
58
+ updatedAt: i
59
+ });
60
+ }, "customerStore"),
61
+ getByTenant: (n) => p(async () => {
62
+ let r = await e.query(m({
63
+ table: t,
64
+ predicate: l("tenantId", n),
65
+ take: 1
66
+ }));
67
+ return r[0] ? g(r[0]) : null;
68
+ }, "customerStore"),
69
+ getByProviderCustomerId: (n) => p(async () => {
70
+ let r = await e.query(m({
71
+ table: t,
72
+ predicate: l("providerCustomerId", n),
73
+ take: 1
74
+ }));
75
+ return r[0] ? g(r[0]) : null;
76
+ }, "customerStore"),
77
+ list: () => p(async () => (await e.query(m({ table: t }))).map(g), "customerStore"),
78
+ removeByProviderCustomerId: (n) => p(async () => {
79
+ let r = await e.query(m({
80
+ table: t,
81
+ predicate: l("providerCustomerId", n)
82
+ })), i = null;
83
+ for (let n of r) i = String(n.tenantId), await e.delete(t, String(n.id));
84
+ return i;
85
+ }, "customerStore")
86
+ }), v = (e) => e === "canceled", y = /* @__PURE__ */ new Set([
87
+ "active",
88
+ "trialing",
89
+ "pastDue",
90
+ "incomplete"
91
+ ]), b = (e) => y.has(e), x = (e) => e.find((e) => b(e.status)) ?? e[0] ?? null, S = (e, t) => e === null ? !1 : t === null || t.getTime() < e.getTime(), C = () => {
92
+ let e = /* @__PURE__ */ new Map(), t = 0, n = (t) => [...e.values()].filter((e) => e.tenantId === t).sort((e, t) => t.touched - e.touched), r = ({ touched: e, ...t }) => t;
93
+ return {
94
+ upsert: (n) => s.sync(() => {
95
+ let r = e.get(n.providerSubscriptionId);
96
+ r && S(r.statusEventAt, n.statusEventAt) || r && v(r.status) && !v(n.status) || e.set(n.providerSubscriptionId, {
97
+ ...n,
98
+ pastDueSince: r ? r.pastDueSince : n.pastDueSince,
99
+ touched: ++t
100
+ });
101
+ }),
102
+ getByTenant: (e) => s.sync(() => {
103
+ let t = x(n(e));
104
+ return t ? r(t) : null;
105
+ }),
106
+ listByTenant: (e) => s.sync(() => n(e).map(r)),
107
+ listByStatus: (t) => s.sync(() => [...e.values()].filter((e) => e.status === t).map(r)),
108
+ markCanceled: (n) => s.sync(() => {
109
+ let r = e.get(n);
110
+ r && e.set(n, {
111
+ ...r,
112
+ status: "canceled",
113
+ touched: ++t
114
+ });
115
+ }),
116
+ patchByTenant: (r, i) => s.sync(() => {
117
+ let a = x(n(r));
118
+ a && e.set(a.providerSubscriptionId, {
119
+ ...a,
120
+ ...i,
121
+ touched: ++t
122
+ });
123
+ }),
124
+ setStatus: (n, r) => s.sync(() => {
125
+ let i = e.get(n);
126
+ i && e.set(n, {
127
+ ...i,
128
+ status: r,
129
+ touched: ++t
130
+ });
131
+ })
132
+ };
133
+ }, w = (e) => ({
134
+ tenantId: String(e.tenantId),
135
+ provider: String(e.provider),
136
+ providerSubscriptionId: String(e.providerSubscriptionId),
137
+ plan: String(e.plan),
138
+ status: String(e.status),
139
+ quantity: e.quantity == null ? 1 : Number(e.quantity),
140
+ currentPeriodStart: e.currentPeriodStart == null ? null : new Date(e.currentPeriodStart),
141
+ currentPeriodEnd: e.currentPeriodEnd == null ? null : new Date(e.currentPeriodEnd),
142
+ cancelAt: e.cancelAt == null ? null : new Date(e.cancelAt),
143
+ pastDueSince: e.pastDueSince == null ? null : new Date(e.pastDueSince),
144
+ statusEventAt: e.statusEventAt == null ? null : new Date(e.statusEventAt)
145
+ }), T = async (e, t) => (await e.query(m({
146
+ table: "_voltro_billing_subscriptions",
147
+ predicate: l("providerSubscriptionId", t),
148
+ take: 1
149
+ })))[0] ?? null, E = async (e, t) => (await e.query(m({
150
+ table: r,
151
+ predicate: l("tenantId", t),
152
+ order: [{
153
+ column: "updatedAt",
154
+ direction: "desc"
155
+ }]
156
+ }))).map(w), D = (e) => ({
157
+ upsert: (t) => p(async () => {
158
+ let n = await T(e, t.providerSubscriptionId), i = /* @__PURE__ */ new Date();
159
+ if (n) {
160
+ if (S(n.statusEventAt == null ? null : new Date(n.statusEventAt), t.statusEventAt) || v(String(n.status)) && !v(t.status)) return;
161
+ await e.update(r, String(n.id), {
162
+ tenantId: t.tenantId,
163
+ provider: t.provider,
164
+ plan: t.plan,
165
+ status: t.status,
166
+ quantity: t.quantity,
167
+ currentPeriodStart: t.currentPeriodStart,
168
+ currentPeriodEnd: t.currentPeriodEnd,
169
+ cancelAt: t.cancelAt,
170
+ statusEventAt: t.statusEventAt,
171
+ updatedAt: i
172
+ });
173
+ } else await e.insert(r, {
174
+ id: d("bsub"),
175
+ tenantId: t.tenantId,
176
+ provider: t.provider,
177
+ providerSubscriptionId: t.providerSubscriptionId,
178
+ plan: t.plan,
179
+ status: t.status,
180
+ quantity: t.quantity,
181
+ currentPeriodStart: t.currentPeriodStart,
182
+ currentPeriodEnd: t.currentPeriodEnd,
183
+ cancelAt: t.cancelAt,
184
+ pastDueSince: null,
185
+ statusEventAt: t.statusEventAt,
186
+ createdAt: i,
187
+ updatedAt: i
188
+ });
189
+ }, "subscriptionStore"),
190
+ getByTenant: (t) => p(async () => x(await E(e, t)), "subscriptionStore"),
191
+ listByTenant: (t) => p(() => E(e, t), "subscriptionStore"),
192
+ listByStatus: (t) => p(async () => (await e.query(m({
193
+ table: r,
194
+ predicate: l("status", t),
195
+ order: [{
196
+ column: "updatedAt",
197
+ direction: "asc"
198
+ }]
199
+ }))).map(w), "subscriptionStore"),
200
+ markCanceled: (t) => p(async () => {
201
+ let n = await T(e, t);
202
+ n && await e.update(r, String(n.id), {
203
+ status: "canceled",
204
+ updatedAt: /* @__PURE__ */ new Date()
205
+ });
206
+ }, "subscriptionStore"),
207
+ patchByTenant: (t, n) => p(async () => {
208
+ let i = x(await E(e, t)), a = i === null ? null : await T(e, i.providerSubscriptionId);
209
+ a && await e.update(r, String(a.id), {
210
+ ...n.plan === void 0 ? {} : { plan: n.plan },
211
+ ...n.quantity === void 0 ? {} : { quantity: n.quantity },
212
+ ...n.status === void 0 ? {} : { status: n.status },
213
+ ...n.currentPeriodStart === void 0 ? {} : { currentPeriodStart: n.currentPeriodStart },
214
+ ...n.currentPeriodEnd === void 0 ? {} : { currentPeriodEnd: n.currentPeriodEnd },
215
+ ...n.pastDueSince === void 0 ? {} : { pastDueSince: n.pastDueSince },
216
+ ...n.statusEventAt === void 0 ? {} : { statusEventAt: n.statusEventAt },
217
+ updatedAt: /* @__PURE__ */ new Date()
218
+ });
219
+ }, "subscriptionStore"),
220
+ setStatus: (t, n) => p(async () => {
221
+ let i = await T(e, t);
222
+ i && await e.update(r, String(i.id), {
223
+ status: n,
224
+ updatedAt: /* @__PURE__ */ new Date()
225
+ });
226
+ }, "subscriptionStore")
227
+ }), O = () => {
228
+ let e = /* @__PURE__ */ new Map();
229
+ return {
230
+ upsert: (t) => s.sync(() => {
231
+ let n = e.get(t.providerInvoiceId);
232
+ n && S(n.statusEventAt, t.statusEventAt) || e.set(t.providerInvoiceId, t);
233
+ }),
234
+ listByTenant: (t) => s.sync(() => [...e.values()].filter((e) => e.tenantId === t))
235
+ };
236
+ }, k = (e) => ({
237
+ tenantId: String(e.tenantId),
238
+ provider: String(e.provider),
239
+ providerInvoiceId: String(e.providerInvoiceId),
240
+ amountMinor: Number(e.amountMinor),
241
+ currency: String(e.currency),
242
+ status: String(e.status),
243
+ statusEventAt: e.statusEventAt == null ? null : new Date(e.statusEventAt)
244
+ }), A = (e) => ({
245
+ upsert: (t) => p(async () => {
246
+ let r = (await e.query(m({
247
+ table: n,
248
+ predicate: l("providerInvoiceId", t.providerInvoiceId),
249
+ take: 1
250
+ })))[0];
251
+ if (r) {
252
+ if (S(r.statusEventAt == null ? null : new Date(r.statusEventAt), t.statusEventAt)) return;
253
+ await e.update(n, String(r.id), {
254
+ amountMinor: t.amountMinor,
255
+ currency: t.currency,
256
+ status: t.status,
257
+ statusEventAt: t.statusEventAt
258
+ });
259
+ } else await e.insert(n, {
260
+ id: d("binv"),
261
+ ...t,
262
+ createdAt: /* @__PURE__ */ new Date()
263
+ });
264
+ }, "invoiceStore"),
265
+ listByTenant: (t) => p(async () => (await e.query(m({
266
+ table: n,
267
+ predicate: l("tenantId", t),
268
+ order: [{
269
+ column: "createdAt",
270
+ direction: "desc"
271
+ }]
272
+ }))).map(k), "invoiceStore")
273
+ }), j = (e, t, n) => `${e}\u0000${t}\u0000${n}`, M = () => {
274
+ let e = /* @__PURE__ */ new Map();
275
+ return {
276
+ used: (t, n, r) => s.sync(() => e.get(j(t, n, r))?.used ?? 0),
277
+ consume: (t, n, r, i, a) => s.sync(() => {
278
+ let o = j(t, n, r), s = e.get(o), c = s?.used ?? 0, l = c + i <= a;
279
+ return l && e.set(o, {
280
+ tenantId: t,
281
+ entitlementKey: n,
282
+ period: r,
283
+ used: c + i,
284
+ reportedToProvider: s?.reportedToProvider ?? 0
285
+ }), {
286
+ allowed: l,
287
+ entitlement: n,
288
+ limit: a,
289
+ used: c,
290
+ cost: i
291
+ };
292
+ }),
293
+ increment: (t, n, r, i) => s.sync(() => {
294
+ let a = j(t, n, r), o = e.get(a), s = (o?.used ?? 0) + i;
295
+ return e.set(a, {
296
+ tenantId: t,
297
+ entitlementKey: n,
298
+ period: r,
299
+ used: s,
300
+ reportedToProvider: o?.reportedToProvider ?? 0
301
+ }), s;
302
+ }),
303
+ pending: () => s.sync(() => [...e.values()].filter((e) => e.used > e.reportedToProvider)),
304
+ markReported: (t, n, r, i) => s.sync(() => {
305
+ let a = j(t, n, r), o = e.get(a);
306
+ o && e.set(a, {
307
+ ...o,
308
+ reportedToProvider: i
309
+ });
310
+ })
311
+ };
312
+ }, N = (e) => ({
313
+ tenantId: String(e.tenantId),
314
+ entitlementKey: String(e.entitlementKey),
315
+ period: String(e.period),
316
+ used: Number(e.used ?? 0),
317
+ reportedToProvider: Number(e.reportedToProvider ?? 0)
318
+ }), P = async (e, t, n, r) => (await e.query(m({
319
+ table: "_voltro_billing_usage",
320
+ predicate: c(l("tenantId", t), l("entitlementKey", n), l("period", r)),
321
+ take: 1
322
+ })))[0] ?? null, F = (e) => ({
323
+ used: (t, n, r) => p(async () => {
324
+ let i = await P(e, t, n, r);
325
+ return i ? Number(i.used ?? 0) : 0;
326
+ }, "usageStore"),
327
+ consume: (t, n, r, a, o) => p(async () => {
328
+ for (let s = 0; s < 32; s++) {
329
+ s > 0 && await new Promise((e) => setTimeout(e, s));
330
+ let u = await P(e, t, n, r), f = u ? Number(u.used ?? 0) : 0;
331
+ if (f + a > o) return {
332
+ allowed: !1,
333
+ entitlement: n,
334
+ limit: o,
335
+ used: f,
336
+ cost: a
337
+ };
338
+ if (u) {
339
+ if (await e.updateMany("_voltro_billing_usage", {
340
+ used: f + a,
341
+ updatedAt: /* @__PURE__ */ new Date()
342
+ }, { where: c(l("id", String(u.id)), l("used", f)) }) === 1) return {
343
+ allowed: !0,
344
+ entitlement: n,
345
+ limit: o,
346
+ used: f,
347
+ cost: a
348
+ };
349
+ } else {
350
+ let s = d("buse"), c = await e.insertIgnore(i, {
351
+ id: s,
352
+ tenantId: t,
353
+ entitlementKey: n,
354
+ period: r,
355
+ used: a,
356
+ reportedToProvider: 0,
357
+ updatedAt: /* @__PURE__ */ new Date()
358
+ }, { conflictColumns: [
359
+ "tenantId",
360
+ "entitlementKey",
361
+ "period"
362
+ ] });
363
+ if (String(c.id) === s) return {
364
+ allowed: !0,
365
+ entitlement: n,
366
+ limit: o,
367
+ used: 0,
368
+ cost: a
369
+ };
370
+ }
371
+ }
372
+ throw Error(`billing usage consume: contention on (${t}, ${n}, ${r}) exceeded 32 attempts`);
373
+ }, "usageStore"),
374
+ increment: (t, n, r, a) => p(async () => {
375
+ for (let o = 0; o < 32; o++) {
376
+ o > 0 && await new Promise((e) => setTimeout(e, o));
377
+ let s = await P(e, t, n, r);
378
+ if (s) {
379
+ let t = Number(s.used ?? 0);
380
+ if (await e.updateMany("_voltro_billing_usage", {
381
+ used: t + a,
382
+ updatedAt: /* @__PURE__ */ new Date()
383
+ }, { where: c(l("id", String(s.id)), l("used", t)) }) === 1) return t + a;
384
+ } else {
385
+ let o = d("buse"), s = await e.insertIgnore(i, {
386
+ id: o,
387
+ tenantId: t,
388
+ entitlementKey: n,
389
+ period: r,
390
+ used: a,
391
+ reportedToProvider: 0,
392
+ updatedAt: /* @__PURE__ */ new Date()
393
+ }, { conflictColumns: [
394
+ "tenantId",
395
+ "entitlementKey",
396
+ "period"
397
+ ] });
398
+ if (String(s.id) === o) return a;
399
+ }
400
+ }
401
+ throw Error(`billing usage increment: contention on (${t}, ${n}, ${r}) exceeded 32 attempts`);
402
+ }, "usageStore"),
403
+ pending: () => p(async () => (await e.query(m({ table: i }))).map(N).filter((e) => e.used > e.reportedToProvider), "usageStore"),
404
+ markReported: (t, n, r, a) => p(async () => {
405
+ let o = await P(e, t, n, r);
406
+ o && await e.update(i, String(o.id), {
407
+ reportedToProvider: a,
408
+ updatedAt: /* @__PURE__ */ new Date()
409
+ });
410
+ }, "usageStore")
411
+ }), I = () => ({
412
+ customers: h(),
413
+ subscriptions: C(),
414
+ invoices: O(),
415
+ usage: M(),
416
+ dunningNotices: o()
417
+ }), L = (e) => ({
418
+ customers: _(e),
419
+ subscriptions: D(e),
420
+ invoices: A(e),
421
+ usage: F(e),
422
+ dunningNotices: a(e)
423
+ });
424
+ //#endregion
425
+ export { _ as dataStoreCustomerStore, A as dataStoreInvoiceStore, L as dataStoreStores, D as dataStoreSubscriptionStore, F as dataStoreUsageStore, b as isLive, S as isStaleStatusEvent, v as isTerminal, h as memoryCustomerStore, O as memoryInvoiceStore, I as memoryStores, C as memorySubscriptionStore, M as memoryUsageStore };
@@ -0,0 +1,53 @@
1
+ import { BillingError as e } from "./types.js";
2
+ import { redactStripeKeys as t } from "./stripeRest.js";
3
+ import { Effect as n } from "effect";
4
+ import { createLogger as r } from "@voltro/logger";
5
+ import i from "stripe";
6
+ //#region src/stripeClient.ts
7
+ var a = r({ scope: "@voltro/plugin-billing" }), o = 3, s = 2e4, c = (e) => e.client ?? new i(e.apiKey, {
8
+ maxNetworkRetries: o,
9
+ timeout: s,
10
+ appInfo: {
11
+ name: "voltro",
12
+ url: "https://voltro.dev"
13
+ }
14
+ }), l = (e) => e instanceof i.errors.StripeConnectionError || e instanceof i.errors.StripeAPIError || e instanceof i.errors.StripeRateLimitError, u = (e) => e instanceof i.errors.StripeError ? e.code ?? e.type : void 0, d = (r, i) => n.tryPromise({
15
+ try: i,
16
+ catch: (n) => {
17
+ let i = l(n);
18
+ return a.warn("stripe call failed", {
19
+ source: r,
20
+ transient: i,
21
+ detail: t(f(n))
22
+ }), new e({
23
+ source: r,
24
+ message: p(n),
25
+ transient: i
26
+ });
27
+ }
28
+ }), f = (e) => {
29
+ if (e instanceof i.errors.StripeError) {
30
+ let t = u(e);
31
+ return t === void 0 ? e.message : `${e.message} (${t})`;
32
+ }
33
+ return String(e?.message ?? e);
34
+ }, p = (e) => {
35
+ if (e instanceof i.errors.StripeError) {
36
+ let t = [e.code, e.decline_code].filter((e) => typeof e == "string" && e.length > 0);
37
+ return t.length > 0 ? `${e.type} (${t.join(", ")})` : e.type;
38
+ }
39
+ return "provider call failed";
40
+ }, m = (e) => {
41
+ let t = e.items?.data?.[0];
42
+ return {
43
+ start: h(t?.current_period_start),
44
+ end: h(t?.current_period_end)
45
+ };
46
+ }, h = (e) => typeof e == "number" && Number.isFinite(e) ? /* @__PURE__ */ new Date(e * 1e3) : null, g = (e) => {
47
+ let t = e.items?.data ?? [];
48
+ if (t.length === 0) return 1;
49
+ let n = t.reduce((e, t) => e + (t.quantity ?? 0), 0);
50
+ return n > 0 ? n : 1;
51
+ };
52
+ //#endregion
53
+ export { i as Stripe, c as makeStripeClient, m as periodOf, g as quantityOf, d as stripeCall, h as unixToDate };