@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/index.js ADDED
@@ -0,0 +1,1201 @@
1
+ import { BillingError as e, BillingService as t, DEFAULT_PLAN as n, EntitlementExceeded as r } from "./types.js";
2
+ import { billingRpcClientImports as i, changePlanDescriptor as a, changeSeatsDescriptor as o, portalUrlDescriptor as s, reportUsageDescriptor as c, startCheckoutDescriptor as l, subscriptionDescriptor as u } from "./rpc.js";
3
+ import { Effect as d, Layer as f, Schedule as p, Schema as m } from "effect";
4
+ import { pluginEnv as h } from "@voltro/env";
5
+ import { composeRpcInterceptors as ee, definePlugin as te } from "@voltro/protocol";
6
+ import { and as g, eq as _, gte as ne, id as v, integer as y, lte as b, registerRetention as x, retentionTtlMsFromEnv as S, table as C, text as w, timestamp as T } from "@voltro/database";
7
+ import { defineIncomingWebhook as E, mountIncomingWebhook as re } from "@voltro/plugin-webhooks";
8
+ import { stripeProvider as D } from "@voltro/plugin-webhooks/providers";
9
+ import { createLogger as O } from "@voltro/logger";
10
+ //#region src/entitlement.ts
11
+ var k = (e, t, n, r) => ({
12
+ allowed: !Number.isFinite(t) || r <= 0 || n + r <= t,
13
+ entitlement: e,
14
+ limit: t,
15
+ used: n,
16
+ cost: r
17
+ }), A = (e) => e.tenantId, j = (e, n, i) => d.gen(function* () {
18
+ let a = A(e.request.subject);
19
+ if (a === null) return i <= 0 ? void 0 : yield* d.fail(new r({
20
+ entitlement: n,
21
+ limit: 0,
22
+ used: 0,
23
+ cost: i
24
+ }));
25
+ yield* (yield* t).consumeEntitlement(a, n, i);
26
+ }), ie = (e, t) => (n, i) => {
27
+ let a = e[i.tag];
28
+ if (a === void 0) return n;
29
+ let o = a.cost ?? 1, s = i.subject.tenantId;
30
+ return s === null ? o <= 0 ? n : d.fail(new r({
31
+ entitlement: a.entitlement,
32
+ limit: 0,
33
+ used: 0,
34
+ cost: o
35
+ })) : d.flatMap(t.consumeEntitlement(s, a.entitlement, o), () => n);
36
+ }, M = (e) => e < 0 ? -Math.round(-e) : Math.round(e), ae = (t) => {
37
+ let n = t.periodEnd.getTime() - t.periodStart.getTime();
38
+ if (n <= 0) throw new e({
39
+ source: "proration",
40
+ message: `degenerate billing period: periodEnd (${t.periodEnd.toISOString()}) must be after periodStart (${t.periodStart.toISOString()})`,
41
+ transient: !1
42
+ });
43
+ let r = t.periodEnd.getTime() - t.changeAt.getTime(), i = Math.min(1, Math.max(0, r / n));
44
+ return {
45
+ deltaMinor: M((t.newAmountMinor - t.oldAmountMinor) * i),
46
+ unusedFraction: i
47
+ };
48
+ }, oe = 1440 * 60 * 1e3, N = [
49
+ 1,
50
+ 3,
51
+ 5,
52
+ 7
53
+ ], P = (e, t, n) => t < 0 || t >= n.length ? null : new Date(e.getTime() + n[t] * oe), se = (e) => {
54
+ if (e.length === 0) throw Error("billing dunning: the retry schedule must have at least one offset");
55
+ let t = 0;
56
+ for (let n of e) {
57
+ if (!Number.isFinite(n) || n <= 0) throw Error(`billing dunning: schedule offsets must be positive day counts, got ${n}`);
58
+ if (n <= t) throw Error(`billing dunning: schedule offsets must strictly increase, got ${n} after ${t}`);
59
+ t = n;
60
+ }
61
+ }, ce = (e, t, n, r) => d.gen(function* () {
62
+ let i = P(r, 0, e.schedule);
63
+ return (yield* e.dunning.open(t, n, i, r)) ? (yield* e.subscriptions.setStatus(n, "pastDue"), {
64
+ tenantId: t,
65
+ providerSubscriptionId: n,
66
+ attempt: 0,
67
+ nextRetryAt: i,
68
+ startedAt: r
69
+ }) : null;
70
+ }), le = (e, t) => d.gen(function* () {
71
+ yield* e.dunning.close(t), yield* e.subscriptions.setStatus(t, "active");
72
+ }), ue = (e, t) => d.gen(function* () {
73
+ let n = t.attempt + 1, r = P(t.startedAt, n, e.schedule);
74
+ return r === null ? (yield* e.dunning.advance(t.providerSubscriptionId, t.attempt, n, t.nextRetryAt)) ? (yield* e.dunning.close(t.providerSubscriptionId), yield* e.subscriptions.setStatus(t.providerSubscriptionId, "canceled"), "canceled") : null : (yield* e.dunning.advance(t.providerSubscriptionId, t.attempt, n, r)) ? "retried" : null;
75
+ }), de = (e, t) => d.gen(function* () {
76
+ let n = yield* e.dunning.due(t), r = 0, i = 0;
77
+ for (let t of n) {
78
+ let n = yield* ue(e, t);
79
+ n === "retried" ? r++ : n === "canceled" && i++;
80
+ }
81
+ return {
82
+ retried: r,
83
+ canceled: i
84
+ };
85
+ }), F = "_voltro_billing_customers", I = "_voltro_billing_subscriptions", L = "_voltro_billing_invoices", R = "_voltro_billing_usage", z = "_voltro_billing_dunning", fe = C(F, {
86
+ id: v({ prefix: "bcust" }),
87
+ tenantId: w(),
88
+ provider: w(),
89
+ providerCustomerId: w(),
90
+ createdAt: T(),
91
+ updatedAt: T()
92
+ }).index("byBillingCustomerTenant", ["tenantId"]).index("byBillingProviderCustomer", ["providerCustomerId"]), pe = C(I, {
93
+ id: v({ prefix: "bsub" }),
94
+ tenantId: w(),
95
+ provider: w(),
96
+ providerSubscriptionId: w(),
97
+ plan: w(),
98
+ status: w(),
99
+ quantity: y().default(1),
100
+ currentPeriodStart: T().nullable(),
101
+ currentPeriodEnd: T().nullable(),
102
+ cancelAt: T().nullable(),
103
+ createdAt: T(),
104
+ updatedAt: T()
105
+ }).index("byBillingSubscriptionTenant", ["tenantId"]).index("byBillingProviderSubscription", ["providerSubscriptionId"]), me = C(L, {
106
+ id: v({ prefix: "binv" }),
107
+ tenantId: w(),
108
+ provider: w(),
109
+ providerInvoiceId: w(),
110
+ amountMinor: y(),
111
+ currency: w(),
112
+ status: w(),
113
+ createdAt: T()
114
+ }).index("byBillingInvoiceTenant", ["tenantId"]).index("byBillingProviderInvoice", ["providerInvoiceId"]), he = C(R, {
115
+ id: v({ prefix: "buse" }),
116
+ tenantId: w().maxLength(191),
117
+ entitlementKey: w().maxLength(191),
118
+ period: w().maxLength(16),
119
+ used: y().default(0),
120
+ reportedToProvider: y().default(0),
121
+ updatedAt: T()
122
+ }).unique("byBillingUsageWindow", [
123
+ "tenantId",
124
+ "entitlementKey",
125
+ "period"
126
+ ]), ge = C(z, {
127
+ id: v({ prefix: "bdun" }),
128
+ tenantId: w().maxLength(191),
129
+ providerSubscriptionId: w().maxLength(191),
130
+ attempt: y().default(0),
131
+ nextRetryAt: T(),
132
+ startedAt: T(),
133
+ updatedAt: T()
134
+ }).unique("byBillingDunningSubscription", ["providerSubscriptionId"]).index("byBillingDunningTenant", ["tenantId"]), _e = () => [
135
+ fe,
136
+ pe,
137
+ me,
138
+ he,
139
+ ge
140
+ ], B = (e) => `${e}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, V = (t, n) => d.tryPromise({
141
+ try: t,
142
+ catch: (t) => new e({
143
+ source: n,
144
+ message: String(t?.message ?? t),
145
+ transient: !0
146
+ })
147
+ }), H = (e) => ({
148
+ table: e.table,
149
+ predicate: e.predicate,
150
+ order: e.order ?? [],
151
+ take: e.take,
152
+ skip: void 0,
153
+ projection: void 0
154
+ }), ve = () => {
155
+ let e = /* @__PURE__ */ new Map();
156
+ return {
157
+ upsert: (t) => d.sync(() => {
158
+ e.set(t.tenantId, t);
159
+ }),
160
+ getByTenant: (t) => d.sync(() => e.get(t) ?? null)
161
+ };
162
+ }, ye = (e) => ({
163
+ tenantId: String(e.tenantId),
164
+ provider: String(e.provider),
165
+ providerCustomerId: String(e.providerCustomerId)
166
+ }), be = (e) => ({
167
+ upsert: (t) => V(async () => {
168
+ let n = await e.query(H({
169
+ table: F,
170
+ predicate: _("tenantId", t.tenantId),
171
+ take: 1
172
+ })), r = /* @__PURE__ */ new Date();
173
+ n[0] ? await e.update(F, String(n[0].id), {
174
+ provider: t.provider,
175
+ providerCustomerId: t.providerCustomerId,
176
+ updatedAt: r
177
+ }) : await e.insert(F, {
178
+ id: B("bcust"),
179
+ ...t,
180
+ createdAt: r,
181
+ updatedAt: r
182
+ });
183
+ }, "customerStore"),
184
+ getByTenant: (t) => V(async () => {
185
+ let n = await e.query(H({
186
+ table: F,
187
+ predicate: _("tenantId", t),
188
+ take: 1
189
+ }));
190
+ return n[0] ? ye(n[0]) : null;
191
+ }, "customerStore")
192
+ }), xe = () => {
193
+ let e = /* @__PURE__ */ new Map();
194
+ return {
195
+ upsert: (t) => d.sync(() => {
196
+ e.set(t.tenantId, t);
197
+ }),
198
+ getByTenant: (t) => d.sync(() => e.get(t) ?? null),
199
+ markCanceled: (t) => d.sync(() => {
200
+ for (let [n, r] of e) r.providerSubscriptionId === t && e.set(n, {
201
+ ...r,
202
+ status: "canceled"
203
+ });
204
+ }),
205
+ patchByTenant: (t, n) => d.sync(() => {
206
+ let r = e.get(t);
207
+ r && e.set(t, {
208
+ ...r,
209
+ ...n
210
+ });
211
+ }),
212
+ setStatus: (t, n) => d.sync(() => {
213
+ for (let [r, i] of e) i.providerSubscriptionId === t && e.set(r, {
214
+ ...i,
215
+ status: n
216
+ });
217
+ })
218
+ };
219
+ }, Se = (e) => ({
220
+ tenantId: String(e.tenantId),
221
+ provider: String(e.provider),
222
+ providerSubscriptionId: String(e.providerSubscriptionId),
223
+ plan: String(e.plan),
224
+ status: String(e.status),
225
+ quantity: e.quantity == null ? 1 : Number(e.quantity),
226
+ currentPeriodStart: e.currentPeriodStart == null ? null : new Date(e.currentPeriodStart),
227
+ currentPeriodEnd: e.currentPeriodEnd == null ? null : new Date(e.currentPeriodEnd),
228
+ cancelAt: e.cancelAt == null ? null : new Date(e.cancelAt)
229
+ }), U = async (e, t) => (await e.query(H({
230
+ table: "_voltro_billing_subscriptions",
231
+ predicate: _("providerSubscriptionId", t),
232
+ take: 1
233
+ })))[0] ?? null, Ce = (e) => ({
234
+ upsert: (t) => V(async () => {
235
+ let n = await U(e, t.providerSubscriptionId), r = /* @__PURE__ */ new Date();
236
+ n ? await e.update(I, String(n.id), {
237
+ tenantId: t.tenantId,
238
+ provider: t.provider,
239
+ plan: t.plan,
240
+ status: t.status,
241
+ quantity: t.quantity,
242
+ currentPeriodStart: t.currentPeriodStart,
243
+ currentPeriodEnd: t.currentPeriodEnd,
244
+ cancelAt: t.cancelAt,
245
+ updatedAt: r
246
+ }) : await e.insert(I, {
247
+ id: B("bsub"),
248
+ tenantId: t.tenantId,
249
+ provider: t.provider,
250
+ providerSubscriptionId: t.providerSubscriptionId,
251
+ plan: t.plan,
252
+ status: t.status,
253
+ quantity: t.quantity,
254
+ currentPeriodStart: t.currentPeriodStart,
255
+ currentPeriodEnd: t.currentPeriodEnd,
256
+ cancelAt: t.cancelAt,
257
+ createdAt: r,
258
+ updatedAt: r
259
+ });
260
+ }, "subscriptionStore"),
261
+ getByTenant: (t) => V(async () => {
262
+ let n = await e.query(H({
263
+ table: I,
264
+ predicate: _("tenantId", t),
265
+ order: [{
266
+ column: "updatedAt",
267
+ direction: "desc"
268
+ }],
269
+ take: 1
270
+ }));
271
+ return n[0] ? Se(n[0]) : null;
272
+ }, "subscriptionStore"),
273
+ markCanceled: (t) => V(async () => {
274
+ let n = await U(e, t);
275
+ n && await e.update(I, String(n.id), {
276
+ status: "canceled",
277
+ updatedAt: /* @__PURE__ */ new Date()
278
+ });
279
+ }, "subscriptionStore"),
280
+ patchByTenant: (t, n) => V(async () => {
281
+ let r = (await e.query(H({
282
+ table: I,
283
+ predicate: _("tenantId", t),
284
+ order: [{
285
+ column: "updatedAt",
286
+ direction: "desc"
287
+ }],
288
+ take: 1
289
+ })))[0];
290
+ r && await e.update(I, String(r.id), {
291
+ ...n.plan === void 0 ? {} : { plan: n.plan },
292
+ ...n.quantity === void 0 ? {} : { quantity: n.quantity },
293
+ ...n.status === void 0 ? {} : { status: n.status },
294
+ updatedAt: /* @__PURE__ */ new Date()
295
+ });
296
+ }, "subscriptionStore"),
297
+ setStatus: (t, n) => V(async () => {
298
+ let r = await U(e, t);
299
+ r && await e.update(I, String(r.id), {
300
+ status: n,
301
+ updatedAt: /* @__PURE__ */ new Date()
302
+ });
303
+ }, "subscriptionStore")
304
+ }), we = () => {
305
+ let e = /* @__PURE__ */ new Map();
306
+ return {
307
+ upsert: (t) => d.sync(() => {
308
+ e.set(t.providerInvoiceId, t);
309
+ }),
310
+ listByTenant: (t) => d.sync(() => [...e.values()].filter((e) => e.tenantId === t))
311
+ };
312
+ }, Te = (e) => ({
313
+ tenantId: String(e.tenantId),
314
+ provider: String(e.provider),
315
+ providerInvoiceId: String(e.providerInvoiceId),
316
+ amountMinor: Number(e.amountMinor),
317
+ currency: String(e.currency),
318
+ status: String(e.status)
319
+ }), Ee = (e) => ({
320
+ upsert: (t) => V(async () => {
321
+ let n = await e.query(H({
322
+ table: L,
323
+ predicate: _("providerInvoiceId", t.providerInvoiceId),
324
+ take: 1
325
+ }));
326
+ n[0] ? await e.update(L, String(n[0].id), {
327
+ amountMinor: t.amountMinor,
328
+ currency: t.currency,
329
+ status: t.status
330
+ }) : await e.insert(L, {
331
+ id: B("binv"),
332
+ ...t,
333
+ createdAt: /* @__PURE__ */ new Date()
334
+ });
335
+ }, "invoiceStore"),
336
+ listByTenant: (t) => V(async () => (await e.query(H({
337
+ table: L,
338
+ predicate: _("tenantId", t),
339
+ order: [{
340
+ column: "createdAt",
341
+ direction: "desc"
342
+ }]
343
+ }))).map(Te), "invoiceStore")
344
+ }), W = (e, t, n) => `${e}${t}${n}`, De = () => {
345
+ let e = /* @__PURE__ */ new Map();
346
+ return {
347
+ used: (t, n, r) => d.sync(() => e.get(W(t, n, r))?.used ?? 0),
348
+ consume: (t, n, r, i, a) => d.sync(() => {
349
+ let o = W(t, n, r), s = e.get(o), c = s?.used ?? 0, l = c + i <= a;
350
+ return l && e.set(o, {
351
+ tenantId: t,
352
+ entitlementKey: n,
353
+ period: r,
354
+ used: c + i,
355
+ reportedToProvider: s?.reportedToProvider ?? 0
356
+ }), {
357
+ allowed: l,
358
+ entitlement: n,
359
+ limit: a,
360
+ used: c,
361
+ cost: i
362
+ };
363
+ }),
364
+ increment: (t, n, r, i) => d.sync(() => {
365
+ let a = W(t, n, r), o = e.get(a), s = (o?.used ?? 0) + i;
366
+ return e.set(a, {
367
+ tenantId: t,
368
+ entitlementKey: n,
369
+ period: r,
370
+ used: s,
371
+ reportedToProvider: o?.reportedToProvider ?? 0
372
+ }), s;
373
+ }),
374
+ pending: () => d.sync(() => [...e.values()].filter((e) => e.used > e.reportedToProvider)),
375
+ markReported: (t, n, r, i) => d.sync(() => {
376
+ let a = W(t, n, r), o = e.get(a);
377
+ o && e.set(a, {
378
+ ...o,
379
+ reportedToProvider: i
380
+ });
381
+ })
382
+ };
383
+ }, Oe = (e) => ({
384
+ tenantId: String(e.tenantId),
385
+ entitlementKey: String(e.entitlementKey),
386
+ period: String(e.period),
387
+ used: Number(e.used ?? 0),
388
+ reportedToProvider: Number(e.reportedToProvider ?? 0)
389
+ }), G = async (e, t, n, r) => (await e.query(H({
390
+ table: "_voltro_billing_usage",
391
+ predicate: g(_("tenantId", t), _("entitlementKey", n), _("period", r)),
392
+ take: 1
393
+ })))[0] ?? null, ke = (e) => ({
394
+ used: (t, n, r) => V(async () => {
395
+ let i = await G(e, t, n, r);
396
+ return i ? Number(i.used ?? 0) : 0;
397
+ }, "usageStore"),
398
+ consume: (t, n, r, i, a) => V(async () => {
399
+ for (let o = 0; o < 32; o++) {
400
+ o > 0 && await new Promise((e) => setTimeout(e, o));
401
+ let s = await G(e, t, n, r), c = s ? Number(s.used ?? 0) : 0;
402
+ if (c + i > a) return {
403
+ allowed: !1,
404
+ entitlement: n,
405
+ limit: a,
406
+ used: c,
407
+ cost: i
408
+ };
409
+ if (s) {
410
+ if (await e.updateMany("_voltro_billing_usage", {
411
+ used: c + i,
412
+ updatedAt: /* @__PURE__ */ new Date()
413
+ }, { where: g(_("id", String(s.id)), _("used", c)) }) === 1) return {
414
+ allowed: !0,
415
+ entitlement: n,
416
+ limit: a,
417
+ used: c,
418
+ cost: i
419
+ };
420
+ } else {
421
+ let o = B("buse"), s = await e.insertIgnore(R, {
422
+ id: o,
423
+ tenantId: t,
424
+ entitlementKey: n,
425
+ period: r,
426
+ used: i,
427
+ reportedToProvider: 0,
428
+ updatedAt: /* @__PURE__ */ new Date()
429
+ }, { conflictColumns: [
430
+ "tenantId",
431
+ "entitlementKey",
432
+ "period"
433
+ ] });
434
+ if (String(s.id) === o) return {
435
+ allowed: !0,
436
+ entitlement: n,
437
+ limit: a,
438
+ used: 0,
439
+ cost: i
440
+ };
441
+ }
442
+ }
443
+ throw Error(`billing usage consume: contention on (${t}, ${n}, ${r}) exceeded 32 attempts`);
444
+ }, "usageStore"),
445
+ increment: (t, n, r, i) => V(async () => {
446
+ for (let a = 0; a < 32; a++) {
447
+ a > 0 && await new Promise((e) => setTimeout(e, a));
448
+ let o = await G(e, t, n, r);
449
+ if (o) {
450
+ let t = Number(o.used ?? 0);
451
+ if (await e.updateMany("_voltro_billing_usage", {
452
+ used: t + i,
453
+ updatedAt: /* @__PURE__ */ new Date()
454
+ }, { where: g(_("id", String(o.id)), _("used", t)) }) === 1) return t + i;
455
+ } else {
456
+ let a = B("buse"), o = await e.insertIgnore(R, {
457
+ id: a,
458
+ tenantId: t,
459
+ entitlementKey: n,
460
+ period: r,
461
+ used: i,
462
+ reportedToProvider: 0,
463
+ updatedAt: /* @__PURE__ */ new Date()
464
+ }, { conflictColumns: [
465
+ "tenantId",
466
+ "entitlementKey",
467
+ "period"
468
+ ] });
469
+ if (String(o.id) === a) return i;
470
+ }
471
+ }
472
+ throw Error(`billing usage increment: contention on (${t}, ${n}, ${r}) exceeded 32 attempts`);
473
+ }, "usageStore"),
474
+ pending: () => V(async () => (await e.query(H({ table: R }))).map(Oe).filter((e) => e.used > e.reportedToProvider), "usageStore"),
475
+ markReported: (t, n, r, i) => V(async () => {
476
+ let a = await G(e, t, n, r);
477
+ a && await e.update(R, String(a.id), {
478
+ reportedToProvider: i,
479
+ updatedAt: /* @__PURE__ */ new Date()
480
+ });
481
+ }, "usageStore")
482
+ }), Ae = (e) => ({
483
+ tenantId: String(e.tenantId),
484
+ providerSubscriptionId: String(e.providerSubscriptionId),
485
+ attempt: Number(e.attempt ?? 0),
486
+ nextRetryAt: new Date(e.nextRetryAt),
487
+ startedAt: new Date(e.startedAt)
488
+ }), je = () => {
489
+ let e = /* @__PURE__ */ new Map();
490
+ return {
491
+ open: (t, n, r, i) => d.sync(() => e.has(n) ? !1 : (e.set(n, {
492
+ tenantId: t,
493
+ providerSubscriptionId: n,
494
+ attempt: 0,
495
+ nextRetryAt: r,
496
+ startedAt: i
497
+ }), !0)),
498
+ getByTenant: (t) => d.sync(() => {
499
+ for (let n of e.values()) if (n.tenantId === t) return n;
500
+ return null;
501
+ }),
502
+ due: (t) => d.sync(() => [...e.values()].filter((e) => e.nextRetryAt.getTime() <= t.getTime())),
503
+ advance: (t, n, r, i) => d.sync(() => {
504
+ let a = e.get(t);
505
+ return !a || a.attempt !== n ? !1 : (e.set(t, {
506
+ ...a,
507
+ attempt: r,
508
+ nextRetryAt: i
509
+ }), !0);
510
+ }),
511
+ close: (t) => d.sync(() => {
512
+ e.delete(t);
513
+ })
514
+ };
515
+ }, Me = async (e, t) => (await e.query(H({
516
+ table: "_voltro_billing_dunning",
517
+ predicate: _("providerSubscriptionId", t),
518
+ take: 1
519
+ })))[0] ?? null, Ne = (e) => ({
520
+ open: (t, n, r, i) => V(async () => {
521
+ let a = B("bdun"), o = await e.insertIgnore(z, {
522
+ id: a,
523
+ tenantId: t,
524
+ providerSubscriptionId: n,
525
+ attempt: 0,
526
+ nextRetryAt: r,
527
+ startedAt: i,
528
+ updatedAt: i
529
+ }, { conflictColumns: ["providerSubscriptionId"] });
530
+ return String(o.id) === a;
531
+ }, "dunningStore"),
532
+ getByTenant: (t) => V(async () => {
533
+ let n = await e.query(H({
534
+ table: z,
535
+ predicate: _("tenantId", t),
536
+ take: 1
537
+ }));
538
+ return n[0] ? Ae(n[0]) : null;
539
+ }, "dunningStore"),
540
+ due: (t) => V(async () => (await e.query(H({
541
+ table: z,
542
+ predicate: b("nextRetryAt", t)
543
+ }))).map(Ae), "dunningStore"),
544
+ advance: (t, n, r, i) => V(async () => {
545
+ let a = await Me(e, t);
546
+ return a ? await e.updateMany(z, {
547
+ attempt: r,
548
+ nextRetryAt: i,
549
+ updatedAt: /* @__PURE__ */ new Date()
550
+ }, { where: g(_("id", String(a.id)), _("attempt", n)) }) === 1 : !1;
551
+ }, "dunningStore"),
552
+ close: (t) => V(async () => {
553
+ let n = await Me(e, t);
554
+ n && await e.delete(z, String(n.id));
555
+ }, "dunningStore")
556
+ }), Pe = () => ({
557
+ customers: ve(),
558
+ subscriptions: xe(),
559
+ invoices: we(),
560
+ usage: De(),
561
+ dunning: je()
562
+ }), Fe = (e) => ({
563
+ customers: be(e),
564
+ subscriptions: Ce(e),
565
+ invoices: Ee(e),
566
+ usage: ke(e),
567
+ dunning: Ne(e)
568
+ }), K = (e = /* @__PURE__ */ new Date()) => `${e.getUTCFullYear()}-${String(e.getUTCMonth() + 1).padStart(2, "0")}`, Ie = (e) => ({ stores: e ?? Pe() }), q = (e, t) => e.pipe(d.retry({
569
+ schedule: p.recurs(Math.max(0, t - 1)),
570
+ while: (e) => e.transient
571
+ })), Le = (t) => {
572
+ let { provider: i, plans: a } = t, o = t.attempts ?? 3, s = t.now ?? (() => /* @__PURE__ */ new Date()), c = Ie(t.stores), l = t.dunningSchedule ?? N, u = () => ({
573
+ subscriptions: c.stores.subscriptions,
574
+ dunning: c.stores.dunning,
575
+ schedule: l
576
+ }), f = (e) => c.stores.subscriptions.getByTenant(e).pipe(d.map((e) => e !== null && (e.status === "active" || e.status === "trialing") ? e.plan : n)), p = (e) => c.stores.subscriptions.getByTenant(e), m = (e, n) => d.gen(function* () {
577
+ if (t.resolveEntitlementLimit !== void 0) {
578
+ let r = yield* t.resolveEntitlementLimit(e, n);
579
+ if (r !== null) return r;
580
+ }
581
+ let r = yield* f(e);
582
+ return a.entitlementLimit(r, n);
583
+ }), h = (e, t, n) => d.gen(function* () {
584
+ return k(t, yield* m(e, t), yield* c.stores.usage.used(e, t, K(s())), n);
585
+ }), ee = (e, t, n) => d.gen(function* () {
586
+ let i = yield* m(e, t);
587
+ if (!Number.isFinite(i) || n <= 0) return;
588
+ let a = K(s()), o = yield* c.stores.usage.consume(e, t, a, n, i);
589
+ if (!o.allowed) return yield* d.fail(new r({
590
+ entitlement: t,
591
+ limit: o.limit,
592
+ used: o.used,
593
+ cost: n
594
+ }));
595
+ }), te = (e, t, n) => n <= 0 ? d.void : c.stores.usage.increment(e, t, K(s()), n).pipe(d.asVoid), g = () => d.gen(function* () {
596
+ if (!i.supportsMeteredUsage) return;
597
+ let e = yield* c.stores.usage.pending();
598
+ for (let t of e) {
599
+ let e = t.used - t.reportedToProvider;
600
+ e <= 0 || (yield* q(i.reportUsage({
601
+ tenantId: t.tenantId,
602
+ entitlementKey: t.entitlementKey,
603
+ quantity: e
604
+ }), o), yield* c.stores.usage.markReported(t.tenantId, t.entitlementKey, t.period, t.used));
605
+ }
606
+ }), _ = (t) => d.gen(function* () {
607
+ let n = a.priceIdFor(t.plan);
608
+ if (n === null) return yield* d.fail(new e({
609
+ source: "billing",
610
+ message: `plan '${t.plan}' has no provider priceId — not a paid plan`,
611
+ transient: !1
612
+ }));
613
+ let r = yield* c.stores.customers.getByTenant(t.tenantId);
614
+ return yield* q(i.createCheckoutSession({
615
+ tenantId: t.tenantId,
616
+ priceId: n,
617
+ successUrl: t.successUrl,
618
+ cancelUrl: t.cancelUrl,
619
+ ...r === null ? {} : { providerCustomerId: r.providerCustomerId }
620
+ }), o);
621
+ }), ne = (t, n) => d.gen(function* () {
622
+ let r = yield* c.stores.customers.getByTenant(t);
623
+ return r === null ? yield* d.fail(new e({
624
+ source: "billing",
625
+ message: `no provider customer linked for tenant '${t}'`,
626
+ transient: !1
627
+ })) : yield* q(i.createPortalSession({
628
+ tenantId: t,
629
+ providerCustomerId: r.providerCustomerId,
630
+ returnUrl: n
631
+ }), o);
632
+ }), v = (e) => {
633
+ switch (e._tag) {
634
+ case "subscriptionUpserted": return c.stores.subscriptions.upsert({
635
+ tenantId: e.tenantId,
636
+ provider: i.name,
637
+ providerSubscriptionId: e.providerSubscriptionId,
638
+ plan: e.plan,
639
+ status: e.status,
640
+ quantity: e.quantity ?? 1,
641
+ currentPeriodStart: e.currentPeriodStart ?? null,
642
+ currentPeriodEnd: e.currentPeriodEnd,
643
+ cancelAt: e.cancelAt
644
+ });
645
+ case "subscriptionCanceled": return c.stores.subscriptions.markCanceled(e.providerSubscriptionId);
646
+ case "invoicePaid": return c.stores.invoices.upsert({
647
+ tenantId: e.tenantId,
648
+ provider: i.name,
649
+ providerInvoiceId: e.providerInvoiceId,
650
+ amountMinor: e.amountMinor,
651
+ currency: e.currency,
652
+ status: "paid"
653
+ }).pipe(d.andThen(w(e.tenantId)));
654
+ case "invoicePaymentFailed": return c.stores.invoices.upsert({
655
+ tenantId: e.tenantId,
656
+ provider: i.name,
657
+ providerInvoiceId: e.providerInvoiceId,
658
+ amountMinor: e.amountMinor,
659
+ currency: e.currency,
660
+ status: "open"
661
+ }).pipe(d.andThen(C(e.tenantId).pipe(d.asVoid)));
662
+ case "customerLinked": return c.stores.customers.upsert({
663
+ tenantId: e.tenantId,
664
+ provider: i.name,
665
+ providerCustomerId: e.providerCustomerId
666
+ });
667
+ }
668
+ }, y = (e, t) => a.unitAmountFor(e) * t, b = (t, n, r) => d.gen(function* () {
669
+ let i = yield* c.stores.subscriptions.getByTenant(t);
670
+ if (i === null) return yield* d.fail(new e({
671
+ source: "billing",
672
+ message: `no subscription for tenant '${t}' to change`,
673
+ transient: !1
674
+ }));
675
+ let o = n.plan ?? i.plan, s = n.quantity ?? i.quantity;
676
+ if (n.plan !== void 0 && !a.has(o)) return yield* d.fail(new e({
677
+ source: "billing",
678
+ message: `unknown plan '${o}' — not in the plan registry`,
679
+ transient: !1
680
+ }));
681
+ if (!Number.isInteger(s) || s <= 0) return yield* d.fail(new e({
682
+ source: "billing",
683
+ message: `seat quantity must be a positive integer, got ${s}`,
684
+ transient: !1
685
+ }));
686
+ let l = y(i.plan, i.quantity), u = y(o, s), f = a.currencyFor(o), p = 0;
687
+ return i.currentPeriodEnd !== null && i.currentPeriodStart !== null && u !== l && (p = ae({
688
+ oldAmountMinor: l,
689
+ newAmountMinor: u,
690
+ periodStart: i.currentPeriodStart,
691
+ periodEnd: i.currentPeriodEnd,
692
+ changeAt: r
693
+ }).deltaMinor), yield* c.stores.subscriptions.patchByTenant(t, {
694
+ ...n.plan === void 0 ? {} : { plan: o },
695
+ ...n.quantity === void 0 ? {} : { quantity: s }
696
+ }), {
697
+ tenantId: t,
698
+ plan: o,
699
+ quantity: s,
700
+ prorationMinor: p,
701
+ currency: f
702
+ };
703
+ }), x = (e, t, n = s()) => b(e, { plan: t }, n), S = (e, t, n = s()) => b(e, { quantity: t }, n), C = (e, t = s()) => d.gen(function* () {
704
+ let n = yield* c.stores.subscriptions.getByTenant(e);
705
+ return n === null ? null : yield* ce(u(), e, n.providerSubscriptionId, t);
706
+ }), w = (e, t = s()) => d.gen(function* () {
707
+ let t = yield* c.stores.dunning.getByTenant(e);
708
+ t !== null && (yield* le(u(), t.providerSubscriptionId));
709
+ });
710
+ return {
711
+ service: {
712
+ subscription: p,
713
+ plan: f,
714
+ checkEntitlement: h,
715
+ consumeEntitlement: ee,
716
+ reportUsage: te,
717
+ flushUsage: g,
718
+ startCheckout: _,
719
+ portalUrl: ne,
720
+ applyEvent: v,
721
+ changePlan: x,
722
+ changeSeats: S,
723
+ recordPaymentFailure: C,
724
+ recordPaymentSuccess: w,
725
+ runDunningCycle: (e = s()) => de(u(), e)
726
+ },
727
+ holder: c
728
+ };
729
+ }, Re = (e) => f.succeed(t, e), ze = "_voltro_ai_usage", Be = (e) => {
730
+ for (let [t, n] of Object.entries(e)) {
731
+ if (t.length === 0) throw Error("billing metering: a meter key must be non-empty");
732
+ if (n.from === "rpc") {
733
+ if (n.match === void 0 || typeof n.match == "string" && n.match.length === 0) throw Error(`billing metering: rpc meter '${t}' needs a non-empty 'match'`);
734
+ } else if (n.from === "cdc") {
735
+ if (typeof n.table != "string" || n.table.length === 0) throw Error(`billing metering: cdc meter '${t}' needs a 'table'`);
736
+ } else if (n.from !== "ai") throw Error(`billing metering: meter '${t}' has unknown source '${String(n.from)}'`);
737
+ }
738
+ }, Ve = (e, t) => typeof e == "string" ? t === e : e.test(t), He = (e, t, n) => Object.entries(e).filter(([, e]) => e.from === "rpc" && (e.kind === void 0 || e.kind === n) && Ve(e.match, t)).map(([e]) => e), Ue = (e, t, n) => Object.entries(e).filter(([, e]) => e.from === "cdc" && e.table === t && (e.op ?? "insert") === n).map(([e]) => e), We = (e) => Object.entries(e).filter(([, e]) => e.from === "ai").map(([e, t]) => [e, t.metric ?? "tokens"]), Ge = (e) => e.tenantId, Ke = (e, t, n, r) => d.forEach(n, (n) => e.reportUsage(t, n, r).pipe(d.catchAll(() => d.void)), { discard: !0 }), qe = (e, t) => (n, r) => {
739
+ let i = He(t, r.tag, r.kind);
740
+ if (i.length === 0) return n;
741
+ let a = Ge(r.subject);
742
+ return a === null ? n : n.pipe(d.tap(() => Ke(e, a, i, 1)));
743
+ }, Je = (e, t) => (n) => {
744
+ let r = Ue(t, n.table, n.op);
745
+ if (r.length === 0) return d.void;
746
+ let i = n.new?.tenantId ?? n.old?.tenantId ?? null;
747
+ return i === null ? d.void : Ke(e, i, r, 1);
748
+ }, Ye = (e) => new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), 1, 0, 0, 0, 0)), Xe = (e, t, n, r = () => /* @__PURE__ */ new Date()) => d.gen(function* () {
749
+ let i = We(n);
750
+ if (i.length === 0) return;
751
+ let a = r();
752
+ K(a);
753
+ let o = {
754
+ table: ze,
755
+ predicate: ne("calledAt", Ye(a)),
756
+ order: [],
757
+ take: void 0,
758
+ skip: void 0,
759
+ projection: void 0
760
+ }, s = yield* d.promise(() => Promise.resolve(t.query(o)).then((e) => e).catch(() => [])), c = /* @__PURE__ */ new Map();
761
+ for (let e of s) {
762
+ let t = e.tenantId ?? null;
763
+ if (t === null) continue;
764
+ let n = c.get(t) ?? {
765
+ tokens: 0,
766
+ cost: 0
767
+ };
768
+ n.tokens += (Number(e.inputTokens) || 0) + (Number(e.outputTokens) || 0), n.cost += Number(e.costMicroUsd) || 0, c.set(t, n);
769
+ }
770
+ for (let [t, n] of i) for (let [r, i] of c) {
771
+ let a = n === "costMicroUsd" ? i.cost : i.tokens;
772
+ if (a <= 0) continue;
773
+ let o = a - (yield* e.checkEntitlement(r, t, 0).pipe(d.map((e) => e.used), d.catchAll(() => d.succeed(0))));
774
+ o > 0 && (yield* e.reportUsage(r, t, o).pipe(d.catchAll(() => d.void)));
775
+ }
776
+ }), Ze = (e, t, n, r) => Xe(e, t, n, r).pipe(d.andThen(e.flushUsage().pipe(d.catchAll(() => d.void)))), Qe = (e) => Object.values(e).some((e) => e.from === "cdc"), $e = (e) => e === void 0 ? 0 : e === "unlimited" ? Infinity : e, J = (e) => {
777
+ let t = /* @__PURE__ */ new Map();
778
+ for (let [n, r] of Object.entries(e)) r.priceId !== void 0 && t.set(r.priceId, n);
779
+ return {
780
+ has: (t) => Object.prototype.hasOwnProperty.call(e, t),
781
+ entitlementLimit: (t, n) => {
782
+ let r = e[t];
783
+ return r === void 0 ? 0 : $e(r.entitlements[n]);
784
+ },
785
+ planForPriceId: (e) => t.get(e) ?? null,
786
+ priceIdFor: (t) => e[t]?.priceId ?? null,
787
+ unitAmountFor: (t) => {
788
+ let n = e[t]?.unitAmountMinor;
789
+ return n !== void 0 && Number.isFinite(n) ? n : 0;
790
+ },
791
+ currencyFor: (t) => (e[t]?.currency ?? "usd").toLowerCase(),
792
+ planIds: () => Object.keys(e)
793
+ };
794
+ }, et = () => J({ [n]: { entitlements: {} } }), Y = "_voltro_billing_flush_claims", tt = C(Y, {
795
+ id: v({ scheme: "numeric" }),
796
+ windowKey: w(),
797
+ claimedBy: w(),
798
+ claimedAt: T().default("now")
799
+ }).unique("billingFlushClaimWindow", ["windowKey"]), nt = (e, t) => `billing-flush:${Math.floor(e / Math.max(1, t))}`, rt = async (e, t, n, r) => {
800
+ try {
801
+ return (await e.insertIgnore(Y, {
802
+ windowKey: t,
803
+ claimedBy: n,
804
+ claimedAt: new Date(r)
805
+ }, { conflictColumns: ["windowKey"] })).claimedBy === n;
806
+ } catch {
807
+ return !0;
808
+ }
809
+ }, it = (e) => {
810
+ let t = e?.baseUrl ?? "https://mock.billing.local", n = [], r = (e) => typeof e == "object" && !!e && typeof e._tag == "string";
811
+ return {
812
+ name: "mock",
813
+ supportsMeteredUsage: e?.supportsMeteredUsage ?? !0,
814
+ pushed: n,
815
+ createCheckoutSession: (e) => d.succeed({ url: `${t}/checkout?tenant=${encodeURIComponent(e.tenantId)}&price=${encodeURIComponent(e.priceId)}` }),
816
+ createPortalSession: (e) => d.succeed({ url: `${t}/portal?customer=${encodeURIComponent(e.providerCustomerId)}` }),
817
+ reportUsage: (e) => d.sync(() => {
818
+ n.push(e);
819
+ }),
820
+ normalizeEvent: (e) => d.succeed(r(e) ? e : null)
821
+ };
822
+ }, at = "https://api.stripe.com/v1", X = (t, n) => new e({
823
+ source: "stripe",
824
+ message: t,
825
+ transient: n
826
+ }), ot = (e) => {
827
+ let t = new URLSearchParams();
828
+ for (let [n, r] of Object.entries(e)) r !== void 0 && t.set(n, r);
829
+ return t.toString();
830
+ }, st = (e, t, n) => d.tryPromise({
831
+ try: async () => {
832
+ let r = await fetch(`${at}${t}`, {
833
+ method: "POST",
834
+ headers: {
835
+ Authorization: `Bearer ${e}`,
836
+ "Content-Type": "application/x-www-form-urlencoded"
837
+ },
838
+ body: ot(n)
839
+ }), i = await r.json();
840
+ if (!r.ok) {
841
+ let e = r.status >= 500 || r.status === 429, t = i.error?.message ?? `HTTP ${r.status}`;
842
+ throw Object.assign(Error(t), { transient: e });
843
+ }
844
+ return i;
845
+ },
846
+ catch: (e) => {
847
+ let t = e.transient ?? !0;
848
+ return X(String(e?.message ?? e), t);
849
+ }
850
+ }), ct = {
851
+ active: "active",
852
+ trialing: "trialing",
853
+ past_due: "pastDue",
854
+ unpaid: "pastDue",
855
+ canceled: "canceled",
856
+ incomplete: "incomplete",
857
+ incomplete_expired: "canceled"
858
+ }, Z = (e) => typeof e == "object" && e ? e : null, lt = (e) => typeof e == "number" && Number.isFinite(e) ? /* @__PURE__ */ new Date(e * 1e3) : null, Q = (e) => {
859
+ let t = Z(e.metadata)?.tenantId;
860
+ if (typeof t == "string" && t.length > 0) return t;
861
+ let n = e.client_reference_id;
862
+ return typeof n == "string" && n.length > 0 ? n : null;
863
+ }, ut = (e) => {
864
+ let t = Z(e.metadata)?.plan;
865
+ return typeof t == "string" && t.length > 0 ? t : null;
866
+ }, dt = (e) => {
867
+ let t = Z(e.items)?.data, n = (Array.isArray(t) ? Z(t[0]) : null)?.quantity;
868
+ return typeof n == "number" && Number.isInteger(n) && n > 0 ? n : 1;
869
+ }, ft = (e) => {
870
+ let t = Z(e);
871
+ if (t === null) return null;
872
+ let n = t.type;
873
+ if (typeof n != "string") return null;
874
+ let r = Z(Z(t.data)?.object);
875
+ if (r === null) return null;
876
+ switch (n) {
877
+ case "customer.subscription.created":
878
+ case "customer.subscription.updated": {
879
+ let e = Q(r), t = ut(r), n = r.id, i = ct[String(r.status)];
880
+ return e === null || t === null || typeof n != "string" || i === void 0 ? null : {
881
+ _tag: "subscriptionUpserted",
882
+ tenantId: e,
883
+ providerSubscriptionId: n,
884
+ plan: t,
885
+ status: i,
886
+ quantity: dt(r),
887
+ currentPeriodStart: lt(r.current_period_start),
888
+ currentPeriodEnd: lt(r.current_period_end),
889
+ cancelAt: lt(r.cancel_at)
890
+ };
891
+ }
892
+ case "customer.subscription.deleted": {
893
+ let e = r.id, t = Q(r);
894
+ return typeof e != "string" || t === null ? null : {
895
+ _tag: "subscriptionCanceled",
896
+ tenantId: t,
897
+ providerSubscriptionId: e
898
+ };
899
+ }
900
+ case "invoice.paid":
901
+ case "invoice.payment_failed": {
902
+ let e = Q(r), t = r.id, i = r.amount_due, a = r.currency;
903
+ return e === null || typeof t != "string" || typeof i != "number" || typeof a != "string" ? null : {
904
+ _tag: n === "invoice.paid" ? "invoicePaid" : "invoicePaymentFailed",
905
+ tenantId: e,
906
+ providerInvoiceId: t,
907
+ amountMinor: i,
908
+ currency: a
909
+ };
910
+ }
911
+ case "customer.created": {
912
+ let e = Q(r), t = r.id;
913
+ return e === null || typeof t != "string" ? null : {
914
+ _tag: "customerLinked",
915
+ tenantId: e,
916
+ providerCustomerId: t
917
+ };
918
+ }
919
+ default: return null;
920
+ }
921
+ }, pt = (e) => {
922
+ let { apiKey: t } = e;
923
+ return {
924
+ name: "stripe",
925
+ supportsMeteredUsage: e.supportsMeteredUsage ?? !0,
926
+ createCheckoutSession: (e) => st(t, "/checkout/sessions", {
927
+ mode: "subscription",
928
+ "line_items[0][price]": e.priceId,
929
+ "line_items[0][quantity]": "1",
930
+ success_url: e.successUrl,
931
+ cancel_url: e.cancelUrl,
932
+ client_reference_id: e.tenantId,
933
+ "metadata[tenantId]": e.tenantId,
934
+ "subscription_data[metadata][tenantId]": e.tenantId,
935
+ customer: e.providerCustomerId
936
+ }).pipe(d.flatMap((e) => {
937
+ let t = e.url;
938
+ return typeof t == "string" ? d.succeed({ url: t }) : d.fail(X("checkout session response missing url", !1));
939
+ })),
940
+ createPortalSession: (e) => st(t, "/billing_portal/sessions", {
941
+ customer: e.providerCustomerId,
942
+ return_url: e.returnUrl
943
+ }).pipe(d.flatMap((e) => {
944
+ let t = e.url;
945
+ return typeof t == "string" ? d.succeed({ url: t }) : d.fail(X("portal session response missing url", !1));
946
+ })),
947
+ reportUsage: (e) => st(t, "/billing/meter_events", {
948
+ event_name: e.entitlementKey,
949
+ "payload[value]": String(e.quantity),
950
+ "payload[stripe_customer_id]": e.tenantId
951
+ }).pipe(d.asVoid),
952
+ normalizeEvent: (e) => d.succeed(ft(e))
953
+ };
954
+ }, mt = (e) => {
955
+ let { provider: t, env: n } = e;
956
+ if (t !== void 0 && typeof t == "object") return t;
957
+ if ((t ?? (n.STRIPE_SECRET_KEY ? "stripe" : "mock")) === "stripe") {
958
+ let t = e.apiKey ?? n.STRIPE_SECRET_KEY;
959
+ if (!t) throw Error("billingPlugin: provider 'stripe' needs an apiKey (option or STRIPE_SECRET_KEY env)");
960
+ return pt({ apiKey: t });
961
+ }
962
+ return it();
963
+ }, ht = O({ scope: "@voltro/plugin-billing" }), gt = "billing", $ = "/billing/webhook", _t = (e) => E({
964
+ id: gt,
965
+ path: $,
966
+ provider: D(),
967
+ payload: m.Unknown,
968
+ handler: async (t) => {
969
+ let n = await d.runPromise(e.provider.normalizeEvent(t.body));
970
+ if (n === null) return;
971
+ await d.runPromise(e.service.applyEvent(n));
972
+ let r = e.onEvent?.[n._tag];
973
+ r !== void 0 && await d.runPromise(r(n).pipe(d.catchAllCause((e) => d.sync(() => {
974
+ ht.warn("billing onEvent side effect failed", {
975
+ event: n._tag,
976
+ cause: String(e)
977
+ });
978
+ }))));
979
+ }
980
+ }), vt = (e) => re(_t(e), {
981
+ resolveSecret: () => Promise.resolve(e.webhookSecret),
982
+ ...e.idempotencyCache === void 0 ? {} : { idempotencyCache: e.idempotencyCache },
983
+ log: (e) => {
984
+ e.status >= 400 && ht.warn("billing webhook rejected", { ...e });
985
+ }
986
+ }), yt = (e) => e === null ? null : {
987
+ plan: e.plan,
988
+ status: e.status,
989
+ quantity: e.quantity,
990
+ currentPeriodEnd: e.currentPeriodEnd ? e.currentPeriodEnd.toISOString() : null,
991
+ cancelAt: e.cancelAt ? e.cancelAt.toISOString() : null
992
+ }, bt = h([{
993
+ name: "STRIPE_SECRET_KEY",
994
+ required: !1,
995
+ secret: !0,
996
+ description: "Stripe API secret key; its presence selects the stripe provider (else mock)."
997
+ }, {
998
+ name: "STRIPE_WEBHOOK_SECRET",
999
+ required: !1,
1000
+ secret: !0,
1001
+ description: "Stripe webhook signing secret (whsec_…) for verifying webhook payloads."
1002
+ }]), xt = (t = {}) => {
1003
+ let n = process.env, f = mt({
1004
+ ...t.provider === void 0 ? {} : { provider: t.provider },
1005
+ ...t.apiKey === void 0 ? {} : { apiKey: t.apiKey },
1006
+ env: n
1007
+ }), p = J(t.plans ?? {}), m = t.webhookSecret ?? bt.read("STRIPE_WEBHOOK_SECRET") ?? null;
1008
+ x({
1009
+ table: R,
1010
+ timeColumn: "updatedAt",
1011
+ ttlMs: S(process.env.VOLTRO_BILLING_USAGE_TTL_HOURS, 24 * 400),
1012
+ label: "billing usage counters"
1013
+ }), x({
1014
+ table: Y,
1015
+ timeColumn: "claimedAt",
1016
+ ttlMs: S(process.env.VOLTRO_BILLING_FLUSH_CLAIM_TTL_HOURS, 1),
1017
+ label: "billing flush claims"
1018
+ });
1019
+ let h = t.dunningSchedule ?? N;
1020
+ se(h);
1021
+ let { service: g, holder: _ } = Le({
1022
+ provider: f,
1023
+ plans: p,
1024
+ dunningSchedule: h,
1025
+ ...t.attempts === void 0 ? {} : { attempts: t.attempts },
1026
+ ...t.resolveEntitlementLimit === void 0 ? {} : { resolveEntitlementLimit: t.resolveEntitlementLimit }
1027
+ }), ne = Re(g), v = vt({
1028
+ provider: f,
1029
+ service: g,
1030
+ webhookSecret: m,
1031
+ ...t.onEvent === void 0 ? {} : { onEvent: t.onEvent }
1032
+ }), y = {
1033
+ method: "POST",
1034
+ path: $,
1035
+ handle: async (e) => {
1036
+ let t = await v({
1037
+ method: e.method,
1038
+ path: e.path,
1039
+ headers: e.headers,
1040
+ rawBody: e.rawBody
1041
+ });
1042
+ return {
1043
+ status: t.status,
1044
+ body: t.body,
1045
+ contentType: t.contentType,
1046
+ ...t.headers === void 0 ? {} : { headers: { ...t.headers } }
1047
+ };
1048
+ }
1049
+ }, b = [
1050
+ {
1051
+ ...l,
1052
+ execute: (e, t) => {
1053
+ let n = e;
1054
+ return g.startCheckout({
1055
+ tenantId: t.request.subject.tenantId ?? "",
1056
+ plan: n.plan,
1057
+ successUrl: n.successUrl,
1058
+ cancelUrl: n.cancelUrl
1059
+ });
1060
+ }
1061
+ },
1062
+ {
1063
+ ...s,
1064
+ execute: (e, t) => {
1065
+ let n = e;
1066
+ return g.portalUrl(t.request.subject.tenantId ?? "", n.returnUrl);
1067
+ }
1068
+ },
1069
+ {
1070
+ ...u,
1071
+ execute: (e, t) => g.subscription(t.request.subject.tenantId ?? "").pipe(d.map((e) => ({ subscription: yt(e) })))
1072
+ },
1073
+ {
1074
+ ...c,
1075
+ execute: (e, t) => {
1076
+ let n = e;
1077
+ return g.reportUsage(t.request.subject.tenantId ?? "", n.key, n.quantity).pipe(d.map(() => ({ ok: !0 })));
1078
+ }
1079
+ },
1080
+ {
1081
+ ...a,
1082
+ execute: (e, t) => {
1083
+ let n = e;
1084
+ return g.changePlan(t.request.subject.tenantId ?? "", n.plan).pipe(d.map((e) => ({
1085
+ plan: e.plan,
1086
+ quantity: e.quantity,
1087
+ prorationMinor: e.prorationMinor,
1088
+ currency: e.currency
1089
+ })));
1090
+ }
1091
+ },
1092
+ {
1093
+ ...o,
1094
+ execute: (e, t) => {
1095
+ let n = e;
1096
+ return g.changeSeats(t.request.subject.tenantId ?? "", n.quantity).pipe(d.map((e) => ({
1097
+ plan: e.plan,
1098
+ quantity: e.quantity,
1099
+ prorationMinor: e.prorationMinor,
1100
+ currency: e.currency
1101
+ })));
1102
+ }
1103
+ }
1104
+ ], C = (e, t) => ({
1105
+ kind: "json",
1106
+ status: e,
1107
+ data: { error: t }
1108
+ }), w = [{
1109
+ method: "GET",
1110
+ path: "/stats",
1111
+ description: "Billing config: provider, declared plans, enforced tags",
1112
+ handler: () => d.succeed({
1113
+ kind: "json",
1114
+ data: {
1115
+ provider: f.name,
1116
+ supportsMeteredUsage: f.supportsMeteredUsage,
1117
+ plans: p.planIds(),
1118
+ enforce: t.enforce ? Object.keys(t.enforce) : [],
1119
+ webhookPath: $
1120
+ }
1121
+ })
1122
+ }, {
1123
+ method: "GET",
1124
+ path: "/usage",
1125
+ description: "Pending (un-flushed) metered usage counters",
1126
+ handler: () => _.stores.usage.pending().pipe(d.map((e) => ({
1127
+ kind: "json",
1128
+ data: { usage: e }
1129
+ })), d.catchAll((e) => d.succeed(C(500, e.message))))
1130
+ }], T = [{
1131
+ schema: e,
1132
+ import: {
1133
+ module: "@voltro/plugin-billing/errors",
1134
+ name: "BillingError"
1135
+ }
1136
+ }, {
1137
+ schema: r,
1138
+ import: {
1139
+ module: "@voltro/plugin-billing/errors",
1140
+ name: "EntitlementExceeded"
1141
+ }
1142
+ }], E = t.enforce, re = E !== void 0 && Object.keys(E).length > 0 ? ie(E, g) : void 0, D = t.metering;
1143
+ D !== void 0 && Be(D);
1144
+ let O = D !== void 0 && Object.keys(D).length > 0 ? qe(g, D) : void 0, k = D !== void 0 && Qe(D), A = ee([O, re].filter((e) => e !== void 0)), j, M, ae = [
1145
+ "store:write",
1146
+ "inspect:read",
1147
+ "network:outbound:*",
1148
+ ...A === void 0 ? [] : ["rpc:intercept:mutation", "rpc:intercept:action"],
1149
+ ...O === void 0 ? [] : ["rpc:intercept:query"],
1150
+ ...k ? ["store:changes:read"] : []
1151
+ ];
1152
+ return te({
1153
+ name: t.name ? `@voltro/plugin-billing#${t.name}` : "@voltro/plugin-billing",
1154
+ description: "Billing — subscriptions, plans, entitlements + usage metering over a pluggable BillingProvider (Stripe + mock). Ships the BillingService, the requireEntitlement() in-handler guard + the enforce-map interceptor, the /billing/webhook receiver (riding @voltro/plugin-webhooks for signature + idempotency), and the _voltro_billing_* tables. Money is integer minor units.",
1155
+ permissions: ae,
1156
+ declaredEnv: bt.declared,
1157
+ services: ne,
1158
+ routes: b,
1159
+ rpcClientDescriptors: i,
1160
+ httpRoutes: [y],
1161
+ inspectEndpoints: w,
1162
+ extendSchema: { tables: [..._e(), tt] },
1163
+ errorSchemas: T,
1164
+ ...A === void 0 ? {} : {
1165
+ interceptMutation: A,
1166
+ interceptAction: A
1167
+ },
1168
+ ...O === void 0 ? {} : { interceptQuery: O },
1169
+ ...k ? { onChangeEvent: Je(g, D) } : {},
1170
+ bindDataStore: (e) => {
1171
+ _.stores = Fe(e), j = e;
1172
+ },
1173
+ onActivate: (e) => d.sync(() => {
1174
+ e.logger.info("billing active", {
1175
+ provider: f.name,
1176
+ plans: p.planIds().length,
1177
+ enforce: E ? Object.keys(E).length : 0,
1178
+ metering: D ? Object.keys(D).length : 0,
1179
+ dunningRetries: h.length,
1180
+ webhook: $
1181
+ });
1182
+ let n = t.flushIntervalMs ?? 6e4, r = D, i = r !== void 0 && Object.keys(r).length > 0;
1183
+ if (n > 0) {
1184
+ let e = process.env.HOSTNAME ?? `pid-${process.pid}`;
1185
+ M = setInterval(() => {
1186
+ let t = j;
1187
+ if (t === void 0) return;
1188
+ let a = Date.now();
1189
+ (async () => {
1190
+ await rt(t, nt(a, n), e, a) && (i && await d.runPromise(Ze(g, t, r).pipe(d.catchAll(() => d.void))), await d.runPromise(g.runDunningCycle(new Date(a)).pipe(d.catchAll(() => d.void))));
1191
+ })();
1192
+ }, n), M.unref?.();
1193
+ }
1194
+ }),
1195
+ onDeactivate: () => d.sync(() => {
1196
+ M !== void 0 && (clearInterval(M), M = void 0);
1197
+ })
1198
+ });
1199
+ };
1200
+ //#endregion
1201
+ export { F as BILLING_CUSTOMERS_TABLE, z as BILLING_DUNNING_TABLE, Y as BILLING_FLUSH_CLAIMS_TABLE, L as BILLING_INVOICES_TABLE, I as BILLING_SUBSCRIPTIONS_TABLE, R as BILLING_USAGE_TABLE, gt as BILLING_WEBHOOK_ID, $ as BILLING_WEBHOOK_PATH, e as BillingError, t as BillingService, N as DEFAULT_DUNNING_SCHEDULE, n as DEFAULT_PLAN, r as EntitlementExceeded, ue as advanceDunning, fe as billingCustomersTable, ge as billingDunningTable, tt as billingFlushClaimsTable, me as billingInvoicesTable, xt as billingPlugin, Re as billingServiceLayer, pe as billingSubscriptionsTable, _e as billingTables, he as billingUsageTable, _t as billingWebhookDescriptor, Le as buildBillingService, ie as buildEnforceInterceptor, J as buildPlanRegistry, rt as claimFlushWindow, ae as computeProration, Fe as dataStoreStores, et as emptyPlanRegistry, k as evaluateEntitlement, nt as flushWindowKey, Pe as memoryStores, it as mockProvider, vt as mountBillingWebhook, ft as normalizeStripeEvent, ce as openDunning, K as periodKey, le as recoverDunning, j as requireEntitlement, mt as resolveProvider, P as retryTimeFor, M as roundHalfAwayFromZero, de as runDunningCycle, pt as stripeProvider, se as validateDunningSchedule };