@voltro/plugin-billing 0.85.0 → 0.87.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 CHANGED
@@ -1,2653 +1,43 @@
1
1
  import { BillingError as e, BillingService as t, DEFAULT_PLAN as n, EntitlementExceeded as r, SubscriptionLocked as i } from "./types.js";
2
- import { billingRpcClientImports as a, cancelSubscriptionDescriptor as o, changePlanDescriptor as s, changeSeatsDescriptor as c, entitlementStatusDescriptor as l, invoicesDescriptor as u, portalUrlDescriptor as d, previewChangeDescriptor as f, reportUsageDescriptor as p, resumeSubscriptionDescriptor as m, startCheckoutDescriptor as h, subscriptionDescriptor as g, syncDescriptor as _ } from "./rpc.js";
3
- import { Effect as v, Layer as y, Schedule as b, Schema as x } from "effect";
4
- import { pluginEnv as S } from "@voltro/env";
5
- import { composeRpcInterceptors as C, definePlugin as ee, pluginInstanceName as w } from "@voltro/protocol";
6
- import { OccurrenceCounter as T, and as E, changeDigest as te, composeChangeKey as D, eq as O, gte as ne, id as k, integer as A, occurrenceWindowMs as re, registerRetention as ie, retentionTtlMsFromEnv as ae, table as j, text as M, timestamp as N } from "@voltro/database";
7
- import { replicaId as oe } from "@voltro/protocol/identity";
8
- import { createLogger as P } from "@voltro/logger";
9
- import F from "stripe";
10
- import { defineIncomingWebhook as I, mountIncomingWebhook as L } from "@voltro/plugin-webhooks";
11
- import { stripeProvider as se } from "@voltro/plugin-webhooks/providers";
12
- import { execFileSync as R } from "node:child_process";
13
- import { chmodSync as z, existsSync as B, readFileSync as ce, realpathSync as le, writeFileSync as ue } from "node:fs";
14
- import { dirname as de, resolve as V } from "node:path";
15
- //#region src/entitlement.ts
16
- var fe = (e, t, n, r) => ({
17
- allowed: !Number.isFinite(t) || r <= 0 || n + r <= t,
18
- entitlement: e,
19
- limit: t,
20
- used: n,
21
- cost: r
22
- }), pe = (e) => e.tenantId, me = (e, n, i) => v.gen(function* () {
23
- let a = pe(e.request.subject);
24
- if (a === null) return i <= 0 ? void 0 : yield* v.fail(new r({
25
- entitlement: n,
26
- limit: 0,
27
- used: 0,
28
- cost: i
29
- }));
30
- yield* (yield* t).consumeEntitlement(a, n, i);
31
- }), he = (e) => v.gen(function* () {
32
- let n = pe(e.request.subject);
33
- if (n === null) return;
34
- let r = yield* (yield* t).entitlementStatus(n);
35
- if (!r.entitled) return yield* v.fail(new i({
36
- tenantId: n,
37
- status: r.status,
38
- lockedSince: (r.lockedSince ?? /* @__PURE__ */ new Date()).toISOString(),
39
- lockout: r.lockout
40
- }));
41
- }), ge = (e, t) => (n, i) => {
42
- let a = e[i.tag];
43
- if (a === void 0) return n;
44
- let o = a.cost ?? 1, s = i.subject.tenantId;
45
- return s === null ? o <= 0 ? n : v.fail(new r({
46
- entitlement: a.entitlement,
47
- limit: 0,
48
- used: 0,
49
- cost: o
50
- })) : v.flatMap(t.consumeEntitlement(s, a.entitlement, o), () => n);
51
- }, H = "_voltro_billing_customers", U = "_voltro_billing_subscriptions", W = "_voltro_billing_invoices", G = "_voltro_billing_usage", _e = j(H, {
52
- id: k({ prefix: "bcust" }),
53
- tenantId: M(),
54
- provider: M(),
55
- providerCustomerId: M(),
56
- createdAt: N(),
57
- updatedAt: N()
58
- }).index("byBillingCustomerTenant", ["tenantId"]).index("byBillingProviderCustomer", ["providerCustomerId"]), ve = j(U, {
59
- id: k({ prefix: "bsub" }),
60
- tenantId: M(),
61
- provider: M(),
62
- providerSubscriptionId: M(),
63
- plan: M(),
64
- status: M(),
65
- quantity: A().default(1),
66
- currentPeriodStart: N().nullable(),
67
- currentPeriodEnd: N().nullable(),
68
- cancelAt: N().nullable(),
69
- pastDueSince: N().nullable(),
70
- statusEventAt: N().nullable(),
71
- createdAt: N(),
72
- updatedAt: N()
73
- }).index("byBillingSubscriptionTenant", ["tenantId"]).index("byBillingProviderSubscription", ["providerSubscriptionId"]).index("byBillingSubscriptionStatus", ["status"]), ye = j(W, {
74
- id: k({ prefix: "binv" }),
75
- tenantId: M(),
76
- provider: M(),
77
- providerInvoiceId: M(),
78
- amountMinor: A(),
79
- currency: M(),
80
- status: M(),
81
- statusEventAt: N().nullable(),
82
- createdAt: N()
83
- }).index("byBillingInvoiceTenant", ["tenantId"]).index("byBillingProviderInvoice", ["providerInvoiceId"]), be = j(G, {
84
- id: k({ prefix: "buse" }),
85
- tenantId: M().maxLength(191),
86
- entitlementKey: M().maxLength(191),
87
- period: M().maxLength(16),
88
- used: A().default(0),
89
- reportedToProvider: A().default(0),
90
- updatedAt: N()
91
- }).unique("byBillingUsageWindow", [
92
- "tenantId",
93
- "entitlementKey",
94
- "period"
95
- ]), xe = () => [
96
- _e,
97
- ve,
98
- ye,
99
- be
100
- ], Se = "_voltro_billing_dunning_notices", Ce = j(Se, {
101
- id: k({ prefix: "bdun" }),
102
- tenantId: M().maxLength(191),
103
- episode: M().maxLength(32),
104
- stepId: M().maxLength(64),
105
- sentAt: N()
106
- }).unique("byBillingDunningNotice", [
107
- "tenantId",
108
- "episode",
109
- "stepId"
110
- ]), we = "locked", Te = [
111
- {
112
- id: "payment-failed",
113
- afterHours: 0
114
- },
115
- {
116
- id: "reminder",
117
- afterHours: 72
118
- },
119
- {
120
- id: "final-warning",
121
- afterHours: 144
122
- }
123
- ], Ee = 168, De = "hard", Oe = (e, t, n) => {
124
- if (e === void 0 || e.trim() === "") return t;
125
- let r = Number(e);
126
- if (!Number.isFinite(r) || r < 0) throw Error(`billingPlugin: ${n} must be a non-negative number, got '${e}'`);
127
- return r;
128
- }, ke = (e, t) => {
129
- let n = t.VOLTRO_BILLING_DUNNING;
130
- if (n !== void 0 && n !== "" && n !== "on" && n !== "off") throw Error(`billingPlugin: VOLTRO_BILLING_DUNNING must be 'on' or 'off', got '${n}'`);
131
- let r = n === "off" ? !1 : n === "on" ? !0 : e?.enabled ?? !0, i = t.VOLTRO_BILLING_LOCKOUT;
132
- if (i !== void 0 && i !== "" && i !== "hard" && i !== "soft") throw Error(`billingPlugin: VOLTRO_BILLING_LOCKOUT must be 'hard' or 'soft', got '${i}'`);
133
- let a = i === "hard" || i === "soft" ? i : e?.lockout ?? "hard", o = Oe(t.VOLTRO_BILLING_GRACE_HOURS, e?.graceHours ?? 168, "VOLTRO_BILLING_GRACE_HOURS"), s = e?.steps ?? Te, c = t.VOLTRO_BILLING_DUNNING_STEP_HOURS, l = s;
134
- if (c !== void 0 && c.trim() !== "") {
135
- let e = c.split(",").map((e) => e.trim());
136
- if (e.length !== s.length) throw Error(`billingPlugin: VOLTRO_BILLING_DUNNING_STEP_HOURS lists ${e.length} value(s) but the sequence declares ${s.length} step(s) (${s.map((e) => e.id).join(", ")})`);
137
- l = s.map((t, n) => ({
138
- id: t.id,
139
- afterHours: Oe(e[n], t.afterHours, "VOLTRO_BILLING_DUNNING_STEP_HOURS")
140
- }));
141
- }
142
- let u = /* @__PURE__ */ new Set();
143
- for (let e of l) {
144
- if (e.id === "locked") throw Error(`billingPlugin: '${we}' is the reserved lockout step id`);
145
- if (u.has(e.id)) throw Error(`billingPlugin: duplicate dunning step id '${e.id}'`);
146
- u.add(e.id);
147
- }
148
- return {
149
- enabled: r,
150
- graceHours: o,
151
- lockout: a,
152
- steps: [...l].sort((e, t) => e.afterHours - t.afterHours),
153
- ...e?.notify === void 0 ? {} : { notify: e.notify },
154
- ...e?.resolveRecipient === void 0 ? {} : { resolveRecipient: e.resolveRecipient },
155
- ...e?.portalReturnUrl === void 0 ? {} : { portalReturnUrl: e.portalReturnUrl }
156
- };
157
- }, Ae = 36e5, je = (e) => String(e.getTime()), Me = (e, t) => new Date(e.getTime() + t * Ae), Ne = (e, t, n) => {
158
- let r = (n.getTime() - t.getTime()) / Ae;
159
- return e.filter((e) => r >= e.afterHours);
160
- }, Pe = (e, t, r, i) => {
161
- let a = {
162
- tenantId: e?.tenantId ?? i ?? "",
163
- lockout: t.lockout
164
- };
165
- if (e === null) return {
166
- ...a,
167
- plan: n,
168
- billedPlan: n,
169
- status: "none",
170
- entitled: !0,
171
- inGrace: !1,
172
- graceEndsAt: null,
173
- lockedSince: null
174
- };
175
- if (e.status === "active" || e.status === "trialing") return {
176
- ...a,
177
- plan: e.plan,
178
- billedPlan: e.plan,
179
- status: e.status,
180
- entitled: !0,
181
- inGrace: !1,
182
- graceEndsAt: null,
183
- lockedSince: null
184
- };
185
- if (e.status === "pastDue") {
186
- if (!t.enabled) return {
187
- ...a,
188
- plan: n,
189
- billedPlan: e.plan,
190
- status: e.status,
191
- entitled: !0,
192
- inGrace: !1,
193
- graceEndsAt: null,
194
- lockedSince: null
195
- };
196
- if (e.pastDueSince === null) return {
197
- ...a,
198
- plan: e.plan,
199
- billedPlan: e.plan,
200
- status: e.status,
201
- entitled: !0,
202
- inGrace: !0,
203
- graceEndsAt: null,
204
- lockedSince: null
205
- };
206
- let i = Me(e.pastDueSince, t.graceHours), o = r.getTime() >= i.getTime();
207
- return {
208
- ...a,
209
- plan: o && t.lockout === "hard" ? n : e.plan,
210
- billedPlan: e.plan,
211
- status: e.status,
212
- entitled: !o,
213
- inGrace: !o,
214
- graceEndsAt: i,
215
- lockedSince: o ? i : null
216
- };
217
- }
218
- return {
219
- ...a,
220
- plan: n,
221
- billedPlan: e.plan,
222
- status: e.status,
223
- entitled: !0,
224
- inGrace: !1,
225
- graceEndsAt: null,
226
- lockedSince: null
227
- };
228
- }, Fe = (e) => e.toISOString().slice(0, 10), Ie = (e) => {
229
- let t = e.portalUrl, n = t === null ? "<p>Please update your payment method in the billing settings.</p>" : `<p><a href="${t}">Update your payment method</a></p>`, r = t === null ? "Please update your payment method in the billing settings." : `Update your payment method: ${t}`;
230
- if (e.locked) return {
231
- subject: "Your subscription has been suspended",
232
- html: `<p>We were unable to collect payment, and your subscription is now suspended.</p>${n}`,
233
- text: `We were unable to collect payment, and your subscription is now suspended.\n\n${r}`
234
- };
235
- let i = Fe(e.graceEndsAt), a = e.stepId === "payment-failed" ? "Your last payment could not be collected." : "Your payment is still outstanding.";
236
- return {
237
- subject: "Action needed: your payment could not be collected",
238
- html: `<p>${a} Your subscription stays active until ${i}.</p>${n}`,
239
- text: `${a} Your subscription stays active until ${i}.\n\n${r}`
240
- };
241
- }, Le = (e) => (t) => t.to === null ? v.void : e.send({
242
- to: t.to,
243
- subject: t.subject,
244
- html: t.html,
245
- text: t.text
246
- }).pipe(v.asVoid), Re = (e, t, n) => `${e}\u0000${t}\u0000${n}`, ze = () => {
247
- let e = /* @__PURE__ */ new Map();
248
- return {
249
- claim: (t, n, r, i) => v.sync(() => {
250
- let a = Re(t, n, r);
251
- return !e.has(a) && (e.set(a, {
252
- tenantId: t,
253
- episode: n,
254
- stepId: r,
255
- sentAt: i
256
- }), !0);
257
- }),
258
- sentSteps: (t, n) => v.sync(() => [...e.values()].filter((e) => e.tenantId === t && e.episode === n))
259
- };
260
- }, Be = () => `bdun_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, Ve = (t) => ({
261
- claim: (n, r, i, a) => v.tryPromise({
262
- try: async () => {
263
- let e = Be(), o = await t.insertIgnore(Se, {
264
- id: e,
265
- tenantId: n,
266
- episode: r,
267
- stepId: i,
268
- sentAt: a
269
- }, { conflictColumns: [
270
- "tenantId",
271
- "episode",
272
- "stepId"
273
- ] });
274
- return String(o.id) === e;
275
- },
276
- catch: (t) => new e({
277
- source: "dunningNoticeStore",
278
- message: String(t?.message ?? t),
279
- transient: !0
280
- })
281
- }),
282
- sentSteps: (n, r) => v.tryPromise({
283
- try: async () => (await t.query({
284
- table: Se,
285
- predicate: E(O("tenantId", n), O("episode", r)),
286
- order: [],
287
- take: void 0,
288
- skip: void 0,
289
- projection: void 0
290
- })).map((e) => ({
291
- tenantId: String(e.tenantId),
292
- episode: String(e.episode),
293
- stepId: String(e.stepId),
294
- sentAt: e.sentAt == null ? null : new Date(e.sentAt)
295
- })),
296
- catch: (t) => new e({
297
- source: "dunningNoticeStore",
298
- message: String(t?.message ?? t),
299
- transient: !0
300
- })
301
- })
302
- }), He = (e) => `${e}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`, Ue = P({ scope: "@voltro/plugin-billing" }), K = (t, n) => v.tryPromise({
303
- try: t,
304
- catch: (t) => (Ue.warn("billing store operation failed", {
305
- source: n,
306
- detail: String(t?.message ?? t)
307
- }), new e({
308
- source: n,
309
- message: `${n} failed`,
310
- transient: !0
311
- }))
312
- }), q = (e) => ({
313
- table: e.table,
314
- predicate: e.predicate,
315
- order: e.order ?? [],
316
- take: e.take,
317
- skip: void 0,
318
- projection: void 0
319
- }), We = () => {
320
- let e = /* @__PURE__ */ new Map();
321
- return {
322
- upsert: (t) => v.sync(() => {
323
- e.set(t.tenantId, t);
324
- }),
325
- getByTenant: (t) => v.sync(() => e.get(t) ?? null),
326
- getByProviderCustomerId: (t) => v.sync(() => [...e.values()].find((e) => e.providerCustomerId === t) ?? null),
327
- list: () => v.sync(() => [...e.values()]),
328
- removeByProviderCustomerId: (t) => v.sync(() => {
329
- for (let [n, r] of e) if (r.providerCustomerId === t) return e.delete(n), n;
330
- return null;
331
- })
332
- };
333
- }, Ge = (e) => ({
334
- tenantId: String(e.tenantId),
335
- provider: String(e.provider),
336
- providerCustomerId: String(e.providerCustomerId)
337
- }), Ke = (e) => ({
338
- upsert: (t) => K(async () => {
339
- let n = await e.query(q({
340
- table: H,
341
- predicate: O("tenantId", t.tenantId),
342
- take: 1
343
- })), r = /* @__PURE__ */ new Date();
344
- n[0] ? await e.update(H, String(n[0].id), {
345
- provider: t.provider,
346
- providerCustomerId: t.providerCustomerId,
347
- updatedAt: r
348
- }) : await e.insert(H, {
349
- id: He("bcust"),
350
- ...t,
351
- createdAt: r,
352
- updatedAt: r
353
- });
354
- }, "customerStore"),
355
- getByTenant: (t) => K(async () => {
356
- let n = await e.query(q({
357
- table: H,
358
- predicate: O("tenantId", t),
359
- take: 1
360
- }));
361
- return n[0] ? Ge(n[0]) : null;
362
- }, "customerStore"),
363
- getByProviderCustomerId: (t) => K(async () => {
364
- let n = await e.query(q({
365
- table: H,
366
- predicate: O("providerCustomerId", t),
367
- take: 1
368
- }));
369
- return n[0] ? Ge(n[0]) : null;
370
- }, "customerStore"),
371
- list: () => K(async () => (await e.query(q({ table: H }))).map(Ge), "customerStore"),
372
- removeByProviderCustomerId: (t) => K(async () => {
373
- let n = await e.query(q({
374
- table: H,
375
- predicate: O("providerCustomerId", t)
376
- })), r = null;
377
- for (let t of n) r = String(t.tenantId), await e.delete(H, String(t.id));
378
- return r;
379
- }, "customerStore")
380
- }), qe = (e) => e === "canceled", Je = /* @__PURE__ */ new Set([
381
- "active",
382
- "trialing",
383
- "pastDue",
384
- "incomplete"
385
- ]), Ye = (e) => Je.has(e), Xe = (e) => e.find((e) => Ye(e.status)) ?? e[0] ?? null, Ze = (e, t) => e === null ? !1 : t === null || t.getTime() < e.getTime(), Qe = () => {
386
- 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;
387
- return {
388
- upsert: (n) => v.sync(() => {
389
- let r = e.get(n.providerSubscriptionId);
390
- r && Ze(r.statusEventAt, n.statusEventAt) || r && qe(r.status) && !qe(n.status) || e.set(n.providerSubscriptionId, {
391
- ...n,
392
- pastDueSince: r ? r.pastDueSince : n.pastDueSince,
393
- touched: ++t
394
- });
395
- }),
396
- getByTenant: (e) => v.sync(() => {
397
- let t = Xe(n(e));
398
- return t ? r(t) : null;
399
- }),
400
- listByTenant: (e) => v.sync(() => n(e).map(r)),
401
- listByStatus: (t) => v.sync(() => [...e.values()].filter((e) => e.status === t).map(r)),
402
- markCanceled: (n) => v.sync(() => {
403
- let r = e.get(n);
404
- r && e.set(n, {
405
- ...r,
406
- status: "canceled",
407
- touched: ++t
408
- });
409
- }),
410
- patchByTenant: (r, i) => v.sync(() => {
411
- let a = Xe(n(r));
412
- a && e.set(a.providerSubscriptionId, {
413
- ...a,
414
- ...i,
415
- touched: ++t
416
- });
417
- }),
418
- setStatus: (n, r) => v.sync(() => {
419
- let i = e.get(n);
420
- i && e.set(n, {
421
- ...i,
422
- status: r,
423
- touched: ++t
424
- });
425
- })
426
- };
427
- }, $e = (e) => ({
428
- tenantId: String(e.tenantId),
429
- provider: String(e.provider),
430
- providerSubscriptionId: String(e.providerSubscriptionId),
431
- plan: String(e.plan),
432
- status: String(e.status),
433
- quantity: e.quantity == null ? 1 : Number(e.quantity),
434
- currentPeriodStart: e.currentPeriodStart == null ? null : new Date(e.currentPeriodStart),
435
- currentPeriodEnd: e.currentPeriodEnd == null ? null : new Date(e.currentPeriodEnd),
436
- cancelAt: e.cancelAt == null ? null : new Date(e.cancelAt),
437
- pastDueSince: e.pastDueSince == null ? null : new Date(e.pastDueSince),
438
- statusEventAt: e.statusEventAt == null ? null : new Date(e.statusEventAt)
439
- }), et = async (e, t) => (await e.query(q({
440
- table: "_voltro_billing_subscriptions",
441
- predicate: O("providerSubscriptionId", t),
442
- take: 1
443
- })))[0] ?? null, tt = async (e, t) => (await e.query(q({
444
- table: U,
445
- predicate: O("tenantId", t),
446
- order: [{
447
- column: "updatedAt",
448
- direction: "desc"
449
- }]
450
- }))).map($e), nt = (e) => ({
451
- upsert: (t) => K(async () => {
452
- let n = await et(e, t.providerSubscriptionId), r = /* @__PURE__ */ new Date();
453
- if (n) {
454
- if (Ze(n.statusEventAt == null ? null : new Date(n.statusEventAt), t.statusEventAt) || qe(String(n.status)) && !qe(t.status)) return;
455
- await e.update(U, String(n.id), {
456
- tenantId: t.tenantId,
457
- provider: t.provider,
458
- plan: t.plan,
459
- status: t.status,
460
- quantity: t.quantity,
461
- currentPeriodStart: t.currentPeriodStart,
462
- currentPeriodEnd: t.currentPeriodEnd,
463
- cancelAt: t.cancelAt,
464
- statusEventAt: t.statusEventAt,
465
- updatedAt: r
466
- });
467
- } else await e.insert(U, {
468
- id: He("bsub"),
469
- tenantId: t.tenantId,
470
- provider: t.provider,
471
- providerSubscriptionId: t.providerSubscriptionId,
472
- plan: t.plan,
473
- status: t.status,
474
- quantity: t.quantity,
475
- currentPeriodStart: t.currentPeriodStart,
476
- currentPeriodEnd: t.currentPeriodEnd,
477
- cancelAt: t.cancelAt,
478
- pastDueSince: null,
479
- statusEventAt: t.statusEventAt,
480
- createdAt: r,
481
- updatedAt: r
482
- });
483
- }, "subscriptionStore"),
484
- getByTenant: (t) => K(async () => Xe(await tt(e, t)), "subscriptionStore"),
485
- listByTenant: (t) => K(() => tt(e, t), "subscriptionStore"),
486
- listByStatus: (t) => K(async () => (await e.query(q({
487
- table: U,
488
- predicate: O("status", t),
489
- order: [{
490
- column: "updatedAt",
491
- direction: "asc"
492
- }]
493
- }))).map($e), "subscriptionStore"),
494
- markCanceled: (t) => K(async () => {
495
- let n = await et(e, t);
496
- n && await e.update(U, String(n.id), {
497
- status: "canceled",
498
- updatedAt: /* @__PURE__ */ new Date()
499
- });
500
- }, "subscriptionStore"),
501
- patchByTenant: (t, n) => K(async () => {
502
- let r = Xe(await tt(e, t)), i = r === null ? null : await et(e, r.providerSubscriptionId);
503
- i && await e.update(U, String(i.id), {
504
- ...n.plan === void 0 ? {} : { plan: n.plan },
505
- ...n.quantity === void 0 ? {} : { quantity: n.quantity },
506
- ...n.status === void 0 ? {} : { status: n.status },
507
- ...n.currentPeriodStart === void 0 ? {} : { currentPeriodStart: n.currentPeriodStart },
508
- ...n.currentPeriodEnd === void 0 ? {} : { currentPeriodEnd: n.currentPeriodEnd },
509
- ...n.pastDueSince === void 0 ? {} : { pastDueSince: n.pastDueSince },
510
- ...n.statusEventAt === void 0 ? {} : { statusEventAt: n.statusEventAt },
511
- updatedAt: /* @__PURE__ */ new Date()
512
- });
513
- }, "subscriptionStore"),
514
- setStatus: (t, n) => K(async () => {
515
- let r = await et(e, t);
516
- r && await e.update(U, String(r.id), {
517
- status: n,
518
- updatedAt: /* @__PURE__ */ new Date()
519
- });
520
- }, "subscriptionStore")
521
- }), rt = () => {
522
- let e = /* @__PURE__ */ new Map();
523
- return {
524
- upsert: (t) => v.sync(() => {
525
- let n = e.get(t.providerInvoiceId);
526
- n && Ze(n.statusEventAt, t.statusEventAt) || e.set(t.providerInvoiceId, t);
527
- }),
528
- listByTenant: (t) => v.sync(() => [...e.values()].filter((e) => e.tenantId === t))
529
- };
530
- }, it = (e) => ({
531
- tenantId: String(e.tenantId),
532
- provider: String(e.provider),
533
- providerInvoiceId: String(e.providerInvoiceId),
534
- amountMinor: Number(e.amountMinor),
535
- currency: String(e.currency),
536
- status: String(e.status),
537
- statusEventAt: e.statusEventAt == null ? null : new Date(e.statusEventAt)
538
- }), at = (e) => ({
539
- upsert: (t) => K(async () => {
540
- let n = (await e.query(q({
541
- table: W,
542
- predicate: O("providerInvoiceId", t.providerInvoiceId),
543
- take: 1
544
- })))[0];
545
- if (n) {
546
- if (Ze(n.statusEventAt == null ? null : new Date(n.statusEventAt), t.statusEventAt)) return;
547
- await e.update(W, String(n.id), {
548
- amountMinor: t.amountMinor,
549
- currency: t.currency,
550
- status: t.status,
551
- statusEventAt: t.statusEventAt
552
- });
553
- } else await e.insert(W, {
554
- id: He("binv"),
555
- ...t,
556
- createdAt: /* @__PURE__ */ new Date()
557
- });
558
- }, "invoiceStore"),
559
- listByTenant: (t) => K(async () => (await e.query(q({
560
- table: W,
561
- predicate: O("tenantId", t),
562
- order: [{
563
- column: "createdAt",
564
- direction: "desc"
565
- }]
566
- }))).map(it), "invoiceStore")
567
- }), ot = (e, t, n) => `${e}\u0000${t}\u0000${n}`, st = () => {
568
- let e = /* @__PURE__ */ new Map();
569
- return {
570
- used: (t, n, r) => v.sync(() => e.get(ot(t, n, r))?.used ?? 0),
571
- consume: (t, n, r, i, a) => v.sync(() => {
572
- let o = ot(t, n, r), s = e.get(o), c = s?.used ?? 0, l = c + i <= a;
573
- return l && e.set(o, {
574
- tenantId: t,
575
- entitlementKey: n,
576
- period: r,
577
- used: c + i,
578
- reportedToProvider: s?.reportedToProvider ?? 0
579
- }), {
580
- allowed: l,
581
- entitlement: n,
582
- limit: a,
583
- used: c,
584
- cost: i
585
- };
586
- }),
587
- increment: (t, n, r, i) => v.sync(() => {
588
- let a = ot(t, n, r), o = e.get(a), s = (o?.used ?? 0) + i;
589
- return e.set(a, {
590
- tenantId: t,
591
- entitlementKey: n,
592
- period: r,
593
- used: s,
594
- reportedToProvider: o?.reportedToProvider ?? 0
595
- }), s;
596
- }),
597
- pending: () => v.sync(() => [...e.values()].filter((e) => e.used > e.reportedToProvider)),
598
- markReported: (t, n, r, i) => v.sync(() => {
599
- let a = ot(t, n, r), o = e.get(a);
600
- o && e.set(a, {
601
- ...o,
602
- reportedToProvider: i
603
- });
604
- })
605
- };
606
- }, ct = (e) => ({
607
- tenantId: String(e.tenantId),
608
- entitlementKey: String(e.entitlementKey),
609
- period: String(e.period),
610
- used: Number(e.used ?? 0),
611
- reportedToProvider: Number(e.reportedToProvider ?? 0)
612
- }), lt = async (e, t, n, r) => (await e.query(q({
613
- table: "_voltro_billing_usage",
614
- predicate: E(O("tenantId", t), O("entitlementKey", n), O("period", r)),
615
- take: 1
616
- })))[0] ?? null, ut = (e) => ({
617
- used: (t, n, r) => K(async () => {
618
- let i = await lt(e, t, n, r);
619
- return i ? Number(i.used ?? 0) : 0;
620
- }, "usageStore"),
621
- consume: (t, n, r, i, a) => K(async () => {
622
- for (let o = 0; o < 32; o++) {
623
- o > 0 && await new Promise((e) => setTimeout(e, o));
624
- let s = await lt(e, t, n, r), c = s ? Number(s.used ?? 0) : 0;
625
- if (c + i > a) return {
626
- allowed: !1,
627
- entitlement: n,
628
- limit: a,
629
- used: c,
630
- cost: i
631
- };
632
- if (s) {
633
- if (await e.updateMany("_voltro_billing_usage", {
634
- used: c + i,
635
- updatedAt: /* @__PURE__ */ new Date()
636
- }, { where: E(O("id", String(s.id)), O("used", c)) }) === 1) return {
637
- allowed: !0,
638
- entitlement: n,
639
- limit: a,
640
- used: c,
641
- cost: i
642
- };
643
- } else {
644
- let o = He("buse"), s = await e.insertIgnore(G, {
645
- id: o,
646
- tenantId: t,
647
- entitlementKey: n,
648
- period: r,
649
- used: i,
650
- reportedToProvider: 0,
651
- updatedAt: /* @__PURE__ */ new Date()
652
- }, { conflictColumns: [
653
- "tenantId",
654
- "entitlementKey",
655
- "period"
656
- ] });
657
- if (String(s.id) === o) return {
658
- allowed: !0,
659
- entitlement: n,
660
- limit: a,
661
- used: 0,
662
- cost: i
663
- };
664
- }
665
- }
666
- throw Error(`billing usage consume: contention on (${t}, ${n}, ${r}) exceeded 32 attempts`);
667
- }, "usageStore"),
668
- increment: (t, n, r, i) => K(async () => {
669
- for (let a = 0; a < 32; a++) {
670
- a > 0 && await new Promise((e) => setTimeout(e, a));
671
- let o = await lt(e, t, n, r);
672
- if (o) {
673
- let t = Number(o.used ?? 0);
674
- if (await e.updateMany("_voltro_billing_usage", {
675
- used: t + i,
676
- updatedAt: /* @__PURE__ */ new Date()
677
- }, { where: E(O("id", String(o.id)), O("used", t)) }) === 1) return t + i;
678
- } else {
679
- let a = He("buse"), o = await e.insertIgnore(G, {
680
- id: a,
681
- tenantId: t,
682
- entitlementKey: n,
683
- period: r,
684
- used: i,
685
- reportedToProvider: 0,
686
- updatedAt: /* @__PURE__ */ new Date()
687
- }, { conflictColumns: [
688
- "tenantId",
689
- "entitlementKey",
690
- "period"
691
- ] });
692
- if (String(o.id) === a) return i;
693
- }
694
- }
695
- throw Error(`billing usage increment: contention on (${t}, ${n}, ${r}) exceeded 32 attempts`);
696
- }, "usageStore"),
697
- pending: () => K(async () => (await e.query(q({ table: G }))).map(ct).filter((e) => e.used > e.reportedToProvider), "usageStore"),
698
- markReported: (t, n, r, i) => K(async () => {
699
- let a = await lt(e, t, n, r);
700
- a && await e.update(G, String(a.id), {
701
- reportedToProvider: i,
702
- updatedAt: /* @__PURE__ */ new Date()
703
- });
704
- }, "usageStore")
705
- }), dt = () => ({
706
- customers: We(),
707
- subscriptions: Qe(),
708
- invoices: rt(),
709
- usage: st(),
710
- dunningNotices: ze()
711
- }), ft = (e) => ({
712
- customers: Ke(e),
713
- subscriptions: nt(e),
714
- invoices: at(e),
715
- usage: ut(e),
716
- dunningNotices: Ve(e)
717
- }), J = P({ scope: "@voltro/plugin-billing" }), pt = 3, Y = /* @__PURE__ */ new Set([
718
- "active",
719
- "trialing",
720
- "pastDue"
721
- ]), mt = (e = /* @__PURE__ */ new Date()) => `${e.getUTCFullYear()}-${String(e.getUTCMonth() + 1).padStart(2, "0")}`, ht = (e) => ({ stores: e ?? dt() }), X = (e, t) => e.pipe(v.retry({
722
- schedule: b.recurs(Math.max(0, t - 1)),
723
- while: (e) => e.transient
724
- })), gt = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$/, _t = "usage key must be 1–100 characters of A–Z, a–z, 0–9, \"_\", \".\", \":\" or \"-\", starting with a letter or digit", vt = (t) => {
725
- let { provider: n, plans: i } = t, a = t.attempts ?? 3, o = t.checkout, s = t.now ?? (() => /* @__PURE__ */ new Date()), c = (n, r) => n.length === 0 ? v.fail(new e({
726
- source: "billing",
727
- message: "no tenant on this call",
728
- transient: !1
729
- })) : t.admit === void 0 ? v.void : t.admit(n, r), l = t.redirectOrigins?.map((e) => new URL(e).origin), u = (t, n) => {
730
- let r = (t) => v.fail(new e({
731
- source: "billing",
732
- message: `${n} ${t}`,
733
- transient: !1
734
- })), i;
735
- try {
736
- i = new URL(t);
737
- } catch {
738
- return r("is not an absolute URL");
739
- }
740
- return i.protocol !== "https:" && i.protocol !== "http:" ? r("must be an http(s) URL") : l !== void 0 && !l.includes(i.origin) ? r(`must point at one of the configured redirect origins, not ${i.origin}`) : v.void;
741
- }, d = ht(t.stores), f = t.dunning ?? ke(void 0, {}), p = (e) => d.stores.subscriptions.getByTenant(e).pipe(v.map((t) => Pe(t, f, s(), e))), m = (e) => p(e).pipe(v.map((e) => e.plan)), h = (e) => d.stores.subscriptions.getByTenant(e), g = (e, n) => v.gen(function* () {
742
- if (t.resolveEntitlementLimit !== void 0) {
743
- let r = yield* t.resolveEntitlementLimit(e, n);
744
- if (r !== null) return r;
745
- }
746
- let r = yield* m(e);
747
- return i.entitlementLimit(r, n);
748
- }), _ = (e, t, n) => v.gen(function* () {
749
- return fe(t, yield* g(e, t), yield* d.stores.usage.used(e, t, mt(s())), n);
750
- }), y = (e, t, n) => v.gen(function* () {
751
- let i = yield* g(e, t);
752
- if (!Number.isFinite(i) || n <= 0) return;
753
- let a = mt(s()), o = yield* d.stores.usage.consume(e, t, a, n, i);
754
- if (!o.allowed) return yield* v.fail(new r({
755
- entitlement: t,
756
- limit: o.limit,
757
- used: o.used,
758
- cost: n
759
- }));
760
- }), b = (t, n, r) => gt.test(n) ? Number.isFinite(r) ? r <= 0 ? v.void : d.stores.usage.increment(t, n, mt(s()), r).pipe(v.asVoid) : v.fail(new e({
761
- source: "billing",
762
- message: `usage quantity must be a finite number, got ${r}`,
763
- transient: !1
764
- })) : v.fail(new e({
765
- source: "billing",
766
- message: _t,
767
- transient: !1
768
- })), x = () => v.gen(function* () {
769
- if (!n.supportsMeteredUsage) return;
770
- let e = yield* d.stores.usage.pending();
771
- for (let t of e) {
772
- let e = t.used - t.reportedToProvider;
773
- if (e <= 0) continue;
774
- let r = yield* d.stores.customers.getByTenant(t.tenantId);
775
- if (r === null && n.usageRequiresCustomer === !0) {
776
- yield* v.logWarning("billing usage not flushed: no provider customer linked").pipe(v.annotateLogs({
777
- tenantId: t.tenantId,
778
- key: t.entitlementKey,
779
- pending: e
780
- }));
781
- continue;
782
- }
783
- yield* X(n.reportUsage({
784
- tenantId: t.tenantId,
785
- entitlementKey: t.entitlementKey,
786
- quantity: e,
787
- idempotencyKey: `${t.tenantId}:${t.entitlementKey}:${t.period}:${t.used}`,
788
- ...r === null ? {} : { providerCustomerId: r.providerCustomerId }
789
- }), a), yield* d.stores.usage.markReported(t.tenantId, t.entitlementKey, t.period, t.used);
790
- }
791
- }), S = (t) => v.fail(new e({
792
- source: "billing",
793
- message: `tenant '${t}' already has a paying subscription — change its plan instead of checking out again`,
794
- transient: !1
795
- })), C = (e, t, r) => t.status === "canceled" ? d.stores.subscriptions.markCanceled(t.providerSubscriptionId) : d.stores.subscriptions.upsert({
796
- tenantId: e,
797
- provider: n.name,
798
- providerSubscriptionId: t.providerSubscriptionId,
799
- plan: t.plan ?? "free",
800
- status: t.status,
801
- quantity: t.quantity,
802
- currentPeriodStart: t.currentPeriodStart,
803
- currentPeriodEnd: t.currentPeriodEnd,
804
- cancelAt: t.cancelAt,
805
- pastDueSince: null,
806
- statusEventAt: r
807
- }), ee = (t) => v.gen(function* () {
808
- let r = i.priceIdFor(t.plan);
809
- if (r === null) return yield* v.fail(new e({
810
- source: "billing",
811
- message: `plan '${t.plan}' has no provider priceId — not a paid plan`,
812
- transient: !1
813
- }));
814
- if (yield* c(t.tenantId, "checkout"), t.quantity !== void 0 && (!Number.isSafeInteger(t.quantity) || t.quantity <= 0)) return yield* v.fail(new e({
815
- source: "billing",
816
- message: `seat quantity must be a positive integer, got ${t.quantity}`,
817
- transient: !1
818
- }));
819
- yield* u(t.successUrl, "successUrl"), yield* u(t.cancelUrl, "cancelUrl");
820
- let l = yield* d.stores.subscriptions.getByTenant(t.tenantId);
821
- if (l !== null && Y.has(l.status)) return yield* S(t.tenantId);
822
- let f = yield* d.stores.customers.getByTenant(t.tenantId), p = f?.providerCustomerId ?? (yield* X(n.ensureCustomer({ tenantId: t.tenantId }), a));
823
- f === null && (yield* d.stores.customers.upsert({
824
- tenantId: t.tenantId,
825
- provider: n.name,
826
- providerCustomerId: p
827
- }));
828
- let m = (yield* X(n.listSubscriptions({ providerCustomerId: p }), a)).filter((e) => Y.has(e.status));
829
- if (m.length > 0) {
830
- for (let e of m) yield* C(t.tenantId, e, s());
831
- return yield* S(t.tenantId);
832
- }
833
- return yield* X(n.expireOpenCheckouts({ providerCustomerId: p }), a), yield* X(n.createCheckoutSession({
834
- tenantId: t.tenantId,
835
- priceId: r,
836
- meteredPriceIds: i.meteredPriceIdsFor(t.plan),
837
- successUrl: t.successUrl,
838
- cancelUrl: t.cancelUrl,
839
- providerCustomerId: p,
840
- ...t.quantity === void 0 ? {} : { quantity: t.quantity },
841
- ...o?.adjustableQuantity === void 0 ? {} : { adjustableQuantity: o.adjustableQuantity },
842
- ...o?.trialDays === void 0 ? {} : { trialDays: o.trialDays }
843
- }), a);
844
- }), w = (t, r) => v.gen(function* () {
845
- yield* c(t, "portal"), yield* u(r, "returnUrl");
846
- let i = yield* d.stores.customers.getByTenant(t);
847
- return i === null ? yield* v.fail(new e({
848
- source: "billing",
849
- message: `no provider customer linked for tenant '${t}'`,
850
- transient: !1
851
- })) : yield* X(n.createPortalSession({
852
- tenantId: t,
853
- providerCustomerId: i.providerCustomerId,
854
- returnUrl: r
855
- }), a);
856
- }), T = (e) => v.gen(function* () {
857
- if (f.resolveRecipient !== void 0) {
858
- let t = yield* f.resolveRecipient(e);
859
- if (t !== null) return t;
860
- }
861
- if (n.customerEmail === void 0) return null;
862
- let t = yield* d.stores.customers.getByTenant(e);
863
- return t === null ? null : yield* n.customerEmail(t.providerCustomerId);
864
- }), E = (e) => {
865
- let t = f.portalReturnUrl;
866
- return t === void 0 ? v.succeed(null) : w(e, t).pipe(v.map((e) => e.url), v.catch(() => v.succeed(null)));
867
- }, te = (e, t, n, r, i) => v.gen(function* () {
868
- let a = yield* T(e.tenantId).pipe(v.catch(() => v.succeed(null))), o = yield* E(e.tenantId), s = t.lockedSince !== null, c = Ie({
869
- stepId: n,
870
- locked: s,
871
- graceEndsAt: i,
872
- portalUrl: o
873
- }), l = {
874
- stepId: n,
875
- tenantId: e.tenantId,
876
- to: a,
877
- plan: e.plan,
878
- status: e.status,
879
- pastDueSince: r,
880
- graceEndsAt: i,
881
- locked: s,
882
- lockout: f.lockout,
883
- portalUrl: o,
884
- ...c
885
- };
886
- J.info("billing dunning notice", {
887
- tenant: e.tenantId,
888
- step: n,
889
- locked: s,
890
- graceEndsAt: i.toISOString(),
891
- delivered: f.notify !== void 0 && a !== null
892
- });
893
- let u = f.notify;
894
- u !== void 0 && (yield* u(l).pipe(v.catchCause((t) => v.sync(() => {
895
- J.warn("billing dunning notify failed", {
896
- tenant: e.tenantId,
897
- step: n,
898
- cause: String(t)
899
- });
900
- }))));
901
- }), D = (e) => v.gen(function* () {
902
- let t = yield* d.stores.subscriptions.getByTenant(e);
903
- if (t === null) return Pe(null, f, s(), e);
904
- let r = yield* n.fetchSubscription(t.providerSubscriptionId).pipe(v.catch((t) => v.sync(() => (J.warn("billing dunning reconcile: provider unreachable, not escalating", {
905
- tenant: e,
906
- source: t.source,
907
- message: t.message
908
- }), null)))), i = t;
909
- if (r !== null) {
910
- let n = s(), a = r.status === "pastDue" ? t.status === "pastDue" && t.pastDueSince !== null ? t.pastDueSince : n : null;
911
- yield* d.stores.subscriptions.patchByTenant(e, {
912
- status: r.status,
913
- ...r.plan === null ? {} : { plan: r.plan },
914
- quantity: r.quantity,
915
- currentPeriodStart: r.currentPeriodStart,
916
- currentPeriodEnd: r.currentPeriodEnd,
917
- pastDueSince: a,
918
- statusEventAt: n
919
- }), i = {
920
- ...t,
921
- status: r.status,
922
- plan: r.plan ?? t.plan,
923
- quantity: r.quantity,
924
- currentPeriodStart: r.currentPeriodStart,
925
- currentPeriodEnd: r.currentPeriodEnd,
926
- pastDueSince: a,
927
- statusEventAt: n
928
- };
929
- }
930
- let a = Pe(i, f, s(), e);
931
- if (!f.enabled || i.status !== "pastDue" || i.pastDueSince === null) return a;
932
- let o = i.pastDueSince, c = je(o), l = a.graceEndsAt ?? o, u = Ne(f.steps, o, s()).map((e) => e.id);
933
- a.lockedSince !== null && u.push(we);
934
- for (let t of u) (yield* d.stores.dunningNotices.claim(e, c, t, s())) && (yield* te(i, a, t, o, l));
935
- return a;
936
- }), O = () => v.gen(function* () {
937
- let e = yield* d.stores.subscriptions.listByStatus("pastDue"), t = 0;
938
- for (let n of e) yield* D(n.tenantId).pipe(v.catch((e) => v.sync(() => {
939
- J.warn("billing dunning sweep: tenant failed", {
940
- tenant: n.tenantId,
941
- message: e.message
942
- });
943
- }))), t += 1;
944
- return t;
945
- }), ne = (e) => {
946
- if (!f.enabled) return v.void;
947
- if (e._tag === "invoicePaymentFailed") return D(e.tenantId).pipe(v.asVoid);
948
- if (e._tag !== "invoicePaid" && e._tag !== "subscriptionUpserted") return v.void;
949
- let t = e.tenantId;
950
- return d.stores.subscriptions.getByTenant(t).pipe(v.flatMap((e) => e !== null && (e.status === "pastDue" || e.pastDueSince !== null) ? D(t).pipe(v.asVoid) : v.void));
951
- }, k = (e) => {
952
- switch (e._tag) {
953
- case "subscriptionUpserted": return d.stores.subscriptions.upsert({
954
- tenantId: e.tenantId,
955
- provider: n.name,
956
- providerSubscriptionId: e.providerSubscriptionId,
957
- plan: e.plan,
958
- status: e.status,
959
- quantity: e.quantity ?? 1,
960
- currentPeriodStart: e.currentPeriodStart ?? null,
961
- currentPeriodEnd: e.currentPeriodEnd,
962
- cancelAt: e.cancelAt,
963
- pastDueSince: null,
964
- statusEventAt: e.occurredAt
965
- });
966
- case "subscriptionCanceled": return d.stores.subscriptions.markCanceled(e.providerSubscriptionId);
967
- case "invoicePaid": return d.stores.invoices.upsert({
968
- tenantId: e.tenantId,
969
- provider: n.name,
970
- providerInvoiceId: e.providerInvoiceId,
971
- amountMinor: e.amountMinor,
972
- currency: e.currency,
973
- status: "paid",
974
- statusEventAt: e.occurredAt
975
- });
976
- case "invoicePaymentFailed": return d.stores.invoices.upsert({
977
- tenantId: e.tenantId,
978
- provider: n.name,
979
- providerInvoiceId: e.providerInvoiceId,
980
- amountMinor: e.amountMinor,
981
- currency: e.currency,
982
- status: "open",
983
- statusEventAt: e.occurredAt
984
- });
985
- case "customerLinked": return d.stores.customers.upsert({
986
- tenantId: e.tenantId,
987
- provider: n.name,
988
- providerCustomerId: e.providerCustomerId
989
- });
990
- case "customerUnlinked": return d.stores.customers.removeByProviderCustomerId(e.providerCustomerId).pipe(v.asVoid);
991
- }
992
- }, A = (e) => k(e).pipe(v.andThen(ne(e))), re = (t, r) => v.gen(function* () {
993
- yield* c(t, "change"), yield* R(t);
994
- let o = yield* d.stores.subscriptions.getByTenant(t);
995
- if (o === null) return yield* v.fail(new e({
996
- source: "billing",
997
- message: `no subscription for tenant '${t}' to change`,
998
- transient: !1
999
- }));
1000
- let s = r.plan ?? o.plan, l = r.quantity ?? o.quantity;
1001
- if (r.plan !== void 0 && !i.has(s)) return yield* v.fail(new e({
1002
- source: "billing",
1003
- message: `unknown plan '${s}' — not in the plan registry`,
1004
- transient: !1
1005
- }));
1006
- if (!Number.isInteger(l) || l <= 0) return yield* v.fail(new e({
1007
- source: "billing",
1008
- message: `seat quantity must be a positive integer, got ${l}`,
1009
- transient: !1
1010
- }));
1011
- let u = yield* X(n.updateSubscription({
1012
- providerSubscriptionId: o.providerSubscriptionId,
1013
- ...r.plan === void 0 ? {} : { priceId: i.priceIdFor(s) ?? s },
1014
- ...r.quantity === void 0 ? {} : { quantity: l },
1015
- proration: "prorate"
1016
- }), a);
1017
- return yield* d.stores.subscriptions.patchByTenant(t, {
1018
- ...r.plan === void 0 ? {} : { plan: s },
1019
- quantity: u.quantity,
1020
- ...u.currentPeriodStart === null ? {} : { currentPeriodStart: u.currentPeriodStart },
1021
- ...u.currentPeriodEnd === null ? {} : { currentPeriodEnd: u.currentPeriodEnd }
1022
- }), {
1023
- tenantId: t,
1024
- plan: s,
1025
- quantity: u.quantity,
1026
- prorationMinor: u.prorationMinor,
1027
- currency: u.currency
1028
- };
1029
- }), ie = (t, r) => v.gen(function* () {
1030
- let o = yield* d.stores.subscriptions.getByTenant(t);
1031
- if (o === null) return yield* v.fail(new e({
1032
- source: "billing",
1033
- message: `no subscription for tenant '${t}' to preview`,
1034
- transient: !1
1035
- }));
1036
- let s = r.plan ?? o.plan, c = yield* X(n.previewSubscriptionChange({
1037
- providerSubscriptionId: o.providerSubscriptionId,
1038
- ...r.plan === void 0 ? {} : { priceId: i.priceIdFor(s) ?? s },
1039
- ...r.quantity === void 0 ? {} : { quantity: r.quantity },
1040
- proration: "prorate"
1041
- }), a);
1042
- return {
1043
- tenantId: t,
1044
- plan: s,
1045
- quantity: c.quantity,
1046
- prorationMinor: c.prorationMinor,
1047
- currency: c.currency
1048
- };
1049
- }), ae = (e) => v.gen(function* () {
1050
- let t = yield* d.stores.customers.getByTenant(e);
1051
- return t === null ? [] : yield* X(n.listInvoices({ providerCustomerId: t.providerCustomerId }), a);
1052
- }), j = (t, n) => v.gen(function* () {
1053
- if (n === "free") {
1054
- let e = yield* oe(t);
1055
- return {
1056
- tenantId: t,
1057
- plan: e.plan,
1058
- quantity: e.quantity,
1059
- prorationMinor: 0,
1060
- currency: i.currencyFor(e.plan)
1061
- };
1062
- }
1063
- return i.has(n) && i.priceIdFor(n) === null ? yield* v.fail(new e({
1064
- source: "billing",
1065
- message: `plan '${n}' has no provider price — it is not a self-serve plan`,
1066
- transient: !1
1067
- })) : yield* re(t, { plan: n });
1068
- }), M = (t, n) => d.stores.subscriptions.getByTenant(t).pipe(v.flatMap((r) => r !== null && Ye(r.status) ? v.succeed(r) : v.fail(new e({
1069
- source: "billing",
1070
- message: `no live subscription for tenant '${t}' to ${n}`,
1071
- transient: !1
1072
- })))), N = (e, t) => v.gen(function* () {
1073
- let n = s();
1074
- return yield* C(e.tenantId, {
1075
- ...t,
1076
- providerSubscriptionId: e.providerSubscriptionId
1077
- }, n), {
1078
- ...e,
1079
- ...t,
1080
- plan: t.plan ?? e.plan,
1081
- statusEventAt: n
1082
- };
1083
- }), oe = (e) => v.gen(function* () {
1084
- yield* c(e, "cancel"), yield* R(e);
1085
- let t = yield* M(e, "cancel"), r = yield* X(n.cancelSubscription({
1086
- providerSubscriptionId: t.providerSubscriptionId,
1087
- mode: "periodEnd"
1088
- }), a);
1089
- return yield* N(t, r);
1090
- }), P = (e) => v.gen(function* () {
1091
- yield* c(e, "resume"), yield* R(e);
1092
- let t = yield* M(e, "resume"), r = yield* X(n.resumeSubscription(t.providerSubscriptionId), a);
1093
- return yield* N(t, r);
1094
- }), F = (e) => v.gen(function* () {
1095
- yield* x();
1096
- let t = yield* d.stores.customers.getByTenant(e), r = yield* d.stores.subscriptions.listByTenant(e), i = new Set(r.filter((e) => Ye(e.status)).map((e) => e.providerSubscriptionId));
1097
- if (t !== null) {
1098
- let e = yield* X(n.listSubscriptions({ providerCustomerId: t.providerCustomerId }), a);
1099
- for (let t of e) i.add(t.providerSubscriptionId);
1100
- }
1101
- let o = [];
1102
- for (let t of i) {
1103
- let r = yield* X(n.cancelSubscription({
1104
- providerSubscriptionId: t,
1105
- mode: "close"
1106
- }), a);
1107
- yield* C(e, {
1108
- ...r,
1109
- providerSubscriptionId: t
1110
- }, s()), o.push(t);
1111
- }
1112
- return J.info("billing account closed", {
1113
- tenant: e,
1114
- canceled: o.length
1115
- }), {
1116
- tenantId: e,
1117
- canceled: o
1118
- };
1119
- }), I = (e) => {
1120
- let n = t.isTenantClosed;
1121
- return n === void 0 ? v.succeed(!1) : n(e).pipe(v.catch((t) => v.sync(() => (J.warn("billing: could not tell whether a tenant is closed — leaving it", {
1122
- tenant: e,
1123
- message: t.message
1124
- }), !1))));
1125
- }, L = (e, t) => v.gen(function* () {
1126
- let r = s(), o = yield* X(n.listSubscriptions({ providerCustomerId: e }), a), c = yield* d.stores.customers.getByProviderCustomerId(e), l = c?.tenantId ?? t ?? o.find((e) => e.tenantId !== null)?.tenantId ?? null;
1127
- if (l === null) return J.warn("billing sync: customer names no tenant — nothing to attribute it to", { customer: e }), {
1128
- tenantId: null,
1129
- events: []
1130
- };
1131
- let u = [];
1132
- c === null && (yield* d.stores.customers.upsert({
1133
- tenantId: l,
1134
- provider: n.name,
1135
- providerCustomerId: e
1136
- }), u.push({
1137
- _tag: "customerLinked",
1138
- tenantId: l,
1139
- providerCustomerId: e,
1140
- occurredAt: r
1141
- })), o.some((e) => Y.has(e.status) && e.cancelAt === null) && (yield* I(l)) && (J.error("billing sync: a closed tenant still has a paying subscription — closing it", { tenant: l }), yield* F(l), o = yield* X(n.listSubscriptions({ providerCustomerId: e }), a));
1142
- for (let e of o) if (yield* C(l, e, r), u.push(B(l, e, r)), e.plan !== null && Y.has(e.status)) {
1143
- let t = i.meteredPriceIdsFor(e.plan).filter((t) => !e.priceIds.includes(t));
1144
- t.length > 0 && (yield* X(n.addSubscriptionPrices({
1145
- providerSubscriptionId: e.providerSubscriptionId,
1146
- priceIds: t
1147
- }), a));
1148
- }
1149
- let f = new Set(o.map((e) => e.providerSubscriptionId));
1150
- for (let e of yield* d.stores.subscriptions.listByTenant(l)) {
1151
- if (!Ye(e.status) || f.has(e.providerSubscriptionId)) continue;
1152
- let t = yield* X(n.fetchSubscription(e.providerSubscriptionId), a), i = t === null ? {
1153
- ...e,
1154
- status: "canceled",
1155
- providerSubscriptionId: e.providerSubscriptionId
1156
- } : {
1157
- ...t,
1158
- providerSubscriptionId: e.providerSubscriptionId
1159
- };
1160
- yield* C(l, i, r), u.push(B(l, i, r));
1161
- }
1162
- for (let e of u) yield* ne(e);
1163
- return {
1164
- tenantId: l,
1165
- events: u
1166
- };
1167
- }), se = (e) => v.forEach(e, (e) => {
1168
- let n = t.onEvent?.[e._tag];
1169
- return n === void 0 ? v.void : n(e).pipe(v.catchCause((t) => v.sync(() => {
1170
- J.error("billing: onEvent failed for a correction — the next sync of this tenant retries it", {
1171
- event: e._tag,
1172
- cause: String(t)
1173
- });
1174
- })));
1175
- }, { discard: !0 }), R = (e) => v.gen(function* () {
1176
- let t = yield* d.stores.customers.getByTenant(e);
1177
- if (t !== null) {
1178
- let n = yield* L(t.providerCustomerId, e);
1179
- yield* se(n.events);
1180
- }
1181
- return yield* d.stores.subscriptions.getByTenant(e);
1182
- }), z = (e, t) => e !== void 0 && e.status === t.status && e.plan === (t.plan ?? "free") && e.quantity === t.quantity && (e.cancelAt?.getTime() ?? null) === (t.cancelAt?.getTime() ?? null), B = (e, t, n) => t.status === "canceled" ? {
1183
- _tag: "subscriptionCanceled",
1184
- tenantId: e,
1185
- providerSubscriptionId: t.providerSubscriptionId,
1186
- occurredAt: n
1187
- } : {
1188
- _tag: "subscriptionUpserted",
1189
- tenantId: e,
1190
- providerSubscriptionId: t.providerSubscriptionId,
1191
- plan: t.plan ?? "free",
1192
- status: t.status,
1193
- quantity: t.quantity,
1194
- currentPeriodStart: t.currentPeriodStart,
1195
- currentPeriodEnd: t.currentPeriodEnd,
1196
- cancelAt: t.cancelAt,
1197
- occurredAt: n
1198
- };
1199
- return {
1200
- service: {
1201
- subscription: h,
1202
- plan: m,
1203
- entitlementStatus: p,
1204
- reconcileDunning: D,
1205
- dunningSweep: O,
1206
- checkEntitlement: _,
1207
- consumeEntitlement: y,
1208
- reportUsage: b,
1209
- flushUsage: x,
1210
- startCheckout: ee,
1211
- portalUrl: w,
1212
- applyEvent: A,
1213
- changePlan: j,
1214
- changeSeats: (e, t) => re(e, { quantity: t }),
1215
- previewChange: ie,
1216
- invoices: ae,
1217
- cancelSubscription: oe,
1218
- resumeSubscription: P,
1219
- closeAccount: F,
1220
- reconcileFromProvider: () => v.gen(function* () {
1221
- let e = s(), t = yield* X(n.listSubscriptions({}), a), r = yield* d.stores.customers.list(), o = new Map(r.map((e) => [e.providerCustomerId, e.tenantId])), c = /* @__PURE__ */ new Set(), l = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Set(), f = [], p = /* @__PURE__ */ new Set(), m = /* @__PURE__ */ new Map(), h = 0, g = 0, _ = (e) => {
1222
- let t = m.get(e);
1223
- return t === void 0 ? I(e).pipe(v.tap((t) => v.sync(() => {
1224
- m.set(e, t);
1225
- }))) : v.succeed(t);
1226
- };
1227
- for (let r of t) {
1228
- u.add(r.providerSubscriptionId);
1229
- let t = r.tenantId ?? (r.providerCustomerId === null ? null : o.get(r.providerCustomerId) ?? null);
1230
- if (t === null) {
1231
- h += 1, J.warn("billing reconcile: provider subscription names no tenant", { subscription: r.providerSubscriptionId });
1232
- continue;
1233
- }
1234
- if (Y.has(r.status) && r.cancelAt === null && !p.has(t) && (yield* _(t))) {
1235
- J.error("billing reconcile: a closed tenant still has a paying subscription — closing it", {
1236
- tenant: t,
1237
- subscription: r.providerSubscriptionId
1238
- }), (yield* F(t).pipe(v.map(() => !0), v.catch((e) => v.sync(() => (J.error("billing reconcile: closing a closed tenant failed — retried next run", {
1239
- tenant: t,
1240
- message: e.message
1241
- }), !1))))) && p.add(t), c.add(t);
1242
- continue;
1243
- }
1244
- if (p.has(t)) continue;
1245
- Y.has(r.status) && l.set(t, (l.get(t) ?? 0) + 1);
1246
- let s = yield* d.stores.subscriptions.listByTenant(t);
1247
- z(s.find((e) => e.providerSubscriptionId === r.providerSubscriptionId), r) || (yield* C(t, r, e), f.push(B(t, r, e)), c.add(t)), r.providerCustomerId !== null && o.get(r.providerCustomerId) !== t && (yield* d.stores.customers.upsert({
1248
- tenantId: t,
1249
- provider: n.name,
1250
- providerCustomerId: r.providerCustomerId
1251
- }), o.set(r.providerCustomerId, t), f.push({
1252
- _tag: "customerLinked",
1253
- tenantId: t,
1254
- providerCustomerId: r.providerCustomerId,
1255
- occurredAt: e
1256
- }), c.add(t));
1257
- let m = r.plan;
1258
- if (m !== null && Y.has(r.status)) {
1259
- let e = i.meteredPriceIdsFor(m).filter((e) => !r.priceIds.includes(e));
1260
- e.length > 0 && (yield* X(n.addSubscriptionPrices({
1261
- providerSubscriptionId: r.providerSubscriptionId,
1262
- priceIds: e
1263
- }), a), g += 1);
1264
- }
1265
- }
1266
- let y = 0, b = [];
1267
- for (let e of [
1268
- "active",
1269
- "trialing",
1270
- "pastDue",
1271
- "incomplete"
1272
- ]) b.push(...yield* d.stores.subscriptions.listByStatus(e));
1273
- let x = [];
1274
- for (let t of b) {
1275
- if (u.has(t.providerSubscriptionId)) continue;
1276
- y += 1;
1277
- let r = yield* X(n.fetchSubscription(t.providerSubscriptionId), a);
1278
- if (r === null) {
1279
- x.push(t);
1280
- continue;
1281
- }
1282
- let i = {
1283
- ...r,
1284
- providerSubscriptionId: t.providerSubscriptionId
1285
- };
1286
- yield* C(t.tenantId, i, e), f.push(B(t.tenantId, i, e)), c.add(t.tenantId);
1287
- }
1288
- let S = x.length >= pt && x.length * 2 >= b.length;
1289
- if (S) J.error("billing reconcile: the provider knows none of these live subscriptions — wrong account or provider? Nothing was canceled", {
1290
- unknown: x.length,
1291
- live: b.length,
1292
- provider: n.name
1293
- });
1294
- else for (let t of x) yield* d.stores.subscriptions.markCanceled(t.providerSubscriptionId), f.push(B(t.tenantId, {
1295
- ...t,
1296
- status: "canceled",
1297
- providerSubscriptionId: t.providerSubscriptionId
1298
- }, e)), c.add(t.tenantId);
1299
- let ee = [...l].filter(([, e]) => e > 1).map(([e]) => e);
1300
- for (let e of ee) J.error("billing reconcile: tenant has more than one paying subscription — billed twice, resolve by hand", { tenant: e });
1301
- let w = {
1302
- checked: t.length + y,
1303
- corrected: [...c],
1304
- duplicates: ee,
1305
- unattributed: h,
1306
- meteredItemsRestored: g,
1307
- closedTenants: [...p],
1308
- refusedUnknown: S ? x.length : 0,
1309
- events: f
1310
- };
1311
- return J.info("billing reconcile from provider", {
1312
- checked: w.checked,
1313
- corrected: w.corrected.length,
1314
- duplicates: ee.length,
1315
- unattributed: h,
1316
- meteredItemsRestored: g,
1317
- closedTenants: w.closedTenants.length
1318
- }), yield* se(f), w;
1319
- }),
1320
- syncCustomer: L,
1321
- syncTenant: R
1322
- },
1323
- holder: d
1324
- };
1325
- }, yt = (e) => y.succeed(t, e), bt = "_voltro_ai_usage", xt = (e) => {
1326
- for (let [t, n] of Object.entries(e)) {
1327
- if (t.length === 0) throw Error("billing metering: a meter key must be non-empty");
1328
- if (n.from === "rpc") {
1329
- 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'`);
1330
- } else if (n.from === "cdc") {
1331
- if (typeof n.table != "string" || n.table.length === 0) throw Error(`billing metering: cdc meter '${t}' needs a 'table'`);
1332
- } else if (n.from !== "ai") throw Error(`billing metering: meter '${t}' has unknown source '${String(n.from)}'`);
1333
- }
1334
- }, St = /* @__PURE__ */ new WeakMap(), Ct = (e) => {
1335
- if (!e.global && !e.sticky) return e;
1336
- let t = St.get(e);
1337
- return t === void 0 && (t = new RegExp(e.source, e.flags.replace(/[gy]/g, "")), St.set(e, t)), t;
1338
- }, wt = (e, t) => typeof e == "string" ? t === e : Ct(e).test(t), Tt = (e, t, n) => Object.entries(e).filter(([, e]) => e.from === "rpc" && (e.kind === void 0 || e.kind === n) && wt(e.match, t)).map(([e]) => e), Et = (e, t, n) => Object.entries(e).filter(([, e]) => e.from === "cdc" && e.table === t && (e.op ?? "insert") === n).map(([e]) => e), Dt = (e) => Object.entries(e).filter(([, e]) => e.from === "ai").map(([e, t]) => [e, t.metric ?? "tokens"]), Ot = (e) => e.tenantId, kt = (e, t, n, r) => v.forEach(n, (n) => e.reportUsage(t, n, r).pipe(v.catch(() => v.void)), { discard: !0 }), At = (e, t) => (n, r) => {
1339
- let i = Tt(t, r.tag, r.kind);
1340
- if (i.length === 0) return n;
1341
- let a = Ot(r.subject);
1342
- return a === null ? n : n.pipe(v.tap(() => kt(e, a, i, 1)));
1343
- }, jt = "billing-meter", Mt = 5e4, Nt = (e, t, n = {}) => {
1344
- let r = new T({
1345
- windowMs: re(),
1346
- maxEntries: Mt
1347
- }), i = n.now ?? (() => Date.now()), a = !1;
1348
- return (o) => {
1349
- let s = Et(t, o.table, o.op);
1350
- if (s.length === 0) return v.void;
1351
- let c = o.new?.tenantId ?? o.old?.tenantId ?? null;
1352
- if (c === null) return v.void;
1353
- let l = n.claimChange?.();
1354
- if (l === void 0) return a || (a = !0, n.onUngated?.()), kt(e, c, s, 1);
1355
- let u = String(o.new?.id ?? o.old?.id ?? ""), d = te({
1356
- pipe: jt,
1357
- op: o.op,
1358
- key: u,
1359
- next: o.new ?? null,
1360
- prev: o.old ?? null
1361
- }), f = D(d, r.next(d, i()));
1362
- return v.promise(() => l(jt, f)).pipe(v.flatMap((t) => t ? kt(e, c, s, 1) : v.void));
1363
- };
1364
- }, Pt = (e) => new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), 1, 0, 0, 0, 0)), Ft = (e, t, n, r = () => /* @__PURE__ */ new Date()) => v.gen(function* () {
1365
- let i = Dt(n);
1366
- if (i.length === 0) return;
1367
- let a = r();
1368
- mt(a);
1369
- let o = {
1370
- table: bt,
1371
- predicate: ne("calledAt", Pt(a)),
1372
- order: [],
1373
- take: void 0,
1374
- skip: void 0,
1375
- projection: void 0
1376
- }, s = yield* v.promise(() => Promise.resolve(t.query(o)).then((e) => e).catch(() => [])), c = /* @__PURE__ */ new Map();
1377
- for (let e of s) {
1378
- let t = e.tenantId ?? null;
1379
- if (t === null) continue;
1380
- let n = c.get(t) ?? {
1381
- tokens: 0,
1382
- cost: 0
1383
- };
1384
- n.tokens += (Number(e.inputTokens) || 0) + (Number(e.outputTokens) || 0), n.cost += Number(e.costMicroUsd) || 0, c.set(t, n);
1385
- }
1386
- for (let [t, n] of i) for (let [r, i] of c) {
1387
- let a = n === "costMicroUsd" ? i.cost : i.tokens;
1388
- if (a <= 0) continue;
1389
- let o = a - (yield* e.checkEntitlement(r, t, 0).pipe(v.map((e) => e.used), v.catch(() => v.succeed(0))));
1390
- o > 0 && (yield* e.reportUsage(r, t, o).pipe(v.catch(() => v.void)));
1391
- }
1392
- }), It = (e, t, n, r) => Ft(e, t, n, r).pipe(v.andThen(e.flushUsage().pipe(v.catch(() => v.void)))), Lt = (e) => Object.values(e).some((e) => e.from === "cdc"), Rt = (e) => e === void 0 ? 0 : e === "unlimited" ? Infinity : e, zt = (e) => {
1393
- let t = /* @__PURE__ */ new Map();
1394
- for (let [n, r] of Object.entries(e)) r.priceId !== void 0 && t.set(r.priceId, n);
1395
- return {
1396
- has: (t) => Object.prototype.hasOwnProperty.call(e, t),
1397
- entitlementLimit: (t, n) => {
1398
- let r = e[t];
1399
- return r === void 0 ? 0 : Rt(r.entitlements[n]);
1400
- },
1401
- planForPriceId: (e) => t.get(e) ?? null,
1402
- priceIdFor: (t) => e[t]?.priceId ?? null,
1403
- meteredPriceIdsFor: (t) => e[t]?.meteredPriceIds ?? [],
1404
- unitAmountFor: (t) => {
1405
- let n = e[t]?.unitAmountMinor;
1406
- return n !== void 0 && Number.isFinite(n) ? n : 0;
1407
- },
1408
- currencyFor: (t) => (e[t]?.currency ?? "usd").toLowerCase(),
1409
- planIds: () => Object.keys(e)
1410
- };
1411
- }, Bt = () => zt({ [n]: { entitlements: {} } }), Vt = "_voltro_billing_flush_claims", Ht = j(Vt, {
1412
- id: k({ scheme: "numeric" }),
1413
- windowKey: M(),
1414
- claimedBy: M(),
1415
- claimedAt: N().default("now")
1416
- }).unique("billingFlushClaimWindow", ["windowKey"]), Ut = (e, t) => `billing-flush:${Math.floor(e / Math.max(1, t))}`, Wt = async (e, t, n, r) => {
1417
- try {
1418
- return (await e.insertIgnore(Vt, {
1419
- windowKey: t,
1420
- claimedBy: n,
1421
- claimedAt: new Date(r)
1422
- }, { conflictColumns: ["windowKey"] })).claimedBy === n;
1423
- } catch {
1424
- return !0;
1425
- }
1426
- }, Gt = F.API_VERSION, Kt = [
1427
- "Checkout → \"Limit customers to one subscription\" (with the customer portal login link enabled) — Stripe's own refusal of a second subscription",
1428
- "Smart Retries + failed-payment emails (Billing → Revenue recovery)",
1429
- "invoice branding and email settings",
1430
- "a restricted key (rk_) for the app, separate from the provisioning key"
1431
- ], qt = (e) => typeof e.tax_code == "string" ? e.tax_code : e.tax_code?.id ?? null, Jt = (e) => ({
1432
- name: e.name,
1433
- description: e.description,
1434
- unit_label: e.unitLabel,
1435
- tax_code: e.taxCode,
1436
- metadata: {
1437
- ...e.metadata,
1438
- voltro_key: e.key
1439
- }
1440
- }), Yt = (e, t) => {
1441
- let n = [];
1442
- t.active || n.push("active"), t.name !== e.name && n.push("name"), (t.description ?? "") !== e.description && n.push("description"), (t.unit_label ?? "") !== e.unitLabel && n.push("unit_label"), qt(t) !== e.taxCode && n.push("tax_code");
1443
- for (let [r, i] of Object.entries({
1444
- ...e.metadata,
1445
- voltro_key: e.key
1446
- })) t.metadata[r] !== i && n.push(`metadata.${r}`);
1447
- return n;
1448
- }, Xt = (e, t) => {
1449
- let n = `product ${e.key}`, r = t.filter((t) => t.metadata.voltro_key === e.key);
1450
- if (r.length > 1) return {
1451
- finding: {
1452
- kind: "manual",
1453
- object: n,
1454
- detail: `${r.length} products carry this key — archive the extra ones`
1455
- },
1456
- id: null
1457
- };
1458
- let i = r[0];
1459
- if (i !== void 0) {
1460
- let t = Yt(e, i);
1461
- return t.length === 0 ? {
1462
- finding: {
1463
- kind: "ok",
1464
- object: n
1465
- },
1466
- id: i.id
1467
- } : {
1468
- finding: {
1469
- kind: "update",
1470
- object: n,
1471
- detail: t.join(", "),
1472
- apply: {
1473
- method: "POST",
1474
- path: `/v1/products/${i.id}`,
1475
- params: {
1476
- ...Jt(e),
1477
- active: !0
1478
- }
1479
- }
1480
- },
1481
- id: i.id
1482
- };
1483
- }
1484
- let a = t.filter((t) => t.active && t.metadata.voltro_key === void 0 && Object.keys(e.metadata).length > 0 && Object.entries(e.metadata).every(([e, n]) => t.metadata[e] === n));
1485
- if (a.length === 1) {
1486
- let t = a[0];
1487
- return {
1488
- finding: {
1489
- kind: "adopt",
1490
- object: n,
1491
- detail: "existing product gets the catalog key",
1492
- apply: {
1493
- method: "POST",
1494
- path: `/v1/products/${t.id}`,
1495
- params: Jt(e)
1496
- }
1497
- },
1498
- id: t.id
1499
- };
1500
- }
1501
- return a.length > 1 ? {
1502
- finding: {
1503
- kind: "manual",
1504
- object: n,
1505
- detail: `${a.length} products could be this one — archive all but one`
1506
- },
1507
- id: null
1508
- } : {
1509
- finding: {
1510
- kind: "create",
1511
- object: n,
1512
- detail: e.name,
1513
- apply: {
1514
- method: "POST",
1515
- path: "/v1/products",
1516
- params: Jt(e)
1517
- }
1518
- },
1519
- id: null
1520
- };
1521
- }, Zt = (e, t, n, r) => {
1522
- let i = [];
1523
- return r.product !== t && i.push("product"), r.currency !== e.currency && i.push("currency"), r.unit_amount !== e.unitAmount && i.push("unit_amount"), r.recurring?.interval !== e.interval && i.push("interval"), r.recurring?.usage_type !== e.usageType && i.push("usage_type"), e.usageType === "metered" && r.recurring?.meter !== n && i.push("meter"), r.tax_behavior !== e.taxBehavior && i.push("tax_behavior"), i;
1524
- }, Qt = (e, t, n, r) => ({
1525
- product: t,
1526
- currency: e.currency,
1527
- unit_amount: e.unitAmount,
1528
- recurring: {
1529
- interval: e.interval,
1530
- usage_type: e.usageType,
1531
- ...e.usageType === "metered" && n !== null ? { meter: n } : {}
1532
- },
1533
- tax_behavior: e.taxBehavior,
1534
- lookup_key: e.lookupKey,
1535
- ...r ? { transfer_lookup_key: !0 } : {},
1536
- metadata: { voltro_key: e.lookupKey }
1537
- }), $t = (e, t, n, r, i, a) => {
1538
- let o = `price ${e.lookupKey}`;
1539
- if (t === null) return {
1540
- finding: {
1541
- kind: "manual",
1542
- object: o,
1543
- detail: "its product is unresolved"
1544
- },
1545
- id: null
1546
- };
1547
- if (e.usageType === "metered" && n === null) return {
1548
- finding: {
1549
- kind: "manual",
1550
- object: o,
1551
- detail: "its meter is unresolved"
1552
- },
1553
- id: null
1554
- };
1555
- if (r !== null) {
1556
- let i = Zt(e, t, n, r);
1557
- if (i.length > 0) {
1558
- let s = {
1559
- method: "POST",
1560
- path: "/v1/prices",
1561
- params: Qt(e, t, n, !0)
1562
- };
1563
- return a ? {
1564
- finding: {
1565
- kind: "drift",
1566
- object: o,
1567
- detail: `replaced (${i.join(", ")}); existing subscriptions keep the old price`,
1568
- apply: s
1569
- },
1570
- id: null
1571
- } : {
1572
- finding: {
1573
- kind: "drift",
1574
- object: o,
1575
- detail: `${i.join(", ")} differ — a price change; rerun with --allow-price-change to replace it`
1576
- },
1577
- id: r.id
1578
- };
1579
- }
1580
- return r.active ? {
1581
- finding: {
1582
- kind: "ok",
1583
- object: o
1584
- },
1585
- id: r.id
1586
- } : {
1587
- finding: {
1588
- kind: "update",
1589
- object: o,
1590
- detail: "inactive",
1591
- apply: {
1592
- method: "POST",
1593
- path: `/v1/prices/${r.id}`,
1594
- params: { active: !0 }
1595
- }
1596
- },
1597
- id: r.id
1598
- };
1599
- }
1600
- let s = i.filter((r) => r.active && r.lookup_key === null && Zt(e, t, n, r).length === 0)[0];
1601
- return s === void 0 ? {
1602
- finding: {
1603
- kind: "create",
1604
- object: o,
1605
- detail: `${e.unitAmount} ${e.currency} / ${e.interval}`,
1606
- apply: {
1607
- method: "POST",
1608
- path: "/v1/prices",
1609
- params: Qt(e, t, n, !1)
1610
- }
1611
- },
1612
- id: null
1613
- } : {
1614
- finding: {
1615
- kind: "adopt",
1616
- object: o,
1617
- detail: "existing price gets the lookup key",
1618
- apply: {
1619
- method: "POST",
1620
- path: `/v1/prices/${s.id}`,
1621
- params: {
1622
- lookup_key: e.lookupKey,
1623
- metadata: { voltro_key: e.lookupKey }
1624
- }
1625
- }
1626
- },
1627
- id: s.id
1628
- };
1629
- }, en = (e, t) => {
1630
- let n = `meter ${e.eventName}`, r = t.find((t) => t.event_name === e.eventName);
1631
- if (r === void 0) return {
1632
- finding: {
1633
- kind: "create",
1634
- object: n,
1635
- detail: e.displayName,
1636
- apply: {
1637
- method: "POST",
1638
- path: "/v1/billing/meters",
1639
- params: {
1640
- display_name: e.displayName,
1641
- event_name: e.eventName,
1642
- default_aggregation: { formula: e.formula },
1643
- customer_mapping: {
1644
- type: "by_id",
1645
- event_payload_key: e.customerPayloadKey
1646
- },
1647
- value_settings: { event_payload_key: e.valuePayloadKey }
1648
- }
1649
- }
1650
- },
1651
- id: null
1652
- };
1653
- let i = [];
1654
- return r.default_aggregation.formula !== e.formula && i.push("aggregation"), (r.customer_mapping.type !== "by_id" || r.customer_mapping.event_payload_key !== e.customerPayloadKey) && i.push("customer mapping"), r.value_settings.event_payload_key !== e.valuePayloadKey && i.push("value key"), i.length > 0 ? {
1655
- finding: {
1656
- kind: "drift",
1657
- object: n,
1658
- detail: `${i.join(", ")} differ and cannot be edited — a new meter needs a new event name`
1659
- },
1660
- id: r.id
1661
- } : r.status === "active" ? r.display_name === e.displayName ? {
1662
- finding: {
1663
- kind: "ok",
1664
- object: n
1665
- },
1666
- id: r.id
1667
- } : {
1668
- finding: {
1669
- kind: "update",
1670
- object: n,
1671
- detail: "display name",
1672
- apply: {
1673
- method: "POST",
1674
- path: `/v1/billing/meters/${r.id}`,
1675
- params: { display_name: e.displayName }
1676
- }
1677
- },
1678
- id: r.id
1679
- } : {
1680
- finding: {
1681
- kind: "update",
1682
- object: n,
1683
- detail: "inactive",
1684
- apply: {
1685
- method: "POST",
1686
- path: `/v1/billing/meters/${r.id}/reactivate`,
1687
- params: {}
1688
- }
1689
- },
1690
- id: r.id
1691
- };
1692
- }, tn = (e, t) => {
1693
- let n = "tax settings", r = t.head_office?.address ?? {}, i = [];
1694
- for (let [t, n] of Object.entries(e.headOffice)) r[t] !== n && i.push(`head_office.${t}`);
1695
- return t.defaults.tax_code !== e.defaultTaxCode && i.push("defaults.tax_code"), t.defaults.tax_behavior !== e.defaultTaxBehavior && i.push("defaults.tax_behavior"), i.length === 0 ? t.status === "active" ? {
1696
- kind: "ok",
1697
- object: n
1698
- } : {
1699
- kind: "manual",
1700
- object: n,
1701
- detail: `status ${t.status}`
1702
- } : {
1703
- kind: "update",
1704
- object: n,
1705
- detail: i.join(", "),
1706
- apply: {
1707
- method: "POST",
1708
- path: "/v1/tax/settings",
1709
- params: {
1710
- head_office: { address: e.headOffice },
1711
- defaults: {
1712
- tax_code: e.defaultTaxCode,
1713
- tax_behavior: e.defaultTaxBehavior
1714
- }
1715
- }
1716
- }
1717
- };
1718
- }, nn = (e, t, n) => ({
1719
- business_profile: {
1720
- headline: e.headline,
1721
- privacy_policy_url: e.privacyPolicyUrl,
1722
- terms_of_service_url: e.termsOfServiceUrl
1723
- },
1724
- default_return_url: t.portalReturnUrl,
1725
- features: {
1726
- customer_update: {
1727
- enabled: !0,
1728
- allowed_updates: [
1729
- "email",
1730
- "address",
1731
- "tax_id",
1732
- "name"
1733
- ]
1734
- },
1735
- invoice_history: { enabled: !0 },
1736
- payment_method_update: { enabled: !0 },
1737
- subscription_cancel: {
1738
- enabled: !0,
1739
- mode: "at_period_end"
1740
- },
1741
- subscription_update: {
1742
- enabled: !0,
1743
- default_allowed_updates: ["price", "quantity"],
1744
- proration_behavior: "create_prorations",
1745
- products: n.map((e) => ({
1746
- product: e.productId,
1747
- prices: [e.priceId]
1748
- }))
1749
- }
1750
- },
1751
- metadata: { voltro_key: e.key }
1752
- }), rn = (e) => JSON.stringify([...e ?? []].sort()), an = (e, t) => {
1753
- let n = [], r = t.features, i = e.features;
1754
- t.default_return_url !== e.default_return_url && n.push("default_return_url");
1755
- let a = e.business_profile;
1756
- for (let e of Object.keys(a)) t.business_profile[e] !== a[e] && n.push(`business_profile.${e}`);
1757
- for (let e of [
1758
- "customer_update",
1759
- "invoice_history",
1760
- "payment_method_update",
1761
- "subscription_cancel"
1762
- ]) r[e]?.enabled !== !0 && n.push(`${e}.enabled`);
1763
- rn(r.customer_update?.allowed_updates) !== rn(i.customer_update.allowed_updates) && n.push("customer_update.allowed_updates"), r.subscription_cancel?.mode !== "at_period_end" && n.push("subscription_cancel.mode");
1764
- let o = r.subscription_update ?? {}, s = i.subscription_update;
1765
- o.enabled !== !0 && n.push("subscription_update.enabled"), o.proration_behavior !== s.proration_behavior && n.push("subscription_update.proration_behavior");
1766
- let c = (e) => JSON.stringify((e ?? []).map((e) => `${e.product}:${[...e.prices].sort().join("|")}`).sort());
1767
- return c(o.products) !== c(s.products) && n.push("subscription_update.products"), rn(o.default_allowed_updates) !== rn(s.default_allowed_updates) && n.push("subscription_update.default_allowed_updates"), n;
1768
- }, on = (e, t, n, r) => {
1769
- let i = "customer portal";
1770
- if (n === null) return [{
1771
- kind: "manual",
1772
- object: i,
1773
- detail: "its plan prices are unresolved"
1774
- }];
1775
- let a = nn(e, t, n), o = r.find((t) => t.metadata.voltro_key === e.key) ?? r.find((t) => t.active && t.name === e.name);
1776
- if (o === void 0) return [{
1777
- kind: "create",
1778
- object: i,
1779
- detail: "self-service configuration",
1780
- apply: {
1781
- method: "POST",
1782
- path: "/v1/billing_portal/configurations",
1783
- params: {
1784
- ...a,
1785
- name: e.name
1786
- }
1787
- }
1788
- }, ...r.some((e) => e.is_default) ? [{
1789
- kind: "manual",
1790
- object: i,
1791
- detail: "another configuration is the default — make this one the default in the Dashboard"
1792
- }] : []];
1793
- let s = [], c = an(a, o), l = o.metadata.voltro_key !== e.key;
1794
- return c.length > 0 || l || !o.active ? s.push({
1795
- kind: l ? "adopt" : "update",
1796
- object: i,
1797
- detail: [...c, ...o.active ? [] : ["active"]].join(", ") || "catalog key",
1798
- apply: {
1799
- method: "POST",
1800
- path: `/v1/billing_portal/configurations/${o.id}`,
1801
- params: {
1802
- ...a,
1803
- active: !0
1804
- }
1805
- }
1806
- }) : s.push({
1807
- kind: "ok",
1808
- object: i
1809
- }), o.is_default || s.push({
1810
- kind: "manual",
1811
- object: i,
1812
- detail: "not the account default — portal sessions would use another configuration"
1813
- }), s;
1814
- }, sn = (e, t) => {
1815
- if (e.webhookUrl === null) return {
1816
- kind: "ok",
1817
- object: "webhook endpoint (none: stripe listen forwards locally)"
1818
- };
1819
- let n = `webhook endpoint ${e.webhookUrl}`, r = t.filter((t) => t.url === e.webhookUrl);
1820
- if (r.length > 1) return {
1821
- kind: "manual",
1822
- object: n,
1823
- detail: `${r.length} endpoints for this url — delete all but one`
1824
- };
1825
- let i = r[0], a = [...On].sort();
1826
- return i === void 0 ? {
1827
- kind: "create",
1828
- object: n,
1829
- detail: `${a.length} events`,
1830
- apply: {
1831
- method: "POST",
1832
- path: "/v1/webhook_endpoints",
1833
- params: {
1834
- url: e.webhookUrl,
1835
- enabled_events: a,
1836
- api_version: Gt,
1837
- description: "Voltro billing"
1838
- }
1839
- }
1840
- } : i.api_version !== null && i.api_version !== Gt ? {
1841
- kind: "drift",
1842
- object: n,
1843
- detail: `api version ${i.api_version} — payloads would not match the SDK; recreate it (and rotate STRIPE_WEBHOOK_SECRET)`
1844
- } : JSON.stringify([...i.enabled_events].sort()) !== JSON.stringify(a) || i.status !== "enabled" ? {
1845
- kind: "update",
1846
- object: n,
1847
- detail: i.status === "enabled" ? "events" : "disabled",
1848
- apply: {
1849
- method: "POST",
1850
- path: `/v1/webhook_endpoints/${i.id}`,
1851
- params: {
1852
- enabled_events: a,
1853
- disabled: !1
1854
- }
1855
- }
1856
- } : {
1857
- kind: "ok",
1858
- object: n
1859
- };
1860
- }, cn = (e) => {
1861
- let t = [];
1862
- for (let n of e.catalog.tax.registrationCountries) e.activeRegistrations.includes(n) || t.push({
1863
- kind: "manual",
1864
- object: `tax registration ${n}`,
1865
- detail: "none active — automatic tax collects nothing until it is registered (Dashboard → Tax → Registrations)"
1866
- });
1867
- if (!e.env.requireSellingReadiness) return t;
1868
- e.chargesEnabled === !1 && t.push({
1869
- kind: "manual",
1870
- object: "account",
1871
- detail: "charges not enabled — finish activation (business details, bank account) in the Dashboard"
1872
- }), e.chargesEnabled === null && t.push({
1873
- kind: "manual",
1874
- object: "account",
1875
- detail: "activation not readable with this key — check it in the Dashboard"
1876
- }), e.accountTaxIds === 0 && t.push({
1877
- kind: "manual",
1878
- object: "account tax id",
1879
- detail: "no VAT id on the account — invoices lack the issuer id"
1880
- }), e.accountTaxIds === null && t.push({
1881
- kind: "manual",
1882
- object: "account tax id",
1883
- detail: "not readable with this key — check Dashboard → Settings → Tax ids"
1884
- });
1885
- for (let n of e.catalog.dashboardReminders ?? Kt) t.push({
1886
- kind: "manual",
1887
- object: "dashboard",
1888
- detail: `${n} — not checkable through the API`
1889
- });
1890
- return t;
1891
- }, ln = (e) => {
1892
- let t = [], n = (e, r) => {
1893
- if (r !== void 0) {
1894
- if (r === null) {
1895
- t.push(`${encodeURIComponent(e)}=`);
1896
- return;
1897
- }
1898
- if (Array.isArray(r)) {
1899
- r.forEach((t, r) => n(`${e}[${r}]`, t));
1900
- return;
1901
- }
1902
- if (typeof r == "object") {
1903
- for (let [t, i] of Object.entries(r)) n(`${e}[${t}]`, i);
1904
- return;
1905
- }
1906
- t.push(`${encodeURIComponent(e)}=${encodeURIComponent(String(r))}`);
1907
- }
1908
- };
1909
- for (let [t, r] of Object.entries(e)) n(t, r);
1910
- return t.join("&");
1911
- }, un = (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]"), dn = class extends Error {
1912
- status;
1913
- constructor(e, t) {
1914
- super(t), this.status = e;
1915
- }
1916
- }, fn = (e, t = fetch) => {
1917
- let n = async (n, r, i = {}) => {
1918
- if (!/^\/v[12]\/[A-Za-z0-9_/.-]+$/.test(r)) throw new dn(0, `refusing request path ${JSON.stringify(r)}`);
1919
- let a = ln(i), o = await t(`https://api.stripe.com${r}${n === "GET" && a.length > 0 ? `?${a}` : ""}`, {
1920
- method: n,
1921
- headers: {
1922
- authorization: `Bearer ${e}`,
1923
- "stripe-version": Gt,
1924
- ...n === "POST" ? { "content-type": "application/x-www-form-urlencoded" } : {}
1925
- },
1926
- ...n === "POST" ? { body: a } : {}
1927
- }), s = await o.json();
1928
- if (!o.ok) throw new dn(o.status, un(s.error?.message ?? `HTTP ${o.status}`));
1929
- return s;
1930
- };
1931
- return {
1932
- get: (e, t) => n("GET", e, t),
1933
- post: (e, t) => n("POST", e, t),
1934
- list: async (e, t = {}) => {
1935
- let r = [], i;
1936
- for (;;) {
1937
- let a = await n("GET", e, {
1938
- limit: 100,
1939
- ...t,
1940
- ...i === void 0 ? {} : { starting_after: i }
1941
- });
1942
- if (r.push(...a.data), !a.has_more || a.data.length === 0) return r;
1943
- i = a.data[a.data.length - 1].id;
1944
- }
1945
- }
1946
- };
1947
- }, pn = P({ scope: "@voltro/plugin-billing" }), mn = 3, hn = 2e4, gn = (e) => e.client ?? new F(e.apiKey, {
1948
- maxNetworkRetries: mn,
1949
- timeout: hn,
1950
- appInfo: {
1951
- name: "voltro",
1952
- url: "https://voltro.dev"
1953
- }
1954
- }), _n = (e) => e instanceof F.errors.StripeConnectionError || e instanceof F.errors.StripeAPIError || e instanceof F.errors.StripeRateLimitError, vn = (e) => e instanceof F.errors.StripeError ? e.code ?? e.type : void 0, Z = (t, n) => v.tryPromise({
1955
- try: n,
1956
- catch: (n) => {
1957
- let r = _n(n);
1958
- return pn.warn("stripe call failed", {
1959
- source: t,
1960
- transient: r,
1961
- detail: un(yn(n))
1962
- }), new e({
1963
- source: t,
1964
- message: bn(n),
1965
- transient: r
1966
- });
1967
- }
1968
- }), yn = (e) => {
1969
- if (e instanceof F.errors.StripeError) {
1970
- let t = vn(e);
1971
- return t === void 0 ? e.message : `${e.message} (${t})`;
1972
- }
1973
- return String(e?.message ?? e);
1974
- }, bn = (e) => {
1975
- if (e instanceof F.errors.StripeError) {
1976
- let t = [e.code, e.decline_code].filter((e) => typeof e == "string" && e.length > 0);
1977
- return t.length > 0 ? `${e.type} (${t.join(", ")})` : e.type;
1978
- }
1979
- return "provider call failed";
1980
- }, xn = (e) => {
1981
- let t = e.items?.data?.[0];
1982
- return {
1983
- start: Q(t?.current_period_start),
1984
- end: Q(t?.current_period_end)
1985
- };
1986
- }, Q = (e) => typeof e == "number" && Number.isFinite(e) ? /* @__PURE__ */ new Date(e * 1e3) : null, Sn = (e) => {
1987
- let t = e.items?.data ?? [];
1988
- if (t.length === 0) return 1;
1989
- let n = t.reduce((e, t) => e + (t.quantity ?? 0), 0);
1990
- return n > 0 ? n : 1;
1991
- }, Cn = {
1992
- active: "active",
1993
- trialing: "trialing",
1994
- past_due: "pastDue",
1995
- unpaid: "pastDue",
1996
- canceled: "canceled",
1997
- incomplete: "incomplete",
1998
- incomplete_expired: "canceled",
1999
- paused: "pastDue"
2000
- }, $ = (e) => {
2001
- let t = e.metadata?.tenantId;
2002
- if (typeof t == "string" && t.length > 0) return t;
2003
- let n = e.client_reference_id;
2004
- return typeof n == "string" && n.length > 0 ? n : null;
2005
- }, wn = (e) => {
2006
- let t = e.items?.data ?? [];
2007
- return t.find((e) => e.price?.recurring?.usage_type !== "metered") ?? t[0];
2008
- }, Tn = (e) => {
2009
- let t = wn(e)?.price;
2010
- return typeof t?.id == "string" ? t.id : null;
2011
- }, En = (e) => typeof e == "string" ? e : typeof e?.id == "string" ? e.id : null, Dn = (e, t) => {
2012
- let n = Tn(e), r = n !== null && t ? t(n) : null;
2013
- if (r !== null) return r;
2014
- let i = e.metadata?.plan;
2015
- return typeof i == "string" && i.length > 0 ? i : null;
2016
- }, On = [
2017
- "checkout.session.completed",
2018
- "customer.created",
2019
- "customer.deleted",
2020
- "customer.subscription.created",
2021
- "customer.subscription.deleted",
2022
- "customer.subscription.paused",
2023
- "customer.subscription.resumed",
2024
- "customer.subscription.updated",
2025
- "invoice.paid",
2026
- "invoice.payment_failed"
2027
- ], kn = (e, t) => {
2028
- let n = Q(e.created);
2029
- switch (e.type) {
2030
- case "customer.subscription.created":
2031
- case "customer.subscription.updated":
2032
- case "customer.subscription.resumed":
2033
- case "customer.subscription.paused": {
2034
- let r = e.data.object, i = $(r), a = Dn(r, t), o = Cn[r.status];
2035
- if (i === null || a === null || o === void 0) return null;
2036
- let s = xn(r);
2037
- return {
2038
- _tag: "subscriptionUpserted",
2039
- tenantId: i,
2040
- providerSubscriptionId: r.id,
2041
- plan: a,
2042
- status: o,
2043
- quantity: Sn(r),
2044
- currentPeriodStart: s.start,
2045
- currentPeriodEnd: s.end,
2046
- cancelAt: Q(r.cancel_at),
2047
- occurredAt: n
2048
- };
2049
- }
2050
- case "customer.subscription.deleted": {
2051
- let t = e.data.object, r = $(t);
2052
- return r === null ? null : {
2053
- _tag: "subscriptionCanceled",
2054
- tenantId: r,
2055
- providerSubscriptionId: t.id,
2056
- occurredAt: n
2057
- };
2058
- }
2059
- case "invoice.paid":
2060
- case "invoice.payment_failed": {
2061
- let t = e.data.object, r = $(t) ?? $({ metadata: t.parent?.subscription_details?.metadata ?? null });
2062
- return r === null || typeof t.id != "string" ? null : {
2063
- _tag: e.type === "invoice.paid" ? "invoicePaid" : "invoicePaymentFailed",
2064
- tenantId: r,
2065
- providerInvoiceId: t.id,
2066
- amountMinor: t.amount_due,
2067
- currency: t.currency,
2068
- occurredAt: n
2069
- };
2070
- }
2071
- case "checkout.session.completed": {
2072
- let t = e.data.object, r = $(t), i = En(t.customer);
2073
- return r === null || i === null ? null : {
2074
- _tag: "customerLinked",
2075
- tenantId: r,
2076
- providerCustomerId: i,
2077
- occurredAt: n
2078
- };
2079
- }
2080
- case "customer.deleted": {
2081
- let t = e.data.object;
2082
- return {
2083
- _tag: "customerUnlinked",
2084
- tenantId: $(t),
2085
- providerCustomerId: t.id,
2086
- occurredAt: n
2087
- };
2088
- }
2089
- case "customer.created": {
2090
- let t = e.data.object, r = $(t);
2091
- return r === null ? null : {
2092
- _tag: "customerLinked",
2093
- tenantId: r,
2094
- providerCustomerId: t.id,
2095
- occurredAt: n
2096
- };
2097
- }
2098
- default: return null;
2099
- }
2100
- }, An = {
2101
- prorate: "create_prorations",
2102
- none: "none",
2103
- always_invoice: "always_invoice"
2104
- }, jn = (t) => {
2105
- let n = gn(t), r = t.planForPriceId, i = (e) => {
2106
- let t = xn(e);
2107
- return {
2108
- status: Cn[e.status] ?? "incomplete",
2109
- plan: Dn(e, r),
2110
- quantity: Sn(e),
2111
- currentPeriodStart: t.start,
2112
- currentPeriodEnd: t.end,
2113
- cancelAt: Q(e.cancel_at)
2114
- };
2115
- }, a = (e) => ({
2116
- ...i(e),
2117
- providerSubscriptionId: e.id,
2118
- providerCustomerId: En(e.customer),
2119
- tenantId: $(e),
2120
- priceIds: (e.items?.data ?? []).map((e) => e.price?.id).filter((e) => typeof e == "string")
2121
- }), o = t.closeSettleSeconds ?? 600, s = {
2122
- periodEnd: (e) => n.subscriptions.update(e, {
2123
- cancel_at_period_end: !0,
2124
- expand: ["items.data.price"]
2125
- }),
2126
- close: (e) => n.subscriptions.update(e, {
2127
- cancel_at: Math.ceil(Date.now() / 1e3) + o,
2128
- proration_behavior: "create_prorations",
2129
- expand: ["items.data.price"]
2130
- })
2131
- }, c = (e) => n.subscriptions.retrieve(e, { expand: ["items.data.price"] }), l = (e, t) => {
2132
- let n = xn(e), i = wn(e)?.price;
2133
- return {
2134
- plan: Dn(e, r) ?? "free",
2135
- quantity: Sn(e),
2136
- prorationMinor: t,
2137
- currency: (i?.currency ?? "usd").toLowerCase(),
2138
- currentPeriodStart: n.start,
2139
- currentPeriodEnd: n.end
2140
- };
2141
- }, u = (e, t) => {
2142
- let n = wn(e)?.id;
2143
- return {
2144
- items: [{
2145
- ...n === void 0 ? {} : { id: n },
2146
- ...t.priceId === void 0 ? {} : { price: t.priceId },
2147
- ...t.quantity === void 0 ? {} : { quantity: t.quantity }
2148
- }],
2149
- proration_behavior: An[t.proration ?? "prorate"],
2150
- ...t.priceId !== void 0 && r ? { metadata: {
2151
- ...e.metadata,
2152
- plan: r(t.priceId) ?? ""
2153
- } } : {}
2154
- };
2155
- };
2156
- return {
2157
- name: "stripe",
2158
- supportsMeteredUsage: t.supportsMeteredUsage ?? !0,
2159
- usageRequiresCustomer: !0,
2160
- createCheckoutSession: (e) => Z("stripe.checkout.sessions.create", async () => {
2161
- let t = await n.checkout.sessions.create({
2162
- mode: "subscription",
2163
- line_items: [{
2164
- price: e.priceId,
2165
- quantity: e.quantity ?? 1,
2166
- ...e.adjustableQuantity ? { adjustable_quantity: {
2167
- enabled: !0,
2168
- minimum: e.adjustableQuantity.min,
2169
- maximum: e.adjustableQuantity.max
2170
- } } : {}
2171
- }, ...(e.meteredPriceIds ?? []).map((e) => ({ price: e }))],
2172
- success_url: e.successUrl,
2173
- cancel_url: e.cancelUrl,
2174
- client_reference_id: e.tenantId,
2175
- metadata: { tenantId: e.tenantId },
2176
- subscription_data: {
2177
- metadata: { tenantId: e.tenantId },
2178
- ...e.trialDays === void 0 ? {} : { trial_period_days: e.trialDays }
2179
- },
2180
- ...e.providerCustomerId === void 0 ? {} : { customer: e.providerCustomerId },
2181
- automatic_tax: { enabled: !0 },
2182
- tax_id_collection: { enabled: !0 },
2183
- ...e.providerCustomerId === void 0 ? {} : { customer_update: {
2184
- name: "auto",
2185
- address: "auto"
2186
- } },
2187
- allow_promotion_codes: !0
2188
- });
2189
- if (typeof t.url != "string") throw Error("checkout session has no url");
2190
- return { url: t.url };
2191
- }),
2192
- createPortalSession: (e) => Z("stripe.billingPortal.sessions.create", async () => ({ url: (await n.billingPortal.sessions.create({
2193
- customer: e.providerCustomerId,
2194
- return_url: e.returnUrl
2195
- })).url })),
2196
- reportUsage: (t) => {
2197
- let r = t.providerCustomerId;
2198
- return r === void 0 ? v.fail(new e({
2199
- source: "stripe.billing.meterEvents.create",
2200
- message: `no Stripe customer linked for tenant '${t.tenantId}'`,
2201
- transient: !1
2202
- })) : Z("stripe.billing.meterEvents.create", async () => {
2203
- try {
2204
- await n.billing.meterEvents.create({
2205
- event_name: t.entitlementKey,
2206
- identifier: t.idempotencyKey ?? `${t.tenantId}:${t.entitlementKey}:${t.periodEnd?.toISOString() ?? "current"}`,
2207
- payload: {
2208
- value: String(t.quantity),
2209
- stripe_customer_id: r
2210
- }
2211
- });
2212
- } catch (e) {
2213
- if (Mn(e)) return;
2214
- throw e;
2215
- }
2216
- });
2217
- },
2218
- updateSubscription: (e) => Z("stripe.subscriptions.update", async () => {
2219
- let t = await c(e.providerSubscriptionId), r = await n.subscriptions.update(e.providerSubscriptionId, u(t, e)), i = await Fn(n, r);
2220
- return l(r, i);
2221
- }),
2222
- previewSubscriptionChange: (e) => Z("stripe.invoices.createPreview", async () => {
2223
- let t = await c(e.providerSubscriptionId), i = u(t, e), a = (await n.invoices.createPreview({
2224
- subscription: e.providerSubscriptionId,
2225
- subscription_details: {
2226
- items: i.items,
2227
- ...i.proration_behavior === void 0 ? {} : { proration_behavior: i.proration_behavior }
2228
- }
2229
- })).lines.data.filter((e) => e.parent?.subscription_item_details?.proration === !0).reduce((e, t) => e + t.amount, 0);
2230
- return {
2231
- ...l(t, a),
2232
- ...e.quantity === void 0 ? {} : { quantity: e.quantity },
2233
- ...e.priceId !== void 0 && r ? { plan: r(e.priceId) ?? "free" } : {}
2234
- };
2235
- }),
2236
- fetchSubscription: (e) => Z("stripe.subscriptions.retrieve", async () => {
2237
- try {
2238
- return i(await c(e));
2239
- } catch (e) {
2240
- if (e instanceof F.errors.StripeInvalidRequestError && e.statusCode === 404) return null;
2241
- throw e;
2242
- }
2243
- }),
2244
- listSubscriptions: (e) => Z("stripe.subscriptions.list", async () => {
2245
- let t = [];
2246
- for await (let r of n.subscriptions.list({
2247
- limit: 100,
2248
- expand: ["data.items.data.price"],
2249
- ...e.providerCustomerId === void 0 ? {} : { customer: e.providerCustomerId }
2250
- })) t.push(a(r));
2251
- return t;
2252
- }),
2253
- cancelSubscription: (e) => Z("stripe.subscriptions.cancel", async () => i(await s[e.mode](e.providerSubscriptionId))),
2254
- resumeSubscription: (e) => Z("stripe.subscriptions.update", async () => i(await n.subscriptions.update(e, {
2255
- cancel_at_period_end: !1,
2256
- expand: ["items.data.price"]
2257
- }))),
2258
- ensureCustomer: (e) => Z("stripe.customers.create", async () => {
2259
- let t = (await n.customers.search({
2260
- query: `metadata['tenantId']:'${e.tenantId.replace(/['\\]/g, "")}'`,
2261
- limit: 2
2262
- })).data.find((t) => t.metadata?.tenantId === e.tenantId);
2263
- return t === void 0 ? (await n.customers.create({ metadata: { tenantId: e.tenantId } }, { idempotencyKey: `voltro-billing-customer:${e.tenantId}` })).id : t.id;
2264
- }),
2265
- expireOpenCheckouts: (e) => Z("stripe.checkout.sessions.expire", async () => {
2266
- let t = 0;
2267
- for await (let r of n.checkout.sessions.list({
2268
- customer: e.providerCustomerId,
2269
- status: "open",
2270
- limit: 100
2271
- })) await n.checkout.sessions.expire(r.id), t += 1;
2272
- return t;
2273
- }),
2274
- addSubscriptionPrices: (e) => Z("stripe.subscriptionItems.create", async () => {
2275
- for (let t of e.priceIds) await n.subscriptionItems.create({
2276
- subscription: e.providerSubscriptionId,
2277
- price: t,
2278
- proration_behavior: "none"
2279
- });
2280
- }),
2281
- customerEmail: (e) => Z("stripe.customers.retrieve", async () => {
2282
- let t = await n.customers.retrieve(e);
2283
- return t.deleted === !0 ? null : typeof t.email == "string" && t.email.length > 0 ? t.email : null;
2284
- }),
2285
- listInvoices: (e) => Z("stripe.invoices.list", async () => (await n.invoices.list({
2286
- customer: e.providerCustomerId,
2287
- limit: e.limit ?? 20
2288
- })).data.map((e) => ({
2289
- providerInvoiceId: e.id ?? "",
2290
- amountMinor: e.amount_due,
2291
- currency: e.currency,
2292
- status: e.status ?? "unknown",
2293
- createdAt: Q(e.created),
2294
- hostedUrl: e.hosted_invoice_url ?? null,
2295
- pdfUrl: e.invoice_pdf ?? null
2296
- }))),
2297
- verifyWebhook: (t) => v.try({
2298
- try: () => n.webhooks.constructEvent(Buffer.from(t.rawBody), t.signatureHeader, t.secret),
2299
- catch: (t) => new e({
2300
- source: "stripe.webhooks.constructEvent",
2301
- message: String(t?.message ?? t),
2302
- transient: !1
2303
- })
2304
- }),
2305
- eventContext: (e) => Pn(e) ? {
2306
- providerCustomerId: Nn(e.data.object),
2307
- createdAt: Q(e.created)
2308
- } : {
2309
- providerCustomerId: null,
2310
- createdAt: null
2311
- },
2312
- listUndeliveredEvents: () => Z("stripe.events.list", async () => {
2313
- let e = [];
2314
- for await (let t of n.events.list({
2315
- delivery_success: !1,
2316
- types: [...On],
2317
- limit: 100
2318
- })) e.push({
2319
- id: t.id,
2320
- providerCustomerId: Nn(t.data.object),
2321
- createdAt: Q(t.created)
2322
- });
2323
- return e;
2324
- }),
2325
- normalizeEvent: (e) => v.succeed(Pn(e) ? kn(e, r) : null)
2326
- };
2327
- }, Mn = (e) => e instanceof F.errors.StripeInvalidRequestError && typeof e.message == "string" && e.message.startsWith("An event already exists with identifier"), Nn = (e) => {
2328
- let t = e;
2329
- return t.object === "customer" ? typeof t.id == "string" ? t.id : null : En(t.customer ?? null);
2330
- }, Pn = (e) => typeof e == "object" && !!e && typeof e.type == "string" && typeof e.data == "object", Fn = async (e, t) => {
2331
- try {
2332
- 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);
2333
- } catch {
2334
- return 0;
2335
- }
2336
- }, In = (e) => {
2337
- let t = e?.baseUrl ?? "https://mock.billing.local", n = [], r = [], i = [], a = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Map(), c = [], l = [], u = [], d = [], f = [], p = [], m = /* @__PURE__ */ new Map(), h = [], g = 0, _ = (e, t) => {
2338
- let n = {
2339
- ...a.get(e) ?? {
2340
- status: "active",
2341
- plan: null,
2342
- quantity: 1,
2343
- currentPeriodStart: null,
2344
- currentPeriodEnd: null,
2345
- cancelAt: null
2346
- },
2347
- ...t
2348
- };
2349
- a.set(e, n);
2350
- let r = o.get(e);
2351
- return r !== void 0 && o.set(e, {
2352
- ...r,
2353
- ...t
2354
- }), n;
2355
- }, y = (e) => typeof e == "object" && !!e && typeof e._tag == "string";
2356
- return {
2357
- name: "mock",
2358
- supportsMeteredUsage: e?.supportsMeteredUsage ?? !0,
2359
- pushed: n,
2360
- updates: r,
2361
- fetched: i,
2362
- cancels: c,
2363
- resumes: l,
2364
- priceAdds: u,
2365
- checkouts: d,
2366
- customersCreated: f,
2367
- expiredFor: p,
2368
- setUndelivered: (e) => {
2369
- h = e;
2370
- },
2371
- undeliveredReads: () => g,
2372
- listUndeliveredEvents: () => v.sync(() => (g += 1, h)),
2373
- ensureCustomer: (e) => v.sync(() => {
2374
- let t = m.get(e.tenantId);
2375
- if (t !== void 0) return t;
2376
- let n = `mock_cus_${e.tenantId}`;
2377
- return m.set(e.tenantId, n), f.push(e.tenantId), n;
2378
- }),
2379
- expireOpenCheckouts: (e) => v.sync(() => (p.push(e.providerCustomerId), 0)),
2380
- listSubscription: (e) => {
2381
- o.set(e.providerSubscriptionId, e), a.set(e.providerSubscriptionId, e);
2382
- },
2383
- setSubscription: (e, t) => {
2384
- t === null ? a.delete(e) : a.set(e, t);
2385
- },
2386
- setCustomerEmail: (e, t) => {
2387
- t === null ? s.delete(e) : s.set(e, t);
2388
- },
2389
- fetchSubscription: (e) => v.sync(() => (i.push(e), a.get(e) ?? null)),
2390
- listSubscriptions: (e) => v.sync(() => [...o.values()].filter((t) => t.status !== "canceled" && (e.providerCustomerId === void 0 || t.providerCustomerId === e.providerCustomerId))),
2391
- cancelSubscription: (e) => v.sync(() => {
2392
- c.push({
2393
- providerSubscriptionId: e.providerSubscriptionId,
2394
- mode: e.mode
2395
- });
2396
- let t = a.get(e.providerSubscriptionId);
2397
- return e.mode === "close" ? _(e.providerSubscriptionId, { cancelAt: new Date(Date.now() + 6e5) }) : _(e.providerSubscriptionId, { cancelAt: t?.currentPeriodEnd ?? /* @__PURE__ */ new Date(0) });
2398
- }),
2399
- resumeSubscription: (e) => v.sync(() => (l.push(e), _(e, { cancelAt: null }))),
2400
- addSubscriptionPrices: (e) => v.sync(() => {
2401
- u.push({
2402
- providerSubscriptionId: e.providerSubscriptionId,
2403
- priceIds: e.priceIds
2404
- });
2405
- let t = o.get(e.providerSubscriptionId);
2406
- t !== void 0 && _(e.providerSubscriptionId, { priceIds: [...t.priceIds, ...e.priceIds] });
2407
- }),
2408
- customerEmail: (e) => v.sync(() => s.get(e) ?? null),
2409
- createCheckoutSession: (e) => v.sync(() => (d.push(e), { url: `${t}/checkout?tenant=${encodeURIComponent(e.tenantId)}&price=${encodeURIComponent(e.priceId)}` })),
2410
- createPortalSession: (e) => v.succeed({ url: `${t}/portal?customer=${encodeURIComponent(e.providerCustomerId)}` }),
2411
- reportUsage: (e) => v.sync(() => {
2412
- n.push(e);
2413
- }),
2414
- updateSubscription: (e) => v.sync(() => (r.push(e), {
2415
- plan: e.priceId ?? "mock-plan",
2416
- quantity: e.quantity ?? 1,
2417
- prorationMinor: 0,
2418
- currency: "usd",
2419
- currentPeriodStart: null,
2420
- currentPeriodEnd: null
2421
- })),
2422
- previewSubscriptionChange: (e) => v.succeed({
2423
- plan: e.priceId ?? "mock-plan",
2424
- quantity: e.quantity ?? 1,
2425
- prorationMinor: 0,
2426
- currency: "usd",
2427
- currentPeriodStart: null,
2428
- currentPeriodEnd: null
2429
- }),
2430
- listInvoices: () => v.succeed([]),
2431
- normalizeEvent: (e) => v.succeed(y(e) ? e : null)
2432
- };
2433
- }, Ln = /* @__PURE__ */ new Set([
2434
- "",
2435
- "development",
2436
- "dev",
2437
- "test"
2438
- ]), Rn = (e) => !Ln.has((e ?? "").trim().toLowerCase()), zn = (e) => {
2439
- if (Rn(e.env.NODE_ENV)) {
2440
- if (e.resolvedName === "mock" && e.providerOption === void 0) throw Error("billingPlugin: no STRIPE_SECRET_KEY in a production boot — refusing to run the MOCK billing provider, which answers every charge and subscription call successfully and reaches nobody. Set STRIPE_SECRET_KEY, or declare `billingPlugin({ provider: 'mock' })` if this deployment does not sell.");
2441
- if (e.resolvedName === "stripe") {
2442
- if (e.webhookSecret === null || e.webhookSecret.trim() === "") throw Error("billingPlugin: Stripe in a production boot needs STRIPE_WEBHOOK_SECRET — without it no provider event can be verified, so subscriptions, cancellations and payments would never reach this app.");
2443
- if (e.redirectOrigins === void 0 || e.redirectOrigins.length === 0) throw Error("billingPlugin: Stripe in a production boot needs `redirectOrigins` — the origins checkout and the customer portal may send a customer back to. Without it any URL a caller names is accepted.");
2444
- }
2445
- }
2446
- }, Bn = (e) => {
2447
- let { provider: t, env: n } = e;
2448
- if (t !== void 0 && typeof t == "object") return t;
2449
- if ((t ?? (n.STRIPE_SECRET_KEY ? "stripe" : "mock")) === "stripe") {
2450
- let t = e.apiKey ?? n.STRIPE_SECRET_KEY;
2451
- if (!t) throw Error("billingPlugin: provider 'stripe' needs an apiKey (option or STRIPE_SECRET_KEY env)");
2452
- return jn({
2453
- apiKey: t,
2454
- ...e.planForPriceId ? { planForPriceId: e.planForPriceId } : {}
2455
- });
2456
- }
2457
- return In();
2458
- }, Vn = (e) => e.name === "mock" ? [] : [{
2459
- destination: e.name === "stripe" ? "api.stripe.com" : `runtime:the '${e.name}' billing provider`,
2460
- purpose: "customers, subscriptions, checkout and metered usage",
2461
- data: ["personal", "metadata"],
2462
- disable: "without STRIPE_SECRET_KEY the mock provider answers, and nothing leaves (production refuses to boot on it)"
2463
- }], Hn = P({ scope: "@voltro/plugin-billing" }), Un = "billing", Wn = "/billing/webhook", Gn = (e) => I({
2464
- id: Un,
2465
- path: Wn,
2466
- provider: se(),
2467
- payload: x.Unknown,
2468
- handler: async (t) => {
2469
- let n = e.provider.verifyWebhook !== void 0 && e.webhookSecret !== null && e.webhookSecret !== void 0 ? await v.runPromise(e.provider.verifyWebhook({
2470
- rawBody: t.rawBody,
2471
- signatureHeader: t.headers["stripe-signature"] ?? "",
2472
- secret: e.webhookSecret ?? ""
2473
- })) : t.body, r = await v.runPromise(e.provider.normalizeEvent(n)), i = e.provider.eventContext?.(n) ?? {
2474
- providerCustomerId: null,
2475
- createdAt: null
2476
- };
2477
- if (r === null && i.providerCustomerId === null) return;
2478
- let a = r?._tag === "subscriptionUpserted" || r?._tag === "subscriptionCanceled", o = i.providerCustomerId !== null && r?._tag !== "customerUnlinked", s = [];
2479
- if (r !== null && !(a && o) && (await v.runPromise(e.service.applyEvent(r)), s.push(r)), o) {
2480
- let t = await v.runPromise(e.service.syncCustomer(i.providerCustomerId, r !== null && "tenantId" in r && typeof r.tenantId == "string" ? r.tenantId : void 0));
2481
- s.push(...t.events);
2482
- }
2483
- i.createdAt !== null && Date.now() - i.createdAt.getTime() > 3e5 && e.onLateDelivery?.();
2484
- for (let t of s) {
2485
- let n = e.onEvent?.[t._tag];
2486
- n !== void 0 && await v.runPromise(n(t).pipe(v.tapCause((e) => v.sync(() => {
2487
- Hn.error("billing onEvent side effect failed — answering 500 so the provider redelivers", {
2488
- event: t._tag,
2489
- cause: String(e)
2490
- });
2491
- }))));
2492
- }
2493
- }
2494
- }), Kn = (e) => L(Gn(e), {
2495
- resolveSecret: () => Promise.resolve(e.webhookSecret),
2496
- ...e.idempotencyCache === void 0 ? {} : { idempotencyCache: e.idempotencyCache },
2497
- log: (e) => {
2498
- e.status >= 400 && Hn.warn("billing webhook rejected", { ...e });
2499
- }
2500
- }), qn = (e) => e.filter((e) => e.applied !== !0 && (e.kind === "create" || e.kind === "update" || e.kind === "adopt" || e.kind === "drift")), Jn = async (e, t, n) => {
2501
- let r = t.environments[n.env], i = await e.get("/v1/tax/settings"), a = n.env === "live";
2502
- if (i.livemode !== a) throw Error(`the key reaches a ${i.livemode ? "LIVE" : "test"} account, not ${n.env}`);
2503
- let o = [], s = {}, c = async (t, r) => {
2504
- let i = "apply" in t ? t.apply : void 0;
2505
- if (n.apply && i !== void 0) {
2506
- let n = await e.post(i.path, i.params);
2507
- r?.(n), o.push({
2508
- ...t,
2509
- applied: !0
2510
- });
2511
- } else o.push(t);
2512
- }, l = await e.list("/v1/billing/meters"), u = /* @__PURE__ */ new Map();
2513
- for (let e of t.meters) {
2514
- let t = en(e, l);
2515
- u.set(e.eventName, t.id), await c(t.finding, (t) => {
2516
- u.set(e.eventName, t.id);
2517
- });
2518
- }
2519
- let d = await e.list("/v1/products"), f = [];
2520
- for (let e of t.products) {
2521
- let t = Xt(e, d), n = {
2522
- inPortal: e.inPortal,
2523
- productId: t.id,
2524
- priceId: null
2525
- };
2526
- await c(t.finding, (e) => {
2527
- n.productId = e.id;
2528
- }), f.push(n);
2529
- }
2530
- for (let [r, i] of t.products.entries()) {
2531
- let t = f[r], a = i.price.meter;
2532
- if (i.price.usageType === "metered" && (a === void 0 || !u.has(a))) {
2533
- o.push({
2534
- kind: "manual",
2535
- object: `price ${i.price.lookupKey}`,
2536
- detail: `metered, but names no catalog meter (${a ?? "none"})`
2537
- });
2538
- continue;
2539
- }
2540
- let l = a === void 0 ? null : u.get(a) ?? null, d = t.productId ?? (n.apply ? null : `pending:${i.key}`), p = l ?? (n.apply || i.price.usageType !== "metered" ? l : `pending:${a}`), m = (await e.list("/v1/prices", { lookup_keys: [i.price.lookupKey] }))[0] ?? null, h = t.productId === null ? [] : await e.list("/v1/prices", { product: t.productId }), g = $t(i.price, d, p, m, h, n.allowPriceChange);
2541
- t.priceId = g.id, await c(g.finding, (e) => {
2542
- t.priceId = e.id;
2543
- }), t.priceId !== null && (s[i.price.envName] = t.priceId);
2544
- }
2545
- await c(tn(t.tax, i));
2546
- let p = f.filter((e) => e.inPortal), m = p.every((e) => e.productId !== null && e.priceId !== null) ? p.map((e) => ({
2547
- productId: e.productId,
2548
- priceId: e.priceId
2549
- })) : null, h = await e.list("/v1/billing_portal/configurations", { expand: ["data.features.subscription_update.products"] });
2550
- for (let e of on(t.portal, r, m, h)) await c(e);
2551
- let g = sn(r, await e.list("/v1/webhook_endpoints"));
2552
- g.kind === "create" && n.apply && n.envOut === null ? o.push({
2553
- kind: "manual",
2554
- object: g.object,
2555
- detail: "not created: pass --env-out so its signing secret is captured (Stripe shows it once)"
2556
- }) : await c(g, (e) => {
2557
- typeof e.secret == "string" && (s.STRIPE_WEBHOOK_SECRET = e.secret);
2558
- });
2559
- let _ = await e.get("/v1/account").catch(() => null), v = await e.list("/v1/tax_ids", { owner: { type: "account" } }).catch(() => null), y = await e.list("/v1/tax/registrations", { status: "active" }).catch(() => []);
2560
- return o.push(...cn({
2561
- catalog: t,
2562
- env: r,
2563
- chargesEnabled: _ === null ? null : _.charges_enabled === !0,
2564
- accountTaxIds: v === null ? null : v.length,
2565
- activeRegistrations: y.map((e) => e.country)
2566
- })), {
2567
- livemode: i.livemode,
2568
- findings: o,
2569
- envOut: s
2570
- };
2571
- }, Yn = (e, t) => {
2572
- let n = new Set(Object.keys(t)), r = e.split("\n").filter((e) => !n.has(e.split("=")[0] ?? ""));
2573
- for (; r.length > 0 && r[r.length - 1] === "";) r.pop();
2574
- return [
2575
- ...r,
2576
- ...Object.entries(t).map(([e, t]) => `${e}=${t}`),
2577
- ""
2578
- ].join("\n");
2579
- }, Xn = (e, t) => {
2580
- try {
2581
- return R("git", [...e], {
2582
- cwd: t,
2583
- stdio: "ignore"
2584
- }), !0;
2585
- } catch {
2586
- return !1;
2587
- }
2588
- }, Zn = (e, t = Xn, n = (e) => B(e) ? le(e) : e) => {
2589
- for (let r of /* @__PURE__ */ new Set([V(e), V(n(V(e)))])) {
2590
- let n = de(r);
2591
- if (t(["rev-parse", "--is-inside-work-tree"], n) && !t([
2592
- "check-ignore",
2593
- "-q",
2594
- r
2595
- ], n)) return `${e} is inside a git work tree and not ignored — write it to a gitignored file or outside the repo`;
2596
- }
2597
- return null;
2598
- }, Qn = (e) => {
2599
- let t = (t) => {
2600
- let n = e.indexOf(t);
2601
- return n < 0 ? null : e[n + 1];
2602
- }, n = t("--env");
2603
- if (n !== "sandbox" && n !== "live") return "--env sandbox|live is required";
2604
- let r = t("--env-out");
2605
- if (r === void 0 || r?.startsWith("--")) return "--env-out needs a file path";
2606
- let i = {
2607
- env: n,
2608
- apply: e.includes("--apply"),
2609
- allowPriceChange: e.includes("--allow-price-change"),
2610
- confirmLive: e.includes("--confirm-live"),
2611
- strict: e.includes("--strict"),
2612
- envOut: r
2613
- };
2614
- return i.env === "live" && i.apply && !i.confirmLive ? "writing to LIVE needs --confirm-live (run without --apply first to see the plan)" : i;
2615
- }, $n = (e, t, n) => {
2616
- let r = t.STRIPE_PROVISION_KEY?.trim() || (e === "sandbox" ? n?.trim() : void 0);
2617
- return r === void 0 || r.length === 0 ? { refusal: "set STRIPE_PROVISION_KEY (a sandbox run also reads STRIPE_SECRET_KEY from the dotenv file)" } : e === "live" && !/^(sk|rk)_live_/.test(r) ? { refusal: "--env live needs a live key (sk_live_/rk_live_)" } : e === "sandbox" && !/^(sk|rk)_test_/.test(r) ? { refusal: "--env sandbox needs a test key (sk_test_/rk_test_)" } : { key: r };
2618
- }, er = (e, t) => B(e) ? ce(e, "utf8").split("\n").find((e) => e.startsWith(`${t}=`))?.slice(t.length + 1).trim() : void 0, tr = async (e, t, n = {}) => {
2619
- let r = n.vars ?? process.env, i = (e) => (console.error(`stripe provision: ${e}`), 2), a = Qn(t);
2620
- if (typeof a == "string") return i(a);
2621
- if (a.envOut !== null) {
2622
- let e = Zn(a.envOut);
2623
- if (e !== null) return i(e);
2624
- }
2625
- let o = $n(a.env, r, n.dotenv === void 0 ? void 0 : er(n.dotenv, "STRIPE_SECRET_KEY"));
2626
- if ("refusal" in o) return i(o.refusal);
2627
- let s;
2628
- try {
2629
- s = await Jn((n.rest ?? fn)(o.key), e, a);
2630
- } catch (e) {
2631
- return console.error(`stripe provision: ${un(e instanceof Error ? e.message : String(e))}`), 1;
2632
- }
2633
- console.log(`Stripe ${a.env} (${s.livemode ? "LIVE" : "test mode"}) — ${a.apply ? "apply" : "check"}`);
2634
- for (let e of s.findings) {
2635
- let t = e.applied === !0 ? `${e.kind} ✓` : e.kind;
2636
- console.log(` ${t.padEnd(10)} ${e.object}${"detail" in e ? ` — ${e.detail}` : ""}`);
2637
- }
2638
- if (a.envOut !== null) {
2639
- let e = a.envOut;
2640
- ue(e, Yn(B(e) ? ce(e, "utf8") : "", s.envOut), { mode: 384 }), z(e, 384), console.log(` wrote ${Object.keys(s.envOut).sort().join(", ")} to ${e}`);
2641
- }
2642
- let c = qn(s.findings), l = s.findings.filter((e) => e.kind === "manual");
2643
- return console.log(` ${c.length} open, ${l.length} manual`), c.length > 0 || a.strict && l.length > 0 ? 1 : 0;
2644
- }, nr = (e) => e === null ? null : {
2
+ import { makeBillingEventDelivery as a } from "./eventDelivery.js";
3
+ import { BILLING_CUSTODY_INVENTORY_TABLES as o, inspectBillingCustody as s } from "./custodyInventory.js";
4
+ import { BILLING_CUSTOMER_ACCOUNTS_TABLE as c, BILLING_CUSTOMER_CUSTODY_TABLE as l, billingCustomerAccountsTable as u, billingCustomerCustodyTable as ee } from "./customerStore.js";
5
+ import { buildEnforceInterceptor as te, evaluateEntitlement as d, requireEntitled as f, requireEntitlement as ne } from "./entitlement.js";
6
+ import { BILLING_INVOICE_HISTORY_PAGES_TABLE as p, BILLING_INVOICE_HISTORY_REFERENCES_TABLE as m, BILLING_INVOICE_HISTORY_WALKS_TABLE as h, billingInvoiceHistoryPagesTable as g, billingInvoiceHistoryReferencesTable as _, billingInvoiceHistoryWalksTable as v, dataStoreInvoiceHistoryJournal as y } from "./invoiceHistoryJournal.js";
7
+ import { BILLING_INVOICE_OBSERVATIONS_TABLE as re, billingInvoiceObservationsTable as ie, billingRetainedInvoiceObservationsTable as ae } from "./invoiceStore.js";
8
+ import { BILLING_INVOICE_HISTORY_WORK_TABLE as oe, billingInvoiceHistoryWorkTable as se, dataStoreInvoiceHistoryWorkStore as b } from "./invoiceHistoryWork.js";
9
+ import { BILLING_INVOICE_HISTORY_DISPATCH_TABLE as ce, BILLING_INVOICE_HISTORY_SCAN_ACCOUNTS_TABLE as x, billingInvoiceHistoryDispatchTable as S, billingInvoiceHistoryScanAccountsTable as C, dataStoreInvoiceHistoryProgressStore as w } from "./invoiceHistoryProgress.js";
10
+ import { BILLING_USAGE_EVENTS_TABLE as T, billingUsageEventsTable as E } from "./usageEvents.js";
11
+ import { BILLING_CHECKOUT_OPERATIONS_TABLE as D, billingCheckoutOperationsTable as O, dataStoreCheckoutOperationStore as k, memoryCheckoutOperationStore as A } from "./checkoutStore.js";
12
+ import { BILLING_CLOSE_OPERATIONS_TABLE as j, billingCloseOperationsTable as M, dataStoreBillingCloseStore as N, memoryBillingCloseStore as P } from "./closeStore.js";
13
+ import { BILLING_DUNNING_NOTICES_TABLE as le, DEFAULT_DUNNING_STEPS as F, DEFAULT_GRACE_HOURS as I, DEFAULT_LOCKOUT as L, DUNNING_LOCK_STEP_ID as R, billingDunningNoticesTable as z, dataStoreDunningNoticeStore as B, defaultDunningEmail as ue, dueSteps as de, dunningMailNotifier as fe, episodeKeyOf as pe, evaluateBillingEntitlement as me, graceEndOf as he, memoryDunningNoticeStore as ge, resolveDunningConfig as V } from "./dunning.js";
14
+ import { BILLING_CUSTOMERS_TABLE as _e, BILLING_INVOICES_TABLE as ve, BILLING_SUBSCRIPTIONS_TABLE as H, BILLING_USAGE_TABLE as U, billingCustomersTable as ye, billingInvoicesTable as be, billingSubscriptionsTable as xe, billingTables as W, billingUsageTable as Se } from "./table.js";
15
+ import { dataStoreStores as G, isStaleStatusEvent as Ce, memoryStores as we, selectCurrentSubscription as Te } from "./stores.js";
16
+ import { billingServiceLayer as K, buildBillingService as Ee, periodKey as De } from "./service.js";
17
+ import { buildMeteringChangeTap as Oe, buildMeteringInterceptor as ke, hasCdcMeters as Ae, runAutopilotFlush as je, validateMeteringConfig as Me } from "./metering.js";
18
+ import { buildPlanRegistry as Ne, emptyPlanRegistry as Pe } from "./plans.js";
19
+ import { BILLING_FLUSH_CLAIMS_TABLE as Fe, billingFlushClaimsTable as Ie, claimFlushWindow as Le, flushWindowKey as Re } from "./flushClaim.js";
20
+ import { STRIPE_API_VERSION as ze, STRIPE_DASHBOARD_REMINDERS as Be, diffMeter as Ve, diffPortal as He, diffPrice as Ue, diffProduct as We, diffTaxSettings as Ge, diffWebhook as Ke, portalParams as qe, sellingReadiness as Je } from "./catalog.js";
21
+ import { StripeRestError as Ye, formEncode as Xe, redactStripeKeys as Ze, stripeRest as Qe } from "./stripeRest.js";
22
+ import { STRIPE_WEBHOOK_EVENTS as $e, normalizeStripeEvent as et, stripeProvider as tt } from "./stripeProvider.js";
23
+ import { assertBillingProductionReady as nt, billingEgressOf as rt, isProductionBoot as it, mockProvider as at, resolveProvider as ot } from "./providers.js";
24
+ import { BILLING_WEBHOOK_ID as st, BILLING_WEBHOOK_PATH as q, billingWebhookDescriptor as ct, mountBillingWebhook as lt } from "./webhook.js";
25
+ import { billingRpcClientImports as ut, cancelSubscriptionDescriptor as dt, changePlanDescriptor as ft, changeSeatsDescriptor as pt, entitlementStatusDescriptor as mt, invoicesDescriptor as ht, portalUrlDescriptor as gt, previewChangeDescriptor as _t, reportUsageDescriptor as vt, resumeSubscriptionDescriptor as yt, startCheckoutDescriptor as J, subscriptionDescriptor as bt, syncDescriptor as xt } from "./rpc.js";
26
+ import { envOutRefusal as St, mergeEnvFile as Ct, openFindings as wt, parseProvisionArgs as Tt, provisionStripeCatalog as Et, resolveProvisionKey as Dt, runStripeProvision as Ot } from "./provision.js";
27
+ import { Effect as Y } from "effect";
28
+ import { pluginEnv as kt } from "@voltro/env";
29
+ import { composeRpcInterceptors as At, definePlugin as jt, pluginInstanceName as Mt, recheckGuardsEffect as Nt } from "@voltro/protocol";
30
+ import { and as Pt, eq as Ft, registerRetention as X, retentionTtlMsFromEnv as Z } from "@voltro/database";
31
+ import { replicaId as It } from "@voltro/protocol/identity";
32
+ import { dataStoreIdempotencyStore as Lt } from "@voltro/runtime";
33
+ //#region src/index.ts
34
+ var Q = (e) => e === null ? null : {
2645
35
  plan: e.plan,
2646
36
  status: e.status,
2647
37
  quantity: e.quantity,
2648
38
  currentPeriodEnd: e.currentPeriodEnd ? e.currentPeriodEnd.toISOString() : null,
2649
39
  cancelAt: e.cancelAt ? e.cancelAt.toISOString() : null
2650
- }, rr = S([{
40
+ }, Rt = kt([{
2651
41
  name: "STRIPE_SECRET_KEY",
2652
42
  required: !1,
2653
43
  secret: !0,
@@ -2657,43 +47,44 @@ var fe = (e, t, n, r) => ({
2657
47
  required: !1,
2658
48
  secret: !0,
2659
49
  description: "Stripe webhook signing secret (whsec_…) for verifying webhook payloads."
2660
- }]), ir = "@voltro/plugin-billing", ar = (t = {}) => {
2661
- let n = process.env, y = zt(t.plans ?? {}), b = Bn({
50
+ }]), $ = "@voltro/plugin-billing", zt = (t = {}) => {
51
+ let n = process.env, o = Ne(t.plans ?? {}), s = ot({
2662
52
  ...t.provider === void 0 ? {} : { provider: t.provider },
2663
53
  ...t.apiKey === void 0 ? {} : { apiKey: t.apiKey },
2664
- planForPriceId: y.planForPriceId,
54
+ planForPriceId: o.planForPriceId,
2665
55
  env: n
2666
- }), x = t.webhookSecret ?? rr.read("STRIPE_WEBHOOK_SECRET") ?? null;
2667
- zn({
56
+ }), u = t.webhookSecret ?? Rt.read("STRIPE_WEBHOOK_SECRET") ?? null;
57
+ nt({
2668
58
  env: n,
2669
59
  providerOption: t.provider,
2670
- resolvedName: b.name,
2671
- webhookSecret: x,
60
+ resolvedName: s.name,
61
+ webhookSecret: u,
2672
62
  redirectOrigins: t.redirectOrigins
2673
63
  });
2674
- let S = ke(t.dunning, n);
2675
- ie({
64
+ let ee = V(t.dunning, n);
65
+ X({
2676
66
  source: "plugin",
2677
- table: G,
67
+ table: U,
2678
68
  timeColumn: "updatedAt",
2679
- ttlMs: ae(process.env.VOLTRO_BILLING_USAGE_TTL_HOURS, 9600),
2680
- label: "billing usage counters"
2681
- }), ie({
69
+ where: Pt(Ft("used", 0), Ft("reportedToProvider", 0)),
70
+ ttlMs: Z(process.env.VOLTRO_BILLING_USAGE_TTL_HOURS, 9600),
71
+ label: "unused billing usage counters"
72
+ }), X({
2682
73
  source: "plugin",
2683
- table: Vt,
74
+ table: Fe,
2684
75
  timeColumn: "claimedAt",
2685
- ttlMs: ae(process.env.VOLTRO_BILLING_FLUSH_CLAIM_TTL_HOURS, 1),
76
+ ttlMs: Z(process.env.VOLTRO_BILLING_FLUSH_CLAIM_TTL_HOURS, 1),
2686
77
  label: "billing flush claims"
2687
- }), ie({
78
+ }), X({
2688
79
  source: "plugin",
2689
- table: Se,
80
+ table: le,
2690
81
  timeColumn: "sentAt",
2691
- ttlMs: ae(process.env.VOLTRO_BILLING_DUNNING_TTL_HOURS, 9600),
82
+ ttlMs: Z(process.env.VOLTRO_BILLING_DUNNING_TTL_HOURS, 9600),
2692
83
  label: "billing dunning notices"
2693
84
  });
2694
- let { service: T, holder: E } = vt({
2695
- provider: b,
2696
- plans: y,
85
+ let { service: d, holder: f } = Ee({
86
+ provider: s,
87
+ plans: o,
2697
88
  ...t.attempts === void 0 ? {} : { attempts: t.attempts },
2698
89
  ...t.checkout === void 0 ? {} : { checkout: t.checkout },
2699
90
  ...t.admit === void 0 ? {} : { admit: t.admit },
@@ -2701,21 +92,37 @@ var fe = (e, t, n, r) => ({
2701
92
  ...t.isTenantClosed === void 0 ? {} : { isTenantClosed: t.isTenantClosed },
2702
93
  ...t.onEvent === void 0 ? {} : { onEvent: t.onEvent },
2703
94
  ...t.resolveEntitlementLimit === void 0 ? {} : { resolveEntitlementLimit: t.resolveEntitlementLimit },
2704
- dunning: S
2705
- }), te = yt(T), D = Kn({
2706
- provider: b,
2707
- service: T,
2708
- webhookSecret: x,
95
+ dunning: ee
96
+ }), ne = K(d), p, m = {
97
+ scope: Mt({
98
+ base: $,
99
+ alias: t.alias,
100
+ instance: t.name
101
+ }),
102
+ store: () => {
103
+ if (p === void 0) throw Error("Billing delivery requires the bound native store.");
104
+ return p;
105
+ }
106
+ }, h = a({
107
+ provider: s,
108
+ service: d,
109
+ receipts: m,
110
+ ...t.onEvent === void 0 ? {} : { onEvent: t.onEvent }
111
+ }), g = lt({
112
+ provider: s,
113
+ service: d,
114
+ receipts: m,
115
+ webhookSecret: u,
2709
116
  ...t.onEvent === void 0 ? {} : { onEvent: t.onEvent },
2710
117
  onLateDelivery: () => {
2711
- fe();
118
+ R();
2712
119
  }
2713
- }), O = {
120
+ }), _ = {
2714
121
  method: "POST",
2715
- path: Wn,
122
+ path: q,
2716
123
  originGuard: "exempt",
2717
124
  handle: async (e) => {
2718
- let t = await D({
125
+ let t = await g({
2719
126
  method: e.method,
2720
127
  path: e.path,
2721
128
  headers: e.headers,
@@ -2728,44 +135,59 @@ var fe = (e, t, n, r) => ({
2728
135
  ...t.headers === void 0 ? {} : { headers: { ...t.headers } }
2729
136
  };
2730
137
  }
2731
- }, ne = (t) => ({
138
+ }, v = (t) => ({
2732
139
  ...t,
2733
140
  execute: (n, r) => {
2734
141
  let i = r.request.subject;
2735
- return i.type === "anonymous" || typeof i.tenantId != "string" || i.tenantId === "" ? v.fail(new e({
142
+ return i.type === "anonymous" || typeof i.tenantId != "string" || i.tenantId === "" ? Y.fail(new e({
2736
143
  source: "billing",
2737
144
  message: "billing routes answer only a signed-in caller that belongs to a tenant",
2738
145
  transient: !1
2739
146
  })) : t.execute(n, r);
2740
147
  }
2741
- }), k = [
148
+ }), y = (e, t, n, r) => e.pipe(Y.flatMap((e) => Nt(r.request.subject, t.guards, n).pipe(Y.flatMap((t) => t === null ? Y.succeed(e) : Y.fail(t))))), ie = [
2742
149
  {
2743
- ...h,
2744
- execute: (e, t) => {
2745
- let n = e;
2746
- return T.startCheckout({
2747
- tenantId: t.request.subject.tenantId ?? "",
2748
- plan: n.plan,
2749
- successUrl: n.successUrl,
2750
- cancelUrl: n.cancelUrl,
2751
- ...n.quantity === void 0 ? {} : { quantity: n.quantity }
2752
- });
150
+ ...J,
151
+ execute: (t, n) => {
152
+ let r = t;
153
+ return y(d.startCheckout({
154
+ tenantId: n.request.subject.tenantId ?? "",
155
+ plan: r.plan,
156
+ successUrl: r.successUrl,
157
+ cancelUrl: r.cancelUrl,
158
+ ...r.quantity === void 0 ? {} : { quantity: r.quantity },
159
+ authorize: () => Nt(n.request.subject, J.guards, t).pipe(Y.flatMap((t) => t === null ? Y.void : Y.fail(new e({
160
+ source: "billing",
161
+ message: "Current checkout authority could not be confirmed.",
162
+ transient: !1
163
+ }))))
164
+ }), J, t, n);
2753
165
  }
2754
166
  },
2755
167
  {
2756
- ...d,
168
+ ...gt,
2757
169
  execute: (e, t) => {
2758
170
  let n = e;
2759
- return T.portalUrl(t.request.subject.tenantId ?? "", n.returnUrl);
171
+ return y(d.portalUrl(t.request.subject.tenantId ?? "", n.returnUrl), gt, e, t);
2760
172
  }
2761
173
  },
2762
174
  {
2763
- ...g,
2764
- execute: (e, t) => T.subscription(t.request.subject.tenantId ?? "").pipe(v.map((e) => ({ subscription: nr(e) })))
175
+ ...bt,
176
+ source: [
177
+ H,
178
+ c,
179
+ l
180
+ ],
181
+ execute: (e, t) => d.subscription(t.request.subject.tenantId ?? "").pipe(Y.map((e) => ({ subscription: Q(e) })))
2765
182
  },
2766
183
  {
2767
- ...l,
2768
- execute: (e, t) => T.entitlementStatus(t.request.subject.tenantId ?? "").pipe(v.map((e) => ({
184
+ ...mt,
185
+ source: [
186
+ H,
187
+ c,
188
+ l
189
+ ],
190
+ execute: (e, t) => d.entitlementStatus(t.request.subject.tenantId ?? "").pipe(Y.map((e) => ({
2769
191
  plan: e.plan,
2770
192
  billedPlan: e.billedPlan,
2771
193
  status: e.status,
@@ -2777,13 +199,13 @@ var fe = (e, t, n, r) => ({
2777
199
  })))
2778
200
  },
2779
201
  {
2780
- ...f,
202
+ ..._t,
2781
203
  execute: (e, t) => {
2782
204
  let n = e;
2783
- return T.previewChange(t.request.subject.tenantId ?? "", {
205
+ return d.previewChange(t.request.subject.tenantId ?? "", {
2784
206
  ...n.plan === void 0 ? {} : { plan: n.plan },
2785
207
  ...n.quantity === void 0 ? {} : { quantity: n.quantity }
2786
- }).pipe(v.map((e) => ({
208
+ }).pipe(Y.map((e) => ({
2787
209
  plan: e.plan,
2788
210
  quantity: e.quantity,
2789
211
  prorationMinor: e.prorationMinor,
@@ -2792,24 +214,29 @@ var fe = (e, t, n, r) => ({
2792
214
  }
2793
215
  },
2794
216
  {
2795
- ...u,
2796
- execute: (e, t) => T.invoices(t.request.subject.tenantId ?? "").pipe(v.map((e) => ({ invoices: e.map((e) => ({
217
+ ...ht,
218
+ source: [
219
+ re,
220
+ l,
221
+ c
222
+ ],
223
+ execute: (e, t) => d.invoices(t.request.subject.tenantId ?? "").pipe(Y.map((e) => ({ invoices: e.map((e) => ({
2797
224
  ...e,
2798
225
  createdAt: e.createdAt?.toISOString() ?? null
2799
226
  })) })))
2800
227
  },
2801
228
  {
2802
- ...p,
229
+ ...vt,
2803
230
  execute: (e, t) => {
2804
231
  let n = e;
2805
- return T.reportUsage(t.request.subject.tenantId ?? "", n.key, n.quantity).pipe(v.map(() => ({ ok: !0 })));
232
+ return d.reportUsage(t.request.subject.tenantId ?? "", n.key, n.quantity).pipe(Y.map(() => ({ ok: !0 })));
2806
233
  }
2807
234
  },
2808
235
  {
2809
- ...s,
236
+ ...ft,
2810
237
  execute: (e, t) => {
2811
238
  let n = e;
2812
- return T.changePlan(t.request.subject.tenantId ?? "", n.plan).pipe(v.map((e) => ({
239
+ return d.changePlan(t.request.subject.tenantId ?? "", n.plan).pipe(Y.map((e) => ({
2813
240
  plan: e.plan,
2814
241
  quantity: e.quantity,
2815
242
  prorationMinor: e.prorationMinor,
@@ -2818,10 +245,10 @@ var fe = (e, t, n, r) => ({
2818
245
  }
2819
246
  },
2820
247
  {
2821
- ...c,
248
+ ...pt,
2822
249
  execute: (e, t) => {
2823
250
  let n = e;
2824
- return T.changeSeats(t.request.subject.tenantId ?? "", n.quantity).pipe(v.map((e) => ({
251
+ return d.changeSeats(t.request.subject.tenantId ?? "", n.quantity).pipe(Y.map((e) => ({
2825
252
  plan: e.plan,
2826
253
  quantity: e.quantity,
2827
254
  prorationMinor: e.prorationMinor,
@@ -2830,34 +257,34 @@ var fe = (e, t, n, r) => ({
2830
257
  }
2831
258
  },
2832
259
  {
2833
- ...o,
2834
- execute: (e, t) => T.cancelSubscription(t.request.subject.tenantId ?? "").pipe(v.map((e) => ({ subscription: nr(e) })))
260
+ ...dt,
261
+ execute: (e, t) => d.cancelSubscription(t.request.subject.tenantId ?? "").pipe(Y.map((e) => ({ subscription: Q(e) })))
2835
262
  },
2836
263
  {
2837
- ..._,
2838
- execute: (e, t) => T.syncTenant(t.request.subject.tenantId ?? "").pipe(v.map((e) => ({ subscription: e === null ? null : nr(e) })))
264
+ ...xt,
265
+ execute: (e, t) => d.syncTenant(t.request.subject.tenantId ?? "").pipe(Y.map((e) => ({ subscription: e === null ? null : Q(e) })))
2839
266
  },
2840
267
  {
2841
- ...m,
2842
- execute: (e, t) => T.resumeSubscription(t.request.subject.tenantId ?? "").pipe(v.map((e) => ({ subscription: nr(e) })))
268
+ ...yt,
269
+ execute: (e, t) => d.resumeSubscription(t.request.subject.tenantId ?? "").pipe(Y.map((e) => ({ subscription: Q(e) })))
2843
270
  }
2844
- ].map(ne), A = (e, t) => ({
271
+ ].map(v), ae = (e, t) => ({
2845
272
  kind: "json",
2846
273
  status: e,
2847
274
  data: { error: t }
2848
- }), re = [{
275
+ }), oe = [{
2849
276
  method: "GET",
2850
277
  path: "/stats",
2851
278
  scope: "declaration",
2852
279
  description: "Billing config: provider, declared plans, enforced tags",
2853
- handler: () => v.succeed({
280
+ handler: () => Y.succeed({
2854
281
  kind: "json",
2855
282
  data: {
2856
- provider: b.name,
2857
- supportsMeteredUsage: b.supportsMeteredUsage,
2858
- plans: y.planIds(),
283
+ provider: s.name,
284
+ supportsMeteredUsage: s.supportsMeteredUsage,
285
+ plans: o.planIds(),
2859
286
  enforce: t.enforce ? Object.keys(t.enforce) : [],
2860
- webhookPath: Wn
287
+ webhookPath: q
2861
288
  }
2862
289
  })
2863
290
  }, {
@@ -2865,11 +292,11 @@ var fe = (e, t, n, r) => ({
2865
292
  path: "/usage",
2866
293
  scope: "process",
2867
294
  description: "Pending (un-flushed) metered usage counters",
2868
- handler: () => E.stores.usage.pending().pipe(v.map((e) => ({
295
+ handler: () => f.stores.usage.pending().pipe(Y.map((e) => ({
2869
296
  kind: "json",
2870
297
  data: { usage: e }
2871
- })), v.catch((e) => v.succeed(A(500, `${e.source} failed`))))
2872
- }], j = [
298
+ })), Y.catch((e) => Y.succeed(ae(500, `${e.source} failed`))))
299
+ }], se = [
2873
300
  {
2874
301
  schema: e,
2875
302
  import: {
@@ -2891,126 +318,139 @@ var fe = (e, t, n, r) => ({
2891
318
  name: "SubscriptionLocked"
2892
319
  }
2893
320
  }
2894
- ], M = t.enforce, N = M !== void 0 && Object.keys(M).length > 0 ? ge(M, T) : void 0, P = t.metering;
2895
- P !== void 0 && xt(P);
2896
- let F = P !== void 0 && Object.keys(P).length > 0 ? At(T, P) : void 0, I = P !== void 0 && Lt(P), L = { claim: void 0 }, se = P === void 0 ? void 0 : Nt(T, P, {
2897
- claimChange: () => L.claim,
321
+ ], b = t.enforce, ce = b !== void 0 && Object.keys(b).length > 0 ? te(b, d) : void 0, x = t.metering;
322
+ x !== void 0 && Me(x);
323
+ let S = x !== void 0 && Object.keys(x).length > 0 ? ke(d, x) : void 0, C = x !== void 0 && Ae(x), w = { claim: void 0 }, T = x === void 0 ? void 0 : Oe(d, x, {
324
+ claimChange: () => w.claim,
2898
325
  onUngated: () => {
2899
- v.runSync(v.logWarning("metering: CDC meters are accruing PER PROCESS — with N replicas a tenant is billed N times. The framework did not hand this plugin a change claimer (expected on the memory store; on a SQL store this is a bug worth reporting).").pipe(v.annotateLogs({ scope: "voltro:billing" })));
326
+ Y.runSync(Y.logWarning("metering: CDC meters are accruing PER PROCESS — with N replicas a tenant is billed N times. The framework did not hand this plugin a change claimer (expected on the memory store; on a SQL store this is a bug worth reporting).").pipe(Y.annotateLogs({ scope: "voltro:billing" })));
2900
327
  }
2901
- }), R = C([F, N].filter((e) => e !== void 0)), z, B, ce = !1, le = !1, ue = !1, de = 0, V = {
328
+ }), E = At([S, ce].filter((e) => e !== void 0)), D, O, k = !1, A = !1, j, M, N = null, P = !1, F, I = {
2902
329
  info: (e, t) => {
2903
330
  console.info(e, t ?? {});
2904
331
  },
2905
332
  error: (e, t) => {
2906
333
  console.error(e, t ?? {});
2907
334
  }
2908
- }, fe = async () => {
2909
- let e = z, n = b.listUndeliveredEvents;
2910
- if (!(e === void 0 || n === void 0 || ue) && !(Date.now() - de < 6e4)) {
2911
- ue = !0;
335
+ }, L = (e) => {
336
+ k && M === void 0 && (M = setTimeout(() => {
337
+ M = void 0, R();
338
+ }, e), M.unref?.());
339
+ }, R = () => {
340
+ let e = s.listUndeliveredEvents;
341
+ if (!k || D === void 0 || e === void 0) return Promise.resolve();
342
+ if (j !== void 0) return j;
343
+ let t = F === void 0 ? 0 : 6e4 - (performance.now() - F);
344
+ return t > 0 ? (L(t), Promise.resolve()) : (j = Promise.resolve().then(async () => {
345
+ let t = !1, n = 0, r = 0, i = 0, a = /* @__PURE__ */ new Set();
2912
346
  try {
2913
- let r = await v.runPromise(n()), i = oe(), a = /* @__PURE__ */ new Set();
2914
- for (let t of r) t.providerCustomerId !== null && await Wt(e, `event:${t.id}`, i, Date.now()) && a.add(t.providerCustomerId);
2915
- let o = 0;
2916
- for (let e of a) await v.runPromise(T.syncCustomer(e).pipe(v.flatMap((e) => v.forEach(e.events, (e) => {
2917
- let n = t.onEvent?.[e._tag];
2918
- return n === void 0 ? v.void : n(e).pipe(v.catchCause((t) => v.sync(() => {
2919
- V.error("billing catch-up: onEvent failed for a correction", {
2920
- event: e._tag,
2921
- cause: String(t)
2922
- });
2923
- })));
2924
- }, { discard: !0 })), v.tap(() => v.sync(() => {
2925
- o += 1;
2926
- })), v.catch((e) => v.sync(() => {
2927
- V.error("billing catch-up: syncing a customer failed — the provider redelivers its events", { message: e.message });
2928
- }))));
2929
- V.info("billing catch-up", {
2930
- undelivered: r.length,
2931
- customers: a.size,
2932
- synced: o
347
+ for (let o = 0; o < 10 && k; o++) {
348
+ let o = N;
349
+ if (o !== null) {
350
+ if (a.has(o)) throw Error("The provider event cursor repeated.");
351
+ a.add(o);
352
+ }
353
+ let s = await Y.runPromise(e({
354
+ cursor: o,
355
+ limit: 100
356
+ }));
357
+ if (!Array.isArray(s.events) || s.events.length > 100 || s.nextCursor !== null && (typeof s.nextCursor != "string" || !s.nextCursor || s.nextCursor !== s.events.at(-1)?.id || s.nextCursor === o)) throw Error("The provider event page did not supply a bounded advancing cursor.");
358
+ for (let e of s.events) {
359
+ if (!k) return;
360
+ try {
361
+ await Y.runPromise(h(e)) === "processed" ? n++ : r++;
362
+ } catch {
363
+ i++, P = !0, I.error("billing catch-up: delivery remains retryable", { eventId: e.id });
364
+ }
365
+ }
366
+ if (N = s.nextCursor, N === null) {
367
+ t = P, P = !1;
368
+ break;
369
+ }
370
+ t = !0;
371
+ }
372
+ I.info("billing catch-up", {
373
+ processed: n,
374
+ replayed: r,
375
+ failed: i,
376
+ continuation: N !== null
2933
377
  });
2934
- } catch (e) {
2935
- V.error("billing catch-up failed — the next late delivery or restart retries it", { message: String(e?.message ?? e) });
378
+ } catch {
379
+ t = !0, I.error("billing catch-up failed; the current page will be retried");
2936
380
  } finally {
2937
- de = Date.now(), ue = !1;
381
+ F = performance.now(), j = void 0, t && L(6e4);
2938
382
  }
2939
- }
2940
- }, pe = (e) => {
2941
- e !== void 0 && (V = e), !le && ce && z !== void 0 && (le = !0, fe());
2942
- }, me = [
383
+ }), j);
384
+ }, z = (e) => {
385
+ e !== void 0 && (I = e), !A && k && D !== void 0 && (A = !0, R());
386
+ }, B = [
2943
387
  "store:write",
2944
388
  "inspect:read",
2945
389
  "inspect:write",
2946
390
  "network:outbound:*",
2947
- ...R === void 0 ? [] : [
391
+ ...E === void 0 ? [] : [
2948
392
  "rpc:intercept:mutation",
2949
393
  "rpc:intercept:action",
2950
394
  "rpc:intercept:workflow"
2951
395
  ],
2952
- ...F === void 0 ? [] : ["rpc:intercept:query"],
2953
- ...I ? ["store:changes:read"] : []
396
+ ...S === void 0 ? [] : ["rpc:intercept:query"],
397
+ ...C ? ["store:changes:read"] : []
2954
398
  ];
2955
- return ee({
2956
- name: w({
2957
- base: ir,
399
+ return jt({
400
+ name: Mt({
401
+ base: $,
2958
402
  alias: t.alias,
2959
403
  instance: t.name
2960
404
  }),
2961
- baseName: ir,
405
+ baseName: $,
2962
406
  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.",
2963
- permissions: me,
2964
- declaredEnv: rr.declared,
2965
- egress: Vn(b),
2966
- services: te,
2967
- routes: k,
2968
- rpcClientDescriptors: a,
2969
- httpRoutes: [O],
2970
- inspectEndpoints: re,
2971
- extendSchema: { tables: [
2972
- ...xe(),
2973
- Ht,
2974
- Ce
2975
- ] },
2976
- errorSchemas: j,
2977
- ...R === void 0 ? {} : {
2978
- interceptMutation: R,
2979
- interceptAction: R,
2980
- interceptWorkflow: R
2981
- },
2982
- ...F === void 0 ? {} : { interceptQuery: F },
2983
- ...I ? { onChangeEvent: se } : {},
407
+ permissions: B,
408
+ declaredEnv: Rt.declared,
409
+ egress: rt(s),
410
+ services: ne,
411
+ routes: ie,
412
+ rpcClientDescriptors: ut,
413
+ httpRoutes: [_],
414
+ inspectEndpoints: oe,
415
+ extendSchema: { tables: [...W(), Ie] },
416
+ errorSchemas: se,
417
+ ...E === void 0 ? {} : {
418
+ interceptMutation: E,
419
+ interceptAction: E,
420
+ interceptWorkflow: E
421
+ },
422
+ ...S === void 0 ? {} : { interceptQuery: S },
423
+ ...C ? { onChangeEvent: T } : {},
2984
424
  bindDataStore: (e, t) => {
2985
- E.stores = ft(e), z = e, pe(), L.claim = t?.claimChange;
425
+ f.stores = G(e), D = e, p = Lt(e), z(), w.claim = t?.claimChange;
2986
426
  },
2987
- onActivate: (e) => v.sync(() => {
427
+ onActivate: (e) => Y.sync(() => {
2988
428
  e.logger.info("billing active", {
2989
- provider: b.name,
2990
- plans: y.planIds().length,
2991
- enforce: M ? Object.keys(M).length : 0,
2992
- metering: P ? Object.keys(P).length : 0,
2993
- webhook: Wn,
429
+ provider: s.name,
430
+ plans: o.planIds().length,
431
+ enforce: b ? Object.keys(b).length : 0,
432
+ metering: x ? Object.keys(x).length : 0,
433
+ webhook: q,
2994
434
  redirectOrigins: t.redirectOrigins?.length ?? "any"
2995
- }), b.name === "stripe" && t.redirectOrigins === void 0 && e.logger.warn("billing: no redirectOrigins — checkout and portal accept any return URL; set billingPlugin({ redirectOrigins })", {});
2996
- let n = t.flushIntervalMs ?? 6e4, r = P, i = r !== void 0 && Object.keys(r).length > 0;
435
+ }), s.name === "stripe" && t.redirectOrigins === void 0 && e.logger.warn("billing: no redirectOrigins — checkout and portal accept any return URL; set billingPlugin({ redirectOrigins })", {});
436
+ let n = t.flushIntervalMs ?? 6e4, r = x, i = r !== void 0 && Object.keys(r).length > 0;
2997
437
  if (n > 0) {
2998
- let e = oe();
2999
- B = setInterval(() => {
3000
- let t = z;
438
+ let e = It();
439
+ O = setInterval(() => {
440
+ let t = D;
3001
441
  if (t === void 0) return;
3002
442
  let a = Date.now();
3003
443
  (async () => {
3004
- await Wt(t, Ut(a, n), e, a) && await v.runPromise((i ? It(T, t, r) : T.flushUsage()).pipe(v.catch(() => v.void)));
444
+ await Le(t, Re(a, n), e, a) && await Y.runPromise((i ? je(d, t, r) : d.flushUsage()).pipe(Y.catch(() => Y.void)));
3005
445
  })();
3006
- }, n), B.unref?.();
446
+ }, n), O.unref?.();
3007
447
  }
3008
- ce = !0, pe(e.logger);
448
+ k = !0, z(e.logger);
3009
449
  }),
3010
- onDeactivate: () => v.sync(() => {
3011
- B !== void 0 && (clearInterval(B), B = void 0);
450
+ onDeactivate: () => Y.promise(async () => {
451
+ k = !1, A = !1, M !== void 0 && (clearTimeout(M), M = void 0), O !== void 0 && (clearInterval(O), O = void 0), await j;
3012
452
  })
3013
453
  });
3014
454
  };
3015
455
  //#endregion
3016
- export { H as BILLING_CUSTOMERS_TABLE, Se as BILLING_DUNNING_NOTICES_TABLE, Vt as BILLING_FLUSH_CLAIMS_TABLE, W as BILLING_INVOICES_TABLE, U as BILLING_SUBSCRIPTIONS_TABLE, G as BILLING_USAGE_TABLE, Un as BILLING_WEBHOOK_ID, Wn as BILLING_WEBHOOK_PATH, e as BillingError, t as BillingService, Te as DEFAULT_DUNNING_STEPS, Ee as DEFAULT_GRACE_HOURS, De as DEFAULT_LOCKOUT, n as DEFAULT_PLAN, we as DUNNING_LOCK_STEP_ID, r as EntitlementExceeded, Gt as STRIPE_API_VERSION, Kt as STRIPE_DASHBOARD_REMINDERS, On as STRIPE_WEBHOOK_EVENTS, dn as StripeRestError, i as SubscriptionLocked, zn as assertBillingProductionReady, _e as billingCustomersTable, Ce as billingDunningNoticesTable, Vn as billingEgressOf, Ht as billingFlushClaimsTable, ye as billingInvoicesTable, ar as billingPlugin, yt as billingServiceLayer, ve as billingSubscriptionsTable, xe as billingTables, be as billingUsageTable, Gn as billingWebhookDescriptor, vt as buildBillingService, ge as buildEnforceInterceptor, zt as buildPlanRegistry, Wt as claimFlushWindow, Ve as dataStoreDunningNoticeStore, ft as dataStoreStores, Ie as defaultDunningEmail, en as diffMeter, on as diffPortal, $t as diffPrice, Xt as diffProduct, tn as diffTaxSettings, sn as diffWebhook, Ne as dueSteps, Le as dunningMailNotifier, Bt as emptyPlanRegistry, Zn as envOutRefusal, je as episodeKeyOf, Pe as evaluateBillingEntitlement, fe as evaluateEntitlement, Ut as flushWindowKey, ln as formEncode, Me as graceEndOf, Rn as isProductionBoot, Ze as isStaleStatusEvent, ze as memoryDunningNoticeStore, dt as memoryStores, Yn as mergeEnvFile, In as mockProvider, Kn as mountBillingWebhook, kn as normalizeStripeEvent, qn as openFindings, Qn as parseProvisionArgs, mt as periodKey, nn as portalParams, Jn as provisionStripeCatalog, un as redactStripeKeys, he as requireEntitled, me as requireEntitlement, ke as resolveDunningConfig, Bn as resolveProvider, $n as resolveProvisionKey, tr as runStripeProvision, cn as sellingReadiness, jn as stripeProvider, fn as stripeRest };
456
+ export { D as BILLING_CHECKOUT_OPERATIONS_TABLE, j as BILLING_CLOSE_OPERATIONS_TABLE, o as BILLING_CUSTODY_INVENTORY_TABLES, _e as BILLING_CUSTOMERS_TABLE, c as BILLING_CUSTOMER_ACCOUNTS_TABLE, l as BILLING_CUSTOMER_CUSTODY_TABLE, le as BILLING_DUNNING_NOTICES_TABLE, Fe as BILLING_FLUSH_CLAIMS_TABLE, ve as BILLING_INVOICES_TABLE, ce as BILLING_INVOICE_HISTORY_DISPATCH_TABLE, p as BILLING_INVOICE_HISTORY_PAGES_TABLE, m as BILLING_INVOICE_HISTORY_REFERENCES_TABLE, x as BILLING_INVOICE_HISTORY_SCAN_ACCOUNTS_TABLE, h as BILLING_INVOICE_HISTORY_WALKS_TABLE, oe as BILLING_INVOICE_HISTORY_WORK_TABLE, re as BILLING_INVOICE_OBSERVATIONS_TABLE, H as BILLING_SUBSCRIPTIONS_TABLE, T as BILLING_USAGE_EVENTS_TABLE, U as BILLING_USAGE_TABLE, st as BILLING_WEBHOOK_ID, q as BILLING_WEBHOOK_PATH, e as BillingError, t as BillingService, F as DEFAULT_DUNNING_STEPS, I as DEFAULT_GRACE_HOURS, L as DEFAULT_LOCKOUT, n as DEFAULT_PLAN, R as DUNNING_LOCK_STEP_ID, r as EntitlementExceeded, ze as STRIPE_API_VERSION, Be as STRIPE_DASHBOARD_REMINDERS, $e as STRIPE_WEBHOOK_EVENTS, Ye as StripeRestError, i as SubscriptionLocked, nt as assertBillingProductionReady, O as billingCheckoutOperationsTable, M as billingCloseOperationsTable, u as billingCustomerAccountsTable, ee as billingCustomerCustodyTable, ye as billingCustomersTable, z as billingDunningNoticesTable, rt as billingEgressOf, Ie as billingFlushClaimsTable, S as billingInvoiceHistoryDispatchTable, g as billingInvoiceHistoryPagesTable, _ as billingInvoiceHistoryReferencesTable, C as billingInvoiceHistoryScanAccountsTable, v as billingInvoiceHistoryWalksTable, se as billingInvoiceHistoryWorkTable, ie as billingInvoiceObservationsTable, be as billingInvoicesTable, zt as billingPlugin, ae as billingRetainedInvoiceObservationsTable, K as billingServiceLayer, xe as billingSubscriptionsTable, W as billingTables, E as billingUsageEventsTable, Se as billingUsageTable, ct as billingWebhookDescriptor, Ee as buildBillingService, te as buildEnforceInterceptor, Ne as buildPlanRegistry, Le as claimFlushWindow, N as dataStoreBillingCloseStore, k as dataStoreCheckoutOperationStore, B as dataStoreDunningNoticeStore, y as dataStoreInvoiceHistoryJournal, w as dataStoreInvoiceHistoryProgressStore, b as dataStoreInvoiceHistoryWorkStore, G as dataStoreStores, ue as defaultDunningEmail, Ve as diffMeter, He as diffPortal, Ue as diffPrice, We as diffProduct, Ge as diffTaxSettings, Ke as diffWebhook, de as dueSteps, fe as dunningMailNotifier, Pe as emptyPlanRegistry, St as envOutRefusal, pe as episodeKeyOf, me as evaluateBillingEntitlement, d as evaluateEntitlement, Re as flushWindowKey, Xe as formEncode, he as graceEndOf, s as inspectBillingCustody, it as isProductionBoot, Ce as isStaleStatusEvent, P as memoryBillingCloseStore, A as memoryCheckoutOperationStore, ge as memoryDunningNoticeStore, we as memoryStores, Ct as mergeEnvFile, at as mockProvider, lt as mountBillingWebhook, et as normalizeStripeEvent, wt as openFindings, Tt as parseProvisionArgs, De as periodKey, qe as portalParams, Et as provisionStripeCatalog, Ze as redactStripeKeys, f as requireEntitled, ne as requireEntitlement, V as resolveDunningConfig, ot as resolveProvider, Dt as resolveProvisionKey, Ot as runStripeProvision, Te as selectCurrentSubscription, Je as sellingReadiness, tt as stripeProvider, Qe as stripeRest };