@voltro/plugin-billing 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5484 -0
- package/README.md +428 -0
- package/THIRD-PARTY-NOTICES.md +1511 -276
- package/dist/catalog.js +1 -2
- package/dist/checkoutStore.js +164 -0
- package/dist/closeStore.js +124 -0
- package/dist/custodyInventory.js +154 -0
- package/dist/customerStore.js +258 -0
- package/dist/dunning.js +1 -2
- package/dist/economy.d.ts +30 -0
- package/dist/economy.js +265 -0
- package/dist/entitlement.js +1 -2
- package/dist/eventDelivery.js +71 -0
- package/dist/flushClaim.js +1 -2
- package/dist/index.d.ts +669 -65
- package/dist/index.js +230 -173
- package/dist/invoiceHistory.js +90 -0
- package/dist/invoiceHistoryJournal.js +475 -0
- package/dist/invoiceHistoryProgress.js +234 -0
- package/dist/invoiceHistoryWork.js +362 -0
- package/dist/invoiceSnapshot.js +65 -0
- package/dist/invoiceStore.js +132 -0
- package/dist/metering.js +1 -2
- package/dist/plans.js +1 -2
- package/dist/providerIdentity.js +23 -0
- package/dist/providers.js +271 -67
- package/dist/provision.js +1 -2
- package/dist/rpc.js +1 -2
- package/dist/service.js +892 -392
- package/dist/stores.js +125 -291
- package/dist/stripeClient.js +1 -2
- package/dist/stripeIdentity.js +55 -0
- package/dist/stripeInvoiceHistory.js +66 -0
- package/dist/stripeInvoiceSnapshot.js +85 -0
- package/dist/stripeProvider.js +305 -179
- package/dist/stripeRest.js +1 -2
- package/dist/stripeVerifiedEvent.js +28 -0
- package/dist/table.js +70 -48
- package/dist/types.d.ts +298 -27
- package/dist/types.js +1 -2
- package/dist/usageEvents.js +64 -0
- package/dist/web/index.js +1 -2
- package/dist/webhook.js +32 -40
- package/package.json +18 -8
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { BillingError as e } from "./types.js";
|
|
2
|
+
import { captureProviderAccount as t, captureProviderCustomer as n, exactProviderIdentifier as r, sameProviderAccount as i } from "./providerIdentity.js";
|
|
3
|
+
import { BILLING_CUSTODY_INVENTORY_TABLES as a } from "./custodyInventory.js";
|
|
4
|
+
import { Effect as o } from "effect";
|
|
5
|
+
import { and as s, eq as c, generateId as l, gt as u, id as d, integer as f, table as p, text as m, timestamp as h } from "@voltro/database";
|
|
6
|
+
// src/customerStore.ts
|
|
7
|
+
var g = "_voltro_billing_customer_accounts", _ = "_voltro_billing_customer_custody", v = p(g, {
|
|
8
|
+
id: d({ prefix: "bcacct" }),
|
|
9
|
+
tenantId: m().maxLength(191),
|
|
10
|
+
provider: m().maxLength(64),
|
|
11
|
+
providerAccountId: m().maxLength(191),
|
|
12
|
+
livemode: f()
|
|
13
|
+
}).unique("byBillingCustomerAccountTenant", ["tenantId"]), y = p(_, {
|
|
14
|
+
id: d({ prefix: "bcustody" }),
|
|
15
|
+
tenantId: m().maxLength(191),
|
|
16
|
+
provider: m().maxLength(64),
|
|
17
|
+
providerAccountId: m().maxLength(191),
|
|
18
|
+
livemode: f(),
|
|
19
|
+
providerCustomerId: m().maxLength(191),
|
|
20
|
+
identityKey: m().maxLength(64),
|
|
21
|
+
slot: m().maxLength(191),
|
|
22
|
+
observedAt: h(),
|
|
23
|
+
retiredAt: h().nullable()
|
|
24
|
+
}).unique("byBillingCustomerCustodyIdentity", ["identityKey"]).unique("byBillingCustomerCustodySlot", ["tenantId", "slot"]).index("byBillingCustomerCustodyInventory", [
|
|
25
|
+
"provider",
|
|
26
|
+
"providerAccountId",
|
|
27
|
+
"livemode",
|
|
28
|
+
"id"
|
|
29
|
+
]), b = () => new e({
|
|
30
|
+
source: "customerStore",
|
|
31
|
+
message: "Billing customer custody or retained unqualified history requires reconciliation.",
|
|
32
|
+
transient: !0
|
|
33
|
+
}), x = (e, t) => {
|
|
34
|
+
let n = l({
|
|
35
|
+
kind: "typeid",
|
|
36
|
+
prefix: t
|
|
37
|
+
}, e);
|
|
38
|
+
if (typeof n != "string" || n.length === 0) throw b();
|
|
39
|
+
return n;
|
|
40
|
+
}, S = (e) => {
|
|
41
|
+
if (!r(e.provider) || e.provider.length > 64) throw b();
|
|
42
|
+
let n = t(e.account);
|
|
43
|
+
if (n.providerAccountId.length > 191) throw b();
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
provider: e.provider,
|
|
46
|
+
account: n
|
|
47
|
+
});
|
|
48
|
+
}, C = (e) => {
|
|
49
|
+
if (!r(e) || e.length > 191) throw b();
|
|
50
|
+
return e;
|
|
51
|
+
}, w = (e, t) => e.provider === t.provider && i(e.account, t.account), T = (e, t) => {
|
|
52
|
+
if (!w(e, t)) throw b();
|
|
53
|
+
}, E = (e) => {
|
|
54
|
+
let t = S(e);
|
|
55
|
+
if (!r(e.providerCustomerId) || e.providerCustomerId.length > 191) throw b();
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
...t,
|
|
58
|
+
tenantId: C(e.tenantId),
|
|
59
|
+
providerCustomerId: e.providerCustomerId
|
|
60
|
+
});
|
|
61
|
+
}, D = async (e) => {
|
|
62
|
+
let t = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
63
|
+
"voltro.customer-custody.v1",
|
|
64
|
+
e.provider,
|
|
65
|
+
e.account.providerAccountId,
|
|
66
|
+
e.account.livemode,
|
|
67
|
+
e.providerCustomerId
|
|
68
|
+
])));
|
|
69
|
+
return [...new Uint8Array(t)].map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
70
|
+
}, O = (e, t, n) => ({
|
|
71
|
+
table: e,
|
|
72
|
+
predicate: t,
|
|
73
|
+
order: [],
|
|
74
|
+
take: n,
|
|
75
|
+
skip: void 0,
|
|
76
|
+
projection: void 0
|
|
77
|
+
}), k = (t) => o.tryPromise({
|
|
78
|
+
try: t,
|
|
79
|
+
catch: (t) => t instanceof e ? t : b()
|
|
80
|
+
}), A = (e) => {
|
|
81
|
+
if (e.livemode !== 0 && e.livemode !== 1 || !r(e.provider) || !r(e.providerAccountId)) throw b();
|
|
82
|
+
return S({
|
|
83
|
+
provider: e.provider,
|
|
84
|
+
account: {
|
|
85
|
+
providerAccountId: e.providerAccountId,
|
|
86
|
+
livemode: e.livemode === 1
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}, j = (e) => {
|
|
90
|
+
let t = e instanceof Date || typeof e == "string" || typeof e == "number" ? new Date(e) : /* @__PURE__ */ new Date(NaN);
|
|
91
|
+
if (!Number.isFinite(t.getTime())) throw b();
|
|
92
|
+
return t;
|
|
93
|
+
}, M = (e) => {
|
|
94
|
+
let t = A(e), n = e.id, i = e.tenantId, a = e.providerCustomerId;
|
|
95
|
+
if (!r(n) || !r(i) || !r(a) || typeof e.identityKey != "string" || !/^[0-9a-f]{64}$/.test(e.identityKey)) throw b();
|
|
96
|
+
let o = e.retiredAt === null ? null : j(e.retiredAt);
|
|
97
|
+
if (j(e.observedAt), e.slot !== (o === null ? "current" : n)) throw b();
|
|
98
|
+
return {
|
|
99
|
+
...E({
|
|
100
|
+
...t,
|
|
101
|
+
tenantId: i,
|
|
102
|
+
providerCustomerId: a
|
|
103
|
+
}),
|
|
104
|
+
id: n,
|
|
105
|
+
retiredAt: o
|
|
106
|
+
};
|
|
107
|
+
}, N = (e, t) => {
|
|
108
|
+
if (T(e, t), e.tenantId !== t.tenantId || e.providerCustomerId !== t.providerCustomerId) throw b();
|
|
109
|
+
}, P = (e, t, n) => ({
|
|
110
|
+
id: x(_, "bcustody"),
|
|
111
|
+
tenantId: e.tenantId,
|
|
112
|
+
provider: e.provider,
|
|
113
|
+
providerAccountId: e.account.providerAccountId,
|
|
114
|
+
livemode: +!!e.account.livemode,
|
|
115
|
+
providerCustomerId: e.providerCustomerId,
|
|
116
|
+
identityKey: t,
|
|
117
|
+
slot: "current",
|
|
118
|
+
observedAt: n.observedAt,
|
|
119
|
+
retiredAt: null
|
|
120
|
+
}), F = a, I = (e) => {
|
|
121
|
+
let t = async (t) => {
|
|
122
|
+
let n = await e.query(O(g, c("tenantId", C(t)), 2));
|
|
123
|
+
if (n.length > 1 || n[0] && (n[0].tenantId !== t || !r(n[0].id))) throw b();
|
|
124
|
+
return n[0] ? A(n[0]) : null;
|
|
125
|
+
}, a = async (t) => {
|
|
126
|
+
for (let n of F) if ((await e.query(O(n, c("tenantId", t), 1))).length) throw b();
|
|
127
|
+
}, o = async (n, r) => {
|
|
128
|
+
let i = await t(n);
|
|
129
|
+
if (i !== null) {
|
|
130
|
+
T(i, r);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await a(n), await e.insertIgnore(g, {
|
|
134
|
+
id: x(g, "bcacct"),
|
|
135
|
+
tenantId: n,
|
|
136
|
+
provider: r.provider,
|
|
137
|
+
providerAccountId: r.account.providerAccountId,
|
|
138
|
+
livemode: +!!r.account.livemode
|
|
139
|
+
}, { conflictColumns: ["tenantId"] });
|
|
140
|
+
let o = await t(n);
|
|
141
|
+
if (o === null) throw b();
|
|
142
|
+
T(o, r);
|
|
143
|
+
}, l = async (e) => {
|
|
144
|
+
if (e.length > 1) throw b();
|
|
145
|
+
if (!e[0]) return null;
|
|
146
|
+
let n = M(e[0]);
|
|
147
|
+
if (await D(n) !== e[0].identityKey) throw b();
|
|
148
|
+
let r = await t(n.tenantId);
|
|
149
|
+
if (r === null) throw b();
|
|
150
|
+
return T(r, n), n;
|
|
151
|
+
}, d = async (t) => {
|
|
152
|
+
let n = await D(t), r = await l(await e.query(O(_, c("identityKey", n), 2)));
|
|
153
|
+
if (r !== null && await D(r) !== n || r === null && (await e.query(O("_voltro_billing_customers", s(c("provider", t.provider), c("providerCustomerId", t.providerCustomerId)), 1))).length) throw b();
|
|
154
|
+
return r;
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
accountForTenant: (e) => k(() => t(e)),
|
|
158
|
+
assertAccount: (e, n) => k(async () => {
|
|
159
|
+
let r = S(n), i = C(e), o = await t(i);
|
|
160
|
+
return o === null ? (await a(i), !1) : (T(o, r), !0);
|
|
161
|
+
}),
|
|
162
|
+
reserveAccount: (e, t) => k(async () => {
|
|
163
|
+
let n = S(t);
|
|
164
|
+
await o(C(e), n);
|
|
165
|
+
}),
|
|
166
|
+
bind: (t, r) => k(async () => {
|
|
167
|
+
let a = E(t), s = n(r);
|
|
168
|
+
if (s.deleted || s.providerCustomerId !== a.providerCustomerId || !i(s.account, a.account)) throw b();
|
|
169
|
+
let c = await d(a);
|
|
170
|
+
if (c !== null) {
|
|
171
|
+
if (N(c, a), c.retiredAt !== null) throw b();
|
|
172
|
+
return c;
|
|
173
|
+
}
|
|
174
|
+
if (await f(a.tenantId) !== null) throw b();
|
|
175
|
+
await o(a.tenantId, a);
|
|
176
|
+
let l = P(a, await D(a), s);
|
|
177
|
+
await e.insertIgnore(_, l, { conflictColumns: ["identityKey"] });
|
|
178
|
+
let u = await d(a);
|
|
179
|
+
if (u === null || u.retiredAt !== null) throw b();
|
|
180
|
+
N(u, a);
|
|
181
|
+
let p = await f(a.tenantId);
|
|
182
|
+
if (p === null || p.id !== u.id) throw b();
|
|
183
|
+
return u;
|
|
184
|
+
}),
|
|
185
|
+
getByTenant: (e) => k(() => f(e)),
|
|
186
|
+
getByProviderCustomerId: (e, t) => k(async () => {
|
|
187
|
+
let n = S(t);
|
|
188
|
+
if (!r(e)) throw b();
|
|
189
|
+
return d({
|
|
190
|
+
...n,
|
|
191
|
+
tenantId: "lookup",
|
|
192
|
+
providerCustomerId: e
|
|
193
|
+
});
|
|
194
|
+
}),
|
|
195
|
+
inventory: (t, n) => k(async () => {
|
|
196
|
+
let i = S(t), a = n?.limit ?? 25, o = n?.afterCustodyId ?? null;
|
|
197
|
+
if (n?.afterCustodyId !== void 0 && typeof n.afterCustodyId != "string" || !Number.isInteger(a) || a < 1 || a > 100 || o !== null && (!r(o) || o.length > 191 || !o.isWellFormed()) || (await e.query(O("_voltro_billing_customers", void 0, 1))).length) throw b();
|
|
198
|
+
let d = s(c("provider", i.provider), c("providerAccountId", i.account.providerAccountId), c("livemode", +!!i.account.livemode));
|
|
199
|
+
if (o !== null) {
|
|
200
|
+
let t = await l(await e.query(O(_, s(d, c("id", o)), 2)));
|
|
201
|
+
if (t === null || t.id !== o) throw b();
|
|
202
|
+
T(t, i);
|
|
203
|
+
}
|
|
204
|
+
let f = await e.query({
|
|
205
|
+
...O(_, o === null ? d : s(d, u("id", o)), a + 1),
|
|
206
|
+
order: [{
|
|
207
|
+
column: "id",
|
|
208
|
+
direction: "asc"
|
|
209
|
+
}]
|
|
210
|
+
});
|
|
211
|
+
if (f.length > a + 1) throw b();
|
|
212
|
+
let p = [], m = o;
|
|
213
|
+
for (let e of f) {
|
|
214
|
+
let t = await l([e]);
|
|
215
|
+
if (t === null || m !== null && t.id <= m) throw b();
|
|
216
|
+
T(t, i), p.push(t), m = t.id;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
customers: p.slice(0, a),
|
|
220
|
+
nextCustodyId: p.length > a ? p[a - 1].id : null
|
|
221
|
+
};
|
|
222
|
+
}),
|
|
223
|
+
list: (t) => k(async () => {
|
|
224
|
+
let n = S(t);
|
|
225
|
+
if ((await e.query(O("_voltro_billing_customers", void 0, 1))).length) throw b();
|
|
226
|
+
let r = await e.query(O(_, s(c("provider", n.provider), c("providerAccountId", n.account.providerAccountId), c("livemode", +!!n.account.livemode), c("slot", "current")))), i = [];
|
|
227
|
+
for (let e of r) {
|
|
228
|
+
let t = await l([e]);
|
|
229
|
+
t && i.push(t);
|
|
230
|
+
}
|
|
231
|
+
return i;
|
|
232
|
+
}),
|
|
233
|
+
retire: (t, r) => k(async () => {
|
|
234
|
+
let a = S(t), o = n(r);
|
|
235
|
+
if (!o.deleted || !i(a.account, o.account)) throw b();
|
|
236
|
+
let l = {
|
|
237
|
+
...a,
|
|
238
|
+
tenantId: "lookup",
|
|
239
|
+
providerCustomerId: o.providerCustomerId
|
|
240
|
+
}, u = await d(l);
|
|
241
|
+
if (u === null || u.retiredAt !== null) return u;
|
|
242
|
+
let f = await D(u);
|
|
243
|
+
await e.updateMany(_, {
|
|
244
|
+
slot: u.id,
|
|
245
|
+
retiredAt: o.observedAt
|
|
246
|
+
}, { where: s(c("id", u.id), c("identityKey", f), c("slot", "current"), c("tenantId", u.tenantId), c("provider", a.provider), c("providerAccountId", a.account.providerAccountId), c("livemode", +!!a.account.livemode), c("providerCustomerId", o.providerCustomerId)) });
|
|
247
|
+
let p = await d(l);
|
|
248
|
+
if (p === null || p.id !== u.id || p.retiredAt === null) throw b();
|
|
249
|
+
return p;
|
|
250
|
+
})
|
|
251
|
+
};
|
|
252
|
+
async function f(n) {
|
|
253
|
+
let r = C(n), i = await l(await e.query(O(_, s(c("tenantId", r), c("slot", "current")), 2)));
|
|
254
|
+
if (i !== null && (i.tenantId !== r || i.retiredAt !== null)) throw b();
|
|
255
|
+
return i === null && await t(r) === null && await a(r), i;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
export { g as BILLING_CUSTOMER_ACCOUNTS_TABLE, _ as BILLING_CUSTOMER_CUSTODY_TABLE, v as billingCustomerAccountsTable, y as billingCustomerCustodyTable, I as dataStoreCustomerStore };
|
package/dist/dunning.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BillingError as e, DEFAULT_PLAN as t } from "./types.js";
|
|
2
2
|
import { Effect as n } from "effect";
|
|
3
3
|
import { and as r, eq as i, id as a, table as o, text as s, timestamp as c } from "@voltro/database";
|
|
4
|
-
|
|
4
|
+
// src/dunning.ts
|
|
5
5
|
var l = "_voltro_billing_dunning_notices", u = o(l, {
|
|
6
6
|
id: a({ prefix: "bdun" }),
|
|
7
7
|
tenantId: s().maxLength(191),
|
|
@@ -205,5 +205,4 @@ var l = "_voltro_billing_dunning_notices", u = o(l, {
|
|
|
205
205
|
})
|
|
206
206
|
})
|
|
207
207
|
});
|
|
208
|
-
//#endregion
|
|
209
208
|
export { l as BILLING_DUNNING_NOTICES_TABLE, f as DEFAULT_DUNNING_STEPS, p as DEFAULT_GRACE_HOURS, m as DEFAULT_LOCKOUT, d as DUNNING_LOCK_STEP_ID, u as billingDunningNoticesTable, O as dataStoreDunningNoticeStore, C as defaultDunningEmail, b as dueSteps, w as dunningMailNotifier, v as episodeKeyOf, x as evaluateBillingEntitlement, y as graceEndOf, E as memoryDunningNoticeStore, g as resolveDunningConfig };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IapProviderDefinition } from '@voltro/plugin-economy/server';
|
|
2
|
+
|
|
3
|
+
export declare interface StripeIapWebOptions {
|
|
4
|
+
/** Stripe secret or restricted key; `sk_test_`/`rk_test_` for sandbox, `_live_` for production. */
|
|
5
|
+
readonly secretKey: string;
|
|
6
|
+
readonly environment: 'sandbox' | 'production';
|
|
7
|
+
/** The economy's application ID for this provider (an identifier you choose, e.g. the app name). */
|
|
8
|
+
readonly applicationId: string;
|
|
9
|
+
/** Origins players return to after paying (`https://game.example`). */
|
|
10
|
+
readonly returnOrigins: readonly string[];
|
|
11
|
+
/** Minutes a started checkout stays open (Stripe allows 30–1440; default 30). */
|
|
12
|
+
readonly checkoutTtlMinutes?: number;
|
|
13
|
+
/** Signing secret of a Stripe webhook endpoint (`whsec_…`) pointed at the economy's notification
|
|
14
|
+
* route (`/_voltro/economy/iap/web`). Set: refunds and lost disputes revoke the purchase
|
|
15
|
+
* (`charge.refunded`, `charge.dispute.created`, `charge.dispute.closed`), and paid checkouts and
|
|
16
|
+
* subscription renewals arrive without the player (`checkout.session.completed`, `invoice.paid`). */
|
|
17
|
+
readonly webhookSecret?: string;
|
|
18
|
+
/** Transport for tests; defaults to global `fetch`. */
|
|
19
|
+
readonly fetch?: typeof fetch;
|
|
20
|
+
/** Clock for webhook timestamps (tests). */
|
|
21
|
+
readonly now?: () => number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A Stripe Checkout provider for the economy's `web` channel. */
|
|
25
|
+
export declare const stripeIapWebProvider: (options: StripeIapWebOptions) => IapProviderDefinition;
|
|
26
|
+
|
|
27
|
+
/** The request ID a renewal invoice is redeemed under (economy identifiers have no `_`). */
|
|
28
|
+
export declare const stripeRenewalRequestId: (invoice: string) => string;
|
|
29
|
+
|
|
30
|
+
export { }
|
package/dist/economy.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { StripeRestError as e, stripeRest as t } from "./stripeRest.js";
|
|
2
|
+
import { Effect as n } from "effect";
|
|
3
|
+
import { createHmac as r, timingSafeEqual as i } from "node:crypto";
|
|
4
|
+
import { EconomyError as a, economyError as o } from "@voltro/plugin-economy/errors";
|
|
5
|
+
import { iapCurrentStatePort as s, iapVerificationPort as c, issueVerifiedIapNotification as l } from "@voltro/plugin-economy/server";
|
|
6
|
+
// src/economy.ts
|
|
7
|
+
var u = /* @__PURE__ */ new Set([
|
|
8
|
+
"BIF",
|
|
9
|
+
"CLP",
|
|
10
|
+
"DJF",
|
|
11
|
+
"GNF",
|
|
12
|
+
"JPY",
|
|
13
|
+
"KMF",
|
|
14
|
+
"KRW",
|
|
15
|
+
"MGA",
|
|
16
|
+
"PYG",
|
|
17
|
+
"RWF",
|
|
18
|
+
"UGX",
|
|
19
|
+
"VND",
|
|
20
|
+
"VUV",
|
|
21
|
+
"XAF",
|
|
22
|
+
"XOF",
|
|
23
|
+
"XPF"
|
|
24
|
+
]), d = /* @__PURE__ */ new Set([
|
|
25
|
+
"BHD",
|
|
26
|
+
"JOD",
|
|
27
|
+
"KWD",
|
|
28
|
+
"OMR",
|
|
29
|
+
"TND"
|
|
30
|
+
]), f = (e) => u.has(e) ? 0 : d.has(e) ? 3 : 2, p = /^cs_(test|live)_[A-Za-z0-9]{1,200}$/, m = /^in_[A-Za-z0-9]{1,200}$/, h = /^pi_[A-Za-z0-9]{1,200}$/, g = /^sub_[A-Za-z0-9]{1,200}$/, _ = /^cus_[A-Za-z0-9]{1,200}$/, v = (e) => "renew." + e.replace("_", "."), y = (t) => t instanceof a ? t : o(t instanceof e && t.status >= 400 && t.status < 500 && t.status !== 429 ? "forbidden" : "unavailable"), b = (e) => {
|
|
31
|
+
let u = /^(sk|rk)_live_/.test(e.secretKey), d = /^(sk|rk)_test_/.test(e.secretKey);
|
|
32
|
+
if (!u && !d || u !== (e.environment === "production")) throw o("invalid_input");
|
|
33
|
+
let b = e.checkoutTtlMinutes ?? 30;
|
|
34
|
+
if (!Number.isInteger(b) || b < 30 || b > 1440) throw o("invalid_input");
|
|
35
|
+
let x = t(e.secretKey, e.fetch ?? fetch), S = (e) => n.tryPromise({
|
|
36
|
+
try: e,
|
|
37
|
+
catch: y
|
|
38
|
+
}), C = () => S(() => x.get("/v1/checkout/sessions", { limit: 1 })).pipe(n.asVoid), w = (t, n, r) => t.voltro_account === n && t.voltro_application === e.applicationId && t.voltro_offer === r.offer.id && t.voltro_offer_version === r.offer.version, T = (e) => n.tryPromise({
|
|
39
|
+
try: e,
|
|
40
|
+
catch: (e) => e instanceof a ? e : o("unavailable")
|
|
41
|
+
}), E = (t) => ({
|
|
42
|
+
initialize: C,
|
|
43
|
+
start: (t) => T(async () => {
|
|
44
|
+
let n = new URL(t.returnUrl);
|
|
45
|
+
n.searchParams.set("checkout", "success");
|
|
46
|
+
let r = new URL(t.returnUrl);
|
|
47
|
+
r.searchParams.set("checkout", "cancel");
|
|
48
|
+
let i = {
|
|
49
|
+
voltro_account: t.accountToken,
|
|
50
|
+
voltro_offer: t.product.offer.id,
|
|
51
|
+
voltro_offer_version: t.product.offer.version,
|
|
52
|
+
voltro_application: e.applicationId
|
|
53
|
+
}, a = t.product.kind === "subscription", s = await x.post("/v1/checkout/sessions", {
|
|
54
|
+
mode: a ? "subscription" : "payment",
|
|
55
|
+
line_items: [{
|
|
56
|
+
price: t.product.storeProductId,
|
|
57
|
+
quantity: 1
|
|
58
|
+
}],
|
|
59
|
+
success_url: n.toString() + "&checkout_id={CHECKOUT_SESSION_ID}",
|
|
60
|
+
cancel_url: r.toString(),
|
|
61
|
+
client_reference_id: t.accountToken,
|
|
62
|
+
expires_at: Math.floor(t.at / 1e3) + b * 60,
|
|
63
|
+
metadata: {
|
|
64
|
+
...i,
|
|
65
|
+
voltro_request: t.requestId
|
|
66
|
+
},
|
|
67
|
+
...a ? { subscription_data: { metadata: i } } : {}
|
|
68
|
+
});
|
|
69
|
+
if (!s.url || !p.test(s.id)) throw o("invalid_record");
|
|
70
|
+
return {
|
|
71
|
+
checkoutId: s.id,
|
|
72
|
+
redirectUrl: s.url,
|
|
73
|
+
expiresAt: s.expires_at * 1e3
|
|
74
|
+
};
|
|
75
|
+
})
|
|
76
|
+
}), D = async (t, n, r, i) => {
|
|
77
|
+
let a = await x.get(`/v1/invoices/${t}`, { expand: ["payments"] }), s = a.parent?.subscription_details, c = s?.subscription ?? null, l = a.lines.data.filter((e) => e.parent?.subscription_item_details && !e.parent.subscription_item_details.proration && e.pricing?.price_details?.price === i.storeProductId), d = (a.payments?.data ?? []).find((e) => e.status === "paid" && e.payment.type === "payment_intent")?.payment.payment_intent;
|
|
78
|
+
if (a.id !== t || a.status !== "paid" || a.livemode !== u || c === null || !g.test(c) || n !== null && c !== n || !w(s?.metadata ?? {}, r, i) || l.length !== 1 || l[0].quantity !== 1 || !(a.amount_paid > 0) || !d || !h.test(d)) throw o("forbidden");
|
|
79
|
+
let p = l[0], m = a.currency.toUpperCase();
|
|
80
|
+
return {
|
|
81
|
+
invoice: a,
|
|
82
|
+
facts: {
|
|
83
|
+
channel: "web",
|
|
84
|
+
environment: e.environment,
|
|
85
|
+
applicationId: e.applicationId,
|
|
86
|
+
transactionId: d,
|
|
87
|
+
originalTransactionId: c,
|
|
88
|
+
storeProductId: i.storeProductId,
|
|
89
|
+
kind: "subscription",
|
|
90
|
+
quantity: 1,
|
|
91
|
+
purchasedAt: p.period.start * 1e3,
|
|
92
|
+
expiresAt: p.period.end * 1e3,
|
|
93
|
+
paid: {
|
|
94
|
+
currency: m,
|
|
95
|
+
units: String(a.amount_paid),
|
|
96
|
+
scale: f(m)
|
|
97
|
+
},
|
|
98
|
+
acknowledgement: "none",
|
|
99
|
+
consumption: "none"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}, O = (t) => c(t, {
|
|
103
|
+
initialize: C,
|
|
104
|
+
verify: (t) => n.suspend(() => {
|
|
105
|
+
let r = t.intent.receipt, i = t.product.kind === "subscription";
|
|
106
|
+
return i && m.test(r) ? t.intent.requestId === v(r) ? S(async () => {
|
|
107
|
+
let { invoice: e, facts: n } = await D(r, null, t.accountToken, t.product);
|
|
108
|
+
if (e.billing_reason === "subscription_create") throw o("forbidden");
|
|
109
|
+
return n;
|
|
110
|
+
}) : n.fail(o("forbidden")) : p.test(r) ? S(async () => {
|
|
111
|
+
let n = await x.get(`/v1/checkout/sessions/${r}`, { expand: ["line_items"] }), a = n.metadata ?? {}, s = n.line_items?.data ?? [], c = s[0];
|
|
112
|
+
if (n.id !== r || n.mode !== (i ? "subscription" : "payment") || n.status !== "complete" || n.payment_status !== "paid" || n.livemode !== u || !w(a, t.accountToken, t.product) || a.voltro_request !== t.intent.requestId || s.length !== 1 || c?.price?.id !== t.product.storeProductId || !c.quantity || n.currency === null || n.amount_total === null) throw o("forbidden");
|
|
113
|
+
if (i) {
|
|
114
|
+
if (!n.invoice || !m.test(n.invoice) || !n.subscription) throw o("forbidden");
|
|
115
|
+
return (await D(n.invoice, n.subscription, t.accountToken, t.product)).facts;
|
|
116
|
+
}
|
|
117
|
+
let l = n.currency.toUpperCase();
|
|
118
|
+
return {
|
|
119
|
+
channel: "web",
|
|
120
|
+
environment: e.environment,
|
|
121
|
+
applicationId: e.applicationId,
|
|
122
|
+
transactionId: n.payment_intent ?? n.id,
|
|
123
|
+
originalTransactionId: null,
|
|
124
|
+
storeProductId: c.price.id,
|
|
125
|
+
kind: t.product.kind,
|
|
126
|
+
quantity: c.quantity,
|
|
127
|
+
purchasedAt: n.created * 1e3,
|
|
128
|
+
expiresAt: null,
|
|
129
|
+
paid: {
|
|
130
|
+
currency: l,
|
|
131
|
+
units: String(n.amount_total),
|
|
132
|
+
scale: f(l)
|
|
133
|
+
},
|
|
134
|
+
acknowledgement: "none",
|
|
135
|
+
consumption: "none"
|
|
136
|
+
};
|
|
137
|
+
}) : n.fail(o("forbidden"));
|
|
138
|
+
})
|
|
139
|
+
}), k = e.webhookSecret, A = e.now ?? Date.now;
|
|
140
|
+
if (k !== void 0 && !/^whsec_[A-Za-z0-9+/=_-]{8,}$/.test(k)) throw o("invalid_input");
|
|
141
|
+
let j = (e) => {
|
|
142
|
+
let t = e.headers?.["stripe-signature"], n = /* @__PURE__ */ new Map();
|
|
143
|
+
for (let e of (t ?? "").split(",")) {
|
|
144
|
+
let [t, r] = e.split("=");
|
|
145
|
+
t && r && n.set(t, [...n.get(t) ?? [], r]);
|
|
146
|
+
}
|
|
147
|
+
let a = Number(n.get("t")?.[0]), s = r("sha256", k).update(`${n.get("t")?.[0]}.${e.body}`).digest();
|
|
148
|
+
if (!(n.get("v1") ?? []).some((e) => /^[0-9a-f]{64}$/.test(e) && i(Buffer.from(e, "hex"), s)) || !Number.isSafeInteger(a) || Math.abs(A() / 1e3 - a) > 300) throw o("forbidden");
|
|
149
|
+
let c = JSON.parse(e.body);
|
|
150
|
+
if (typeof c.id != "string" || !/^evt_[A-Za-z0-9]{1,200}$/.test(c.id) || typeof c.type != "string" || typeof c.created != "number" || c.livemode !== u) throw o("invalid_record");
|
|
151
|
+
return {
|
|
152
|
+
id: c.id,
|
|
153
|
+
type: c.type,
|
|
154
|
+
created: c.created,
|
|
155
|
+
object: c.data?.object ?? {}
|
|
156
|
+
};
|
|
157
|
+
}, M = (e) => e instanceof a ? e : o("invalid_record"), N = (t) => ({
|
|
158
|
+
initialize: () => n.void,
|
|
159
|
+
verify: (r) => n.try({
|
|
160
|
+
try: () => {
|
|
161
|
+
let n = j(r), i = n.object, a = typeof i.payment_intent == "string" && h.test(i.payment_intent) ? i.payment_intent : null, o = a !== null && (n.type === "charge.refunded" || n.type === "charge.dispute.created" || n.type === "charge.dispute.closed");
|
|
162
|
+
return l({
|
|
163
|
+
channel: "web",
|
|
164
|
+
applicationId: t.applicationId,
|
|
165
|
+
environment: e.environment,
|
|
166
|
+
noticeId: n.id,
|
|
167
|
+
sourceType: "web." + n.type.replace(/[^a-z._]/g, "").slice(0, 100),
|
|
168
|
+
kind: o ? n.type === "charge.dispute.created" ? "review" : "voided" : "test",
|
|
169
|
+
occurredAt: Math.min(n.created * 1e3, A()),
|
|
170
|
+
transactionId: o ? a : null,
|
|
171
|
+
originalTransactionId: null,
|
|
172
|
+
storeProductId: null
|
|
173
|
+
}, { event: {
|
|
174
|
+
id: n.id,
|
|
175
|
+
type: n.type
|
|
176
|
+
} });
|
|
177
|
+
},
|
|
178
|
+
catch: M
|
|
179
|
+
})
|
|
180
|
+
}), P = (e) => typeof e == "string" ? e : "";
|
|
181
|
+
return {
|
|
182
|
+
channel: "web",
|
|
183
|
+
environment: e.environment,
|
|
184
|
+
applicationId: e.applicationId,
|
|
185
|
+
checkoutReturnOrigins: e.returnOrigins,
|
|
186
|
+
create: O,
|
|
187
|
+
createCheckout: E,
|
|
188
|
+
createManagement: (t) => ({
|
|
189
|
+
initialize: C,
|
|
190
|
+
open: (t) => S(async () => {
|
|
191
|
+
for (let n of t.originals) {
|
|
192
|
+
if (!g.test(n)) continue;
|
|
193
|
+
let r = await x.get(`/v1/subscriptions/${n}`), i = r.metadata ?? {};
|
|
194
|
+
if (r.id === n && r.livemode === u && _.test(r.customer) && i.voltro_account === t.accountToken && i.voltro_application === e.applicationId) return { url: (await x.post("/v1/billing_portal/sessions", {
|
|
195
|
+
customer: r.customer,
|
|
196
|
+
return_url: t.returnUrl
|
|
197
|
+
})).url };
|
|
198
|
+
}
|
|
199
|
+
throw o("not_found");
|
|
200
|
+
})
|
|
201
|
+
}),
|
|
202
|
+
...k === void 0 ? {} : {
|
|
203
|
+
createNotifications: N,
|
|
204
|
+
createCurrentState: (t) => s({
|
|
205
|
+
realMoney: !0,
|
|
206
|
+
channel: "web",
|
|
207
|
+
applicationId: t.applicationId,
|
|
208
|
+
environment: e.environment,
|
|
209
|
+
accounts: t.accounts,
|
|
210
|
+
now: t.now
|
|
211
|
+
}, {
|
|
212
|
+
initialize: C,
|
|
213
|
+
read: (e) => S(async () => {
|
|
214
|
+
let n = e.facts.transactionId;
|
|
215
|
+
if (!h.test(n)) throw o("forbidden");
|
|
216
|
+
let r = await x.get(`/v1/payment_intents/${n}`, { expand: ["latest_charge"] });
|
|
217
|
+
if (r.id !== n || r.livemode !== u || !r.latest_charge || !(r.amount_received > 0)) throw o("forbidden");
|
|
218
|
+
let i = r.latest_charge.dispute !== null && (await x.get(`/v1/disputes/${r.latest_charge.dispute}`)).status === "lost", a = r.amount_received, s = i ? a : Math.min(r.latest_charge.amount_refunded, a), c = (e, t) => t === 0 ? e : c(t, e % t), l = c(s, a) || 1, d = s / l, f = a / l;
|
|
219
|
+
f > 1e5 && (d = s === a ? 1 : Math.floor(s * 1e5 / a), f = s === a ? 1 : 1e5);
|
|
220
|
+
let p = t.now();
|
|
221
|
+
return {
|
|
222
|
+
orderId: e.orderId,
|
|
223
|
+
channel: "web",
|
|
224
|
+
state: d === 0 ? "active" : d === f ? "revoked" : "partially-refunded",
|
|
225
|
+
refundNumerator: d,
|
|
226
|
+
refundDenominator: f,
|
|
227
|
+
observedAt: p,
|
|
228
|
+
revokedAt: d === 0 ? null : p
|
|
229
|
+
};
|
|
230
|
+
})
|
|
231
|
+
}),
|
|
232
|
+
createDeliveries: (t) => ({
|
|
233
|
+
initialize: () => n.void,
|
|
234
|
+
read: (t) => n.try({
|
|
235
|
+
try: () => {
|
|
236
|
+
let n = j(t), r = n.object;
|
|
237
|
+
if (n.type === "checkout.session.completed" || n.type === "checkout.session.async_payment_succeeded") {
|
|
238
|
+
let t = r.metadata ?? {};
|
|
239
|
+
return r.payment_status !== "paid" || !p.test(P(r.id)) || t.voltro_application !== e.applicationId ? null : {
|
|
240
|
+
accountToken: P(t.voltro_account),
|
|
241
|
+
requestId: P(t.voltro_request),
|
|
242
|
+
offerId: P(t.voltro_offer),
|
|
243
|
+
offerVersion: P(t.voltro_offer_version),
|
|
244
|
+
receipt: P(r.id)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (n.type === "invoice.paid") {
|
|
248
|
+
let t = r.parent?.subscription_details?.metadata ?? {};
|
|
249
|
+
return r.billing_reason === "subscription_create" || !m.test(P(r.id)) || t.voltro_application !== e.applicationId ? null : {
|
|
250
|
+
accountToken: P(t.voltro_account),
|
|
251
|
+
requestId: v(P(r.id)),
|
|
252
|
+
offerId: P(t.voltro_offer),
|
|
253
|
+
offerVersion: P(t.voltro_offer_version),
|
|
254
|
+
receipt: P(r.id)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
},
|
|
259
|
+
catch: M
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
export { b as stripeIapWebProvider, v as stripeRenewalRequestId };
|
package/dist/entitlement.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BillingService as e, EntitlementExceeded as t, SubscriptionLocked as n } from "./types.js";
|
|
2
2
|
import { Effect as r } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/entitlement.ts
|
|
4
4
|
var i = (e, t, n, r) => ({
|
|
5
5
|
allowed: !Number.isFinite(t) || r <= 0 || n + r <= t,
|
|
6
6
|
entitlement: e,
|
|
@@ -37,5 +37,4 @@ var i = (e, t, n, r) => ({
|
|
|
37
37
|
cost: s
|
|
38
38
|
})) : r.flatMap(n.consumeEntitlement(c, o.entitlement, s), () => i);
|
|
39
39
|
};
|
|
40
|
-
//#endregion
|
|
41
40
|
export { c as buildEnforceInterceptor, i as evaluateEntitlement, s as requireEntitled, o as requireEntitlement };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { BillingError as e } from "./types.js";
|
|
2
|
+
import { captureProviderAccount as t, sameProviderAccount as n } from "./providerIdentity.js";
|
|
3
|
+
import { Effect as r, Schema as i } from "effect";
|
|
4
|
+
import { beginIdempotent as a, failIdempotent as o, finishIdempotent as s, holdIdempotentClaim as c, memoryIdempotencyStore as l, prepareIdempotentInput as u, systemSubject as d } from "@voltro/protocol";
|
|
5
|
+
// src/eventDelivery.ts
|
|
6
|
+
var f = 864e5, p = (t) => new e({
|
|
7
|
+
source: "billing.delivery",
|
|
8
|
+
message: t,
|
|
9
|
+
transient: !0
|
|
10
|
+
}), m = (m) => {
|
|
11
|
+
let h = l(), g, _ = d("billing.provider-delivery", []);
|
|
12
|
+
return (l) => r.tryPromise({
|
|
13
|
+
try: async () => {
|
|
14
|
+
let e = structuredClone(l);
|
|
15
|
+
if (e.rejected) throw p("The provider event could not be normalized and requires reconciliation.");
|
|
16
|
+
if (typeof e.id != "string" || !e.id || e.id.trim() !== e.id || e.createdAt !== null && (!(e.createdAt instanceof Date) || !Number.isFinite(e.createdAt.getTime()))) throw p("The verified provider delivery requires exact identity and time evidence.");
|
|
17
|
+
let d = m.receipts?.store() ?? h;
|
|
18
|
+
if (g === void 0) {
|
|
19
|
+
let e = m.provider.fetchAccountIdentity?.bind(m.provider);
|
|
20
|
+
if (e === void 0) throw p("Account-qualified provider delivery is unsupported.");
|
|
21
|
+
let i = t(await r.runPromise(e()));
|
|
22
|
+
if (g !== void 0 && !n(g, i)) throw p("The provider delivery account changed.");
|
|
23
|
+
g ??= i;
|
|
24
|
+
}
|
|
25
|
+
let v = g, y = JSON.stringify([
|
|
26
|
+
"voltro.billing-delivery.v2",
|
|
27
|
+
m.provider.name,
|
|
28
|
+
m.receipts?.scope ?? "standalone",
|
|
29
|
+
v.providerAccountId,
|
|
30
|
+
v.livemode
|
|
31
|
+
]), b = await u(i.Unknown, e, _), x = await a(d, y, e.id, b.fingerprint, f, Date.now());
|
|
32
|
+
if (x.kind === "replay") {
|
|
33
|
+
if (x.response.status !== 204 || x.response.body !== null) throw p("The retained delivery receipt is invalid.");
|
|
34
|
+
return "replayed";
|
|
35
|
+
}
|
|
36
|
+
if (x.kind !== "fresh") throw p("The provider delivery is busy or its retained input needs reconciliation.");
|
|
37
|
+
let S = c(d, x.claim);
|
|
38
|
+
try {
|
|
39
|
+
if (!await d.renew(x.claim, Date.now())) throw p("The provider delivery claim changed before processing.");
|
|
40
|
+
let t = e.event, n = e.providerCustomerId ?? (t !== null && "providerCustomerId" in t ? t.providerCustomerId : null);
|
|
41
|
+
if (t !== null && "providerCustomerId" in t && e.providerCustomerId !== null && t.providerCustomerId !== e.providerCustomerId) throw p("The provider delivery has inconsistent customer identities.");
|
|
42
|
+
let i = t?._tag === "invoicePaid" || t?._tag === "invoicePaymentFailed", a = n !== null && t?._tag !== "customerUnlinked" && !i, o = [];
|
|
43
|
+
if (t !== null && (!a || t._tag !== "subscriptionUpserted" && t._tag !== "subscriptionCanceled" && t._tag !== "customerLinked") && o.push(await r.runPromise(m.service.applyEvent(t))), a) {
|
|
44
|
+
let e = await r.runPromise(m.service.syncCustomer(n, t !== null && "tenantId" in t && typeof t.tenantId == "string" ? t.tenantId : void 0));
|
|
45
|
+
o.push(...e.events);
|
|
46
|
+
}
|
|
47
|
+
for (let t of o) {
|
|
48
|
+
if (!await d.renew(x.claim, Date.now())) throw p("The provider delivery claim changed before callback dispatch.");
|
|
49
|
+
let n = m.onEvent?.[t._tag];
|
|
50
|
+
n !== void 0 && await r.runPromise(n(t, Object.freeze({
|
|
51
|
+
provider: m.provider.name,
|
|
52
|
+
providerEventId: e.id,
|
|
53
|
+
receiptScope: y,
|
|
54
|
+
...v
|
|
55
|
+
})));
|
|
56
|
+
}
|
|
57
|
+
if (!await s(d, x.claim, {
|
|
58
|
+
status: 204,
|
|
59
|
+
body: null
|
|
60
|
+
}, Date.now())) throw p("Delivery processing finished without confirmed receipt ownership.");
|
|
61
|
+
return "processed";
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw await o(d, x.claim), e;
|
|
64
|
+
} finally {
|
|
65
|
+
S();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
catch: (t) => t instanceof e ? t : p("Provider delivery processing failed and remains retryable.")
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
export { m as makeBillingEventDelivery };
|
package/dist/flushClaim.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { id as e, table as t, text as n, timestamp as r } from "@voltro/database";
|
|
2
|
-
|
|
2
|
+
// src/flushClaim.ts
|
|
3
3
|
var i = "_voltro_billing_flush_claims", a = t(i, {
|
|
4
4
|
id: e({ scheme: "numeric" }),
|
|
5
5
|
windowKey: n(),
|
|
@@ -16,5 +16,4 @@ var i = "_voltro_billing_flush_claims", a = t(i, {
|
|
|
16
16
|
return !0;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
-
//#endregion
|
|
20
19
|
export { i as BILLING_FLUSH_CLAIMS_TABLE, a as billingFlushClaimsTable, s as claimFlushWindow, o as flushWindowKey };
|