@voltro/plugin-billing 0.85.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,615 @@
1
+ import { BillingError as e, BillingService as t, EntitlementExceeded as n } from "./types.js";
2
+ import { evaluateEntitlement as r } from "./entitlement.js";
3
+ import { DUNNING_LOCK_STEP_ID as i, defaultDunningEmail as a, dueSteps as o, episodeKeyOf as s, evaluateBillingEntitlement as c, resolveDunningConfig as l } from "./dunning.js";
4
+ import { isLive as u, memoryStores as d } from "./stores.js";
5
+ import { Effect as f, Layer as p, Schedule as m } from "effect";
6
+ import { createLogger as h } from "@voltro/logger";
7
+ //#region src/service.ts
8
+ var g = h({ scope: "@voltro/plugin-billing" }), _ = 3, v = /* @__PURE__ */ new Set([
9
+ "active",
10
+ "trialing",
11
+ "pastDue"
12
+ ]), y = (e = /* @__PURE__ */ new Date()) => `${e.getUTCFullYear()}-${String(e.getUTCMonth() + 1).padStart(2, "0")}`, b = (e) => ({ stores: e ?? d() }), x = (e, t) => e.pipe(f.retry({
13
+ schedule: m.recurs(Math.max(0, t - 1)),
14
+ while: (e) => e.transient
15
+ })), S = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$/, C = "usage key must be 1–100 characters of A–Z, a–z, 0–9, \"_\", \".\", \":\" or \"-\", starting with a letter or digit", w = (t) => {
16
+ let { provider: d, plans: p } = t, m = t.attempts ?? 3, h = t.checkout, w = t.now ?? (() => /* @__PURE__ */ new Date()), T = (n, r) => n.length === 0 ? f.fail(new e({
17
+ source: "billing",
18
+ message: "no tenant on this call",
19
+ transient: !1
20
+ })) : t.admit === void 0 ? f.void : t.admit(n, r), E = t.redirectOrigins?.map((e) => new URL(e).origin), D = (t, n) => {
21
+ let r = (t) => f.fail(new e({
22
+ source: "billing",
23
+ message: `${n} ${t}`,
24
+ transient: !1
25
+ })), i;
26
+ try {
27
+ i = new URL(t);
28
+ } catch {
29
+ return r("is not an absolute URL");
30
+ }
31
+ return i.protocol !== "https:" && i.protocol !== "http:" ? r("must be an http(s) URL") : E !== void 0 && !E.includes(i.origin) ? r(`must point at one of the configured redirect origins, not ${i.origin}`) : f.void;
32
+ }, O = b(t.stores), k = t.dunning ?? l(void 0, {}), A = (e) => O.stores.subscriptions.getByTenant(e).pipe(f.map((t) => c(t, k, w(), e))), j = (e) => A(e).pipe(f.map((e) => e.plan)), ee = (e) => O.stores.subscriptions.getByTenant(e), M = (e, n) => f.gen(function* () {
33
+ if (t.resolveEntitlementLimit !== void 0) {
34
+ let r = yield* t.resolveEntitlementLimit(e, n);
35
+ if (r !== null) return r;
36
+ }
37
+ let r = yield* j(e);
38
+ return p.entitlementLimit(r, n);
39
+ }), te = (e, t, n) => f.gen(function* () {
40
+ let i = yield* M(e, t), a = yield* O.stores.usage.used(e, t, y(w()));
41
+ return r(t, i, a, n);
42
+ }), ne = (e, t, r) => f.gen(function* () {
43
+ let i = yield* M(e, t);
44
+ if (!Number.isFinite(i) || r <= 0) return;
45
+ let a = y(w()), o = yield* O.stores.usage.consume(e, t, a, r, i);
46
+ if (!o.allowed) return yield* f.fail(new n({
47
+ entitlement: t,
48
+ limit: o.limit,
49
+ used: o.used,
50
+ cost: r
51
+ }));
52
+ }), N = (t, n, r) => S.test(n) ? Number.isFinite(r) ? r <= 0 ? f.void : O.stores.usage.increment(t, n, y(w()), r).pipe(f.asVoid) : f.fail(new e({
53
+ source: "billing",
54
+ message: `usage quantity must be a finite number, got ${r}`,
55
+ transient: !1
56
+ })) : f.fail(new e({
57
+ source: "billing",
58
+ message: C,
59
+ transient: !1
60
+ })), P = () => f.gen(function* () {
61
+ if (!d.supportsMeteredUsage) return;
62
+ let e = yield* O.stores.usage.pending();
63
+ for (let t of e) {
64
+ let e = t.used - t.reportedToProvider;
65
+ if (e <= 0) continue;
66
+ let n = yield* O.stores.customers.getByTenant(t.tenantId);
67
+ n === null && d.usageRequiresCustomer === !0 ? yield* f.logWarning("billing usage not flushed: no provider customer linked").pipe(f.annotateLogs({
68
+ tenantId: t.tenantId,
69
+ key: t.entitlementKey,
70
+ pending: e
71
+ })) : (yield* x(d.reportUsage({
72
+ tenantId: t.tenantId,
73
+ entitlementKey: t.entitlementKey,
74
+ quantity: e,
75
+ idempotencyKey: `${t.tenantId}:${t.entitlementKey}:${t.period}:${t.used}`,
76
+ ...n === null ? {} : { providerCustomerId: n.providerCustomerId }
77
+ }), m), yield* O.stores.usage.markReported(t.tenantId, t.entitlementKey, t.period, t.used));
78
+ }
79
+ }), F = (t) => f.fail(new e({
80
+ source: "billing",
81
+ message: `tenant '${t}' already has a paying subscription — change its plan instead of checking out again`,
82
+ transient: !1
83
+ })), I = (e, t, n) => t.status === "canceled" ? O.stores.subscriptions.markCanceled(t.providerSubscriptionId) : O.stores.subscriptions.upsert({
84
+ tenantId: e,
85
+ provider: d.name,
86
+ providerSubscriptionId: t.providerSubscriptionId,
87
+ plan: t.plan ?? "free",
88
+ status: t.status,
89
+ quantity: t.quantity,
90
+ currentPeriodStart: t.currentPeriodStart,
91
+ currentPeriodEnd: t.currentPeriodEnd,
92
+ cancelAt: t.cancelAt,
93
+ pastDueSince: null,
94
+ statusEventAt: n
95
+ }), L = (t) => f.gen(function* () {
96
+ let n = p.priceIdFor(t.plan);
97
+ if (n === null) return yield* f.fail(new e({
98
+ source: "billing",
99
+ message: `plan '${t.plan}' has no provider priceId — not a paid plan`,
100
+ transient: !1
101
+ }));
102
+ if (yield* T(t.tenantId, "checkout"), t.quantity !== void 0 && (!Number.isSafeInteger(t.quantity) || t.quantity <= 0)) return yield* f.fail(new e({
103
+ source: "billing",
104
+ message: `seat quantity must be a positive integer, got ${t.quantity}`,
105
+ transient: !1
106
+ }));
107
+ yield* D(t.successUrl, "successUrl"), yield* D(t.cancelUrl, "cancelUrl");
108
+ let r = yield* O.stores.subscriptions.getByTenant(t.tenantId);
109
+ if (r !== null && v.has(r.status)) return yield* F(t.tenantId);
110
+ let i = yield* O.stores.customers.getByTenant(t.tenantId), a = i?.providerCustomerId ?? (yield* x(d.ensureCustomer({ tenantId: t.tenantId }), m));
111
+ i === null && (yield* O.stores.customers.upsert({
112
+ tenantId: t.tenantId,
113
+ provider: d.name,
114
+ providerCustomerId: a
115
+ }));
116
+ let o = (yield* x(d.listSubscriptions({ providerCustomerId: a }), m)).filter((e) => v.has(e.status));
117
+ if (o.length > 0) {
118
+ for (let e of o) yield* I(t.tenantId, e, w());
119
+ return yield* F(t.tenantId);
120
+ }
121
+ return yield* x(d.expireOpenCheckouts({ providerCustomerId: a }), m), yield* x(d.createCheckoutSession({
122
+ tenantId: t.tenantId,
123
+ priceId: n,
124
+ meteredPriceIds: p.meteredPriceIdsFor(t.plan),
125
+ successUrl: t.successUrl,
126
+ cancelUrl: t.cancelUrl,
127
+ providerCustomerId: a,
128
+ ...t.quantity === void 0 ? {} : { quantity: t.quantity },
129
+ ...h?.adjustableQuantity === void 0 ? {} : { adjustableQuantity: h.adjustableQuantity },
130
+ ...h?.trialDays === void 0 ? {} : { trialDays: h.trialDays }
131
+ }), m);
132
+ }), R = (t, n) => f.gen(function* () {
133
+ yield* T(t, "portal"), yield* D(n, "returnUrl");
134
+ let r = yield* O.stores.customers.getByTenant(t);
135
+ return r === null ? yield* f.fail(new e({
136
+ source: "billing",
137
+ message: `no provider customer linked for tenant '${t}'`,
138
+ transient: !1
139
+ })) : yield* x(d.createPortalSession({
140
+ tenantId: t,
141
+ providerCustomerId: r.providerCustomerId,
142
+ returnUrl: n
143
+ }), m);
144
+ }), z = (e) => f.gen(function* () {
145
+ if (k.resolveRecipient !== void 0) {
146
+ let t = yield* k.resolveRecipient(e);
147
+ if (t !== null) return t;
148
+ }
149
+ if (d.customerEmail === void 0) return null;
150
+ let t = yield* O.stores.customers.getByTenant(e);
151
+ return t === null ? null : yield* d.customerEmail(t.providerCustomerId);
152
+ }), B = (e) => {
153
+ let t = k.portalReturnUrl;
154
+ return t === void 0 ? f.succeed(null) : R(e, t).pipe(f.map((e) => e.url), f.catch(() => f.succeed(null)));
155
+ }, V = (e, t, n, r, i) => f.gen(function* () {
156
+ let o = yield* z(e.tenantId).pipe(f.catch(() => f.succeed(null))), s = yield* B(e.tenantId), c = t.lockedSince !== null, l = a({
157
+ stepId: n,
158
+ locked: c,
159
+ graceEndsAt: i,
160
+ portalUrl: s
161
+ }), u = {
162
+ stepId: n,
163
+ tenantId: e.tenantId,
164
+ to: o,
165
+ plan: e.plan,
166
+ status: e.status,
167
+ pastDueSince: r,
168
+ graceEndsAt: i,
169
+ locked: c,
170
+ lockout: k.lockout,
171
+ portalUrl: s,
172
+ ...l
173
+ };
174
+ g.info("billing dunning notice", {
175
+ tenant: e.tenantId,
176
+ step: n,
177
+ locked: c,
178
+ graceEndsAt: i.toISOString(),
179
+ delivered: k.notify !== void 0 && o !== null
180
+ });
181
+ let d = k.notify;
182
+ d !== void 0 && (yield* d(u).pipe(f.catchCause((t) => f.sync(() => {
183
+ g.warn("billing dunning notify failed", {
184
+ tenant: e.tenantId,
185
+ step: n,
186
+ cause: String(t)
187
+ });
188
+ }))));
189
+ }), H = (e) => f.gen(function* () {
190
+ let t = yield* O.stores.subscriptions.getByTenant(e);
191
+ if (t === null) return c(null, k, w(), e);
192
+ let n = yield* d.fetchSubscription(t.providerSubscriptionId).pipe(f.catch((t) => f.sync(() => (g.warn("billing dunning reconcile: provider unreachable, not escalating", {
193
+ tenant: e,
194
+ source: t.source,
195
+ message: t.message
196
+ }), null)))), r = t;
197
+ if (n !== null) {
198
+ let i = w(), a = n.status === "pastDue" ? t.status === "pastDue" && t.pastDueSince !== null ? t.pastDueSince : i : null;
199
+ yield* O.stores.subscriptions.patchByTenant(e, {
200
+ status: n.status,
201
+ ...n.plan === null ? {} : { plan: n.plan },
202
+ quantity: n.quantity,
203
+ currentPeriodStart: n.currentPeriodStart,
204
+ currentPeriodEnd: n.currentPeriodEnd,
205
+ pastDueSince: a,
206
+ statusEventAt: i
207
+ }), r = {
208
+ ...t,
209
+ status: n.status,
210
+ plan: n.plan ?? t.plan,
211
+ quantity: n.quantity,
212
+ currentPeriodStart: n.currentPeriodStart,
213
+ currentPeriodEnd: n.currentPeriodEnd,
214
+ pastDueSince: a,
215
+ statusEventAt: i
216
+ };
217
+ }
218
+ let a = c(r, k, w(), e);
219
+ if (!k.enabled || r.status !== "pastDue" || r.pastDueSince === null) return a;
220
+ let l = r.pastDueSince, u = s(l), p = a.graceEndsAt ?? l, m = o(k.steps, l, w()).map((e) => e.id);
221
+ a.lockedSince !== null && m.push(i);
222
+ for (let t of m) (yield* O.stores.dunningNotices.claim(e, u, t, w())) && (yield* V(r, a, t, l, p));
223
+ return a;
224
+ }), re = () => f.gen(function* () {
225
+ let e = yield* O.stores.subscriptions.listByStatus("pastDue"), t = 0;
226
+ for (let n of e) yield* H(n.tenantId).pipe(f.catch((e) => f.sync(() => {
227
+ g.warn("billing dunning sweep: tenant failed", {
228
+ tenant: n.tenantId,
229
+ message: e.message
230
+ });
231
+ }))), t += 1;
232
+ return t;
233
+ }), U = (e) => {
234
+ if (!k.enabled) return f.void;
235
+ if (e._tag === "invoicePaymentFailed") return H(e.tenantId).pipe(f.asVoid);
236
+ if (e._tag !== "invoicePaid" && e._tag !== "subscriptionUpserted") return f.void;
237
+ let t = e.tenantId;
238
+ return O.stores.subscriptions.getByTenant(t).pipe(f.flatMap((e) => e !== null && (e.status === "pastDue" || e.pastDueSince !== null) ? H(t).pipe(f.asVoid) : f.void));
239
+ }, ie = (e) => {
240
+ switch (e._tag) {
241
+ case "subscriptionUpserted": return O.stores.subscriptions.upsert({
242
+ tenantId: e.tenantId,
243
+ provider: d.name,
244
+ providerSubscriptionId: e.providerSubscriptionId,
245
+ plan: e.plan,
246
+ status: e.status,
247
+ quantity: e.quantity ?? 1,
248
+ currentPeriodStart: e.currentPeriodStart ?? null,
249
+ currentPeriodEnd: e.currentPeriodEnd,
250
+ cancelAt: e.cancelAt,
251
+ pastDueSince: null,
252
+ statusEventAt: e.occurredAt
253
+ });
254
+ case "subscriptionCanceled": return O.stores.subscriptions.markCanceled(e.providerSubscriptionId);
255
+ case "invoicePaid": return O.stores.invoices.upsert({
256
+ tenantId: e.tenantId,
257
+ provider: d.name,
258
+ providerInvoiceId: e.providerInvoiceId,
259
+ amountMinor: e.amountMinor,
260
+ currency: e.currency,
261
+ status: "paid",
262
+ statusEventAt: e.occurredAt
263
+ });
264
+ case "invoicePaymentFailed": return O.stores.invoices.upsert({
265
+ tenantId: e.tenantId,
266
+ provider: d.name,
267
+ providerInvoiceId: e.providerInvoiceId,
268
+ amountMinor: e.amountMinor,
269
+ currency: e.currency,
270
+ status: "open",
271
+ statusEventAt: e.occurredAt
272
+ });
273
+ case "customerLinked": return O.stores.customers.upsert({
274
+ tenantId: e.tenantId,
275
+ provider: d.name,
276
+ providerCustomerId: e.providerCustomerId
277
+ });
278
+ case "customerUnlinked": return O.stores.customers.removeByProviderCustomerId(e.providerCustomerId).pipe(f.asVoid);
279
+ }
280
+ }, ae = (e) => ie(e).pipe(f.andThen(U(e))), W = (t, n) => f.gen(function* () {
281
+ yield* T(t, "change"), yield* Q(t);
282
+ let r = yield* O.stores.subscriptions.getByTenant(t);
283
+ if (r === null) return yield* f.fail(new e({
284
+ source: "billing",
285
+ message: `no subscription for tenant '${t}' to change`,
286
+ transient: !1
287
+ }));
288
+ let i = n.plan ?? r.plan, a = n.quantity ?? r.quantity;
289
+ if (n.plan !== void 0 && !p.has(i)) return yield* f.fail(new e({
290
+ source: "billing",
291
+ message: `unknown plan '${i}' — not in the plan registry`,
292
+ transient: !1
293
+ }));
294
+ if (!Number.isInteger(a) || a <= 0) return yield* f.fail(new e({
295
+ source: "billing",
296
+ message: `seat quantity must be a positive integer, got ${a}`,
297
+ transient: !1
298
+ }));
299
+ let o = yield* x(d.updateSubscription({
300
+ providerSubscriptionId: r.providerSubscriptionId,
301
+ ...n.plan === void 0 ? {} : { priceId: p.priceIdFor(i) ?? i },
302
+ ...n.quantity === void 0 ? {} : { quantity: a },
303
+ proration: "prorate"
304
+ }), m);
305
+ return yield* O.stores.subscriptions.patchByTenant(t, {
306
+ ...n.plan === void 0 ? {} : { plan: i },
307
+ quantity: o.quantity,
308
+ ...o.currentPeriodStart === null ? {} : { currentPeriodStart: o.currentPeriodStart },
309
+ ...o.currentPeriodEnd === null ? {} : { currentPeriodEnd: o.currentPeriodEnd }
310
+ }), {
311
+ tenantId: t,
312
+ plan: i,
313
+ quantity: o.quantity,
314
+ prorationMinor: o.prorationMinor,
315
+ currency: o.currency
316
+ };
317
+ }), oe = (t, n) => f.gen(function* () {
318
+ let r = yield* O.stores.subscriptions.getByTenant(t);
319
+ if (r === null) return yield* f.fail(new e({
320
+ source: "billing",
321
+ message: `no subscription for tenant '${t}' to preview`,
322
+ transient: !1
323
+ }));
324
+ let i = n.plan ?? r.plan, a = yield* x(d.previewSubscriptionChange({
325
+ providerSubscriptionId: r.providerSubscriptionId,
326
+ ...n.plan === void 0 ? {} : { priceId: p.priceIdFor(i) ?? i },
327
+ ...n.quantity === void 0 ? {} : { quantity: n.quantity },
328
+ proration: "prorate"
329
+ }), m);
330
+ return {
331
+ tenantId: t,
332
+ plan: i,
333
+ quantity: a.quantity,
334
+ prorationMinor: a.prorationMinor,
335
+ currency: a.currency
336
+ };
337
+ }), se = (e) => f.gen(function* () {
338
+ let t = yield* O.stores.customers.getByTenant(e);
339
+ return t === null ? [] : yield* x(d.listInvoices({ providerCustomerId: t.providerCustomerId }), m);
340
+ }), ce = (t, n) => f.gen(function* () {
341
+ if (n === "free") {
342
+ let e = yield* q(t);
343
+ return {
344
+ tenantId: t,
345
+ plan: e.plan,
346
+ quantity: e.quantity,
347
+ prorationMinor: 0,
348
+ currency: p.currencyFor(e.plan)
349
+ };
350
+ }
351
+ return p.has(n) && p.priceIdFor(n) === null ? yield* f.fail(new e({
352
+ source: "billing",
353
+ message: `plan '${n}' has no provider price — it is not a self-serve plan`,
354
+ transient: !1
355
+ })) : yield* W(t, { plan: n });
356
+ }), G = (t, n) => O.stores.subscriptions.getByTenant(t).pipe(f.flatMap((r) => r !== null && u(r.status) ? f.succeed(r) : f.fail(new e({
357
+ source: "billing",
358
+ message: `no live subscription for tenant '${t}' to ${n}`,
359
+ transient: !1
360
+ })))), K = (e, t) => f.gen(function* () {
361
+ let n = w();
362
+ return yield* I(e.tenantId, {
363
+ ...t,
364
+ providerSubscriptionId: e.providerSubscriptionId
365
+ }, n), {
366
+ ...e,
367
+ ...t,
368
+ plan: t.plan ?? e.plan,
369
+ statusEventAt: n
370
+ };
371
+ }), q = (e) => f.gen(function* () {
372
+ yield* T(e, "cancel"), yield* Q(e);
373
+ let t = yield* G(e, "cancel"), n = yield* x(d.cancelSubscription({
374
+ providerSubscriptionId: t.providerSubscriptionId,
375
+ mode: "periodEnd"
376
+ }), m);
377
+ return yield* K(t, n);
378
+ }), le = (e) => f.gen(function* () {
379
+ yield* T(e, "resume"), yield* Q(e);
380
+ let t = yield* G(e, "resume"), n = yield* x(d.resumeSubscription(t.providerSubscriptionId), m);
381
+ return yield* K(t, n);
382
+ }), J = (e) => f.gen(function* () {
383
+ yield* P();
384
+ let t = yield* O.stores.customers.getByTenant(e), n = yield* O.stores.subscriptions.listByTenant(e), r = new Set(n.filter((e) => u(e.status)).map((e) => e.providerSubscriptionId));
385
+ if (t !== null) {
386
+ let e = yield* x(d.listSubscriptions({ providerCustomerId: t.providerCustomerId }), m);
387
+ for (let t of e) r.add(t.providerSubscriptionId);
388
+ }
389
+ let i = [];
390
+ for (let t of r) {
391
+ let n = yield* x(d.cancelSubscription({
392
+ providerSubscriptionId: t,
393
+ mode: "close"
394
+ }), m);
395
+ yield* I(e, {
396
+ ...n,
397
+ providerSubscriptionId: t
398
+ }, w()), i.push(t);
399
+ }
400
+ return g.info("billing account closed", {
401
+ tenant: e,
402
+ canceled: i.length
403
+ }), {
404
+ tenantId: e,
405
+ canceled: i
406
+ };
407
+ }), Y = (e) => {
408
+ let n = t.isTenantClosed;
409
+ return n === void 0 ? f.succeed(!1) : n(e).pipe(f.catch((t) => f.sync(() => (g.warn("billing: could not tell whether a tenant is closed — leaving it", {
410
+ tenant: e,
411
+ message: t.message
412
+ }), !1))));
413
+ }, X = (e, t) => f.gen(function* () {
414
+ let n = w(), r = yield* x(d.listSubscriptions({ providerCustomerId: e }), m), i = yield* O.stores.customers.getByProviderCustomerId(e), a = i?.tenantId ?? t ?? r.find((e) => e.tenantId !== null)?.tenantId ?? null;
415
+ if (a === null) return g.warn("billing sync: customer names no tenant — nothing to attribute it to", { customer: e }), {
416
+ tenantId: null,
417
+ events: []
418
+ };
419
+ let o = [];
420
+ i === null && (yield* O.stores.customers.upsert({
421
+ tenantId: a,
422
+ provider: d.name,
423
+ providerCustomerId: e
424
+ }), o.push({
425
+ _tag: "customerLinked",
426
+ tenantId: a,
427
+ providerCustomerId: e,
428
+ occurredAt: n
429
+ })), r.some((e) => v.has(e.status) && e.cancelAt === null) && (yield* Y(a)) && (g.error("billing sync: a closed tenant still has a paying subscription — closing it", { tenant: a }), yield* J(a), r = yield* x(d.listSubscriptions({ providerCustomerId: e }), m));
430
+ for (let e of r) if (yield* I(a, e, n), o.push($(a, e, n)), e.plan !== null && v.has(e.status)) {
431
+ let t = p.meteredPriceIdsFor(e.plan).filter((t) => !e.priceIds.includes(t));
432
+ t.length > 0 && (yield* x(d.addSubscriptionPrices({
433
+ providerSubscriptionId: e.providerSubscriptionId,
434
+ priceIds: t
435
+ }), m));
436
+ }
437
+ let s = new Set(r.map((e) => e.providerSubscriptionId));
438
+ for (let e of yield* O.stores.subscriptions.listByTenant(a)) {
439
+ if (!u(e.status) || s.has(e.providerSubscriptionId)) continue;
440
+ let t = yield* x(d.fetchSubscription(e.providerSubscriptionId), m), r = t === null ? {
441
+ ...e,
442
+ status: "canceled",
443
+ providerSubscriptionId: e.providerSubscriptionId
444
+ } : {
445
+ ...t,
446
+ providerSubscriptionId: e.providerSubscriptionId
447
+ };
448
+ yield* I(a, r, n), o.push($(a, r, n));
449
+ }
450
+ for (let e of o) yield* U(e);
451
+ return {
452
+ tenantId: a,
453
+ events: o
454
+ };
455
+ }), Z = (e) => f.forEach(e, (e) => {
456
+ let n = t.onEvent?.[e._tag];
457
+ return n === void 0 ? f.void : n(e).pipe(f.catchCause((t) => f.sync(() => {
458
+ g.error("billing: onEvent failed for a correction — the next sync of this tenant retries it", {
459
+ event: e._tag,
460
+ cause: String(t)
461
+ });
462
+ })));
463
+ }, { discard: !0 }), Q = (e) => f.gen(function* () {
464
+ let t = yield* O.stores.customers.getByTenant(e);
465
+ if (t !== null) {
466
+ let n = yield* X(t.providerCustomerId, e);
467
+ yield* Z(n.events);
468
+ }
469
+ return yield* O.stores.subscriptions.getByTenant(e);
470
+ }), ue = (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), $ = (e, t, n) => t.status === "canceled" ? {
471
+ _tag: "subscriptionCanceled",
472
+ tenantId: e,
473
+ providerSubscriptionId: t.providerSubscriptionId,
474
+ occurredAt: n
475
+ } : {
476
+ _tag: "subscriptionUpserted",
477
+ tenantId: e,
478
+ providerSubscriptionId: t.providerSubscriptionId,
479
+ plan: t.plan ?? "free",
480
+ status: t.status,
481
+ quantity: t.quantity,
482
+ currentPeriodStart: t.currentPeriodStart,
483
+ currentPeriodEnd: t.currentPeriodEnd,
484
+ cancelAt: t.cancelAt,
485
+ occurredAt: n
486
+ };
487
+ return {
488
+ service: {
489
+ subscription: ee,
490
+ plan: j,
491
+ entitlementStatus: A,
492
+ reconcileDunning: H,
493
+ dunningSweep: re,
494
+ checkEntitlement: te,
495
+ consumeEntitlement: ne,
496
+ reportUsage: N,
497
+ flushUsage: P,
498
+ startCheckout: L,
499
+ portalUrl: R,
500
+ applyEvent: ae,
501
+ changePlan: ce,
502
+ changeSeats: (e, t) => W(e, { quantity: t }),
503
+ previewChange: oe,
504
+ invoices: se,
505
+ cancelSubscription: q,
506
+ resumeSubscription: le,
507
+ closeAccount: J,
508
+ reconcileFromProvider: () => f.gen(function* () {
509
+ let e = w(), t = yield* x(d.listSubscriptions({}), m), n = yield* O.stores.customers.list(), r = new Map(n.map((e) => [e.providerCustomerId, e.tenantId])), i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Set(), s = [], c = /* @__PURE__ */ new Set(), l = /* @__PURE__ */ new Map(), u = 0, h = 0, y = (e) => {
510
+ let t = l.get(e);
511
+ return t === void 0 ? Y(e).pipe(f.tap((t) => f.sync(() => {
512
+ l.set(e, t);
513
+ }))) : f.succeed(t);
514
+ };
515
+ for (let n of t) {
516
+ o.add(n.providerSubscriptionId);
517
+ let t = n.tenantId ?? (n.providerCustomerId === null ? null : r.get(n.providerCustomerId) ?? null);
518
+ if (t === null) {
519
+ u += 1, g.warn("billing reconcile: provider subscription names no tenant", { subscription: n.providerSubscriptionId });
520
+ continue;
521
+ }
522
+ if (v.has(n.status) && n.cancelAt === null && !c.has(t) && (yield* y(t))) {
523
+ g.error("billing reconcile: a closed tenant still has a paying subscription — closing it", {
524
+ tenant: t,
525
+ subscription: n.providerSubscriptionId
526
+ }), (yield* J(t).pipe(f.map(() => !0), f.catch((e) => f.sync(() => (g.error("billing reconcile: closing a closed tenant failed — retried next run", {
527
+ tenant: t,
528
+ message: e.message
529
+ }), !1))))) && c.add(t), i.add(t);
530
+ continue;
531
+ }
532
+ if (c.has(t)) continue;
533
+ v.has(n.status) && a.set(t, (a.get(t) ?? 0) + 1);
534
+ let l = yield* O.stores.subscriptions.listByTenant(t);
535
+ ue(l.find((e) => e.providerSubscriptionId === n.providerSubscriptionId), n) || (yield* I(t, n, e), s.push($(t, n, e)), i.add(t)), n.providerCustomerId !== null && r.get(n.providerCustomerId) !== t && (yield* O.stores.customers.upsert({
536
+ tenantId: t,
537
+ provider: d.name,
538
+ providerCustomerId: n.providerCustomerId
539
+ }), r.set(n.providerCustomerId, t), s.push({
540
+ _tag: "customerLinked",
541
+ tenantId: t,
542
+ providerCustomerId: n.providerCustomerId,
543
+ occurredAt: e
544
+ }), i.add(t));
545
+ let _ = n.plan;
546
+ if (_ !== null && v.has(n.status)) {
547
+ let e = p.meteredPriceIdsFor(_).filter((e) => !n.priceIds.includes(e));
548
+ e.length > 0 && (yield* x(d.addSubscriptionPrices({
549
+ providerSubscriptionId: n.providerSubscriptionId,
550
+ priceIds: e
551
+ }), m), h += 1);
552
+ }
553
+ }
554
+ let b = 0, S = [];
555
+ for (let e of [
556
+ "active",
557
+ "trialing",
558
+ "pastDue",
559
+ "incomplete"
560
+ ]) S.push(...yield* O.stores.subscriptions.listByStatus(e));
561
+ let C = [];
562
+ for (let t of S) {
563
+ if (o.has(t.providerSubscriptionId)) continue;
564
+ b += 1;
565
+ let n = yield* x(d.fetchSubscription(t.providerSubscriptionId), m);
566
+ if (n === null) {
567
+ C.push(t);
568
+ continue;
569
+ }
570
+ let r = {
571
+ ...n,
572
+ providerSubscriptionId: t.providerSubscriptionId
573
+ };
574
+ yield* I(t.tenantId, r, e), s.push($(t.tenantId, r, e)), i.add(t.tenantId);
575
+ }
576
+ let T = C.length >= _ && C.length * 2 >= S.length;
577
+ if (T) g.error("billing reconcile: the provider knows none of these live subscriptions — wrong account or provider? Nothing was canceled", {
578
+ unknown: C.length,
579
+ live: S.length,
580
+ provider: d.name
581
+ });
582
+ else for (let t of C) yield* O.stores.subscriptions.markCanceled(t.providerSubscriptionId), s.push($(t.tenantId, {
583
+ ...t,
584
+ status: "canceled",
585
+ providerSubscriptionId: t.providerSubscriptionId
586
+ }, e)), i.add(t.tenantId);
587
+ let E = [...a].filter(([, e]) => e > 1).map(([e]) => e);
588
+ for (let e of E) g.error("billing reconcile: tenant has more than one paying subscription — billed twice, resolve by hand", { tenant: e });
589
+ let D = {
590
+ checked: t.length + b,
591
+ corrected: [...i],
592
+ duplicates: E,
593
+ unattributed: u,
594
+ meteredItemsRestored: h,
595
+ closedTenants: [...c],
596
+ refusedUnknown: T ? C.length : 0,
597
+ events: s
598
+ };
599
+ return g.info("billing reconcile from provider", {
600
+ checked: D.checked,
601
+ corrected: D.corrected.length,
602
+ duplicates: E.length,
603
+ unattributed: u,
604
+ meteredItemsRestored: h,
605
+ closedTenants: D.closedTenants.length
606
+ }), yield* Z(s), D;
607
+ }),
608
+ syncCustomer: X,
609
+ syncTenant: Q
610
+ },
611
+ holder: O
612
+ };
613
+ }, T = (e) => p.succeed(t, e);
614
+ //#endregion
615
+ export { S as USAGE_KEY, C as USAGE_KEY_RULE, T as billingServiceLayer, w as buildBillingService, b as makeStoreHolder, y as periodKey };