@odla-ai/chapter 0.54.2 → 0.56.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/README.md +32 -0
- package/dist/{chunk-SPEL4OGE.js → chunk-IQZWCG42.js} +581 -362
- package/dist/chunk-IQZWCG42.js.map +1 -0
- package/dist/index.cjs +522 -266
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +518 -262
- package/dist/index.js.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +12 -1
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +1110 -616
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +22 -1
- package/dist/worker/index.d.ts +22 -1
- package/dist/worker/index.js +1103 -609
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-SPEL4OGE.js.map +0 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -133,10 +133,10 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch, source =
|
|
|
133
133
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
134
134
|
return user ? toRecord(user, source) : null;
|
|
135
135
|
}
|
|
136
|
-
async function clerkGetUser(secretKey,
|
|
137
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
136
|
+
async function clerkGetUser(secretKey, id7, fetchImpl = fetch, source = "public") {
|
|
137
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id7)}`, secretKey, fetchImpl).catch(() => null);
|
|
138
138
|
const record4 = data ? toRecord(data, source) : null;
|
|
139
|
-
return record4?.id ===
|
|
139
|
+
return record4?.id === id7 ? record4 : null;
|
|
140
140
|
}
|
|
141
141
|
async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
142
142
|
const out = [];
|
|
@@ -152,8 +152,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
|
152
152
|
}
|
|
153
153
|
return out;
|
|
154
154
|
}
|
|
155
|
-
async function clerkSetRole(secretKey,
|
|
156
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
155
|
+
async function clerkSetRole(secretKey, id7, role, fetchImpl = fetch, source = "public") {
|
|
156
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id7)}/metadata`, {
|
|
157
157
|
method: "PATCH",
|
|
158
158
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
159
159
|
redirect: "manual",
|
|
@@ -212,10 +212,10 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
212
212
|
const existing = await find();
|
|
213
213
|
if (existing) return { record: existing, created: false };
|
|
214
214
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
215
|
-
const
|
|
215
|
+
const id7 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
216
216
|
try {
|
|
217
217
|
const created = await (0, import_crm.createRecord)({ ...deps, db: deps.db }, {
|
|
218
|
-
id:
|
|
218
|
+
id: id7,
|
|
219
219
|
type: "person",
|
|
220
220
|
input: { ...input, email },
|
|
221
221
|
...options
|
|
@@ -668,7 +668,10 @@ function verifyInitialMembershipInvoice(intent, evidence) {
|
|
|
668
668
|
if (!intent.providerSubscriptionId || !intent.providerCustomerId) return { status: "retry" };
|
|
669
669
|
try {
|
|
670
670
|
const terms2 = intent.billingTerms ? subscriptionBillingTerms(intent.billingTerms) : void 0;
|
|
671
|
-
const
|
|
671
|
+
const quote = intent.paymentQuote;
|
|
672
|
+
const seatCents = quote?.namedSeat ? quote.lineItems.namedSeatCents : 0;
|
|
673
|
+
if (!Number.isSafeInteger(seatCents) || Number(seatCents) < 0 || quote?.namedSeat && quote.seatOffer?.amountCents !== seatCents) return { status: "mismatch" };
|
|
674
|
+
const due = subscriptionAmountDue({ priceCents: Number(intent.priceCents), billingTerms: terms2 }) + Number(seatCents);
|
|
672
675
|
if (!evidence || !/^in_[A-Za-z0-9]+$/.test(evidence.invoiceId ?? "") || evidence.subscriptionId !== intent.providerSubscriptionId || evidence.customerId !== intent.providerCustomerId || evidence.currency !== intent.currency || evidence.total !== due || evidence.amountPaid !== due || due <= 0 || evidence.status !== "paid") return { status: "mismatch" };
|
|
673
676
|
return { status: "verified", ...terms2?.initialTermEndsAt ? { renewalAt: terms2.initialTermEndsAt } : {} };
|
|
674
677
|
} catch {
|
|
@@ -1019,8 +1022,8 @@ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now =
|
|
|
1019
1022
|
return projection;
|
|
1020
1023
|
}
|
|
1021
1024
|
async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
1022
|
-
const
|
|
1023
|
-
const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id:
|
|
1025
|
+
const id7 = exceptionId(fact.eventId, "collision");
|
|
1026
|
+
const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id: id7 }, limit: 1 } } })).networkCommercialExceptions?.[0];
|
|
1024
1027
|
const exact = stored.digest === fact.digest || stored.digest === legacyDigest;
|
|
1025
1028
|
const canonical3 = !exact && sameNetworkCommercialFact(stored.fact, fact);
|
|
1026
1029
|
if (exact || canonical3) {
|
|
@@ -1041,7 +1044,7 @@ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
|
1041
1044
|
action: "canonical_replay",
|
|
1042
1045
|
reason: "a resend matched the retained canonical fact"
|
|
1043
1046
|
} },
|
|
1044
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
1047
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
1045
1048
|
status: "resolved",
|
|
1046
1049
|
resolvedDigest: stored.digest,
|
|
1047
1050
|
reviewedDigest: fact.digest,
|
|
@@ -1050,7 +1053,7 @@ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
|
1050
1053
|
updatedAt: now
|
|
1051
1054
|
} }
|
|
1052
1055
|
], { mutationId: `network-commercial-resolve:${fact.eventId}:${fact.digest}`, guards: [
|
|
1053
|
-
{ ns: "networkCommercialExceptions", id:
|
|
1056
|
+
{ ns: "networkCommercialExceptions", id: id7, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
1054
1057
|
] });
|
|
1055
1058
|
const projection2 = await rebuildNetworkCommercialProjection(db, sources, `network-commercial-resolve-rebuild:${fact.eventId}:${fact.digest}`, now, alerts);
|
|
1056
1059
|
return { status: "replayed", resolvedCollision: true, projection: projection2 };
|
|
@@ -1060,9 +1063,9 @@ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
|
1060
1063
|
}
|
|
1061
1064
|
const stale = RESOLUTION_ATTRS.filter((attr3) => exception !== void 0 && Object.hasOwn(exception, attr3));
|
|
1062
1065
|
await db.transact([
|
|
1063
|
-
...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id:
|
|
1064
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
1065
|
-
id:
|
|
1066
|
+
...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id: id7, attrs: stale }] : [],
|
|
1067
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
1068
|
+
id: id7,
|
|
1066
1069
|
eventId: fact.eventId,
|
|
1067
1070
|
kind: "collision",
|
|
1068
1071
|
status: "active",
|
|
@@ -1082,12 +1085,12 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
|
|
|
1082
1085
|
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
1083
1086
|
const existing = found.networkCommercialEvents?.[0];
|
|
1084
1087
|
if (existing) return collide(db, sources, existing, fact, now, legacyDigest, alerts);
|
|
1085
|
-
const
|
|
1088
|
+
const id7 = eventRowId(fact.eventId);
|
|
1086
1089
|
let lostRace = false;
|
|
1087
1090
|
let duplicate = false;
|
|
1088
1091
|
try {
|
|
1089
|
-
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id:
|
|
1090
|
-
id:
|
|
1092
|
+
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id: id7, attrs: {
|
|
1093
|
+
id: id7,
|
|
1091
1094
|
eventId: fact.eventId,
|
|
1092
1095
|
digest: fact.digest,
|
|
1093
1096
|
eventType: fact.eventType,
|
|
@@ -1097,7 +1100,7 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
|
|
|
1097
1100
|
receivedAt: now
|
|
1098
1101
|
} }], {
|
|
1099
1102
|
mutationId: `network-commercial-ingest:${fact.eventId}:${fact.digest}`,
|
|
1100
|
-
guards: [{ ns: "networkCommercialEvents", id:
|
|
1103
|
+
guards: [{ ns: "networkCommercialEvents", id: id7, exists: false }]
|
|
1101
1104
|
});
|
|
1102
1105
|
duplicate = write.duplicate;
|
|
1103
1106
|
} catch (error) {
|
|
@@ -1141,10 +1144,10 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1141
1144
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
1142
1145
|
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
1143
1146
|
if (!sameNetworkCommercialFact(stored.fact, fact)) throw new Error("Stripe facts differ; collision requires investigation");
|
|
1144
|
-
const
|
|
1147
|
+
const id7 = crypto.randomUUID();
|
|
1145
1148
|
await db.transact([
|
|
1146
|
-
{ t: "update", ns: "paymentRecoveryReviews", id:
|
|
1147
|
-
id:
|
|
1149
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: id7, attrs: {
|
|
1150
|
+
id: id7,
|
|
1148
1151
|
eventId,
|
|
1149
1152
|
actorUserId,
|
|
1150
1153
|
reviewedAt: now,
|
|
@@ -1159,9 +1162,9 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1159
1162
|
t: "update",
|
|
1160
1163
|
ns: "networkCommercialExceptions",
|
|
1161
1164
|
id: exceptionId2,
|
|
1162
|
-
attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId:
|
|
1165
|
+
attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId: id7, updatedAt: now }
|
|
1163
1166
|
}] : []
|
|
1164
|
-
], { mutationId: `payment-recovery:${
|
|
1167
|
+
], { mutationId: `payment-recovery:${id7}`, guards: [
|
|
1165
1168
|
{ ns: "networkCommercialEvents", id: eventRowId2, exists: true, equals: { digest: stored.digest } },
|
|
1166
1169
|
{
|
|
1167
1170
|
ns: "networkCommercialExceptions",
|
|
@@ -1170,8 +1173,8 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1170
1173
|
...exception ? { equals: { status: exception.status, updatedAt: exception.updatedAt } } : {}
|
|
1171
1174
|
}
|
|
1172
1175
|
] });
|
|
1173
|
-
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${
|
|
1174
|
-
return { recovered: true, eventId, reviewId:
|
|
1176
|
+
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id7}`, now, alerts);
|
|
1177
|
+
return { recovered: true, eventId, reviewId: id7 };
|
|
1175
1178
|
}
|
|
1176
1179
|
|
|
1177
1180
|
// src/payments.ts
|
|
@@ -1759,7 +1762,7 @@ function findTier(tiers, tierId) {
|
|
|
1759
1762
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
1760
1763
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
1761
1764
|
}
|
|
1762
|
-
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
1765
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map(), seats = /* @__PURE__ */ new Map()) {
|
|
1763
1766
|
return tiers.map((tier) => ({
|
|
1764
1767
|
id: tier.id,
|
|
1765
1768
|
name: tier.name,
|
|
@@ -1768,7 +1771,8 @@ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Ma
|
|
|
1768
1771
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
1769
1772
|
blurb: tier.blurb ?? "",
|
|
1770
1773
|
free: tierIsFree(tier),
|
|
1771
|
-
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
1774
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {},
|
|
1775
|
+
...seats.has(tier.id) ? { seatOffer: seats.get(tier.id) } : {}
|
|
1772
1776
|
}));
|
|
1773
1777
|
}
|
|
1774
1778
|
|
|
@@ -1826,7 +1830,7 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1826
1830
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
1827
1831
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
1828
1832
|
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
1829
|
-
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
1833
|
+
const amount = quote?.lineItems?.dueTodayCents ?? contract?.dueTodayCents ?? intent?.priceCents;
|
|
1830
1834
|
const currency = offering?.currency ?? intent?.currency;
|
|
1831
1835
|
let formatted = "";
|
|
1832
1836
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
@@ -1950,11 +1954,11 @@ async function sendTemplated(deps, input) {
|
|
|
1950
1954
|
const previous = prior[0];
|
|
1951
1955
|
if (previous && (previous.groupId !== input.group.id || previous.template !== input.template || previous.to !== decision.to || previous.transport !== decision.transport || previous.applicationId !== input.applicationId)) return { sent: false, reason: "delivery-identity-conflict" };
|
|
1952
1956
|
const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
|
|
1953
|
-
const
|
|
1957
|
+
const id7 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1954
1958
|
const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
|
|
1955
1959
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
|
|
1956
1960
|
const row2 = {
|
|
1957
|
-
id:
|
|
1961
|
+
id: id7,
|
|
1958
1962
|
groupId: input.group.id,
|
|
1959
1963
|
to: decision.to,
|
|
1960
1964
|
template: input.template,
|
|
@@ -1972,15 +1976,15 @@ async function sendTemplated(deps, input) {
|
|
|
1972
1976
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
1973
1977
|
if ((0, import_crm7.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
1974
1978
|
try {
|
|
1975
|
-
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1976
|
-
mutationId: `email-claim:${
|
|
1979
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
1980
|
+
mutationId: `email-claim:${id7}:${attempt}`,
|
|
1977
1981
|
guards: [previous ? {
|
|
1978
1982
|
ns: "emailLog",
|
|
1979
|
-
id:
|
|
1983
|
+
id: id7,
|
|
1980
1984
|
exists: true,
|
|
1981
1985
|
equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
|
|
1982
1986
|
...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
|
|
1983
|
-
} : { ns: "emailLog", id:
|
|
1987
|
+
} : { ns: "emailLog", id: id7, exists: false }]
|
|
1984
1988
|
});
|
|
1985
1989
|
if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
|
|
1986
1990
|
} catch (error) {
|
|
@@ -2000,16 +2004,16 @@ async function sendTemplated(deps, input) {
|
|
|
2000
2004
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
2001
2005
|
messageId = response.messageId;
|
|
2002
2006
|
} catch {
|
|
2003
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2004
|
-
guards: [{ ns: "emailLog", id:
|
|
2007
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
2008
|
+
guards: [{ ns: "emailLog", id: id7, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
2005
2009
|
});
|
|
2006
2010
|
return { sent: false, reason: "delivery-uncertain" };
|
|
2007
2011
|
}
|
|
2008
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2009
|
-
mutationId: `email-confirmed:${
|
|
2012
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
2013
|
+
mutationId: `email-confirmed:${id7}:${attempt}`,
|
|
2010
2014
|
guards: [{
|
|
2011
2015
|
ns: "emailLog",
|
|
2012
|
-
id:
|
|
2016
|
+
id: id7,
|
|
2013
2017
|
exists: true,
|
|
2014
2018
|
equals: { deliveryState: "sending", deliveryAttempt: attempt }
|
|
2015
2019
|
}]
|
|
@@ -2194,8 +2198,8 @@ async function hash(value) {
|
|
|
2194
2198
|
async function membershipDiscountCouponId(input, policyId) {
|
|
2195
2199
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
2196
2200
|
}
|
|
2197
|
-
async function load(db, ns,
|
|
2198
|
-
return (await db.query({ [ns]: { $: { where: { id:
|
|
2201
|
+
async function load(db, ns, id7) {
|
|
2202
|
+
return (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2199
2203
|
}
|
|
2200
2204
|
async function membershipOfferIdForRequest(input) {
|
|
2201
2205
|
return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
|
|
@@ -2261,6 +2265,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
2261
2265
|
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
2262
2266
|
dueTodayCents: contract.dueTodayCents,
|
|
2263
2267
|
renewalCents: contract.renewalCents,
|
|
2268
|
+
...offering.seatPriceCents !== void 0 ? { seatPriceCents: offering.seatPriceCents } : {},
|
|
2264
2269
|
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
2265
2270
|
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
2266
2271
|
};
|
|
@@ -2329,20 +2334,20 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2329
2334
|
|
|
2330
2335
|
// src/commercial-evidence-guard.ts
|
|
2331
2336
|
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
2332
|
-
const ns = "networkCommercialExceptions",
|
|
2333
|
-
const row2 = (await db.query({ [ns]: { $: { where: { id:
|
|
2334
|
-
if (!row2) return { ns, id:
|
|
2337
|
+
const ns = "networkCommercialExceptions", id7 = `network-commercial-exception:${eventId}:collision`;
|
|
2338
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2339
|
+
if (!row2) return { ns, id: id7, exists: false };
|
|
2335
2340
|
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
2336
2341
|
throw Object.assign(new Error(code), { code });
|
|
2337
2342
|
}
|
|
2338
|
-
return { ns, id:
|
|
2343
|
+
return { ns, id: id7, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
2339
2344
|
}
|
|
2340
2345
|
|
|
2341
2346
|
// src/membership-authority-state.ts
|
|
2342
2347
|
var membershipFail = (code) => {
|
|
2343
2348
|
throw Object.assign(new Error(code), { code });
|
|
2344
2349
|
};
|
|
2345
|
-
var membershipLoad = async (db, ns,
|
|
2350
|
+
var membershipLoad = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2346
2351
|
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
2347
2352
|
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
2348
2353
|
function requireMembershipScope(row2, input) {
|
|
@@ -2353,18 +2358,18 @@ async function loadMembershipAuthorityState(db, input) {
|
|
|
2353
2358
|
if (!row2 || row2.mode !== "authoritative" || row2.chapterId !== input.chapterId || row2.environment !== input.environment || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1) return membershipFail("membership_authority_unavailable");
|
|
2354
2359
|
return row2;
|
|
2355
2360
|
}
|
|
2356
|
-
var membershipUpdate = (ns,
|
|
2361
|
+
var membershipUpdate = (ns, id7, attrs) => ({ t: "update", ns, id: id7, attrs });
|
|
2357
2362
|
var membershipClock = (now) => {
|
|
2358
2363
|
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
2359
2364
|
};
|
|
2360
2365
|
|
|
2361
2366
|
// src/membership-restart-payment.ts
|
|
2362
2367
|
async function membershipRestartPayment(db, target, member, now) {
|
|
2363
|
-
const
|
|
2368
|
+
const id7 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id7);
|
|
2364
2369
|
requireMembershipScope(restart, target);
|
|
2365
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2370
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2366
2371
|
requireMembershipScope(entitlement, target);
|
|
2367
|
-
if (entitlement.membershipId !==
|
|
2372
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
2368
2373
|
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
2369
2374
|
return {
|
|
2370
2375
|
restartId: String(restart.restartId),
|
|
@@ -2381,49 +2386,117 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
2381
2386
|
graceEndsAt: 0
|
|
2382
2387
|
},
|
|
2383
2388
|
ops: [
|
|
2384
|
-
membershipUpdate("membershipRestart",
|
|
2385
|
-
{ t: "retract", ns: "networkMembership", id:
|
|
2389
|
+
membershipUpdate("membershipRestart", id7, { status: "complete" }),
|
|
2390
|
+
{ t: "retract", ns: "networkMembership", id: id7, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
2386
2391
|
],
|
|
2387
2392
|
guards: [
|
|
2388
|
-
{ ns: "networkMembership", id:
|
|
2389
|
-
{ ns: "membershipRestart", id:
|
|
2393
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
2394
|
+
{ ns: "membershipRestart", id: id7, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
2390
2395
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
2391
2396
|
]
|
|
2392
2397
|
};
|
|
2393
2398
|
}
|
|
2394
2399
|
|
|
2400
|
+
// src/checkout-named-seat.ts
|
|
2401
|
+
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
2402
|
+
const row2 = value;
|
|
2403
|
+
const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
2404
|
+
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
2405
|
+
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
2406
|
+
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(row2.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(purchaser) || email === purchaser) {
|
|
2407
|
+
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
2408
|
+
}
|
|
2409
|
+
return { recipientName: name, recipientEmail: email };
|
|
2410
|
+
}
|
|
2411
|
+
async function checkoutNamedSeatIdentity(target, email) {
|
|
2412
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
2413
|
+
target.appId,
|
|
2414
|
+
target.chapterId,
|
|
2415
|
+
target.environment,
|
|
2416
|
+
target.runtime,
|
|
2417
|
+
email
|
|
2418
|
+
])));
|
|
2419
|
+
return {
|
|
2420
|
+
seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
|
|
2421
|
+
membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
|
|
2422
|
+
recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2395
2426
|
// src/membership-checkout-binding.ts
|
|
2396
2427
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
2397
2428
|
var reject2 = (code) => {
|
|
2398
2429
|
throw Object.assign(new Error(code), { code });
|
|
2399
2430
|
};
|
|
2400
|
-
var load2 = async (db, ns,
|
|
2431
|
+
var load2 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2401
2432
|
async function membershipCheckoutBindingId(input) {
|
|
2402
2433
|
return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
|
|
2403
2434
|
}
|
|
2404
2435
|
async function verifyMembershipCheckoutBinding(value, input) {
|
|
2405
2436
|
const row2 = value;
|
|
2406
2437
|
if (!row2 || typeof row2 !== "object" || row2.id !== await membershipCheckoutBindingId(input) || keys.some((key3) => row2[key3] !== input[key3]) || !Number.isSafeInteger(row2.boundAt) || row2.boundAt < 0) return reject2("membership_checkout_conflict");
|
|
2407
|
-
|
|
2438
|
+
if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
|
|
2439
|
+
if (input.namedSeat) {
|
|
2440
|
+
const expected = {
|
|
2441
|
+
...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
|
|
2442
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
|
|
2443
|
+
};
|
|
2444
|
+
const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
|
|
2445
|
+
if (!row2.namedSeat || Object.entries({ ...expected, ...identity2 }).some(([key3, value2]) => row2.namedSeat[key3] !== value2) || !/^price_[A-Za-z0-9_]+$/.test(row2.namedSeat.priceId) || row2.namedSeat.priceId === input.priceId || !Number.isSafeInteger(row2.namedSeat.amountCents) || row2.namedSeat.amountCents < 0) return reject2("membership_checkout_conflict");
|
|
2446
|
+
}
|
|
2447
|
+
return {
|
|
2448
|
+
id: row2.id,
|
|
2449
|
+
...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
|
|
2450
|
+
boundAt: row2.boundAt,
|
|
2451
|
+
...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
|
|
2452
|
+
};
|
|
2408
2453
|
}
|
|
2409
|
-
async function bindMembershipCheckout(db, input, now) {
|
|
2454
|
+
async function bindMembershipCheckout(db, input, now, seatPriceId) {
|
|
2410
2455
|
if (!Number.isSafeInteger(now) || now < 0 || !/^[a-f0-9]{64}$/.test(input.offerDigest) || !/^[a-f0-9]{64}$/.test(input.quoteDigest) || !/^price_[A-Za-z0-9_]+$/.test(input.priceId)) return reject2("invalid_membership_checkout");
|
|
2411
2456
|
const stateId = `${input.chapterId}:${input.environment}`;
|
|
2412
2457
|
const state = await load2(db, "membershipAuthorityState", stateId);
|
|
2413
2458
|
if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1) return reject2("membership_authority_unavailable");
|
|
2414
|
-
const
|
|
2415
|
-
const existing = await load2(db, "membershipOfferCheckout",
|
|
2459
|
+
const id7 = await membershipCheckoutBindingId(input);
|
|
2460
|
+
const existing = await load2(db, "membershipOfferCheckout", id7);
|
|
2416
2461
|
if (existing) return verifyMembershipCheckoutBinding(existing, input);
|
|
2417
2462
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
2418
2463
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
2419
2464
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
2420
|
-
const restart = await load2(db, "membershipRestart",
|
|
2421
|
-
|
|
2465
|
+
const restart = await load2(db, "membershipRestart", id7.replace(/^checkout_/, "membership_"));
|
|
2466
|
+
let namedSeat;
|
|
2467
|
+
if (input.namedSeat) {
|
|
2468
|
+
if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
|
|
2469
|
+
const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
|
|
2470
|
+
namedSeat = {
|
|
2471
|
+
...selection,
|
|
2472
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
|
|
2473
|
+
...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
|
|
2474
|
+
priceId: seatPriceId,
|
|
2475
|
+
amountCents: offer.contract.offering.seatPriceCents
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
const receipt = { id: id7, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
|
|
2422
2479
|
try {
|
|
2423
|
-
await db.transact([
|
|
2424
|
-
|
|
2480
|
+
await db.transact([
|
|
2481
|
+
{ t: "update", ns: "membershipOfferCheckout", id: id7, attrs: receipt },
|
|
2482
|
+
...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
|
|
2483
|
+
id: namedSeat.recipientClaimId,
|
|
2484
|
+
appId: input.appId,
|
|
2485
|
+
chapterId: input.chapterId,
|
|
2486
|
+
environment: input.environment,
|
|
2487
|
+
runtime: input.runtime,
|
|
2488
|
+
seatId: namedSeat.seatId,
|
|
2489
|
+
recipientEmailNormalized: namedSeat.recipientEmail,
|
|
2490
|
+
createdAt: now
|
|
2491
|
+
} }] : []
|
|
2492
|
+
], {
|
|
2493
|
+
mutationId: `membership-checkout:${id7}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
2425
2494
|
guards: [
|
|
2426
|
-
{ ns: "membershipOfferCheckout", id:
|
|
2495
|
+
{ ns: "membershipOfferCheckout", id: id7, exists: false },
|
|
2496
|
+
...namedSeat ? [
|
|
2497
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2498
|
+
{ ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
|
|
2499
|
+
] : [],
|
|
2427
2500
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
2428
2501
|
{ ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2429
2502
|
]
|
|
@@ -2431,11 +2504,78 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
2431
2504
|
} catch (error) {
|
|
2432
2505
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2433
2506
|
}
|
|
2434
|
-
const saved = await load2(db, "membershipOfferCheckout",
|
|
2507
|
+
const saved = await load2(db, "membershipOfferCheckout", id7);
|
|
2435
2508
|
if (!saved) return reject2("membership_binding_unconfirmed");
|
|
2436
2509
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
2437
2510
|
}
|
|
2438
2511
|
|
|
2512
|
+
// src/membership-checkout-seat-payment.ts
|
|
2513
|
+
function checkoutSeatPaymentOperations(target, seat, input) {
|
|
2514
|
+
const { now, paidAt, endsAt } = input;
|
|
2515
|
+
const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
|
|
2516
|
+
const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
|
|
2517
|
+
return [
|
|
2518
|
+
membershipUpdate("networkSeat", seat.seatId, {
|
|
2519
|
+
id: seat.seatId,
|
|
2520
|
+
...membershipScope(target),
|
|
2521
|
+
primaryMembershipId: seat.membershipId,
|
|
2522
|
+
purchaserEmailNormalized: seat.purchaserEmail,
|
|
2523
|
+
recipientName: seat.recipientName,
|
|
2524
|
+
recipientEmailNormalized: seat.recipientEmail,
|
|
2525
|
+
recipientClaimId: seat.recipientClaimId,
|
|
2526
|
+
priceId: seat.priceId,
|
|
2527
|
+
primaryPriceId: input.primaryPriceId,
|
|
2528
|
+
amountCents: seat.amountCents,
|
|
2529
|
+
currency: input.offering.currency,
|
|
2530
|
+
interval: input.offering.interval,
|
|
2531
|
+
providerCustomerId: input.customerId,
|
|
2532
|
+
providerSubscriptionId: input.subscriptionId,
|
|
2533
|
+
combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
|
|
2534
|
+
status: included ? "pending_acceptance" : "renewal_pending",
|
|
2535
|
+
preparedAt: input.boundAt,
|
|
2536
|
+
purchasedAt: paidAt,
|
|
2537
|
+
termStartedAt: paidAt,
|
|
2538
|
+
termEndsAt: endsAt,
|
|
2539
|
+
acceptanceEndsAt: endsAt,
|
|
2540
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2541
|
+
offeringSnapshot: input.offering,
|
|
2542
|
+
paymentEventId: input.eventId,
|
|
2543
|
+
...included ? { entitlementId } : {},
|
|
2544
|
+
revision: 1,
|
|
2545
|
+
createdAt: now,
|
|
2546
|
+
updatedAt: now
|
|
2547
|
+
}),
|
|
2548
|
+
...included ? [membershipUpdate("networkEntitlement", entitlementId, {
|
|
2549
|
+
id: entitlementId,
|
|
2550
|
+
...membershipScope(target),
|
|
2551
|
+
membershipId: seat.membershipId,
|
|
2552
|
+
seatId: seat.seatId,
|
|
2553
|
+
kind: "named_seat",
|
|
2554
|
+
status: "pending_acceptance",
|
|
2555
|
+
termStartedAt: paidAt,
|
|
2556
|
+
termEndsAt: endsAt,
|
|
2557
|
+
offeringSnapshot: input.offering,
|
|
2558
|
+
sourceEventId: input.eventId,
|
|
2559
|
+
revision: 1,
|
|
2560
|
+
createdAt: now,
|
|
2561
|
+
updatedAt: now
|
|
2562
|
+
})] : [],
|
|
2563
|
+
membershipUpdate("membershipEffect", effectId, {
|
|
2564
|
+
id: effectId,
|
|
2565
|
+
chapterId: target.chapterId,
|
|
2566
|
+
environment: target.environment,
|
|
2567
|
+
runtime: target.runtime,
|
|
2568
|
+
membershipId: seat.membershipId,
|
|
2569
|
+
seatId: seat.seatId,
|
|
2570
|
+
kind: included ? "invite_named_recipient" : "attach_seat_renewal",
|
|
2571
|
+
status: "pending",
|
|
2572
|
+
attempts: 0,
|
|
2573
|
+
createdAt: now,
|
|
2574
|
+
updatedAt: now
|
|
2575
|
+
})
|
|
2576
|
+
];
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2439
2579
|
// src/membership-payment-authority.ts
|
|
2440
2580
|
async function networkMembershipId(input) {
|
|
2441
2581
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -2451,11 +2591,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2451
2591
|
{ ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
|
|
2452
2592
|
);
|
|
2453
2593
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
2594
|
+
const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
|
|
2595
|
+
if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
|
|
2596
|
+
const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
|
|
2454
2597
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
2455
2598
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
2456
2599
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
2457
2600
|
const fact = eventRow.fact;
|
|
2458
|
-
if (!fact || !fact.identity || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.eventType !== "invoice.payment_succeeded" || fact.objectKind !== "invoice" || fact.billingReason !== "subscription_create" || fact.livemode !== (input.environment === "prod") || fact.status !== "paid" || fact.applicationId !== input.applicationId || fact.subscriptionId !== input.providerSubscriptionId || fact.customerId !== input.providerCustomerId || !/^in_[A-Za-z0-9_]+$/.test(fact.invoiceId ?? "") || fact.currency !== offer.contract.offering.currency || fact.invoiceTotalCents !==
|
|
2601
|
+
if (!fact || !fact.identity || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.eventType !== "invoice.payment_succeeded" || fact.objectKind !== "invoice" || fact.billingReason !== "subscription_create" || fact.livemode !== (input.environment === "prod") || fact.status !== "paid" || fact.applicationId !== input.applicationId || fact.subscriptionId !== input.providerSubscriptionId || fact.customerId !== input.providerCustomerId || !/^in_[A-Za-z0-9_]+$/.test(fact.invoiceId ?? "") || fact.currency !== offer.contract.offering.currency || fact.invoiceTotalCents !== totalCents || fact.amountPaidCents !== totalCents || Object.entries({
|
|
2459
2602
|
appId: input.appId,
|
|
2460
2603
|
chapterId: input.chapterId,
|
|
2461
2604
|
environment: input.environment,
|
|
@@ -2467,14 +2610,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2467
2610
|
const paidAt = fact.invoicePaidAt;
|
|
2468
2611
|
const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
|
|
2469
2612
|
if (!Number.isSafeInteger(paidAt) || Number(paidAt) < Math.floor(Number(binding.boundAt) / 1e3) * 1e3 || Number(paidAt) > now + 3e4 || !Number.isSafeInteger(endsAt) || Number(endsAt) <= Number(paidAt)) membershipFail("membership_payment_period_mismatch");
|
|
2470
|
-
const
|
|
2471
|
-
let member = await membershipLoad(db, "networkMembership",
|
|
2613
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
2614
|
+
let member = await membershipLoad(db, "networkMembership", id7);
|
|
2472
2615
|
if (!member || member.status === "restarting") {
|
|
2473
2616
|
const offering = offer.contract.offering;
|
|
2474
2617
|
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
2475
2618
|
const attrs = {
|
|
2476
|
-
id:
|
|
2477
|
-
membershipId:
|
|
2619
|
+
id: id7,
|
|
2620
|
+
membershipId: id7,
|
|
2478
2621
|
...membershipScope(input),
|
|
2479
2622
|
baseSkuId: input.skuId,
|
|
2480
2623
|
status: "paid_pending_approval",
|
|
@@ -2490,8 +2633,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2490
2633
|
founding: offer.contract.founding,
|
|
2491
2634
|
cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
|
|
2492
2635
|
baseAmountCents: offering.basePriceCents,
|
|
2493
|
-
amountCents:
|
|
2636
|
+
amountCents: totalCents,
|
|
2494
2637
|
renewalAmountCents: offer.contract.renewalCents,
|
|
2638
|
+
...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
|
|
2495
2639
|
currency: offering.currency,
|
|
2496
2640
|
interval: offering.interval,
|
|
2497
2641
|
initialTermStartedAt: paidAt,
|
|
@@ -2508,11 +2652,11 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2508
2652
|
};
|
|
2509
2653
|
try {
|
|
2510
2654
|
await db.transact([
|
|
2511
|
-
membershipUpdate("networkMembership",
|
|
2655
|
+
membershipUpdate("networkMembership", id7, attrs),
|
|
2512
2656
|
membershipUpdate("networkEntitlement", entitlementId, {
|
|
2513
2657
|
id: entitlementId,
|
|
2514
2658
|
...membershipScope(input),
|
|
2515
|
-
membershipId:
|
|
2659
|
+
membershipId: id7,
|
|
2516
2660
|
kind: "primary",
|
|
2517
2661
|
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
2518
2662
|
termStartedAt: paidAt,
|
|
@@ -2524,23 +2668,45 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2524
2668
|
updatedAt: now,
|
|
2525
2669
|
...restart?.entitlementAttrs
|
|
2526
2670
|
}),
|
|
2527
|
-
...restart?.ops ?? []
|
|
2528
|
-
|
|
2529
|
-
|
|
2671
|
+
...restart?.ops ?? [],
|
|
2672
|
+
...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
|
|
2673
|
+
offering,
|
|
2674
|
+
primaryPriceId: String(binding.priceId),
|
|
2675
|
+
invoiceId: fact.invoiceId,
|
|
2676
|
+
invoiceTotalCents: totalCents,
|
|
2677
|
+
subscriptionId: input.providerSubscriptionId,
|
|
2678
|
+
customerId: input.providerCustomerId,
|
|
2679
|
+
eventId: input.eventId,
|
|
2680
|
+
paidAt: Number(paidAt),
|
|
2681
|
+
endsAt: Number(endsAt),
|
|
2682
|
+
boundAt: Number(binding.boundAt),
|
|
2683
|
+
now
|
|
2684
|
+
}) : []
|
|
2685
|
+
], { mutationId: `membership-paid:${id7}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
2686
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id7, exists: false }],
|
|
2687
|
+
...namedSeat ? [
|
|
2688
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2689
|
+
{
|
|
2690
|
+
ns: "networkSeatRecipient",
|
|
2691
|
+
id: namedSeat.recipientClaimId,
|
|
2692
|
+
exists: true,
|
|
2693
|
+
equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
|
|
2694
|
+
}
|
|
2695
|
+
] : [],
|
|
2530
2696
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
2531
|
-
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
2697
|
+
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
|
|
2532
2698
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
2533
2699
|
evidenceGuard
|
|
2534
2700
|
] });
|
|
2535
2701
|
} catch (error) {
|
|
2536
2702
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2537
2703
|
}
|
|
2538
|
-
member = await membershipLoad(db, "networkMembership",
|
|
2704
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
2539
2705
|
}
|
|
2540
2706
|
requireMembershipScope(member, input);
|
|
2541
2707
|
if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
|
|
2542
2708
|
return {
|
|
2543
|
-
membershipId:
|
|
2709
|
+
membershipId: id7,
|
|
2544
2710
|
status: String(member.status),
|
|
2545
2711
|
cohort: String(member.cohort),
|
|
2546
2712
|
initialTermStartedAt: Number(member.initialTermStartedAt),
|
|
@@ -2611,19 +2777,19 @@ async function stopPreviousSubscription(member, target, secret) {
|
|
|
2611
2777
|
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
2612
2778
|
membershipClock(now);
|
|
2613
2779
|
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
2614
|
-
const
|
|
2615
|
-
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([
|
|
2780
|
+
const id7 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
2781
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id7, input.previousSubscriptionId]))}`;
|
|
2616
2782
|
const state = await loadMembershipAuthorityState(db, input);
|
|
2617
|
-
const previous = await membershipLoad(db, "networkMembership",
|
|
2783
|
+
const previous = await membershipLoad(db, "networkMembership", id7);
|
|
2618
2784
|
requireMembershipScope(previous, input);
|
|
2619
2785
|
const receipt = {
|
|
2620
2786
|
applicationId: input.applicationId,
|
|
2621
|
-
membershipId:
|
|
2787
|
+
membershipId: id7,
|
|
2622
2788
|
restartId,
|
|
2623
2789
|
memberNumber: Number(previous.memberNumber),
|
|
2624
2790
|
status: "ready"
|
|
2625
2791
|
};
|
|
2626
|
-
const existing = await membershipLoad(db, "membershipRestart",
|
|
2792
|
+
const existing = await membershipLoad(db, "membershipRestart", id7);
|
|
2627
2793
|
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
2628
2794
|
requireMembershipScope(existing, input);
|
|
2629
2795
|
return receipt;
|
|
@@ -2639,33 +2805,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2639
2805
|
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
2640
2806
|
id: restartId,
|
|
2641
2807
|
...membershipScope(input),
|
|
2642
|
-
membershipId:
|
|
2808
|
+
membershipId: id7,
|
|
2643
2809
|
providerSubscriptionId: input.previousSubscriptionId,
|
|
2644
2810
|
memberSnapshot: previous,
|
|
2645
2811
|
bindingSnapshot: binding2,
|
|
2646
2812
|
archivedAt: now
|
|
2647
2813
|
}),
|
|
2648
|
-
membershipUpdate("networkMembership",
|
|
2814
|
+
membershipUpdate("networkMembership", id7, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
2649
2815
|
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
2650
2816
|
...seats.guards,
|
|
2651
|
-
{ ns: "networkMembership", id:
|
|
2817
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
2652
2818
|
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
2653
2819
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
2654
2820
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2655
2821
|
] });
|
|
2656
2822
|
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2657
|
-
const reserved = await membershipLoad(db, "networkMembership",
|
|
2823
|
+
const reserved = await membershipLoad(db, "networkMembership", id7);
|
|
2658
2824
|
requireMembershipScope(reserved, input);
|
|
2659
2825
|
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2660
2826
|
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
2661
2827
|
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
2662
2828
|
requireMembershipScope(binding, input);
|
|
2663
2829
|
await db.transact([
|
|
2664
|
-
membershipUpdate("membershipRestart",
|
|
2830
|
+
membershipUpdate("membershipRestart", id7, { id: id7, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
2665
2831
|
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
2666
2832
|
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
2667
|
-
{ ns: "membershipRestart", id:
|
|
2668
|
-
{ ns: "networkMembership", id:
|
|
2833
|
+
{ ns: "membershipRestart", id: id7, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
2834
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
2669
2835
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
2670
2836
|
] });
|
|
2671
2837
|
return receipt;
|
|
@@ -2675,15 +2841,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2675
2841
|
async function readMembershipAccess(db, input, now) {
|
|
2676
2842
|
membershipClock(now);
|
|
2677
2843
|
await loadMembershipAuthorityState(db, input);
|
|
2678
|
-
const
|
|
2679
|
-
if (!member) return { membershipId:
|
|
2844
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
2845
|
+
if (!member) return { membershipId: id7, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
|
|
2680
2846
|
requireMembershipScope(member, input);
|
|
2681
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2847
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2682
2848
|
requireMembershipScope(entitlement, input);
|
|
2683
2849
|
const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
2684
|
-
const allowed2 = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId ===
|
|
2850
|
+
const allowed2 = ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && member.journeyApprovedAt !== void 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && member.refundedCents === 0 && entitlement.status === "active" && entitlement.membershipId === id7 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
|
|
2685
2851
|
return {
|
|
2686
|
-
membershipId:
|
|
2852
|
+
membershipId: id7,
|
|
2687
2853
|
allowed: allowed2,
|
|
2688
2854
|
status: String(member.status),
|
|
2689
2855
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
@@ -2801,7 +2967,7 @@ async function refundChapterNetworkMembership(db, env, ctx, app) {
|
|
|
2801
2967
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2802
2968
|
const receipt = await ctx.membershipAuthority.refundMembership(target, env);
|
|
2803
2969
|
const offer = intent.membershipOffer;
|
|
2804
|
-
if (receipt.membershipId !== await networkMembershipId(target) || receipt.status !== "refunded" || receipt.subscriptionCanceled !== true || receipt.refundedCents !== offer.contract?.dueTodayCents) throw new Error("membership refund unconfirmed");
|
|
2970
|
+
if (receipt.membershipId !== await networkMembershipId(target) || receipt.status !== "refunded" || receipt.subscriptionCanceled !== true || receipt.refundedCents !== Number(offer.contract?.dueTodayCents) + Number(intent.paymentQuote?.lineItems?.namedSeatCents ?? 0)) throw new Error("membership refund unconfirmed");
|
|
2805
2971
|
return receipt;
|
|
2806
2972
|
}
|
|
2807
2973
|
async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
@@ -2852,14 +3018,14 @@ function facts(snapshots) {
|
|
|
2852
3018
|
["payment_intent.updated", snapshots.payments?.objects ?? []]
|
|
2853
3019
|
];
|
|
2854
3020
|
return groups.flatMap(([eventType, objects]) => objects.map((object2) => {
|
|
2855
|
-
const
|
|
3021
|
+
const id7 = typeof object2.id === "string" ? object2.id : "invalid";
|
|
2856
3022
|
return normalizeNetworkCommercialStripeEvent({
|
|
2857
|
-
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${
|
|
3023
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id7}`,
|
|
2858
3024
|
type: eventType,
|
|
2859
3025
|
created: typeof object2.created === "number" ? object2.created : 0,
|
|
2860
3026
|
livemode: object2.livemode === true,
|
|
2861
3027
|
data: { object: object2 }
|
|
2862
|
-
}, `stripe-snapshot:${eventType}:${
|
|
3028
|
+
}, `stripe-snapshot:${eventType}:${id7}`);
|
|
2863
3029
|
}));
|
|
2864
3030
|
}
|
|
2865
3031
|
var selected = (row2) => ({
|
|
@@ -2934,9 +3100,9 @@ async function chapterBillingSubscriptions(secretKey, chapterId, env) {
|
|
|
2934
3100
|
// src/approve-application.ts
|
|
2935
3101
|
async function approveApplication(input) {
|
|
2936
3102
|
const { db, env, ctx } = input;
|
|
2937
|
-
const
|
|
2938
|
-
if (!
|
|
2939
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
3103
|
+
const id7 = String(input.app.id ?? "");
|
|
3104
|
+
if (!id7) return { ok: false, status: 404, error: "not found" };
|
|
3105
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2940
3106
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) {
|
|
2941
3107
|
return { ok: false, status: 404, error: "not found" };
|
|
2942
3108
|
}
|
|
@@ -2970,10 +3136,10 @@ async function approveApplication(input) {
|
|
|
2970
3136
|
try {
|
|
2971
3137
|
if (!duplicate) {
|
|
2972
3138
|
const write = await db.transact(
|
|
2973
|
-
[{ t: "update", ns: "applications", id:
|
|
2974
|
-
{ mutationId: `approve:${
|
|
3139
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3140
|
+
{ mutationId: `approve:${id7}`, guards: [{
|
|
2975
3141
|
ns: "applications",
|
|
2976
|
-
id:
|
|
3142
|
+
id: id7,
|
|
2977
3143
|
exists: true,
|
|
2978
3144
|
equals: Object.fromEntries(guardFields.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
2979
3145
|
absent: guardFields.filter((key3) => app[key3] === void 0)
|
|
@@ -2985,7 +3151,7 @@ async function approveApplication(input) {
|
|
|
2985
3151
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2986
3152
|
return { ok: false, status: 409, error: "application changed; reload before approving" };
|
|
2987
3153
|
}
|
|
2988
|
-
const current = (await db.query({ applications: { $: { where: { id:
|
|
3154
|
+
const current = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2989
3155
|
if (!current || current.status !== "approved" || current.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => current[key3] !== app[key3])) {
|
|
2990
3156
|
return { ok: false, status: 409, error: "application changed; approval effects were not applied" };
|
|
2991
3157
|
}
|
|
@@ -2995,7 +3161,7 @@ async function approveApplication(input) {
|
|
|
2995
3161
|
} catch {
|
|
2996
3162
|
return { ok: false, status: 503, error: "network membership approval reconciliation pending" };
|
|
2997
3163
|
}
|
|
2998
|
-
const afterAuthority = (await db.query({ applications: { $: { where: { id:
|
|
3164
|
+
const afterAuthority = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2999
3165
|
if (!afterAuthority || afterAuthority.status !== "approved" || afterAuthority.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => afterAuthority[key3] !== app[key3])) {
|
|
3000
3166
|
return { ok: false, status: 409, error: "application changed during network approval" };
|
|
3001
3167
|
}
|
|
@@ -3008,7 +3174,7 @@ async function approveApplication(input) {
|
|
|
3008
3174
|
if (!userId && typeof app.email === "string") {
|
|
3009
3175
|
const found = await clerkGetUserByEmail(secret, app.email, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
3010
3176
|
userId = found?.id ?? null;
|
|
3011
|
-
if (userId) await db.transact([{ t: "update", ns: "applications", id:
|
|
3177
|
+
if (userId) await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { clerkUserId: userId } }]);
|
|
3012
3178
|
}
|
|
3013
3179
|
if (userId) {
|
|
3014
3180
|
const user = await clerkGetUser(secret, userId, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
@@ -3030,8 +3196,8 @@ async function approveApplication(input) {
|
|
|
3030
3196
|
template: send,
|
|
3031
3197
|
to: app.email,
|
|
3032
3198
|
vars: { firstName: String(app.firstName ?? ""), membersUrl: `${input.origin}/members/` },
|
|
3033
|
-
applicationId:
|
|
3034
|
-
dedupeKey: `approve:${
|
|
3199
|
+
applicationId: id7,
|
|
3200
|
+
dedupeKey: `approve:${id7}`
|
|
3035
3201
|
}
|
|
3036
3202
|
);
|
|
3037
3203
|
emailLogged = result.sent;
|
|
@@ -3039,10 +3205,10 @@ async function approveApplication(input) {
|
|
|
3039
3205
|
return { ok: false, status: 503, error: "approval notice reconciliation pending", rolePromoted, emailLogged };
|
|
3040
3206
|
}
|
|
3041
3207
|
}
|
|
3042
|
-
await db.transact([{ t: "update", ns: "applications", id:
|
|
3208
|
+
await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { approvalEffectsPending: false } }], {
|
|
3043
3209
|
guards: [{
|
|
3044
3210
|
ns: "applications",
|
|
3045
|
-
id:
|
|
3211
|
+
id: id7,
|
|
3046
3212
|
exists: true,
|
|
3047
3213
|
equals: { status: "approved", ...app.canceled !== void 0 ? { canceled: app.canceled } : {} },
|
|
3048
3214
|
...app.canceled === void 0 ? { absent: ["canceled"] } : {}
|
|
@@ -3071,6 +3237,48 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
3071
3237
|
}
|
|
3072
3238
|
}
|
|
3073
3239
|
|
|
3240
|
+
// src/payment-receipt.ts
|
|
3241
|
+
async function retainPaymentReceipt(db, intent, rawBody) {
|
|
3242
|
+
const quote = intent.paymentQuote;
|
|
3243
|
+
if (!quote || intent.paymentReceipt) return;
|
|
3244
|
+
const event2 = JSON.parse(rawBody), invoice = event2.data?.object;
|
|
3245
|
+
const paidAt = invoice?.status_transitions?.paid_at ?? event2.created;
|
|
3246
|
+
if (!/^in_[A-Za-z0-9_]+$/.test(invoice?.id ?? "") || !Number.isSafeInteger(paidAt) || paidAt <= 0) return;
|
|
3247
|
+
const receipt = {
|
|
3248
|
+
invoiceId: invoice.id,
|
|
3249
|
+
paidAt: paidAt * 1e3,
|
|
3250
|
+
currency: quote.currency,
|
|
3251
|
+
tierName: quote.tierName,
|
|
3252
|
+
merchant: quote.merchantDisclosureText,
|
|
3253
|
+
membershipCents: quote.lineItems.dueTodayCents - (quote.lineItems.namedSeatCents ?? 0),
|
|
3254
|
+
totalCents: quote.lineItems.dueTodayCents,
|
|
3255
|
+
...quote.namedSeat ? { namedSeat: { recipientName: quote.namedSeat.recipientName, amountCents: quote.lineItems.namedSeatCents } } : {}
|
|
3256
|
+
};
|
|
3257
|
+
await db.transact([{ t: "update", ns: "subscriptionCheckoutIntents", id: String(intent.id), attrs: { paymentReceipt: receipt } }], {
|
|
3258
|
+
mutationId: `payment-receipt:${invoice.id}`,
|
|
3259
|
+
guards: [{
|
|
3260
|
+
ns: "subscriptionCheckoutIntents",
|
|
3261
|
+
id: String(intent.id),
|
|
3262
|
+
exists: true,
|
|
3263
|
+
equals: { providerSubscriptionId: intent.providerSubscriptionId, providerCustomerId: intent.providerCustomerId },
|
|
3264
|
+
absent: ["paymentReceipt"]
|
|
3265
|
+
}]
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
function paymentReceiptText(receipt, chapterName) {
|
|
3269
|
+
const money = (cents) => new Intl.NumberFormat("en-US", { style: "currency", currency: receipt.currency }).format(cents / 100);
|
|
3270
|
+
return [
|
|
3271
|
+
chapterName,
|
|
3272
|
+
"Payment receipt",
|
|
3273
|
+
`Invoice: ${receipt.invoiceId}`,
|
|
3274
|
+
`Paid: ${new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeZone: "UTC" }).format(receipt.paidAt)}`,
|
|
3275
|
+
receipt.merchant,
|
|
3276
|
+
`${receipt.tierName}: ${money(receipt.membershipCents)}`,
|
|
3277
|
+
...receipt.namedSeat ? [`Additional member \u2014 ${receipt.namedSeat.recipientName}: ${receipt.namedSeat.amountCents === 0 ? "Included \u2014 " : ""}${money(receipt.namedSeat.amountCents)}`] : [],
|
|
3278
|
+
`Total paid: ${money(receipt.totalCents)}`
|
|
3279
|
+
].filter(Boolean).join("\n");
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3074
3282
|
// src/payment-notifications.ts
|
|
3075
3283
|
var groupFor = async (db, app) => {
|
|
3076
3284
|
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
@@ -3264,10 +3472,10 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3264
3472
|
tierId: target.tierId,
|
|
3265
3473
|
...typeof existing?.clerkUserId === "string" ? { clerkUserId: existing.clerkUserId } : {}
|
|
3266
3474
|
};
|
|
3267
|
-
const
|
|
3475
|
+
const id7 = existing ? String(existing.id) : `gift-application:${String(claim.id)}`;
|
|
3268
3476
|
const status2 = existing && existing.status !== "submitted" ? String(existing.status) : "paid_pending_vetting";
|
|
3269
|
-
ops.push({ t: "update", ns: "applications", id:
|
|
3270
|
-
id:
|
|
3477
|
+
ops.push({ t: "update", ns: "applications", id: id7, attrs: {
|
|
3478
|
+
id: id7,
|
|
3271
3479
|
groupId: chapterId,
|
|
3272
3480
|
email: beneficiary.email,
|
|
3273
3481
|
emailNormalized,
|
|
@@ -3278,12 +3486,12 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3278
3486
|
clerkUserId: beneficiary.userId,
|
|
3279
3487
|
...existing ? {} : { createdAt: now }
|
|
3280
3488
|
} });
|
|
3281
|
-
result = { kind: "application", id:
|
|
3489
|
+
result = { kind: "application", id: id7 };
|
|
3282
3490
|
} else {
|
|
3283
3491
|
if (beneficiary.applicationId) return null;
|
|
3284
|
-
const
|
|
3285
|
-
ops.push({ t: "update", ns: "giftEntitlements", id:
|
|
3286
|
-
id:
|
|
3492
|
+
const id7 = `gift-entitlement:${String(claim.id)}`;
|
|
3493
|
+
ops.push({ t: "update", ns: "giftEntitlements", id: id7, attrs: {
|
|
3494
|
+
id: id7,
|
|
3287
3495
|
claimId: String(claim.id),
|
|
3288
3496
|
orderId: String(claim.orderId),
|
|
3289
3497
|
chapterId,
|
|
@@ -3293,7 +3501,7 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3293
3501
|
active: true,
|
|
3294
3502
|
createdAt: now
|
|
3295
3503
|
} });
|
|
3296
|
-
result = { kind: "entitlement", id:
|
|
3504
|
+
result = { kind: "entitlement", id: id7 };
|
|
3297
3505
|
}
|
|
3298
3506
|
ops.unshift({ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
|
|
3299
3507
|
status: "redeemed",
|
|
@@ -3467,9 +3675,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
3467
3675
|
return "stored";
|
|
3468
3676
|
}
|
|
3469
3677
|
if (existing) return existing.digest === input.digest || existing.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3470
|
-
const
|
|
3471
|
-
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id:
|
|
3472
|
-
id:
|
|
3678
|
+
const id7 = `stripe-event:${input.eventId}`;
|
|
3679
|
+
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id: id7, attrs: {
|
|
3680
|
+
id: id7,
|
|
3473
3681
|
eventId: input.eventId,
|
|
3474
3682
|
digest: input.digest,
|
|
3475
3683
|
disposition: input.disposition,
|
|
@@ -3633,6 +3841,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3633
3841
|
} catch {
|
|
3634
3842
|
return json({ error: "network membership payment reconciliation pending" }, 503);
|
|
3635
3843
|
}
|
|
3844
|
+
if (intent) await retainPaymentReceipt(db, intent, rawBody);
|
|
3636
3845
|
}
|
|
3637
3846
|
}
|
|
3638
3847
|
const invoiceEvent = event2.kind === "first_payment" || event2.kind === "renewal" || event2.kind === "renewal_failed";
|
|
@@ -3701,9 +3910,9 @@ var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
|
|
|
3701
3910
|
var fail3 = (message, code) => Object.assign(new Error(message), { code });
|
|
3702
3911
|
async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
|
|
3703
3912
|
if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3704
|
-
const
|
|
3913
|
+
const id7 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
|
|
3705
3914
|
const found = await db.query({
|
|
3706
|
-
networkCommercialExceptions: { $: { where: { id:
|
|
3915
|
+
networkCommercialExceptions: { $: { where: { id: id7 }, limit: 1 } },
|
|
3707
3916
|
networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
|
|
3708
3917
|
});
|
|
3709
3918
|
const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
|
|
@@ -3726,7 +3935,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3726
3935
|
action: "resolve",
|
|
3727
3936
|
reason: input.reason
|
|
3728
3937
|
} },
|
|
3729
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
3938
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
3730
3939
|
status: "resolved",
|
|
3731
3940
|
resolvedDigest: stored.digest,
|
|
3732
3941
|
...dismissed ? { reviewedDigest: dismissed } : {},
|
|
@@ -3735,7 +3944,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3735
3944
|
updatedAt: now
|
|
3736
3945
|
} }
|
|
3737
3946
|
], { mutationId: input.mutationId, guards: [
|
|
3738
|
-
{ ns: "networkCommercialExceptions", id:
|
|
3947
|
+
{ ns: "networkCommercialExceptions", id: id7, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
3739
3948
|
] });
|
|
3740
3949
|
if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
|
|
3741
3950
|
const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
|
|
@@ -3850,10 +4059,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3850
4059
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3851
4060
|
const event2 = normalizeWebhookEvent(provider.event, { requireProviderPayment: Boolean(ctx.membershipAuthority) });
|
|
3852
4061
|
if (provider.event.livemode !== (env.ODLA_ENV === "prod") || classifyMemberStripeEvent(stripeEventIdentity(event2), target, false) !== "apply") return json({ error: "provider event belongs to another or unknown runtime" }, 409);
|
|
3853
|
-
const
|
|
4062
|
+
const id7 = crypto.randomUUID();
|
|
3854
4063
|
await db.transact([
|
|
3855
|
-
{ t: "update", ns: "paymentRecoveryReviews", id:
|
|
3856
|
-
id:
|
|
4064
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: id7, attrs: {
|
|
4065
|
+
id: id7,
|
|
3857
4066
|
eventId,
|
|
3858
4067
|
actorUserId: user.userId,
|
|
3859
4068
|
reviewedAt: Date.now(),
|
|
@@ -3862,8 +4071,8 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3862
4071
|
scope: `${target.appId}:${target.environment}:${target.runtime}`,
|
|
3863
4072
|
previousState: row2
|
|
3864
4073
|
} },
|
|
3865
|
-
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId:
|
|
3866
|
-
], { mutationId: `payment-recovery:${
|
|
4074
|
+
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId: id7 } }
|
|
4075
|
+
], { mutationId: `payment-recovery:${id7}`, guards: [{
|
|
3867
4076
|
ns: "stripeEventReceipts",
|
|
3868
4077
|
id: String(row2.id),
|
|
3869
4078
|
exists: true,
|
|
@@ -3932,43 +4141,121 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3932
4141
|
}
|
|
3933
4142
|
|
|
3934
4143
|
// src/membership-seat-refund-stripe.ts
|
|
4144
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4145
|
+
|
|
4146
|
+
// src/membership-checkout-seat-refund.ts
|
|
3935
4147
|
var import_stripe6 = require("@odla-ai/stripe");
|
|
3936
4148
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3937
4149
|
throw Object.assign(new Error(code), { code });
|
|
3938
4150
|
};
|
|
4151
|
+
var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
4152
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
4153
|
+
const invoiceRef = seat.combinedInvoice;
|
|
4154
|
+
if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
|
|
4155
|
+
const call = async (method, path, params, key3) => {
|
|
4156
|
+
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4157
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4158
|
+
return result.body;
|
|
4159
|
+
};
|
|
4160
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
4161
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4162
|
+
if (sub.id !== seat.providerSubscriptionId || id2(sub.customer) !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(sub.items?.data) || sub.items.has_more !== false) fail4();
|
|
4163
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
4164
|
+
if (invoice.id !== invoiceRef.invoiceId || id2(invoice.subscription ?? invoice.parent?.subscription_details?.subscription) !== sub.id || id2(invoice.customer) !== seat.providerCustomerId || invoice.status !== "paid" || invoice.billing_reason !== "subscription_create" || invoice.total !== invoiceRef.totalCents || invoice.amount_paid !== invoiceRef.totalCents || invoice.currency !== seat.currency || invoice.livemode !== (seat.environment === "prod")) fail4();
|
|
4165
|
+
let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
|
|
4166
|
+
if (!chargeId && !paymentId) {
|
|
4167
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
4168
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
4169
|
+
const payment = payments.data[0];
|
|
4170
|
+
if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
4171
|
+
paymentId = id2(payment.payment.payment_intent);
|
|
4172
|
+
}
|
|
4173
|
+
if (!chargeId) {
|
|
4174
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
4175
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
4176
|
+
if (payment.id !== paymentId || id2(payment.customer) !== seat.providerCustomerId || payment.status !== "succeeded" || payment.amount_received !== invoiceRef.totalCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod")) fail4();
|
|
4177
|
+
chargeId = id2(payment.latest_charge);
|
|
4178
|
+
}
|
|
4179
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
4180
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
4181
|
+
if (charge.id !== chargeId || id2(charge.customer) !== seat.providerCustomerId || charge.status !== "succeeded" || charge.livemode !== (seat.environment === "prod") || charge.amount !== invoiceRef.totalCents || charge.currency !== seat.currency || charge.invoice != null && id2(charge.invoice) !== invoice.id || paymentId && id2(charge.payment_intent) !== paymentId || !Number.isSafeInteger(charge.amount_refunded) || charge.amount_refunded < 0 || charge.amount_refunded > charge.amount) fail4();
|
|
4182
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4183
|
+
if (items.length > 1) fail4();
|
|
4184
|
+
if (items.length && sub.status !== "canceled") {
|
|
4185
|
+
const item = items[0];
|
|
4186
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
4187
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4188
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
4189
|
+
}
|
|
4190
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
4191
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
4192
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
4193
|
+
let refundedCents = 0;
|
|
4194
|
+
const refundIds = [];
|
|
4195
|
+
for (const refund of list.data) {
|
|
4196
|
+
if (id2(refund.charge) !== chargeId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !/^re_[A-Za-z0-9_]+$/.test(refund.id) || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail4();
|
|
4197
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
4198
|
+
if (refund.status === "succeeded") {
|
|
4199
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
4200
|
+
refundedCents += refund.amount;
|
|
4201
|
+
refundIds.push(refund.id);
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
4205
|
+
if (refundedCents < seat.amountCents) {
|
|
4206
|
+
const remaining = seat.amountCents - refundedCents;
|
|
4207
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
4208
|
+
charge: chargeId,
|
|
4209
|
+
amount: remaining,
|
|
4210
|
+
reason: "requested_by_customer",
|
|
4211
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
4212
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
4213
|
+
if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
4214
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
4215
|
+
refundedCents += remaining;
|
|
4216
|
+
refundIds.push(refund.id);
|
|
4217
|
+
}
|
|
4218
|
+
return { refundedCents, refundIds };
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
// src/membership-seat-refund-stripe.ts
|
|
4222
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
4223
|
+
throw Object.assign(new Error(code), { code });
|
|
4224
|
+
};
|
|
3939
4225
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3940
|
-
if (
|
|
4226
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4227
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
3941
4228
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3942
|
-
const result = await (0,
|
|
3943
|
-
if (!result.ok || !result.body) return
|
|
4229
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4230
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
3944
4231
|
return result.body;
|
|
3945
4232
|
};
|
|
3946
4233
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3947
4234
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3948
|
-
if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value))
|
|
4235
|
+
if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail5();
|
|
3949
4236
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3950
|
-
if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data))
|
|
4237
|
+
if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail5();
|
|
3951
4238
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3952
|
-
if (items.length > 1)
|
|
4239
|
+
if (items.length > 1) fail5();
|
|
3953
4240
|
if (items.length && sub.status !== "canceled") {
|
|
3954
4241
|
const item = items[0];
|
|
3955
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
4242
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
3956
4243
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3957
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
4244
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
3958
4245
|
}
|
|
3959
4246
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3960
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100)
|
|
4247
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
3961
4248
|
let refundedCents = 0, pendingCents = 0;
|
|
3962
4249
|
const refundIds = [];
|
|
3963
4250
|
for (const refund of list.data) {
|
|
3964
|
-
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status))
|
|
4251
|
+
if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail5();
|
|
3965
4252
|
if (refund.status === "succeeded") {
|
|
3966
4253
|
refundedCents += refund.amount;
|
|
3967
4254
|
refundIds.push(refund.id);
|
|
3968
4255
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3969
4256
|
}
|
|
3970
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3971
|
-
if (pendingCents)
|
|
4257
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
4258
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
3972
4259
|
if (refundedCents < seat.amountCents) {
|
|
3973
4260
|
const remaining = seat.amountCents - refundedCents;
|
|
3974
4261
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3977,8 +4264,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3977
4264
|
reason: "requested_by_customer",
|
|
3978
4265
|
metadata
|
|
3979
4266
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3980
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
3981
|
-
if (refund.status !== "succeeded")
|
|
4267
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
4268
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
3982
4269
|
refundedCents += remaining;
|
|
3983
4270
|
refundIds.push(refund.id);
|
|
3984
4271
|
}
|
|
@@ -4000,8 +4287,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4000
4287
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4001
4288
|
requireMembershipScope(member, input);
|
|
4002
4289
|
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4003
|
-
const
|
|
4004
|
-
let seat = await membershipLoad(db, "networkSeat",
|
|
4290
|
+
const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4291
|
+
let seat = await membershipLoad(db, "networkSeat", id7);
|
|
4005
4292
|
if (!seat) {
|
|
4006
4293
|
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4007
4294
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
@@ -4009,7 +4296,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4009
4296
|
membershipFail("named_seat_purchase_unavailable");
|
|
4010
4297
|
}
|
|
4011
4298
|
const attrs = {
|
|
4012
|
-
id:
|
|
4299
|
+
id: id7,
|
|
4013
4300
|
...membershipScope(input),
|
|
4014
4301
|
primaryMembershipId: memberId,
|
|
4015
4302
|
recipientName: name,
|
|
@@ -4034,20 +4321,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4034
4321
|
};
|
|
4035
4322
|
try {
|
|
4036
4323
|
await db.transact([
|
|
4037
|
-
membershipUpdate("networkSeat",
|
|
4324
|
+
membershipUpdate("networkSeat", id7, attrs),
|
|
4038
4325
|
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4039
4326
|
id: claimId,
|
|
4040
4327
|
appId: input.appId,
|
|
4041
4328
|
chapterId: input.chapterId,
|
|
4042
4329
|
environment: input.environment,
|
|
4043
4330
|
runtime: input.runtime,
|
|
4044
|
-
seatId:
|
|
4331
|
+
seatId: id7,
|
|
4045
4332
|
recipientEmailNormalized: email,
|
|
4046
4333
|
createdAt: now
|
|
4047
4334
|
}),
|
|
4048
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId:
|
|
4049
|
-
], { mutationId: `named-seat-reserve:${
|
|
4050
|
-
{ ns: "networkSeat", id:
|
|
4335
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4336
|
+
], { mutationId: `named-seat-reserve:${id7}`, guards: [
|
|
4337
|
+
{ ns: "networkSeat", id: id7, exists: false },
|
|
4051
4338
|
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4052
4339
|
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4053
4340
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
@@ -4055,7 +4342,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4055
4342
|
} catch (error) {
|
|
4056
4343
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4057
4344
|
}
|
|
4058
|
-
seat = await membershipLoad(db, "networkSeat",
|
|
4345
|
+
seat = await membershipLoad(db, "networkSeat", id7);
|
|
4059
4346
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4060
4347
|
}
|
|
4061
4348
|
requireMembershipScope(seat, input);
|
|
@@ -4065,7 +4352,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4065
4352
|
|
|
4066
4353
|
// src/membership-seat-included.ts
|
|
4067
4354
|
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4068
|
-
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4355
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4069
4356
|
function namedSeatRecipientState(seat) {
|
|
4070
4357
|
switch (String(seat.status)) {
|
|
4071
4358
|
case "renewal_pending":
|
|
@@ -4169,7 +4456,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4169
4456
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4170
4457
|
requireMembershipScope(member, input);
|
|
4171
4458
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4172
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
4459
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !seat.combinedInvoice && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
4173
4460
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4174
4461
|
await db.transact([
|
|
4175
4462
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4282,8 +4569,8 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4282
4569
|
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4283
4570
|
for (const member of rows5.slice(0, limit)) {
|
|
4284
4571
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4285
|
-
const
|
|
4286
|
-
receipt.skipped.push({ membershipId:
|
|
4572
|
+
const id7 = String(member.id), skip = (reason) => {
|
|
4573
|
+
receipt.skipped.push({ membershipId: id7, reason });
|
|
4287
4574
|
};
|
|
4288
4575
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4289
4576
|
if (offering.seatPriceCents <= target) {
|
|
@@ -4299,16 +4586,16 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4299
4586
|
skip(`seat is ${String(seat.status)}`);
|
|
4300
4587
|
continue;
|
|
4301
4588
|
}
|
|
4302
|
-
const ops = [membershipUpdate("networkMembership",
|
|
4589
|
+
const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4303
4590
|
const guards = [
|
|
4304
|
-
{ ns: "networkMembership", id:
|
|
4591
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
4305
4592
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4306
4593
|
];
|
|
4307
4594
|
if (seat) {
|
|
4308
4595
|
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4309
4596
|
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4310
4597
|
}
|
|
4311
|
-
await db.transact(ops, { mutationId: `seat-reprice:${
|
|
4598
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
|
|
4312
4599
|
receipt.repriced++;
|
|
4313
4600
|
}
|
|
4314
4601
|
return receipt;
|
|
@@ -4509,25 +4796,28 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4509
4796
|
};
|
|
4510
4797
|
|
|
4511
4798
|
// src/membership-discount-reterm.ts
|
|
4512
|
-
var
|
|
4799
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
4513
4800
|
|
|
4514
4801
|
// src/subscription-provider-contract.ts
|
|
4515
|
-
var
|
|
4802
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4516
4803
|
function subscriptionContractMismatch() {
|
|
4517
4804
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4518
4805
|
}
|
|
4519
4806
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4520
4807
|
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
4521
|
-
const result = await (0,
|
|
4808
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4522
4809
|
const price = result.body;
|
|
4523
4810
|
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
4524
4811
|
return typeof price.product === "string" ? price.product : void 0;
|
|
4525
4812
|
}
|
|
4526
4813
|
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4527
4814
|
const items = sub?.items;
|
|
4528
|
-
|
|
4815
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
4816
|
+
const primary = items?.data?.find((item) => item.price?.id === input.priceId);
|
|
4817
|
+
const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
|
|
4818
|
+
if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== (seat ? 2 : 1) || primary?.quantity !== 1 || seat && (seatItem?.quantity !== 1 || seatItem.metadata?.seatId !== seat.seatId || seatItem.metadata?.membershipId !== seat.membershipId)) subscriptionContractMismatch();
|
|
4529
4819
|
const invoice = sub.latest_invoice;
|
|
4530
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4820
|
+
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) + (seat?.amountCents ?? 0) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4531
4821
|
verifySubscriptionBillingResult(sub, input);
|
|
4532
4822
|
}
|
|
4533
4823
|
|
|
@@ -4539,8 +4829,8 @@ var couponOf = (discount) => {
|
|
|
4539
4829
|
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4540
4830
|
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4541
4831
|
};
|
|
4542
|
-
async function retermPolicy(db, scope2,
|
|
4543
|
-
const row2 = await membershipLoad(db, "discountPolicyVersion",
|
|
4832
|
+
async function retermPolicy(db, scope2, id7) {
|
|
4833
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
|
|
4544
4834
|
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4545
4835
|
let policy2;
|
|
4546
4836
|
try {
|
|
@@ -4548,12 +4838,12 @@ async function retermPolicy(db, scope2, id6) {
|
|
|
4548
4838
|
} catch {
|
|
4549
4839
|
return membershipFail("membership_reterm_policy_ineligible");
|
|
4550
4840
|
}
|
|
4551
|
-
if (policy2.id !==
|
|
4841
|
+
if (policy2.id !== id7 || policy2.chapterId !== scope2.chapterId || policy2.status !== "published" || policy2.benefitDuration.kind !== "while_active" || policy2.percentOffBps <= 0 || policy2.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
|
|
4552
4842
|
return policy2;
|
|
4553
4843
|
}
|
|
4554
4844
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
4555
4845
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4556
|
-
const read2 = await (0,
|
|
4846
|
+
const read2 = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4557
4847
|
const sub = read2.body, items = sub?.items?.data;
|
|
4558
4848
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4559
4849
|
if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
|
|
@@ -4564,7 +4854,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
4564
4854
|
const present = discounts.map(couponOf);
|
|
4565
4855
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
4566
4856
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4567
|
-
const written = await (0,
|
|
4857
|
+
const written = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
4568
4858
|
"discounts[0][coupon]": couponId,
|
|
4569
4859
|
"expand[0]": "discounts",
|
|
4570
4860
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -4580,7 +4870,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4580
4870
|
const limit = input.limit ?? 100;
|
|
4581
4871
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4582
4872
|
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4583
|
-
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((
|
|
4873
|
+
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id7) => typeof id7 !== "string" || !SAFE.test(id7)))) throw new TypeError("invalid membership ids");
|
|
4584
4874
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4585
4875
|
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4586
4876
|
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
@@ -4590,10 +4880,10 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4590
4880
|
let rows5;
|
|
4591
4881
|
if (input.membershipIds) {
|
|
4592
4882
|
rows5 = [];
|
|
4593
|
-
for (const
|
|
4594
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
4595
|
-
if (!member) skip(
|
|
4596
|
-
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(
|
|
4883
|
+
for (const id7 of input.membershipIds) {
|
|
4884
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
4885
|
+
if (!member) skip(id7, "not_found");
|
|
4886
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
4597
4887
|
else rows5.push(member);
|
|
4598
4888
|
}
|
|
4599
4889
|
} else {
|
|
@@ -4603,32 +4893,32 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4603
4893
|
rows5 = found.slice(0, limit);
|
|
4604
4894
|
}
|
|
4605
4895
|
for (const member of rows5) {
|
|
4606
|
-
const
|
|
4896
|
+
const id7 = String(member.id);
|
|
4607
4897
|
if (member.discountPolicyVersionId === policy2.id) {
|
|
4608
|
-
receipt.unchanged.push(
|
|
4898
|
+
receipt.unchanged.push(id7);
|
|
4609
4899
|
continue;
|
|
4610
4900
|
}
|
|
4611
4901
|
if (!RETERMABLE.has(String(member.status))) {
|
|
4612
|
-
skip(
|
|
4902
|
+
skip(id7, "status");
|
|
4613
4903
|
continue;
|
|
4614
4904
|
}
|
|
4615
4905
|
if (member.founding !== true) {
|
|
4616
|
-
skip(
|
|
4906
|
+
skip(id7, "not_founding");
|
|
4617
4907
|
continue;
|
|
4618
4908
|
}
|
|
4619
4909
|
if (member.cohort !== policy2.cohortLabel) {
|
|
4620
|
-
skip(
|
|
4910
|
+
skip(id7, "cohort_mismatch");
|
|
4621
4911
|
continue;
|
|
4622
4912
|
}
|
|
4623
4913
|
const skuId = String(member.baseSkuId);
|
|
4624
4914
|
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
4625
|
-
skip(
|
|
4915
|
+
skip(id7, "sku_not_discounted");
|
|
4626
4916
|
continue;
|
|
4627
4917
|
}
|
|
4628
4918
|
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4629
4919
|
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
4630
4920
|
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4631
|
-
skip(
|
|
4921
|
+
skip(id7, "would_not_widen");
|
|
4632
4922
|
continue;
|
|
4633
4923
|
}
|
|
4634
4924
|
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
@@ -4656,22 +4946,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4656
4946
|
} catch (error) {
|
|
4657
4947
|
const code = error.code;
|
|
4658
4948
|
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4659
|
-
skip(
|
|
4949
|
+
skip(id7, "provider_mismatch");
|
|
4660
4950
|
continue;
|
|
4661
4951
|
}
|
|
4662
4952
|
throw error;
|
|
4663
4953
|
}
|
|
4664
4954
|
try {
|
|
4665
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
4955
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
4666
4956
|
discountPolicyVersionId: policy2.id,
|
|
4667
4957
|
discountSnapshot: policy2,
|
|
4668
4958
|
renewalAmountCents: renewal,
|
|
4669
4959
|
revision: Number(member.revision) + 1,
|
|
4670
4960
|
updatedAt: now
|
|
4671
4961
|
})], {
|
|
4672
|
-
mutationId: `membership-reterm:${
|
|
4962
|
+
mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
|
|
4673
4963
|
guards: [
|
|
4674
|
-
{ ns: "networkMembership", id:
|
|
4964
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4675
4965
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4676
4966
|
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4677
4967
|
]
|
|
@@ -4681,7 +4971,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4681
4971
|
return membershipFail("membership_reterm_retry_required");
|
|
4682
4972
|
}
|
|
4683
4973
|
receipt.retermed.push({
|
|
4684
|
-
membershipId:
|
|
4974
|
+
membershipId: id7,
|
|
4685
4975
|
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4686
4976
|
previousRenewalAmountCents: current,
|
|
4687
4977
|
renewalAmountCents: renewal,
|
|
@@ -4717,7 +5007,7 @@ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
|
4717
5007
|
return json({ error: BAD }, 400);
|
|
4718
5008
|
}
|
|
4719
5009
|
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
4720
|
-
if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((
|
|
5010
|
+
if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((id7) => typeof id7 !== "string" || !ID.test(id7))) || body.limit !== void 0 && (!Number.isSafeInteger(body.limit) || Number(body.limit) < 1 || Number(body.limit) > 200)) {
|
|
4721
5011
|
return json({ error: BAD }, 400);
|
|
4722
5012
|
}
|
|
4723
5013
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
@@ -4992,12 +5282,12 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4992
5282
|
if (!app || app.clerkUserId !== user.userId || app.namedSeatId || app.giftClaimId) return json({ error: "membership is not eligible to restart" }, 409);
|
|
4993
5283
|
if (app.membershipRestartId && app.status === "submitted") return json({ applicationId: app.id });
|
|
4994
5284
|
try {
|
|
4995
|
-
const
|
|
5285
|
+
const id7 = String(app.id), intent = await membershipLoad(db, "subscriptionCheckoutIntents", id7);
|
|
4996
5286
|
if (!intent || !app.stripeSubscriptionId || intent.providerSubscriptionId !== app.stripeSubscriptionId) return json({ error: "membership history needs reconciliation" }, 409);
|
|
4997
5287
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
4998
5288
|
const restart = await ctx.membershipAuthority.restart({ ...target, previousSubscriptionId: String(app.stripeSubscriptionId) }, env);
|
|
4999
|
-
if (restart.status !== "ready" || restart.applicationId !==
|
|
5000
|
-
const now = Date.now(), quote = await membershipLoad(db, "membershipQuoteProjections",
|
|
5289
|
+
if (restart.status !== "ready" || restart.applicationId !== id7 || restart.membershipId !== await networkMembershipId(target) || !/^restart_[a-f0-9]{64}$/.test(restart.restartId) || !Number.isSafeInteger(restart.memberNumber) || restart.memberNumber < 1) throw new Error("invalid restart receipt");
|
|
5290
|
+
const now = Date.now(), quote = await membershipLoad(db, "membershipQuoteProjections", id7);
|
|
5001
5291
|
const attrs = {
|
|
5002
5292
|
status: "submitted",
|
|
5003
5293
|
membershipRestartId: restart.restartId,
|
|
@@ -5017,8 +5307,8 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
5017
5307
|
quoteSnapshot: quote ?? {},
|
|
5018
5308
|
archivedAt: now
|
|
5019
5309
|
} },
|
|
5020
|
-
{ t: "delete", ns: "subscriptionCheckoutIntents", id:
|
|
5021
|
-
{ t: "retract", ns: "applications", id:
|
|
5310
|
+
{ t: "delete", ns: "subscriptionCheckoutIntents", id: id7 },
|
|
5311
|
+
{ t: "retract", ns: "applications", id: id7, attrs: [
|
|
5022
5312
|
"stripeSubscriptionId",
|
|
5023
5313
|
"canceled",
|
|
5024
5314
|
"renewalAt",
|
|
@@ -5029,28 +5319,28 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
5029
5319
|
"approvalEffectsOrigin",
|
|
5030
5320
|
"approvalEffectsLastAttemptAt"
|
|
5031
5321
|
] },
|
|
5032
|
-
{ t: "update", ns: "applications", id:
|
|
5033
|
-
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id:
|
|
5322
|
+
{ t: "update", ns: "applications", id: id7, attrs },
|
|
5323
|
+
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id: id7, attrs: { status: "restart", updatedAt: now } }] : []
|
|
5034
5324
|
];
|
|
5035
5325
|
try {
|
|
5036
5326
|
await db.transact(ops, { mutationId: `membership-restart-application:${restart.restartId}`, guards: [
|
|
5037
|
-
{ ns: "applications", id:
|
|
5327
|
+
{ ns: "applications", id: id7, exists: true, equals: {
|
|
5038
5328
|
clerkUserId: user.userId,
|
|
5039
5329
|
groupId: ctx.chapter.id,
|
|
5040
5330
|
status: app.status,
|
|
5041
5331
|
stripeSubscriptionId: app.stripeSubscriptionId
|
|
5042
5332
|
} },
|
|
5043
|
-
{ ns: "subscriptionCheckoutIntents", id:
|
|
5333
|
+
{ ns: "subscriptionCheckoutIntents", id: id7, exists: true, equals: { providerSubscriptionId: app.stripeSubscriptionId } },
|
|
5044
5334
|
{ ns: "membershipCheckoutHistory", id: restart.restartId, exists: false },
|
|
5045
|
-
{ ns: "membershipQuoteProjections", id:
|
|
5335
|
+
{ ns: "membershipQuoteProjections", id: id7, exists: Boolean(quote), ...quote ? { equals: { revision: quote.revision, status: quote.status } } : {} }
|
|
5046
5336
|
] });
|
|
5047
5337
|
} catch (error) {
|
|
5048
5338
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5049
5339
|
}
|
|
5050
|
-
const saved = await membershipLoad(db, "applications",
|
|
5340
|
+
const saved = await membershipLoad(db, "applications", id7);
|
|
5051
5341
|
if (!saved || saved.clerkUserId !== user.userId || saved.membershipRestartId !== restart.restartId || saved.groupId !== ctx.chapter.id || saved.stripeRuntime !== target.runtime) throw new Error("restart persistence pending");
|
|
5052
5342
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: saved, stage: String(saved.status) }).catch(() => void 0);
|
|
5053
|
-
return json({ applicationId:
|
|
5343
|
+
return json({ applicationId: id7 });
|
|
5054
5344
|
} catch {
|
|
5055
5345
|
return json({ error: "We could not prepare your new subscription. Your account is saved. Please retry." }, 503);
|
|
5056
5346
|
}
|
|
@@ -5181,8 +5471,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5181
5471
|
if (app.requireDisclaimerAck && !acked) {
|
|
5182
5472
|
return { ok: false, error: "disclaimerAck is required" };
|
|
5183
5473
|
}
|
|
5184
|
-
const
|
|
5185
|
-
const row2 = { id:
|
|
5474
|
+
const id7 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
5475
|
+
const row2 = { id: id7, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
5186
5476
|
for (const f of [...app.required, ...app.optional]) {
|
|
5187
5477
|
if (fieldStates[f]?.visible === false) continue;
|
|
5188
5478
|
if (typeof fields[f] === "string") row2[f] = fields[f].trim();
|
|
@@ -5216,12 +5506,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5216
5506
|
}
|
|
5217
5507
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
5218
5508
|
const ops = [
|
|
5219
|
-
{ t: "update", ns: "applications", id:
|
|
5509
|
+
{ t: "update", ns: "applications", id: id7, attrs: row2 }
|
|
5220
5510
|
];
|
|
5221
5511
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
5222
5512
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
5223
5513
|
consumedAt: opts.now,
|
|
5224
|
-
consumedApplicationId:
|
|
5514
|
+
consumedApplicationId: id7
|
|
5225
5515
|
} });
|
|
5226
5516
|
}
|
|
5227
5517
|
try {
|
|
@@ -5229,7 +5519,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5229
5519
|
ops,
|
|
5230
5520
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
5231
5521
|
);
|
|
5232
|
-
return { ok: true, id:
|
|
5522
|
+
return { ok: true, id: id7, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
5233
5523
|
} catch (error) {
|
|
5234
5524
|
if (error.code === "unique_violation") return {
|
|
5235
5525
|
ok: false,
|
|
@@ -5338,7 +5628,7 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
5338
5628
|
}
|
|
5339
5629
|
|
|
5340
5630
|
// src/join-founding-preview.ts
|
|
5341
|
-
async function foundingPreviews(tiers, env, ctx) {
|
|
5631
|
+
async function foundingPreviews(tiers, env, ctx, seats = /* @__PURE__ */ new Map()) {
|
|
5342
5632
|
const result = /* @__PURE__ */ new Map();
|
|
5343
5633
|
const preview = ctx.membershipAuthority?.previewOffer;
|
|
5344
5634
|
if (!preview) return result;
|
|
@@ -5346,6 +5636,10 @@ async function foundingPreviews(tiers, env, ctx) {
|
|
|
5346
5636
|
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
5347
5637
|
try {
|
|
5348
5638
|
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
5639
|
+
if (offer.skuId !== tier.id || offer.basePriceCents !== tier.priceCents) return;
|
|
5640
|
+
if (ctx.membershipAuthority?.namedSeats && Number.isSafeInteger(offer.seatPriceCents) && Number(offer.seatPriceCents) >= 0) {
|
|
5641
|
+
seats.set(tier.id, { amountCents: Number(offer.seatPriceCents) });
|
|
5642
|
+
}
|
|
5349
5643
|
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
5350
5644
|
result.set(tier.id, {
|
|
5351
5645
|
cohortLabel: offer.cohortLabel,
|
|
@@ -5435,14 +5729,14 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5435
5729
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
5436
5730
|
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
5437
5731
|
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
5438
|
-
const
|
|
5732
|
+
const id7 = typeof grant.id === "string" ? grant.id : "";
|
|
5439
5733
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
5440
5734
|
const normalized = normalizeAdmissionEmail(email);
|
|
5441
5735
|
const decision = grant.decision;
|
|
5442
5736
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
5443
5737
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
5444
|
-
if (!ID2.test(
|
|
5445
|
-
if (seenIds.has(
|
|
5738
|
+
if (!ID2.test(id7)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
5739
|
+
if (seenIds.has(id7)) errors.push(`document.grants repeats id "${id7}"`);
|
|
5446
5740
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
5447
5741
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
5448
5742
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -5452,10 +5746,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5452
5746
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
5453
5747
|
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
5454
5748
|
}
|
|
5455
|
-
seenIds.add(
|
|
5749
|
+
seenIds.add(id7);
|
|
5456
5750
|
seenEmails.add(normalized);
|
|
5457
5751
|
grants.push({
|
|
5458
|
-
id:
|
|
5752
|
+
id: id7,
|
|
5459
5753
|
email,
|
|
5460
5754
|
decision,
|
|
5461
5755
|
...note ? { note } : {},
|
|
@@ -5527,8 +5821,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5527
5821
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
5528
5822
|
const ops = [];
|
|
5529
5823
|
for (const row2 of current) {
|
|
5530
|
-
const
|
|
5531
|
-
if (
|
|
5824
|
+
const id7 = String(row2.id ?? "");
|
|
5825
|
+
if (id7 && !incomingIds.has(id7)) ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5532
5826
|
active: false,
|
|
5533
5827
|
revokedAt: receivedAt,
|
|
5534
5828
|
revokedBy: document.source.actor,
|
|
@@ -5537,10 +5831,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5537
5831
|
} });
|
|
5538
5832
|
}
|
|
5539
5833
|
for (const grant of document.grants) {
|
|
5540
|
-
const
|
|
5541
|
-
const existing = current.find((row2) => row2.id ===
|
|
5542
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
5543
|
-
id:
|
|
5834
|
+
const id7 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
5835
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
5836
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5837
|
+
id: id7,
|
|
5544
5838
|
sourceGrantId: grant.id,
|
|
5545
5839
|
sourceScope: "parent",
|
|
5546
5840
|
sourceId: document.source.appId,
|
|
@@ -5561,7 +5855,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5561
5855
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
5562
5856
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
5563
5857
|
];
|
|
5564
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
5858
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id7, attrs: retract });
|
|
5565
5859
|
}
|
|
5566
5860
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
5567
5861
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -5592,9 +5886,9 @@ var APPLICATION_LABELS = {
|
|
|
5592
5886
|
phone: "Phone",
|
|
5593
5887
|
state: "State"
|
|
5594
5888
|
};
|
|
5595
|
-
function humanize(
|
|
5596
|
-
const text8 =
|
|
5597
|
-
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` :
|
|
5889
|
+
function humanize(id7) {
|
|
5890
|
+
const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5891
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
|
|
5598
5892
|
}
|
|
5599
5893
|
function applicationDefs(chapter) {
|
|
5600
5894
|
try {
|
|
@@ -5603,23 +5897,23 @@ function applicationDefs(chapter) {
|
|
|
5603
5897
|
return {};
|
|
5604
5898
|
}
|
|
5605
5899
|
}
|
|
5606
|
-
function applicationType(
|
|
5900
|
+
function applicationType(id7, def) {
|
|
5607
5901
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
5608
|
-
return
|
|
5902
|
+
return id7 === "email" ? "email" : id7 === "linkedin" ? "url" : "string";
|
|
5609
5903
|
}
|
|
5610
5904
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
5611
5905
|
const defs = applicationDefs(chapter);
|
|
5612
|
-
return [...application.required, ...application.optional].map((
|
|
5613
|
-
const def = defs[
|
|
5614
|
-
const conditions = application.conditions[
|
|
5615
|
-
const type = applicationType(
|
|
5906
|
+
return [...application.required, ...application.optional].map((id7, order2) => {
|
|
5907
|
+
const def = defs[id7];
|
|
5908
|
+
const conditions = application.conditions[id7];
|
|
5909
|
+
const type = applicationType(id7, def);
|
|
5616
5910
|
return {
|
|
5617
|
-
id:
|
|
5618
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
5911
|
+
id: id7,
|
|
5912
|
+
label: def?.label ?? APPLICATION_LABELS[id7] ?? humanize(id7),
|
|
5619
5913
|
type,
|
|
5620
|
-
required: application.required.includes(
|
|
5914
|
+
required: application.required.includes(id7),
|
|
5621
5915
|
order: order2,
|
|
5622
|
-
maxLength: application.maxLen[
|
|
5916
|
+
maxLength: application.maxLen[id7] ?? application.defaultMaxLen,
|
|
5623
5917
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
5624
5918
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
5625
5919
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -5629,17 +5923,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
5629
5923
|
function resolveFormationFlowFields(crm, formation) {
|
|
5630
5924
|
if (!formation.enabled) return [];
|
|
5631
5925
|
const def = crm.type(formation.type);
|
|
5632
|
-
return [...formation.required, ...formation.optional].map((
|
|
5633
|
-
const field = def.fields[
|
|
5926
|
+
return [...formation.required, ...formation.optional].map((id7, order2) => {
|
|
5927
|
+
const field = def.fields[id7];
|
|
5634
5928
|
return {
|
|
5635
|
-
id:
|
|
5636
|
-
label: field.label ?? humanize(
|
|
5929
|
+
id: id7,
|
|
5930
|
+
label: field.label ?? humanize(id7),
|
|
5637
5931
|
type: field.type,
|
|
5638
|
-
required: formation.required.includes(
|
|
5932
|
+
required: formation.required.includes(id7),
|
|
5639
5933
|
order: order2,
|
|
5640
|
-
maxLength: formation.maxLen[
|
|
5934
|
+
maxLength: formation.maxLen[id7] ?? formation.defaultMaxLen,
|
|
5641
5935
|
...field.options ? { options: [...field.options] } : {},
|
|
5642
|
-
...
|
|
5936
|
+
...id7 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
5643
5937
|
};
|
|
5644
5938
|
});
|
|
5645
5939
|
}
|
|
@@ -5689,10 +5983,10 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5689
5983
|
}
|
|
5690
5984
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5691
5985
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
5692
|
-
const
|
|
5693
|
-
if (
|
|
5694
|
-
if (ids2.has(
|
|
5695
|
-
ids2.add(
|
|
5986
|
+
const id7 = text3(item.id, `${path}.id`, errors, 80);
|
|
5987
|
+
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5988
|
+
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
5989
|
+
ids2.add(id7);
|
|
5696
5990
|
const targetRaw = record(item.target);
|
|
5697
5991
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
5698
5992
|
if (targetRaw?.type === "tier") {
|
|
@@ -5718,7 +6012,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5718
6012
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
5719
6013
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
5720
6014
|
offers.push({
|
|
5721
|
-
id:
|
|
6015
|
+
id: id7,
|
|
5722
6016
|
target,
|
|
5723
6017
|
billingMode: item.billingMode,
|
|
5724
6018
|
stripePriceId,
|
|
@@ -5818,9 +6112,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5818
6112
|
const ops = [];
|
|
5819
6113
|
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
5820
6114
|
for (const row2 of currentRows) {
|
|
5821
|
-
const
|
|
5822
|
-
if (
|
|
5823
|
-
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers",
|
|
6115
|
+
const id8 = String(row2.id ?? "");
|
|
6116
|
+
if (id8 && !incoming.has(id8)) {
|
|
6117
|
+
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers", id8), attrs: { active: false } });
|
|
5824
6118
|
}
|
|
5825
6119
|
}
|
|
5826
6120
|
for (const tier of document.tiers) {
|
|
@@ -5855,9 +6149,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5855
6149
|
if (!("stripePriceId" in attrs)) {
|
|
5856
6150
|
ops.push({ t: "retract", ns: "groups", id: groupRef, attrs: ["stripePriceId"] });
|
|
5857
6151
|
}
|
|
5858
|
-
const
|
|
5859
|
-
ops.push({ t: "update", ns: "signupControls", id:
|
|
5860
|
-
id:
|
|
6152
|
+
const id7 = `signup-control:${document.target.environment}:${document.revision}`;
|
|
6153
|
+
ops.push({ t: "update", ns: "signupControls", id: id7, attrs: {
|
|
6154
|
+
id: id7,
|
|
5861
6155
|
sourceId: document.source.appId,
|
|
5862
6156
|
targetId: document.target.chapterId,
|
|
5863
6157
|
environment: document.target.environment,
|
|
@@ -5896,6 +6190,8 @@ function foundingOf(offer) {
|
|
|
5896
6190
|
async function createPaymentQuote(input) {
|
|
5897
6191
|
const { group, tier, policy: policy2 } = input;
|
|
5898
6192
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
6193
|
+
if (input.seatOffer && (!terms2 || !Number.isSafeInteger(input.seatOffer.amountCents) || input.seatOffer.amountCents < 0) || input.namedSeat && !input.seatOffer) throw new Error("invalid checkout seat offer");
|
|
6194
|
+
const seatCents = input.namedSeat ? input.seatOffer.amountCents : 0;
|
|
5899
6195
|
const compareAt = input.presentation?.compareAtPriceCents;
|
|
5900
6196
|
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
5901
6197
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
@@ -5904,6 +6200,7 @@ async function createPaymentQuote(input) {
|
|
|
5904
6200
|
discountCents: tier.priceCents - dueToday,
|
|
5905
6201
|
dueTodayCents: dueToday
|
|
5906
6202
|
};
|
|
6203
|
+
if (!Number.isSafeInteger(lines.dueTodayCents + seatCents) || !Number.isSafeInteger(tier.priceCents + seatCents)) throw new Error("invalid checkout total");
|
|
5907
6204
|
const quote = {
|
|
5908
6205
|
applicationId: input.applicationId,
|
|
5909
6206
|
chapterId: input.chapterId,
|
|
@@ -5911,11 +6208,17 @@ async function createPaymentQuote(input) {
|
|
|
5911
6208
|
tierName: tier.name,
|
|
5912
6209
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
5913
6210
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
5914
|
-
lineItems:
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
6211
|
+
lineItems: {
|
|
6212
|
+
...presentation ? {
|
|
6213
|
+
standardCents: presentation.compareAtPriceCents,
|
|
6214
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
6215
|
+
dueTodayCents: lines.dueTodayCents
|
|
6216
|
+
} : lines,
|
|
6217
|
+
dueTodayCents: lines.dueTodayCents + seatCents,
|
|
6218
|
+
...input.namedSeat ? { namedSeatCents: seatCents } : {}
|
|
6219
|
+
},
|
|
6220
|
+
...input.seatOffer ? { seatOffer: input.seatOffer } : {},
|
|
6221
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {},
|
|
5919
6222
|
...presentation ? { presentation } : {},
|
|
5920
6223
|
refundPolicyText: policy2?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
5921
6224
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
@@ -5927,7 +6230,7 @@ async function createPaymentQuote(input) {
|
|
|
5927
6230
|
discountDuration: terms2.discount.duration === "once" ? "first_term" : "while_active"
|
|
5928
6231
|
} : {},
|
|
5929
6232
|
...terms2.initialTermEndsAt !== void 0 ? { initialTermEndsAt: terms2.initialTermEndsAt } : {},
|
|
5930
|
-
renewalCents: terms2.discount?.duration === "forever" ? dueToday : tier.priceCents,
|
|
6233
|
+
renewalCents: (terms2.discount?.duration === "forever" ? dueToday : tier.priceCents) + seatCents,
|
|
5931
6234
|
autoRenew: true
|
|
5932
6235
|
} } : {}
|
|
5933
6236
|
};
|
|
@@ -5947,22 +6250,23 @@ async function createPaymentQuote(input) {
|
|
|
5947
6250
|
// src/membership-quote-projection.ts
|
|
5948
6251
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
5949
6252
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
5950
|
-
var
|
|
6253
|
+
var fail6 = (code) => {
|
|
5951
6254
|
throw Object.assign(new Error(code), { code });
|
|
5952
6255
|
};
|
|
5953
|
-
var load3 = async (db, ns,
|
|
6256
|
+
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
5954
6257
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
5955
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
5956
|
-
if (row2.status !== "ready") return
|
|
6258
|
+
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail6("membership_quote_unavailable");
|
|
6259
|
+
if (row2.status !== "ready") return fail6("membership_quote_pending");
|
|
5957
6260
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
5958
6261
|
...target,
|
|
5959
6262
|
requestId: row2.requestId
|
|
5960
6263
|
}), now, lifetimeMs: 6e4 });
|
|
5961
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6264
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
|
|
5962
6265
|
const tier = row2.tier, quote = row2.quote;
|
|
5963
6266
|
const offering = offer.contract.offering;
|
|
6267
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
|
|
5964
6268
|
if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
|
|
5965
|
-
return
|
|
6269
|
+
return fail6("membership_catalog_mismatch");
|
|
5966
6270
|
}
|
|
5967
6271
|
const expected = await createPaymentQuote({
|
|
5968
6272
|
...target,
|
|
@@ -5971,19 +6275,21 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
5971
6275
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
5972
6276
|
billingTerms: offer.contract.billingTerms,
|
|
5973
6277
|
presentation: quote.presentation,
|
|
5974
|
-
founding: foundingOf(offer)
|
|
6278
|
+
founding: foundingOf(offer),
|
|
6279
|
+
seatOffer: quote.seatOffer,
|
|
6280
|
+
namedSeat: quote.namedSeat
|
|
5975
6281
|
});
|
|
5976
|
-
if (quote.digest !== expected.digest) return
|
|
6282
|
+
if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
|
|
5977
6283
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
5978
6284
|
}
|
|
5979
6285
|
async function readMembershipQuote(db, target, now) {
|
|
5980
6286
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, now);
|
|
5981
6287
|
}
|
|
5982
6288
|
async function prepareMembershipQuote(db, input, issue) {
|
|
5983
|
-
const { applicationId:
|
|
5984
|
-
if (app.id !==
|
|
5985
|
-
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
5986
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
6289
|
+
const { applicationId: id7, now, app } = input;
|
|
6290
|
+
if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail6("membership_quote_unavailable");
|
|
6291
|
+
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6292
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
|
|
5987
6293
|
if (row2?.status === "ready") {
|
|
5988
6294
|
try {
|
|
5989
6295
|
return await restore2(row2, input, now);
|
|
@@ -5994,15 +6300,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5994
6300
|
const identity2 = ["groupId", "tierId", "stripeRuntime", "email", "emailNormalized", "status", "canceled", "membershipRestartId", "namedSeatId", "namedSeatClaimPending", "giftClaimId", "giftOfferId"];
|
|
5995
6301
|
const guards = [{
|
|
5996
6302
|
ns: "applications",
|
|
5997
|
-
id:
|
|
6303
|
+
id: id7,
|
|
5998
6304
|
exists: true,
|
|
5999
6305
|
equals: Object.fromEntries(identity2.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
6000
6306
|
absent: identity2.filter((key3) => app[key3] === void 0)
|
|
6001
|
-
}, { ns: "subscriptionCheckoutIntents", id:
|
|
6307
|
+
}, { ns: "subscriptionCheckoutIntents", id: id7, exists: false }];
|
|
6002
6308
|
if (!row2 || row2.status === "ready" || row2.status === "restart") {
|
|
6003
6309
|
const revision = Number(row2?.revision ?? 0) + 1;
|
|
6004
6310
|
const pending = {
|
|
6005
|
-
id:
|
|
6311
|
+
id: id7,
|
|
6006
6312
|
...Object.fromEntries(keys2.map((key3) => [key3, input[key3]])),
|
|
6007
6313
|
revision,
|
|
6008
6314
|
status: "pending",
|
|
@@ -6010,25 +6316,25 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6010
6316
|
updatedAt: now
|
|
6011
6317
|
};
|
|
6012
6318
|
try {
|
|
6013
|
-
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id:
|
|
6014
|
-
mutationId: `membership-quote:${
|
|
6319
|
+
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id: id7, attrs: pending }], {
|
|
6320
|
+
mutationId: `membership-quote:${id7}:${revision}`,
|
|
6015
6321
|
guards: [...guards, {
|
|
6016
6322
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6017
|
-
id:
|
|
6323
|
+
id: id7,
|
|
6018
6324
|
exists: Boolean(row2),
|
|
6019
6325
|
...row2 ? { equals: { revision: row2.revision, status: row2.status } } : {}
|
|
6020
6326
|
}]
|
|
6021
6327
|
});
|
|
6022
6328
|
} catch (error) {
|
|
6023
6329
|
if (error.code !== "transact_guard_failed") throw error;
|
|
6024
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6330
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6025
6331
|
if (!winner || winner.revision !== revision) throw error;
|
|
6026
6332
|
}
|
|
6027
|
-
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6333
|
+
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6028
6334
|
}
|
|
6029
6335
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
6030
6336
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
6031
|
-
return
|
|
6337
|
+
return fail6("membership_quote_pending");
|
|
6032
6338
|
}
|
|
6033
6339
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
6034
6340
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6049,7 +6355,8 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6049
6355
|
policy: input.policy,
|
|
6050
6356
|
billingTerms: offer.contract.billingTerms,
|
|
6051
6357
|
presentation: input.presentation,
|
|
6052
|
-
founding: foundingOf(offer)
|
|
6358
|
+
founding: foundingOf(offer),
|
|
6359
|
+
...input.namedSeatsEnabled ? { seatOffer: { amountCents: offer.contract.offering.seatPriceCents } } : {}
|
|
6053
6360
|
});
|
|
6054
6361
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
6055
6362
|
await restore2(ready, target, now, true);
|
|
@@ -6057,23 +6364,100 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6057
6364
|
await db.transact([{
|
|
6058
6365
|
t: "update",
|
|
6059
6366
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6060
|
-
id:
|
|
6367
|
+
id: id7,
|
|
6061
6368
|
attrs: { offer, offerDigest: offer.digest, tier, quote, status: "ready", updatedAt: now }
|
|
6062
6369
|
}], {
|
|
6063
|
-
mutationId: `membership-quote-ready:${
|
|
6370
|
+
mutationId: `membership-quote-ready:${id7}:${row2.revision}`,
|
|
6064
6371
|
guards: [...guards, {
|
|
6065
6372
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6066
|
-
id:
|
|
6373
|
+
id: id7,
|
|
6067
6374
|
exists: true,
|
|
6068
6375
|
equals: { status: "pending", revision: row2.revision, requestId: row2.requestId }
|
|
6069
6376
|
}]
|
|
6070
6377
|
});
|
|
6071
6378
|
} catch (error) {
|
|
6072
6379
|
if (error.code !== "transact_guard_failed") throw error;
|
|
6073
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6380
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6074
6381
|
if (winner?.status !== "ready" || winner.requestId !== row2.requestId || winner.offerDigest !== offer.digest) throw error;
|
|
6075
6382
|
}
|
|
6076
|
-
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6383
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6384
|
+
}
|
|
6385
|
+
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6386
|
+
if (!input.enabled) return fail6("named_seat_purchase_unavailable");
|
|
6387
|
+
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6388
|
+
const current = await restore2(row2, target, input.now);
|
|
6389
|
+
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
6390
|
+
const quote = await createPaymentQuote({
|
|
6391
|
+
...target,
|
|
6392
|
+
tier: current.tier,
|
|
6393
|
+
group: { merchantDisclosureText: current.quote.merchantDisclosureText },
|
|
6394
|
+
policy: { refundPolicyText: current.quote.refundPolicyText, trustCopy: current.quote.trustCopy },
|
|
6395
|
+
billingTerms: current.billingTerms,
|
|
6396
|
+
presentation: current.quote.presentation,
|
|
6397
|
+
founding: current.quote.founding,
|
|
6398
|
+
seatOffer: { amountCents: current.membershipOffer.contract.offering.seatPriceCents },
|
|
6399
|
+
namedSeat
|
|
6400
|
+
});
|
|
6401
|
+
if (quote.digest === current.quote.digest) return current;
|
|
6402
|
+
await db.transact([{
|
|
6403
|
+
t: "update",
|
|
6404
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6405
|
+
id: target.applicationId,
|
|
6406
|
+
attrs: { quote, revision: current.projectionRevision + 1, updatedAt: input.now }
|
|
6407
|
+
}], {
|
|
6408
|
+
mutationId: `membership-quote-selection:${target.applicationId}:${current.projectionRevision}:${quote.digest}`,
|
|
6409
|
+
guards: [
|
|
6410
|
+
{
|
|
6411
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6412
|
+
id: target.applicationId,
|
|
6413
|
+
exists: true,
|
|
6414
|
+
equals: { revision: current.projectionRevision, status: "ready" }
|
|
6415
|
+
},
|
|
6416
|
+
{ ns: "subscriptionCheckoutIntents", id: target.applicationId, exists: false }
|
|
6417
|
+
]
|
|
6418
|
+
});
|
|
6419
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, input.now);
|
|
6420
|
+
}
|
|
6421
|
+
|
|
6422
|
+
// src/bounded-request-text.ts
|
|
6423
|
+
async function readBoundedRequestText(req, maxBytes) {
|
|
6424
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
6425
|
+
const reader = req.body?.getReader();
|
|
6426
|
+
const cancel = () => {
|
|
6427
|
+
void reader?.cancel().catch(() => {
|
|
6428
|
+
});
|
|
6429
|
+
};
|
|
6430
|
+
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
6431
|
+
let complete = false;
|
|
6432
|
+
req.signal.addEventListener("abort", cancel, { once: true });
|
|
6433
|
+
try {
|
|
6434
|
+
if (req.signal.aborted) return invalid2();
|
|
6435
|
+
const declared = req.headers.get("content-length");
|
|
6436
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
6437
|
+
return json({ error: "request body too large" }, 413);
|
|
6438
|
+
}
|
|
6439
|
+
if (!reader) return "";
|
|
6440
|
+
const decoder = new TextDecoder();
|
|
6441
|
+
let bytes = 0;
|
|
6442
|
+
let text8 = "";
|
|
6443
|
+
for (; ; ) {
|
|
6444
|
+
const { done, value } = await reader.read();
|
|
6445
|
+
if (req.signal.aborted) return invalid2();
|
|
6446
|
+
if (done) {
|
|
6447
|
+
complete = true;
|
|
6448
|
+
return text8 + decoder.decode();
|
|
6449
|
+
}
|
|
6450
|
+
bytes += value.byteLength;
|
|
6451
|
+
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6452
|
+
text8 += decoder.decode(value, { stream: true });
|
|
6453
|
+
}
|
|
6454
|
+
} catch {
|
|
6455
|
+
return invalid2();
|
|
6456
|
+
} finally {
|
|
6457
|
+
req.signal.removeEventListener("abort", cancel);
|
|
6458
|
+
if (!complete) cancel();
|
|
6459
|
+
reader?.releaseLock();
|
|
6460
|
+
}
|
|
6077
6461
|
}
|
|
6078
6462
|
|
|
6079
6463
|
// src/payment-quote-route.ts
|
|
@@ -6107,8 +6491,11 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6107
6491
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
6108
6492
|
billingTerms,
|
|
6109
6493
|
presentation: quote2.presentation,
|
|
6110
|
-
founding: foundingOf(intent.membershipOffer)
|
|
6494
|
+
founding: foundingOf(intent.membershipOffer),
|
|
6495
|
+
seatOffer: quote2.seatOffer,
|
|
6496
|
+
namedSeat: quote2.namedSeat
|
|
6111
6497
|
});
|
|
6498
|
+
if (quote2.seatOffer && quote2.seatOffer.amountCents !== intent.membershipOffer?.contract.offering.seatPriceCents) throw new Error("invalid seat snapshot");
|
|
6112
6499
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
6113
6500
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
6114
6501
|
}
|
|
@@ -6137,7 +6524,16 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6137
6524
|
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
6138
6525
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
6139
6526
|
db,
|
|
6140
|
-
{
|
|
6527
|
+
{
|
|
6528
|
+
...target,
|
|
6529
|
+
app,
|
|
6530
|
+
group,
|
|
6531
|
+
tier,
|
|
6532
|
+
policy: policy2,
|
|
6533
|
+
presentation,
|
|
6534
|
+
namedSeatsEnabled: Boolean(ctx.membershipAuthority.namedSeats) && !app.membershipRestartId,
|
|
6535
|
+
now: Date.now()
|
|
6536
|
+
},
|
|
6141
6537
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
6142
6538
|
);
|
|
6143
6539
|
const quote = await createPaymentQuote({
|
|
@@ -6153,20 +6549,54 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6153
6549
|
});
|
|
6154
6550
|
return { tier, quote, billingTerms: void 0 };
|
|
6155
6551
|
}
|
|
6156
|
-
async function paymentQuoteRoute(url, env, ctx, prepare = false) {
|
|
6157
|
-
|
|
6158
|
-
if (
|
|
6552
|
+
async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
6553
|
+
let selection;
|
|
6554
|
+
if (request?.body) {
|
|
6555
|
+
try {
|
|
6556
|
+
const text8 = await readBoundedRequestText(request, 2048);
|
|
6557
|
+
if (text8 instanceof Response) return text8;
|
|
6558
|
+
const body = JSON.parse(text8);
|
|
6559
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6560
|
+
selection = body.namedSeat;
|
|
6561
|
+
} catch {
|
|
6562
|
+
return json({ error: "invalid seat selection" }, 400);
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
const id7 = url.searchParams.get("application");
|
|
6566
|
+
if (!id7 || id7.length > 160) return json({ error: "application required" }, 400);
|
|
6159
6567
|
const db = ctx.makeDb(env);
|
|
6160
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
6568
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
6161
6569
|
if (!app || String(app.groupId ?? ctx.chapter.id) !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
6162
6570
|
if (typeof app.stripeRuntime === "string" && app.stripeRuntime !== stripeRuntimeIdentity(env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
6163
6571
|
if (app.status !== "submitted" || app.giftOfferId || app.namedSeatId || app.namedSeatClaimPending || app.giftClaimId || app.canceled === true) return json({ error: "membership checkout unavailable" }, 409);
|
|
6164
6572
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
6165
6573
|
if (!group) return json({ error: "payments not configured" }, 503);
|
|
6166
6574
|
try {
|
|
6167
|
-
|
|
6168
|
-
|
|
6575
|
+
let { quote } = await checkoutQuoteForApplication(db, app, group, env, ctx, prepare);
|
|
6576
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
6577
|
+
if (prepare && !intent && !quote.seatOffer && ctx.membershipAuthority?.namedSeats && !app.membershipRestartId && selection === void 0) selection = null;
|
|
6578
|
+
if (selection !== void 0) {
|
|
6579
|
+
if (intent) return json({ error: "checkout has already started", code: "checkout_intent_mismatch" }, 409);
|
|
6580
|
+
({ quote } = await selectCheckoutNamedSeat(
|
|
6581
|
+
db,
|
|
6582
|
+
{
|
|
6583
|
+
appId: env.ODLA_APP_ID,
|
|
6584
|
+
chapterId: ctx.chapter.id,
|
|
6585
|
+
environment: env.ODLA_ENV,
|
|
6586
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
6587
|
+
applicationId: id7,
|
|
6588
|
+
skuId: String(app.tierId)
|
|
6589
|
+
},
|
|
6590
|
+
{ selection, purchaserEmail: String(app.email ?? ""), now: Date.now(), enabled: Boolean(ctx.membershipAuthority?.namedSeats) && !app.membershipRestartId }
|
|
6591
|
+
));
|
|
6592
|
+
}
|
|
6593
|
+
return new Response(JSON.stringify(quote), { headers: {
|
|
6594
|
+
"content-type": "application/json",
|
|
6595
|
+
"cache-control": "no-store",
|
|
6596
|
+
"x-chapter-checkout-started": intent ? "true" : "false"
|
|
6597
|
+
} });
|
|
6169
6598
|
} catch (error) {
|
|
6599
|
+
if (error.code === "invalid_named_seat_recipient") return json({ error: "Enter a different recipient's name and email." }, 400);
|
|
6170
6600
|
const expired = error.code === "membership_quote_expired";
|
|
6171
6601
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_expired" } : {} }, expired ? 409 : 503);
|
|
6172
6602
|
}
|
|
@@ -6272,47 +6702,6 @@ async function resumeJoin(url, env, ctx) {
|
|
|
6272
6702
|
return json({ error: "Application continuation is unavailable. Retry when its payment or membership configuration is restored." }, 409);
|
|
6273
6703
|
}
|
|
6274
6704
|
|
|
6275
|
-
// src/bounded-request-text.ts
|
|
6276
|
-
async function readBoundedRequestText(req, maxBytes) {
|
|
6277
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
6278
|
-
const reader = req.body?.getReader();
|
|
6279
|
-
const cancel = () => {
|
|
6280
|
-
void reader?.cancel().catch(() => {
|
|
6281
|
-
});
|
|
6282
|
-
};
|
|
6283
|
-
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
6284
|
-
let complete = false;
|
|
6285
|
-
req.signal.addEventListener("abort", cancel, { once: true });
|
|
6286
|
-
try {
|
|
6287
|
-
if (req.signal.aborted) return invalid2();
|
|
6288
|
-
const declared = req.headers.get("content-length");
|
|
6289
|
-
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
6290
|
-
return json({ error: "request body too large" }, 413);
|
|
6291
|
-
}
|
|
6292
|
-
if (!reader) return "";
|
|
6293
|
-
const decoder = new TextDecoder();
|
|
6294
|
-
let bytes = 0;
|
|
6295
|
-
let text8 = "";
|
|
6296
|
-
for (; ; ) {
|
|
6297
|
-
const { done, value } = await reader.read();
|
|
6298
|
-
if (req.signal.aborted) return invalid2();
|
|
6299
|
-
if (done) {
|
|
6300
|
-
complete = true;
|
|
6301
|
-
return text8 + decoder.decode();
|
|
6302
|
-
}
|
|
6303
|
-
bytes += value.byteLength;
|
|
6304
|
-
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6305
|
-
text8 += decoder.decode(value, { stream: true });
|
|
6306
|
-
}
|
|
6307
|
-
} catch {
|
|
6308
|
-
return invalid2();
|
|
6309
|
-
} finally {
|
|
6310
|
-
req.signal.removeEventListener("abort", cancel);
|
|
6311
|
-
if (!complete) cancel();
|
|
6312
|
-
reader?.releaseLock();
|
|
6313
|
-
}
|
|
6314
|
-
}
|
|
6315
|
-
|
|
6316
6705
|
// src/named-seat-application.ts
|
|
6317
6706
|
async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
|
|
6318
6707
|
const app = await membershipLoad(db, "applications", applicationId);
|
|
@@ -6827,11 +7216,12 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
6827
7216
|
group,
|
|
6828
7217
|
Boolean(stripeKey)
|
|
6829
7218
|
);
|
|
6830
|
-
const
|
|
7219
|
+
const seats = /* @__PURE__ */ new Map();
|
|
7220
|
+
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
6831
7221
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
6832
7222
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
6833
7223
|
return json({
|
|
6834
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
7224
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
|
|
6835
7225
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
6836
7226
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
6837
7227
|
...activeControl ? { signupControl: {
|
|
@@ -7207,10 +7597,10 @@ function tiersOf(value, errors) {
|
|
|
7207
7597
|
return [];
|
|
7208
7598
|
}
|
|
7209
7599
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7210
|
-
const
|
|
7211
|
-
if (
|
|
7212
|
-
if (seen.has(
|
|
7213
|
-
seen.add(
|
|
7600
|
+
const id7 = text4(item.id, `${path}.id`, errors, 80);
|
|
7601
|
+
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7602
|
+
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7603
|
+
seen.add(id7);
|
|
7214
7604
|
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
7215
7605
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7216
7606
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
@@ -7231,7 +7621,7 @@ function tiersOf(value, errors) {
|
|
|
7231
7621
|
}
|
|
7232
7622
|
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7233
7623
|
return [{
|
|
7234
|
-
id:
|
|
7624
|
+
id: id7,
|
|
7235
7625
|
name,
|
|
7236
7626
|
available: item.available === true,
|
|
7237
7627
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -7293,15 +7683,15 @@ function policyOf(value, errors, tiers) {
|
|
|
7293
7683
|
if (input.tierPolicies !== void 0) {
|
|
7294
7684
|
const overrides = recordOf(input.tierPolicies);
|
|
7295
7685
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
7296
|
-
else for (const [
|
|
7297
|
-
const path = `document.policy.tierPolicies.${
|
|
7686
|
+
else for (const [id7, raw2] of Object.entries(overrides)) {
|
|
7687
|
+
const path = `document.policy.tierPolicies.${id7}`;
|
|
7298
7688
|
const policy2 = recordOf(raw2);
|
|
7299
|
-
if (!tiers.some((tier) => tier.id ===
|
|
7689
|
+
if (!tiers.some((tier) => tier.id === id7) || !policy2) {
|
|
7300
7690
|
errors.push(`${path} must name a declared tier and policy`);
|
|
7301
7691
|
continue;
|
|
7302
7692
|
}
|
|
7303
7693
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7304
|
-
tierPolicies[
|
|
7694
|
+
tierPolicies[id7] = {
|
|
7305
7695
|
refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
7306
7696
|
...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7307
7697
|
};
|
|
@@ -7594,14 +7984,14 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7594
7984
|
continue;
|
|
7595
7985
|
}
|
|
7596
7986
|
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
7597
|
-
const
|
|
7987
|
+
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7598
7988
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7599
7989
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7600
|
-
if (
|
|
7990
|
+
if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7601
7991
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7602
|
-
if (ids2.has(
|
|
7992
|
+
if (ids2.has(id7) || knownParentIds.has(id7)) errors.push(`${path}.id duplicates page "${id7}"`);
|
|
7603
7993
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
7604
|
-
ids2.add(
|
|
7994
|
+
ids2.add(id7);
|
|
7605
7995
|
slugs.add(slug);
|
|
7606
7996
|
const navRaw = record2(raw2.navigation);
|
|
7607
7997
|
let navigation = { section: "General", order: index2 };
|
|
@@ -7657,7 +8047,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7657
8047
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
7658
8048
|
}
|
|
7659
8049
|
}
|
|
7660
|
-
pages.push({ id:
|
|
8050
|
+
pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
7661
8051
|
}
|
|
7662
8052
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
7663
8053
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -7800,20 +8190,20 @@ async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
|
7800
8190
|
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7801
8191
|
}
|
|
7802
8192
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
7803
|
-
const
|
|
7804
|
-
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id:
|
|
8193
|
+
const id7 = `member-content-local:${environment}`;
|
|
8194
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
|
|
7805
8195
|
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
7806
8196
|
}
|
|
7807
8197
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
7808
8198
|
const { document, digest } = envelope;
|
|
7809
8199
|
const target = document.target;
|
|
7810
|
-
const
|
|
8200
|
+
const id7 = `member-content-control:${target.environment}:${document.revision}`;
|
|
7811
8201
|
const ops = [];
|
|
7812
8202
|
if (previous?.active) {
|
|
7813
8203
|
ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
|
|
7814
8204
|
}
|
|
7815
|
-
ops.push({ t: "update", ns: "memberContentControls", id:
|
|
7816
|
-
id:
|
|
8205
|
+
ops.push({ t: "update", ns: "memberContentControls", id: id7, attrs: {
|
|
8206
|
+
id: id7,
|
|
7817
8207
|
sourceId: document.source.appId,
|
|
7818
8208
|
targetId: target.chapterId,
|
|
7819
8209
|
environment: target.environment,
|
|
@@ -7826,11 +8216,11 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
7826
8216
|
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
|
|
7827
8217
|
}
|
|
7828
8218
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7829
|
-
const
|
|
8219
|
+
const id7 = `member-content-local:${environment}`;
|
|
7830
8220
|
const current = await loadLocalMemberContent(db, document.source.appId, environment);
|
|
7831
8221
|
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
|
|
7832
8222
|
const row2 = {
|
|
7833
|
-
id:
|
|
8223
|
+
id: id7,
|
|
7834
8224
|
chapterId: document.source.appId,
|
|
7835
8225
|
environment,
|
|
7836
8226
|
revision: document.revision,
|
|
@@ -7838,9 +8228,9 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
|
|
|
7838
8228
|
updatedAt,
|
|
7839
8229
|
...updatedBy ? { updatedBy } : {}
|
|
7840
8230
|
};
|
|
7841
|
-
await db.transact([{ t: "update", ns: "memberContentLocal", id:
|
|
8231
|
+
await db.transact([{ t: "update", ns: "memberContentLocal", id: id7, attrs: row2 }], {
|
|
7842
8232
|
mutationId: `member-content-local:${environment}:${document.revision}`,
|
|
7843
|
-
guards: current ? [{ ns: "memberContentLocal", id:
|
|
8233
|
+
guards: current ? [{ ns: "memberContentLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id: id7, exists: false }]
|
|
7844
8234
|
});
|
|
7845
8235
|
return row2;
|
|
7846
8236
|
}
|
|
@@ -7977,7 +8367,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
7977
8367
|
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
7978
8368
|
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
7979
8369
|
effective,
|
|
7980
|
-
tiers: tiers.map(({ id:
|
|
8370
|
+
tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
|
|
7981
8371
|
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
7982
8372
|
});
|
|
7983
8373
|
}
|
|
@@ -8135,11 +8525,11 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8135
8525
|
return json({ error: "expiresAt must be a future epoch timestamp" }, 400);
|
|
8136
8526
|
}
|
|
8137
8527
|
const sourceGrantId = crypto.randomUUID();
|
|
8138
|
-
const
|
|
8528
|
+
const id7 = `admission:local:${ctx.chapter.id}:${sourceGrantId}`;
|
|
8139
8529
|
const createdAt = Date.now();
|
|
8140
8530
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8141
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8142
|
-
id:
|
|
8531
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8532
|
+
id: id7,
|
|
8143
8533
|
sourceGrantId,
|
|
8144
8534
|
sourceScope: "local",
|
|
8145
8535
|
sourceId: ctx.chapter.id,
|
|
@@ -8155,7 +8545,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8155
8545
|
updatedAt: createdAt,
|
|
8156
8546
|
updatedBy: actor
|
|
8157
8547
|
} }], { mutationId: `admission-local-create:${sourceGrantId}` });
|
|
8158
|
-
return json({ grant: { id:
|
|
8548
|
+
return json({ grant: { id: id7, sourceGrantId, sourceScope: "local", email, decision, active: true, createdAt, ...note ? { note } : {}, ...expiresAt ? { expiresAt } : {} } }, 201);
|
|
8159
8549
|
}
|
|
8160
8550
|
const prefix = "/api/admin/admission-grants/";
|
|
8161
8551
|
let revokeId = "";
|
|
@@ -8169,20 +8559,20 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8169
8559
|
if (/^admission:local:[a-z0-9-]+:[0-9a-f-]+$/.test(revokeId) && req.method === "DELETE") {
|
|
8170
8560
|
const auth = await gate(req, env, ctx);
|
|
8171
8561
|
if (auth instanceof Response) return auth;
|
|
8172
|
-
const
|
|
8173
|
-
const found = await auth.db.query({ admissionGrants: { $: { where: { id:
|
|
8562
|
+
const id7 = revokeId;
|
|
8563
|
+
const found = await auth.db.query({ admissionGrants: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8174
8564
|
const row2 = Array.isArray(found.admissionGrants) ? found.admissionGrants[0] : void 0;
|
|
8175
8565
|
if (!row2 || row2.targetId !== ctx.chapter.id || row2.sourceScope !== "local") return json({ error: "not found" }, 404);
|
|
8176
8566
|
if (row2.active !== true) return json({ ok: true, replayed: true });
|
|
8177
8567
|
const revokedAt = Date.now();
|
|
8178
8568
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8179
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8569
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8180
8570
|
active: false,
|
|
8181
8571
|
revokedAt,
|
|
8182
8572
|
revokedBy: actor,
|
|
8183
8573
|
updatedAt: revokedAt,
|
|
8184
8574
|
updatedBy: actor
|
|
8185
|
-
} }], { mutationId: `admission-local-revoke:${
|
|
8575
|
+
} }], { mutationId: `admission-local-revoke:${id7}` });
|
|
8186
8576
|
return json({ ok: true, replayed: false });
|
|
8187
8577
|
}
|
|
8188
8578
|
return null;
|
|
@@ -8244,8 +8634,8 @@ async function loadLeaderAdmissionAssignments(db, targetId) {
|
|
|
8244
8634
|
return await readAdmissionRows(db, "leaderAdmissionAssignments", { targetId }, MAX_ASSIGNMENTS);
|
|
8245
8635
|
}
|
|
8246
8636
|
async function loadLeaderAdmissionDelivery(db, targetId, environment) {
|
|
8247
|
-
const
|
|
8248
|
-
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id:
|
|
8637
|
+
const id7 = deliveryId(targetId, environment);
|
|
8638
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8249
8639
|
return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
|
|
8250
8640
|
}
|
|
8251
8641
|
function personAdmissionEmail(ctx, record4) {
|
|
@@ -8273,8 +8663,8 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8273
8663
|
const now = Date.now();
|
|
8274
8664
|
const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
8275
8665
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
8276
|
-
const
|
|
8277
|
-
const existing = current.find((row2) => row2.id ===
|
|
8666
|
+
const id7 = assignmentId(opts.target.id, opts.personId);
|
|
8667
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
8278
8668
|
const digest = await signupControlDigest({
|
|
8279
8669
|
targetId: opts.target.id,
|
|
8280
8670
|
personId: opts.personId,
|
|
@@ -8304,7 +8694,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8304
8694
|
return { delivery: saved, duplicate: true };
|
|
8305
8695
|
}
|
|
8306
8696
|
const assignment = {
|
|
8307
|
-
id:
|
|
8697
|
+
id: id7,
|
|
8308
8698
|
targetId: opts.target.id,
|
|
8309
8699
|
personId: opts.personId,
|
|
8310
8700
|
grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
@@ -8319,7 +8709,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8319
8709
|
updatedAt: now,
|
|
8320
8710
|
updatedBy: opts.actor
|
|
8321
8711
|
};
|
|
8322
|
-
const desired = current.filter((row2) => row2.id !==
|
|
8712
|
+
const desired = current.filter((row2) => row2.id !== id7 && row2.active).concat(assignment.active ? [assignment] : []);
|
|
8323
8713
|
const grants = await Promise.all(desired.map((row2) => grantInput(opts.db, opts.ctx, row2, opts.resolveEmail)));
|
|
8324
8714
|
const revision = (previous?.revision ?? 0) + 1;
|
|
8325
8715
|
const envelope = await createAdmissionGrantEnvelope({
|
|
@@ -8342,7 +8732,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8342
8732
|
updatedAt: now
|
|
8343
8733
|
};
|
|
8344
8734
|
const ops = [
|
|
8345
|
-
{ t: "update", ns: "leaderAdmissionAssignments", id:
|
|
8735
|
+
{ t: "update", ns: "leaderAdmissionAssignments", id: id7, attrs: assignment },
|
|
8346
8736
|
{ t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
|
|
8347
8737
|
{ t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
|
|
8348
8738
|
id: mutationReceiptId,
|
|
@@ -8357,7 +8747,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8357
8747
|
...!opts.note && existing?.note !== void 0 ? ["note"] : [],
|
|
8358
8748
|
...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
8359
8749
|
];
|
|
8360
|
-
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id:
|
|
8750
|
+
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id: id7, attrs: retract });
|
|
8361
8751
|
const tx = await opts.db.transact(ops, {
|
|
8362
8752
|
mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
|
|
8363
8753
|
guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
|
|
@@ -8417,11 +8807,11 @@ function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
|
|
|
8417
8807
|
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
8418
8808
|
);
|
|
8419
8809
|
const def = crm.type(record4.type);
|
|
8420
|
-
const editFields = editable.map((
|
|
8421
|
-
const field = def.fields[
|
|
8810
|
+
const editFields = editable.map((id7) => {
|
|
8811
|
+
const field = def.fields[id7];
|
|
8422
8812
|
return {
|
|
8423
|
-
id:
|
|
8424
|
-
label: field.label ??
|
|
8813
|
+
id: id7,
|
|
8814
|
+
label: field.label ?? id7,
|
|
8425
8815
|
type: field.type,
|
|
8426
8816
|
required: field.required ?? false,
|
|
8427
8817
|
...field.options ? { options: field.options } : {}
|
|
@@ -8538,8 +8928,8 @@ async function gate2(req, env, ctx) {
|
|
|
8538
8928
|
if (!await networkWriteAllowed(req, db, user, ctx, true)) return { response: reject6(403, "super_admin_required", "super-admin required") };
|
|
8539
8929
|
return { db, user };
|
|
8540
8930
|
}
|
|
8541
|
-
function targetFor(ctx,
|
|
8542
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
8931
|
+
function targetFor(ctx, id7) {
|
|
8932
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7 && target.admissionGrants);
|
|
8543
8933
|
}
|
|
8544
8934
|
async function deliverSaved(db, env, ctx, target, delivery) {
|
|
8545
8935
|
if (delivery.status === "acknowledged") return delivery;
|
|
@@ -8643,7 +9033,7 @@ var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
|
8643
9033
|
if (!personId || !MUTATION_ID2.test(mutationId)) {
|
|
8644
9034
|
return reject6(422, "invalid_request", "personId and a stable mutationId are required");
|
|
8645
9035
|
}
|
|
8646
|
-
const resolveEmail = (
|
|
9036
|
+
const resolveEmail = (id7) => resolveAdmissionPersonEmail(db, env, ctx, target, id7);
|
|
8647
9037
|
let email;
|
|
8648
9038
|
try {
|
|
8649
9039
|
email = await resolveEmail(personId);
|
|
@@ -8882,28 +9272,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8882
9272
|
};
|
|
8883
9273
|
|
|
8884
9274
|
// src/payments-stripe.ts
|
|
8885
|
-
var
|
|
9275
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
8886
9276
|
|
|
8887
9277
|
// src/payments-refund.ts
|
|
8888
|
-
var
|
|
9278
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
8889
9279
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8890
|
-
var
|
|
9280
|
+
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8891
9281
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
8892
|
-
function
|
|
9282
|
+
function fail7(code = "refund_target_mismatch") {
|
|
8893
9283
|
throw Object.assign(new Error(code), { code });
|
|
8894
9284
|
}
|
|
8895
9285
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8896
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9286
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
|
|
8897
9287
|
const call = async (method, path, params, key3) => {
|
|
8898
|
-
const result = await (0,
|
|
8899
|
-
if (!result.ok)
|
|
8900
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
9288
|
+
const result = await (0, import_stripe10.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9289
|
+
if (!result.ok) fail7("stripe_error");
|
|
9290
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
|
|
8901
9291
|
return result.body;
|
|
8902
9292
|
};
|
|
8903
9293
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8904
|
-
if (
|
|
9294
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
|
|
8905
9295
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
8906
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9296
|
+
if (row(sub.metadata)[key3] !== value) fail7();
|
|
8907
9297
|
}
|
|
8908
9298
|
let cursor;
|
|
8909
9299
|
let earliest;
|
|
@@ -8916,79 +9306,79 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8916
9306
|
limit: 100,
|
|
8917
9307
|
...cursor ? { starting_after: cursor } : {}
|
|
8918
9308
|
});
|
|
8919
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9309
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
|
|
8920
9310
|
for (const value of list.data) {
|
|
8921
9311
|
const invoice2 = row(value);
|
|
8922
9312
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
8923
|
-
if (!providerId2(
|
|
8924
|
-
seen.add(
|
|
9313
|
+
if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail7();
|
|
9314
|
+
seen.add(id3(invoice2));
|
|
8925
9315
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
8926
9316
|
}
|
|
8927
9317
|
if (!list.has_more) break;
|
|
8928
|
-
if (!list.data.length || page === 9)
|
|
8929
|
-
cursor =
|
|
9318
|
+
if (!list.data.length || page === 9) fail7("refund_history_incomplete");
|
|
9319
|
+
cursor = id3(list.data[list.data.length - 1]);
|
|
8930
9320
|
}
|
|
8931
|
-
if (!earliest)
|
|
9321
|
+
if (!earliest) fail7("no_charge");
|
|
8932
9322
|
const invoice = earliest;
|
|
8933
|
-
if (input.expectedInvoiceId !== void 0 &&
|
|
8934
|
-
let chargeId =
|
|
8935
|
-
let paymentIntentId =
|
|
9323
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
|
|
9324
|
+
let chargeId = id3(invoice.charge);
|
|
9325
|
+
let paymentIntentId = id3(invoice.payment_intent);
|
|
8936
9326
|
if (!chargeId && !paymentIntentId) {
|
|
8937
|
-
const payments = await call("GET", "/v1/invoice_payments", { invoice:
|
|
8938
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
9327
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
|
|
9328
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
|
|
8939
9329
|
const payment = row(payments.data[0]);
|
|
8940
|
-
if (
|
|
8941
|
-
paymentIntentId =
|
|
9330
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
|
|
9331
|
+
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
8942
9332
|
}
|
|
8943
9333
|
if (!chargeId) {
|
|
8944
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9334
|
+
if (!providerId2(paymentIntentId, "pi")) fail7();
|
|
8945
9335
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
8946
|
-
if (
|
|
8947
|
-
chargeId =
|
|
9336
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
|
|
9337
|
+
chargeId = id3(payment.latest_charge);
|
|
8948
9338
|
}
|
|
8949
|
-
if (!providerId2(chargeId, "ch"))
|
|
9339
|
+
if (!providerId2(chargeId, "ch")) fail7();
|
|
8950
9340
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
8951
|
-
if (
|
|
8952
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
9341
|
+
if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail7();
|
|
9342
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
|
|
8953
9343
|
if (charge.amount_refunded !== charge.amount) {
|
|
8954
9344
|
const refund = await call("POST", "/v1/refunds", {
|
|
8955
9345
|
charge: chargeId,
|
|
8956
9346
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
8957
9347
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
8958
|
-
if (
|
|
9348
|
+
if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail7("refund_not_confirmed");
|
|
8959
9349
|
}
|
|
8960
9350
|
let subscriptionCanceled = sub.status === "canceled";
|
|
8961
9351
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
8962
9352
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8963
|
-
if (
|
|
9353
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
|
|
8964
9354
|
subscriptionCanceled = true;
|
|
8965
9355
|
}
|
|
8966
9356
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
8967
|
-
if (
|
|
9357
|
+
if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail7("refund_not_confirmed");
|
|
8968
9358
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
8969
9359
|
}
|
|
8970
9360
|
|
|
8971
9361
|
// src/subscription-payment-identity.ts
|
|
8972
|
-
var
|
|
8973
|
-
var
|
|
9362
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
9363
|
+
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8974
9364
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8975
|
-
let paymentId =
|
|
9365
|
+
let paymentId = id4(invoice.payment_intent);
|
|
8976
9366
|
if (!paymentId) {
|
|
8977
|
-
if (!/^in_[A-Za-z0-9]+$/.test(
|
|
8978
|
-
const result2 = await (0,
|
|
9367
|
+
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9368
|
+
const result2 = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
8979
9369
|
const list = result2.body;
|
|
8980
9370
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8981
9371
|
const link = list.data[0];
|
|
8982
|
-
if (
|
|
8983
|
-
paymentId =
|
|
9372
|
+
if (id4(link.invoice) !== id4(invoice) || !["open", "paid"].includes(link.status) || link.payment?.type !== "payment_intent") subscriptionContractMismatch();
|
|
9373
|
+
paymentId = id4(link.payment.payment_intent);
|
|
8984
9374
|
}
|
|
8985
9375
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8986
9376
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8987
|
-
const result = await (0,
|
|
9377
|
+
const result = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", path);
|
|
8988
9378
|
const payment = result.body;
|
|
8989
|
-
if (!result.ok || payment?.id !== paymentId ||
|
|
9379
|
+
if (!result.ok || payment?.id !== paymentId || id4(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
|
|
8990
9380
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8991
|
-
const tagged = await (0,
|
|
9381
|
+
const tagged = await (0, import_stripe11.stripeRequest)(
|
|
8992
9382
|
{ secretKey },
|
|
8993
9383
|
"POST",
|
|
8994
9384
|
path,
|
|
@@ -9000,7 +9390,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9000
9390
|
|
|
9001
9391
|
// src/payments-stripe.ts
|
|
9002
9392
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
9003
|
-
return (0,
|
|
9393
|
+
return (0, import_stripe12.stripeRequest)(
|
|
9004
9394
|
{ secretKey: sk },
|
|
9005
9395
|
method,
|
|
9006
9396
|
path,
|
|
@@ -9008,7 +9398,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
9008
9398
|
idempotencyKey ? { idempotencyKey } : {}
|
|
9009
9399
|
);
|
|
9010
9400
|
}
|
|
9011
|
-
function
|
|
9401
|
+
function fail8(op, r) {
|
|
9012
9402
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
9013
9403
|
err.code = "stripe_error";
|
|
9014
9404
|
throw err;
|
|
@@ -9047,6 +9437,15 @@ function createStripeProvider(config) {
|
|
|
9047
9437
|
let customerId = input.existingCustomerId;
|
|
9048
9438
|
if (customerId && !/^cus_[A-Za-z0-9]+$/.test(customerId) || input.existingSubscriptionId && (!customerId || !/^sub_[A-Za-z0-9]+$/.test(input.existingSubscriptionId))) subscriptionContractMismatch();
|
|
9049
9439
|
const productId = await verifySubscriptionPrice(sk, input, Boolean(input.existingSubscriptionId));
|
|
9440
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
9441
|
+
const seatProductId = seat ? await verifySubscriptionPrice(sk, {
|
|
9442
|
+
...input,
|
|
9443
|
+
priceId: seat.priceId,
|
|
9444
|
+
priceCents: seat.amountCents,
|
|
9445
|
+
billingTerms: void 0,
|
|
9446
|
+
namedSeat: void 0
|
|
9447
|
+
}, Boolean(input.existingSubscriptionId)) : void 0;
|
|
9448
|
+
if (seat && (!seatProductId || !productId || seatProductId === productId)) subscriptionContractMismatch();
|
|
9050
9449
|
if (input.billingTerms?.discount && !productId) subscriptionContractMismatch();
|
|
9051
9450
|
await prepareSubscriptionBillingTerms(sk, { ...input, discountProductId: productId }, Boolean(input.existingSubscriptionId));
|
|
9052
9451
|
const billingParams = subscriptionBillingParams(input, productId);
|
|
@@ -9058,7 +9457,7 @@ function createStripeProvider(config) {
|
|
|
9058
9457
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9059
9458
|
`cus:${input.applicationId}`
|
|
9060
9459
|
);
|
|
9061
|
-
if (!cust.ok)
|
|
9460
|
+
if (!cust.ok) fail8("customer create", cust);
|
|
9062
9461
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9063
9462
|
customerId = cust.body.id;
|
|
9064
9463
|
}
|
|
@@ -9078,19 +9477,32 @@ function createStripeProvider(config) {
|
|
|
9078
9477
|
{
|
|
9079
9478
|
customer: customerId,
|
|
9080
9479
|
"items[0][price]": input.priceId,
|
|
9480
|
+
...seat ? {
|
|
9481
|
+
"items[1][price]": seat.priceId,
|
|
9482
|
+
"items[1][quantity]": 1,
|
|
9483
|
+
"items[1][metadata][seatId]": seat.seatId,
|
|
9484
|
+
"items[1][metadata][membershipId]": seat.membershipId
|
|
9485
|
+
} : {},
|
|
9081
9486
|
payment_behavior: "default_incomplete",
|
|
9082
9487
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
9083
9488
|
"payment_settings[payment_method_types][0]": "card",
|
|
9084
9489
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
9085
9490
|
"expand[1]": "latest_invoice.payment_intent",
|
|
9086
9491
|
metadata: meta,
|
|
9087
|
-
...billingParams
|
|
9492
|
+
...billingParams,
|
|
9493
|
+
...seat && input.billingTerms?.initialTermEndsAt ? {
|
|
9494
|
+
"add_invoice_items[1][price_data][product]": seatProductId,
|
|
9495
|
+
"add_invoice_items[1][price_data][currency]": input.currency,
|
|
9496
|
+
"add_invoice_items[1][price_data][unit_amount]": seat.amountCents,
|
|
9497
|
+
"add_invoice_items[1][quantity]": 1,
|
|
9498
|
+
"add_invoice_items[1][discountable]": false
|
|
9499
|
+
} : {}
|
|
9088
9500
|
},
|
|
9089
9501
|
// Hardening: one subscription per application, so a client
|
|
9090
9502
|
// retry between create and the db write can't orphan a second one.
|
|
9091
9503
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9092
9504
|
);
|
|
9093
|
-
if (!sub.ok)
|
|
9505
|
+
if (!sub.ok) fail8("subscription create", sub);
|
|
9094
9506
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9095
9507
|
const subscriptionId = sub.body.id;
|
|
9096
9508
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9100,14 +9512,14 @@ function createStripeProvider(config) {
|
|
|
9100
9512
|
"expand[2]": "discounts",
|
|
9101
9513
|
"expand[3]": "latest_invoice.discounts"
|
|
9102
9514
|
});
|
|
9103
|
-
if (!expanded.ok)
|
|
9515
|
+
if (!expanded.ok) fail8("subscription invoice evidence", expanded);
|
|
9104
9516
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9105
9517
|
sub = expanded;
|
|
9106
9518
|
}
|
|
9107
9519
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9108
9520
|
const invoice = sub.body.latest_invoice;
|
|
9109
9521
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9110
|
-
if (!clientSecret2)
|
|
9522
|
+
if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
|
|
9111
9523
|
await tagSubscriptionPayment(
|
|
9112
9524
|
sk,
|
|
9113
9525
|
invoice,
|
|
@@ -9119,7 +9531,7 @@ function createStripeProvider(config) {
|
|
|
9119
9531
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
9120
9532
|
},
|
|
9121
9533
|
async ingestWebhook(rawBody, sigHeader) {
|
|
9122
|
-
if (!webhookSecret || !await (0,
|
|
9534
|
+
if (!webhookSecret || !await (0, import_stripe12.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
9123
9535
|
return { ok: false, reason: "bad_signature" };
|
|
9124
9536
|
}
|
|
9125
9537
|
const event2 = JSON.parse(rawBody);
|
|
@@ -9319,9 +9731,9 @@ function matchesIntent(stored, expected) {
|
|
|
9319
9731
|
"purchaseAttemptId"
|
|
9320
9732
|
].every((key3) => stored[key3] === expected[key3]) && customerMatches && JSON.stringify(storedTerms) === JSON.stringify(expected.billingTerms) && stored.membershipOffer?.digest === expected.membershipOffer?.digest && stored.paymentQuote?.digest === expected.paymentQuote?.digest;
|
|
9321
9733
|
}
|
|
9322
|
-
async function loadIntent(db,
|
|
9734
|
+
async function loadIntent(db, id7) {
|
|
9323
9735
|
return first3((await db.query({
|
|
9324
|
-
subscriptionCheckoutIntents: { $: { where: { id:
|
|
9736
|
+
subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } }
|
|
9325
9737
|
})).subscriptionCheckoutIntents);
|
|
9326
9738
|
}
|
|
9327
9739
|
async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCheckout) {
|
|
@@ -9342,16 +9754,22 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9342
9754
|
if (input.purchaseAttemptId !== application?.membershipRestartId) mismatch();
|
|
9343
9755
|
if (application?.namedSeatId || application?.namedSeatClaimPending || application?.giftClaimId) mismatch();
|
|
9344
9756
|
if (input.membershipOffer && (!application || application.canceled === true || application.status !== "submitted")) mismatch();
|
|
9757
|
+
if (input.paymentQuote?.namedSeat && application?.email !== input.email) mismatch();
|
|
9345
9758
|
const applicationGuard = {
|
|
9346
9759
|
ns: "applications",
|
|
9347
9760
|
id: input.applicationId,
|
|
9348
9761
|
exists: true,
|
|
9349
|
-
equals: {
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
...
|
|
9354
|
-
|
|
9762
|
+
equals: {
|
|
9763
|
+
status: "submitted",
|
|
9764
|
+
...input.paymentQuote?.namedSeat ? { email: input.email } : {},
|
|
9765
|
+
...input.purchaseAttemptId ? { membershipRestartId: input.purchaseAttemptId } : {},
|
|
9766
|
+
...input.membershipOffer ? {
|
|
9767
|
+
groupId: input.groupId,
|
|
9768
|
+
tierId: input.tierId,
|
|
9769
|
+
stripeRuntime: stripeRuntimeIdentity(input.runtime),
|
|
9770
|
+
...application?.canceled !== void 0 ? { canceled: application.canceled } : {}
|
|
9771
|
+
} : {}
|
|
9772
|
+
},
|
|
9355
9773
|
absent: ["namedSeatId", "namedSeatClaimPending", "giftClaimId", ...!input.purchaseAttemptId ? ["membershipRestartId"] : [], ...input.membershipOffer && application?.canceled === void 0 ? ["canceled"] : []]
|
|
9356
9774
|
};
|
|
9357
9775
|
let stored = await loadIntent(db, input.applicationId);
|
|
@@ -9375,7 +9793,11 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9375
9793
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
9376
9794
|
id: input.applicationId,
|
|
9377
9795
|
exists: true,
|
|
9378
|
-
equals: {
|
|
9796
|
+
equals: {
|
|
9797
|
+
status: "ready",
|
|
9798
|
+
offerDigest: input.membershipOffer.digest,
|
|
9799
|
+
...input.projectionRevision !== void 0 ? { revision: input.projectionRevision } : {}
|
|
9800
|
+
}
|
|
9379
9801
|
}
|
|
9380
9802
|
] : []
|
|
9381
9803
|
]
|
|
@@ -9390,7 +9812,8 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9390
9812
|
if (!stored.providerSubscriptionId && input.now - Number(stored.createdAt) >= 23 * 60 * 6e4) {
|
|
9391
9813
|
throw Object.assign(new Error("checkout requires provider reconciliation"), { code: "checkout_recovery_required" });
|
|
9392
9814
|
}
|
|
9393
|
-
|
|
9815
|
+
const binding = input.membershipOffer ? await authorizeNetworkCheckout() : void 0;
|
|
9816
|
+
if (input.paymentQuote?.namedSeat && (!binding?.namedSeat || binding.namedSeat.amountCents !== input.paymentQuote.lineItems.namedSeatCents || binding.namedSeat.recipientEmail !== input.paymentQuote.namedSeat.recipientEmail || binding.namedSeat.recipientName !== input.paymentQuote.namedSeat.recipientName || binding.namedSeat.purchaserEmail !== input.emailNormalized)) mismatch();
|
|
9394
9817
|
const result = await provider.createSubscription({
|
|
9395
9818
|
appId: input.appId,
|
|
9396
9819
|
chapterId: input.chapterId,
|
|
@@ -9405,6 +9828,7 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9405
9828
|
priceCents: input.priceCents,
|
|
9406
9829
|
currency: input.currency,
|
|
9407
9830
|
interval: input.interval,
|
|
9831
|
+
...binding?.namedSeat ? { namedSeat: binding.namedSeat } : {},
|
|
9408
9832
|
...stored.billingTerms ? { billingTerms: stored.billingTerms } : {},
|
|
9409
9833
|
...typeof stored.providerSubscriptionId === "string" ? { existingSubscriptionId: stored.providerSubscriptionId } : {},
|
|
9410
9834
|
existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
|
|
@@ -9521,6 +9945,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9521
9945
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_changed" } : {} }, expired ? 409 : 503);
|
|
9522
9946
|
}
|
|
9523
9947
|
const { tier, quote, billingTerms } = checkout2;
|
|
9948
|
+
if (quote.namedSeat && body.seatTermsAck !== true) return json({ error: "seatTermsAck required" }, 400);
|
|
9524
9949
|
const membershipOffer = "membershipOffer" in checkout2 ? checkout2.membershipOffer : void 0;
|
|
9525
9950
|
if (ctx.membershipAuthority && !membershipOffer) return json({ error: "checkout requires reconciliation", code: "checkout_recovery_required" }, 409);
|
|
9526
9951
|
if (!tier) return json({ error: "tier not found", code: "tier_unresolved" }, 400);
|
|
@@ -9545,6 +9970,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9545
9970
|
priceCents: billingTerms ? tier.priceCents : quote.lineItems.dueTodayCents,
|
|
9546
9971
|
...billingTerms ? { billingTerms } : {},
|
|
9547
9972
|
...membershipOffer ? { membershipOffer } : {},
|
|
9973
|
+
..."projectionRevision" in checkout2 ? { projectionRevision: checkout2.projectionRevision } : {},
|
|
9548
9974
|
...ctx.requirePaymentQuote || ctx.membershipAuthority || body.quoteDigest ? { paymentQuote: quote } : {},
|
|
9549
9975
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
9550
9976
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
@@ -9566,9 +9992,10 @@ async function startSubscription(req, env, ctx) {
|
|
|
9566
9992
|
offerId: membershipOffer.id,
|
|
9567
9993
|
offerDigest: membershipOffer.digest,
|
|
9568
9994
|
quoteDigest: quote.digest,
|
|
9569
|
-
priceId: String(priceId)
|
|
9995
|
+
priceId: String(priceId),
|
|
9996
|
+
...quote.namedSeat ? { namedSeat: { ...quote.namedSeat, purchaserEmail: normalizeChapterEmail(String(app.email ?? "")) } } : {}
|
|
9570
9997
|
};
|
|
9571
|
-
|
|
9998
|
+
return verifyMembershipCheckoutBinding(await ctx.membershipAuthority.bindCheckout(binding, env), binding);
|
|
9572
9999
|
} : void 0);
|
|
9573
10000
|
} catch (err) {
|
|
9574
10001
|
const code = paymentErrorCode(err);
|
|
@@ -9577,6 +10004,58 @@ async function startSubscription(req, env, ctx) {
|
|
|
9577
10004
|
return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: quote.lineItems });
|
|
9578
10005
|
}
|
|
9579
10006
|
|
|
10007
|
+
// src/worker-payment-receipt.ts
|
|
10008
|
+
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
10009
|
+
var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10010
|
+
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
10011
|
+
const id7 = url.searchParams.get("application");
|
|
10012
|
+
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
10013
|
+
if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
|
|
10014
|
+
const db = ctx.makeDb(env);
|
|
10015
|
+
const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
|
|
10016
|
+
const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
|
|
10017
|
+
if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
|
|
10018
|
+
const receipt = intent.paymentReceipt;
|
|
10019
|
+
if (!receipt) return json2({ pending: true }, 202);
|
|
10020
|
+
const text8 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
10021
|
+
if (req.method === "POST") {
|
|
10022
|
+
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
10023
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
10024
|
+
if (!group) return json2({ error: "Receipt email is unavailable." }, 503);
|
|
10025
|
+
const config = emailGroupFrom(group);
|
|
10026
|
+
const debugEmail = (env.ODLA_DEBUG_EMAIL ?? config.debugEmail)?.trim();
|
|
10027
|
+
if (env.ODLA_ENV !== "prod" && (!debugEmail || !/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(debugEmail))) {
|
|
10028
|
+
return json2({ error: "A test inbox is required to email receipts in this environment." }, 503);
|
|
10029
|
+
}
|
|
10030
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
10031
|
+
group: { ...config, emailTemplates: {
|
|
10032
|
+
...config.emailTemplates,
|
|
10033
|
+
paymentReceipt: { enabled: true, subject: "Your {{chapterName}} payment receipt", text: "{{receiptText}}" }
|
|
10034
|
+
} },
|
|
10035
|
+
template: "paymentReceipt",
|
|
10036
|
+
to: String(intent.purchaserEmailNormalized),
|
|
10037
|
+
applicationId: id7,
|
|
10038
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text8 },
|
|
10039
|
+
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
10040
|
+
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
10041
|
+
});
|
|
10042
|
+
return result.sent ? json2({ sent: true, preview: env.ODLA_ENV !== "prod" }) : json2({ error: "We could not confirm email delivery. You can print or save your receipt." }, 503);
|
|
10043
|
+
}
|
|
10044
|
+
if (url.searchParams.get("format") !== "html") return json2({ receipt, chapterName: ctx.chapter.name });
|
|
10045
|
+
const nonce = crypto.randomUUID();
|
|
10046
|
+
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
10047
|
+
<style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
|
|
10048
|
+
<h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
|
|
10049
|
+
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10050
|
+
headers: {
|
|
10051
|
+
"content-type": "text/html; charset=utf-8",
|
|
10052
|
+
"cache-control": "no-store",
|
|
10053
|
+
"referrer-policy": "no-referrer",
|
|
10054
|
+
"content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; frame-ancestors 'none'; base-uri 'none'`
|
|
10055
|
+
}
|
|
10056
|
+
});
|
|
10057
|
+
}
|
|
10058
|
+
|
|
9580
10059
|
// src/worker-routes-payments.ts
|
|
9581
10060
|
async function ingestWebhook(req, env, ctx) {
|
|
9582
10061
|
const db = ctx.makeDb(env);
|
|
@@ -9589,8 +10068,9 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
9589
10068
|
}
|
|
9590
10069
|
var handlePayments = async (req, url, env, ctx) => {
|
|
9591
10070
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
10071
|
+
if (["GET", "POST"].includes(req.method) && url.pathname === "/api/payments/receipt") return paymentReceiptRoute(req, url, env, ctx);
|
|
9592
10072
|
if (req.method === "GET" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx);
|
|
9593
|
-
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true);
|
|
10073
|
+
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true, req);
|
|
9594
10074
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
9595
10075
|
if (req.method === "POST" && url.pathname === "/api/webhooks/stripe") return ingestWebhook(req, env, ctx);
|
|
9596
10076
|
return null;
|
|
@@ -9604,16 +10084,16 @@ var userFor = async (req, env, ctx) => {
|
|
|
9604
10084
|
const user = await ctx.verifyUser(req, env);
|
|
9605
10085
|
return user?.email ? user : null;
|
|
9606
10086
|
};
|
|
9607
|
-
var loadClaim = async (db,
|
|
10087
|
+
var loadClaim = async (db, id7) => first4((await db.query({ giftClaims: { $: { where: { id: id7 }, limit: 1 } } })).giftClaims);
|
|
9608
10088
|
async function consumeRateLimit(db, chapterId, requester, now) {
|
|
9609
10089
|
const bucket = await giftRequesterBucket(chapterId, requester);
|
|
9610
|
-
const
|
|
9611
|
-
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id:
|
|
10090
|
+
const id7 = `gift-rate:${chapterId}:${bucket}`;
|
|
10091
|
+
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id: id7 }, limit: 1 } } })).giftRedemptionAttempts);
|
|
9612
10092
|
const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
|
|
9613
10093
|
const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
|
|
9614
10094
|
if (attemptCount > 10) return false;
|
|
9615
|
-
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id:
|
|
9616
|
-
id:
|
|
10095
|
+
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id: id7, attrs: {
|
|
10096
|
+
id: id7,
|
|
9617
10097
|
chapterId,
|
|
9618
10098
|
bucket,
|
|
9619
10099
|
attemptCount,
|
|
@@ -9787,14 +10267,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
|
9787
10267
|
async function readNamedSeatOffer(db, input, now) {
|
|
9788
10268
|
membershipClock(now);
|
|
9789
10269
|
await loadMembershipAuthorityState(db, input);
|
|
9790
|
-
const
|
|
10270
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
9791
10271
|
requireMembershipScope(member, input);
|
|
9792
10272
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
9793
10273
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
9794
10274
|
if (seat) requireMembershipScope(seat, input);
|
|
9795
10275
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
9796
10276
|
const offer = {
|
|
9797
|
-
membershipId:
|
|
10277
|
+
membershipId: id7,
|
|
9798
10278
|
amountCents: offering.seatPriceCents,
|
|
9799
10279
|
currency: offering.currency,
|
|
9800
10280
|
interval: offering.interval,
|
|
@@ -10163,18 +10643,18 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
10163
10643
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10164
10644
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10165
10645
|
try {
|
|
10166
|
-
const
|
|
10646
|
+
const id7 = decodeURIComponent(match[1]);
|
|
10167
10647
|
const body = await namedSeatRequest(req);
|
|
10168
10648
|
if (Object.keys(body).some((key3) => !["expectedAttempt", "expectedState", "decision", "reason", "messageId"].includes(key3)) || body.messageId !== void 0 && (typeof body.messageId !== "string" || body.messageId.length > 255 || /[\r\n]/.test(body.messageId))) {
|
|
10169
10649
|
return json({ error: "invalid delivery evidence" }, 400);
|
|
10170
10650
|
}
|
|
10171
|
-
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog",
|
|
10651
|
+
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog", id7);
|
|
10172
10652
|
if (!row2 || row2.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
10173
10653
|
if (row2.applicationId) {
|
|
10174
10654
|
const app = await membershipLoad(chapterDb, "applications", String(row2.applicationId));
|
|
10175
10655
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10176
10656
|
}
|
|
10177
|
-
await reconcileLifecycleDelivery(chapterDb, { ...body, id:
|
|
10657
|
+
await reconcileLifecycleDelivery(chapterDb, { ...body, id: id7, groupId: ctx.chapter.id, actorId: user.userId }, Date.now());
|
|
10178
10658
|
return json({ ok: true, sent: false });
|
|
10179
10659
|
} catch {
|
|
10180
10660
|
return json({ error: "delivery evidence is invalid, stale, or still in progress; reload the exact attempt" }, 409);
|
|
@@ -10277,13 +10757,13 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10277
10757
|
const user = await ctx.verifyUser(req, env);
|
|
10278
10758
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10279
10759
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10280
|
-
let
|
|
10760
|
+
let id7;
|
|
10281
10761
|
try {
|
|
10282
|
-
|
|
10762
|
+
id7 = dedicated ? url.searchParams.get("recordId") ?? "" : decodeURIComponent(match[1]);
|
|
10283
10763
|
} catch {
|
|
10284
10764
|
return json({ error: "invalid record id" }, 422);
|
|
10285
10765
|
}
|
|
10286
|
-
const record4 = await (0, import_crm13.getRecord)({ crm: ctx.chapter.crm, db },
|
|
10766
|
+
const record4 = await (0, import_crm13.getRecord)({ crm: ctx.chapter.crm, db }, id7);
|
|
10287
10767
|
if (!record4) return json({ error: "not found" }, 404);
|
|
10288
10768
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
10289
10769
|
if (match?.[2]) {
|
|
@@ -10298,9 +10778,9 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10298
10778
|
}
|
|
10299
10779
|
if (req.method === "GET" && dedicated) {
|
|
10300
10780
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
10301
|
-
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([
|
|
10302
|
-
id:
|
|
10303
|
-
label: field.label ??
|
|
10781
|
+
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id8]) => !excluded.includes(id8)).map(([id8, field]) => ({
|
|
10782
|
+
id: id8,
|
|
10783
|
+
label: field.label ?? id8,
|
|
10304
10784
|
type: field.type,
|
|
10305
10785
|
required: field.required ?? false,
|
|
10306
10786
|
...field.options ? { options: field.options } : {}
|
|
@@ -11071,12 +11551,12 @@ var readJson = async (req) => {
|
|
|
11071
11551
|
return null;
|
|
11072
11552
|
}
|
|
11073
11553
|
};
|
|
11074
|
-
async function loadApp(db,
|
|
11075
|
-
const { applications } = await db.query({ applications: { $: { where: { id:
|
|
11554
|
+
async function loadApp(db, id7) {
|
|
11555
|
+
const { applications } = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11076
11556
|
return applications?.[0] ?? null;
|
|
11077
11557
|
}
|
|
11078
|
-
async function loadGroup(db,
|
|
11079
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11558
|
+
async function loadGroup(db, id7) {
|
|
11559
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11080
11560
|
return groups?.[0] ?? null;
|
|
11081
11561
|
}
|
|
11082
11562
|
var handleAdminApplicationActions = async (req, url, env, ctx) => {
|
|
@@ -11162,8 +11642,8 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
11162
11642
|
const got = await gate4(req, env, ctx);
|
|
11163
11643
|
if (got instanceof Response) return got;
|
|
11164
11644
|
const db = got;
|
|
11165
|
-
const
|
|
11166
|
-
const app = await loadApp(db,
|
|
11645
|
+
const id7 = m[1];
|
|
11646
|
+
const app = await loadApp(db, id7);
|
|
11167
11647
|
if (!app) return json({ error: "not found" }, 404);
|
|
11168
11648
|
const result = await approveApplication({ db, env, ctx, app, origin: url.origin, reason: "manual" });
|
|
11169
11649
|
if (!result.ok) return json({ error: result.error }, result.status);
|
|
@@ -11192,10 +11672,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
11192
11672
|
const got = await gate4(req, env, ctx);
|
|
11193
11673
|
if (got instanceof Response) return got;
|
|
11194
11674
|
const db = got;
|
|
11195
|
-
const
|
|
11675
|
+
const id7 = m[1];
|
|
11196
11676
|
const body = await readJson(req);
|
|
11197
11677
|
if (!body) return json({ error: "invalid JSON body" }, 400);
|
|
11198
|
-
const app = await loadApp(db,
|
|
11678
|
+
const app = await loadApp(db, id7);
|
|
11199
11679
|
if (!app) return json({ error: "not found" }, 404);
|
|
11200
11680
|
return patchApplication(db, env, ctx, app, body);
|
|
11201
11681
|
};
|
|
@@ -11305,10 +11785,10 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
11305
11785
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
11306
11786
|
await (0, import_crm16.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
11307
11787
|
} else {
|
|
11308
|
-
const
|
|
11788
|
+
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11309
11789
|
await db.transact([
|
|
11310
|
-
{ t: "merge", ns: "groups", id:
|
|
11311
|
-
{ t: "update", ns: "groups", id:
|
|
11790
|
+
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
|
|
11791
|
+
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11312
11792
|
]);
|
|
11313
11793
|
}
|
|
11314
11794
|
return json({ ok: true });
|
|
@@ -11323,8 +11803,8 @@ async function gate5(req, env, ctx) {
|
|
|
11323
11803
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
11324
11804
|
return rawDb;
|
|
11325
11805
|
}
|
|
11326
|
-
async function loadGroup2(db,
|
|
11327
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11806
|
+
async function loadGroup2(db, id7) {
|
|
11807
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11328
11808
|
return groups?.[0] ?? null;
|
|
11329
11809
|
}
|
|
11330
11810
|
var str3 = (v, d = "") => typeof v === "string" ? v : d;
|
|
@@ -11578,7 +12058,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11578
12058
|
if ("response" in got) return got.response;
|
|
11579
12059
|
return json({
|
|
11580
12060
|
targets: ctx.chapter.network.targets.map(({
|
|
11581
|
-
id:
|
|
12061
|
+
id: id7,
|
|
11582
12062
|
name,
|
|
11583
12063
|
url: targetUrl,
|
|
11584
12064
|
fields,
|
|
@@ -11587,7 +12067,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11587
12067
|
stageTransitions,
|
|
11588
12068
|
commercialParity
|
|
11589
12069
|
}) => ({
|
|
11590
|
-
id:
|
|
12070
|
+
id: id7,
|
|
11591
12071
|
name,
|
|
11592
12072
|
url: targetUrl,
|
|
11593
12073
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
@@ -11749,7 +12229,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
11749
12229
|
if (typeof body.recordId !== "string" || !Array.isArray(body.targetIds) || body.targetIds.length === 0) {
|
|
11750
12230
|
return json({ error: "recordId and a non-empty targetIds array are required" }, 400);
|
|
11751
12231
|
}
|
|
11752
|
-
const requested = new Set(body.targetIds.filter((
|
|
12232
|
+
const requested = new Set(body.targetIds.filter((id7) => typeof id7 === "string"));
|
|
11753
12233
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
11754
12234
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
11755
12235
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
@@ -11877,7 +12357,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
11877
12357
|
const appId = record4.fields.applicationId;
|
|
11878
12358
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
11879
12359
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
11880
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((
|
|
12360
|
+
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id7) => ({ id: id7, label: id7.replaceAll("_", " ") })) : result.edit.allowedStages.filter((stage) => !isApplicationStage(ctx, stage.id));
|
|
11881
12361
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
11882
12362
|
const meta = lock?.meta;
|
|
11883
12363
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -11982,9 +12462,9 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
11982
12462
|
// src/network-mutation-receipt.ts
|
|
11983
12463
|
var import_crm22 = require("@odla-ai/crm");
|
|
11984
12464
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
11985
|
-
async function priorMutation(db,
|
|
12465
|
+
async function priorMutation(db, id7) {
|
|
11986
12466
|
const result = await db.query({
|
|
11987
|
-
[import_crm22.CRM_NS.activity]: { $: { where: { id:
|
|
12467
|
+
[import_crm22.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
11988
12468
|
});
|
|
11989
12469
|
const row2 = (result[import_crm22.CRM_NS.activity] ?? [])[0];
|
|
11990
12470
|
const meta = row2?.meta;
|
|
@@ -12244,8 +12724,8 @@ async function gate7(req, env, ctx) {
|
|
|
12244
12724
|
if (!await networkWriteAllowed(req, db, user, ctx)) return { response: json({ error: "super-admin required", code: "super_admin_required" }, 403) };
|
|
12245
12725
|
return { db, user };
|
|
12246
12726
|
}
|
|
12247
|
-
function targetFor2(ctx,
|
|
12248
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
12727
|
+
function targetFor2(ctx, id7) {
|
|
12728
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12249
12729
|
}
|
|
12250
12730
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
12251
12731
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12380,7 +12860,7 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
12380
12860
|
if (ctx.chapter.network.requireSuperAdminWrites && !await ctx.isSuperAdminEmail(got.db, got.user.email, got.user.userId)) {
|
|
12381
12861
|
return json({ ...result.page, records: result.page.records.map(({ edit: _edit, ...record4 }) => ({
|
|
12382
12862
|
...record4,
|
|
12383
|
-
fields: Object.fromEntries(Object.entries(record4.fields).filter(([
|
|
12863
|
+
fields: Object.fromEntries(Object.entries(record4.fields).filter(([id7]) => id7 === "name" || id7 === "email"))
|
|
12384
12864
|
})) });
|
|
12385
12865
|
}
|
|
12386
12866
|
return json(result.page);
|
|
@@ -12450,9 +12930,9 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12450
12930
|
}
|
|
12451
12931
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
12452
12932
|
if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
|
|
12453
|
-
const
|
|
12933
|
+
const id7 = crypto.randomUUID();
|
|
12454
12934
|
const note = {
|
|
12455
|
-
id:
|
|
12935
|
+
id: id7,
|
|
12456
12936
|
targetId: identity2.targetId,
|
|
12457
12937
|
recordType: identity2.recordType,
|
|
12458
12938
|
recordId: identity2.recordId,
|
|
@@ -12464,7 +12944,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12464
12944
|
await got.db.transact([{
|
|
12465
12945
|
t: "update",
|
|
12466
12946
|
ns: "networkNotes",
|
|
12467
|
-
id:
|
|
12947
|
+
id: id7,
|
|
12468
12948
|
attrs: { ...note, recordKey: identity2.recordKey }
|
|
12469
12949
|
}]);
|
|
12470
12950
|
return json({ note }, 201);
|
|
@@ -12564,13 +13044,13 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12564
13044
|
return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
|
|
12565
13045
|
}
|
|
12566
13046
|
const createdAt = Date.now();
|
|
12567
|
-
const
|
|
13047
|
+
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
12568
13048
|
const result = await (0, import_crm24.addActivity)(
|
|
12569
13049
|
{
|
|
12570
13050
|
crm: ctx.chapter.crm,
|
|
12571
13051
|
db,
|
|
12572
13052
|
now: () => createdAt,
|
|
12573
|
-
newId: () =>
|
|
13053
|
+
newId: () => id7
|
|
12574
13054
|
},
|
|
12575
13055
|
{
|
|
12576
13056
|
recordId,
|
|
@@ -12585,11 +13065,11 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12585
13065
|
body
|
|
12586
13066
|
}
|
|
12587
13067
|
},
|
|
12588
|
-
mutationId:
|
|
13068
|
+
mutationId: id7
|
|
12589
13069
|
}
|
|
12590
13070
|
);
|
|
12591
13071
|
const note = {
|
|
12592
|
-
id:
|
|
13072
|
+
id: id7,
|
|
12593
13073
|
sourceId: verified.sender,
|
|
12594
13074
|
recordType,
|
|
12595
13075
|
recordId,
|
|
@@ -12598,8 +13078,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12598
13078
|
};
|
|
12599
13079
|
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
12600
13080
|
};
|
|
12601
|
-
function targetFor3(ctx,
|
|
12602
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
13081
|
+
function targetFor3(ctx, id7) {
|
|
13082
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12603
13083
|
}
|
|
12604
13084
|
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
12605
13085
|
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
@@ -12868,10 +13348,10 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12868
13348
|
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
12869
13349
|
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
12870
13350
|
}
|
|
12871
|
-
const
|
|
12872
|
-
const previous = state.links.find((link2) => link2.id ===
|
|
13351
|
+
const id7 = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
13352
|
+
const previous = state.links.find((link2) => link2.id === id7);
|
|
12873
13353
|
const link = {
|
|
12874
|
-
id:
|
|
13354
|
+
id: id7,
|
|
12875
13355
|
personId: person.id,
|
|
12876
13356
|
chapterId: input.chapterId,
|
|
12877
13357
|
chapterPersonId: input.chapterPersonId,
|
|
@@ -12882,11 +13362,11 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12882
13362
|
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
12883
13363
|
await db.transact([
|
|
12884
13364
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
12885
|
-
{ t: "update", ns: "networkChapterPersonLinks", id:
|
|
13365
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { ...link } },
|
|
12886
13366
|
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
12887
13367
|
], { mutationId, guards: [
|
|
12888
13368
|
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
12889
|
-
{ ns: "networkChapterPersonLinks", id:
|
|
13369
|
+
{ ns: "networkChapterPersonLinks", id: id7, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
12890
13370
|
] });
|
|
12891
13371
|
return { person: nextPerson, link };
|
|
12892
13372
|
}
|
|
@@ -12923,9 +13403,9 @@ var defaultPolicy = (now, consent) => ({
|
|
|
12923
13403
|
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
12924
13404
|
async function linkOps(personId, links, now) {
|
|
12925
13405
|
return Promise.all(links.map(async (link) => {
|
|
12926
|
-
const
|
|
13406
|
+
const id7 = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
12927
13407
|
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
12928
|
-
return { t: "update", ns: "networkChapterPersonLinks", id:
|
|
13408
|
+
return { t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { id: id7, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
12929
13409
|
}));
|
|
12930
13410
|
}
|
|
12931
13411
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -13144,10 +13624,10 @@ async function networkPersonBulkBatchDigest(rows5) {
|
|
|
13144
13624
|
}
|
|
13145
13625
|
async function networkPersonRegistryStateDigest(state) {
|
|
13146
13626
|
const snapshot = {
|
|
13147
|
-
persons: state.persons.map(({ id:
|
|
13148
|
-
claims: state.claims.map(({ id:
|
|
13149
|
-
aliases: state.aliases.map(({ id:
|
|
13150
|
-
links: state.links.map(({ id:
|
|
13627
|
+
persons: state.persons.map(({ id: id7, revision, state: personState, mergedIntoId }) => ({ id: id7, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13628
|
+
claims: state.claims.map(({ id: id7, personId }) => ({ id: id7, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13629
|
+
aliases: state.aliases.map(({ id: id7, revision, status: status2, personId }) => ({ id: id7, revision, status: status2, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13630
|
+
links: state.links.map(({ id: id7, personId, state: linkState }) => ({ id: id7, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
13151
13631
|
};
|
|
13152
13632
|
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
13153
13633
|
}
|
|
@@ -13372,7 +13852,7 @@ var safeNetworkPersonAlias = async (alias) => ({
|
|
|
13372
13852
|
recordedAt: alias.consent.recordedAt,
|
|
13373
13853
|
hasBasis: Boolean(alias.consent.basis)
|
|
13374
13854
|
},
|
|
13375
|
-
identityClaims: alias.identityClaims.map(({ id:
|
|
13855
|
+
identityClaims: alias.identityClaims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13376
13856
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
13377
13857
|
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
13378
13858
|
chapterId: link.chapterId,
|
|
@@ -13401,7 +13881,7 @@ async function projectNetworkPersonRegistryState(state) {
|
|
|
13401
13881
|
return {
|
|
13402
13882
|
...safeNetworkPerson(person, claims),
|
|
13403
13883
|
canonicalPersonId: canonicalId,
|
|
13404
|
-
identityClaims: claims.map(({ id:
|
|
13884
|
+
identityClaims: claims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13405
13885
|
chapterLinks: safeLinks,
|
|
13406
13886
|
chapterProjections: projectNetworkContactPolicy(canonical3.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
13407
13887
|
};
|
|
@@ -13656,7 +14136,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
13656
14136
|
};
|
|
13657
14137
|
|
|
13658
14138
|
// src/worker-routes-network-commercial.ts
|
|
13659
|
-
var
|
|
14139
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
13660
14140
|
|
|
13661
14141
|
// src/worker-routes-network-commercial-parity.ts
|
|
13662
14142
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -14032,7 +14512,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14032
14512
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
14033
14513
|
}
|
|
14034
14514
|
try {
|
|
14035
|
-
const event2 = await (0,
|
|
14515
|
+
const event2 = await (0, import_stripe13.constructStripeEvent)(body, signature, secret);
|
|
14036
14516
|
const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
|
|
14037
14517
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
14038
14518
|
const result = await ingestNetworkCommercialFact(
|
|
@@ -14053,7 +14533,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14053
14533
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
14054
14534
|
});
|
|
14055
14535
|
} catch (error) {
|
|
14056
|
-
if (error instanceof
|
|
14536
|
+
if (error instanceof import_stripe13.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
14057
14537
|
throw error;
|
|
14058
14538
|
}
|
|
14059
14539
|
};
|
|
@@ -14116,7 +14596,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
14116
14596
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
14117
14597
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
14118
14598
|
try {
|
|
14119
|
-
const stripe = (0,
|
|
14599
|
+
const stripe = (0, import_stripe13.initStripe)({ secretKey });
|
|
14120
14600
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
14121
14601
|
listStripeObjects(stripe, "/v1/customers"),
|
|
14122
14602
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -14130,7 +14610,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
14130
14610
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
14131
14611
|
});
|
|
14132
14612
|
} catch (error) {
|
|
14133
|
-
if (error instanceof
|
|
14613
|
+
if (error instanceof import_stripe13.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
14134
14614
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
14135
14615
|
}
|
|
14136
14616
|
}
|
|
@@ -14194,14 +14674,14 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14194
14674
|
}
|
|
14195
14675
|
}
|
|
14196
14676
|
const db = ctx.makeDb(env);
|
|
14197
|
-
const
|
|
14677
|
+
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
14198
14678
|
try {
|
|
14199
14679
|
const created = await (0, import_crm25.createRecord)(
|
|
14200
14680
|
{
|
|
14201
14681
|
crm: ctx.chapter.crm,
|
|
14202
14682
|
db,
|
|
14203
14683
|
now: () => Date.now(),
|
|
14204
|
-
newId: () =>
|
|
14684
|
+
newId: () => id7
|
|
14205
14685
|
},
|
|
14206
14686
|
{
|
|
14207
14687
|
type: formation.type,
|
|
@@ -14210,7 +14690,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14210
14690
|
}
|
|
14211
14691
|
);
|
|
14212
14692
|
return json({
|
|
14213
|
-
id:
|
|
14693
|
+
id: id7,
|
|
14214
14694
|
duplicate: created.duplicate,
|
|
14215
14695
|
status: created.record.stage ?? null
|
|
14216
14696
|
}, created.duplicate ? 200 : 201);
|
|
@@ -14287,8 +14767,8 @@ function resourceValue(value, path, errors) {
|
|
|
14287
14767
|
return null;
|
|
14288
14768
|
}
|
|
14289
14769
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
14290
|
-
const
|
|
14291
|
-
if (
|
|
14770
|
+
const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
14771
|
+
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
14292
14772
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
14293
14773
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
14294
14774
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14345,7 +14825,7 @@ function resourceValue(value, path, errors) {
|
|
|
14345
14825
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
14346
14826
|
...promotedFrom ? { promotedFrom } : {}
|
|
14347
14827
|
};
|
|
14348
|
-
return { id:
|
|
14828
|
+
return { id: id7, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
|
|
14349
14829
|
}
|
|
14350
14830
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
14351
14831
|
const errors = [];
|
|
@@ -14376,7 +14856,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
14376
14856
|
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
14377
14857
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
14378
14858
|
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
14379
|
-
const ids2 = resources.map(({ id:
|
|
14859
|
+
const ids2 = resources.map(({ id: id7 }) => id7);
|
|
14380
14860
|
if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
|
|
14381
14861
|
for (const resource of resources) {
|
|
14382
14862
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -14465,18 +14945,18 @@ async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environme
|
|
|
14465
14945
|
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
14466
14946
|
}
|
|
14467
14947
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
14468
|
-
const
|
|
14469
|
-
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id:
|
|
14948
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14949
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
|
|
14470
14950
|
return local3(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
14471
14951
|
}
|
|
14472
14952
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
14473
14953
|
const { document, digest } = envelope;
|
|
14474
14954
|
const target = document.target;
|
|
14475
|
-
const
|
|
14955
|
+
const id7 = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
14476
14956
|
const ops = [];
|
|
14477
14957
|
if (previous?.active) ops.push({ t: "update", ns: "builderLibraryControls", id: previous.id, attrs: { active: false } });
|
|
14478
|
-
ops.push({ t: "update", ns: "builderLibraryControls", id:
|
|
14479
|
-
id:
|
|
14958
|
+
ops.push({ t: "update", ns: "builderLibraryControls", id: id7, attrs: {
|
|
14959
|
+
id: id7,
|
|
14480
14960
|
sourceId: document.source.appId,
|
|
14481
14961
|
targetId: target.appId,
|
|
14482
14962
|
environment: target.environment,
|
|
@@ -14489,13 +14969,13 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
14489
14969
|
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest}` });
|
|
14490
14970
|
}
|
|
14491
14971
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
14492
|
-
const
|
|
14972
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14493
14973
|
const current = await loadLocalBuilderLibrary(db, document.source.appId, environment);
|
|
14494
14974
|
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("builder_library_revision_conflict");
|
|
14495
|
-
const row2 = { id:
|
|
14496
|
-
await db.transact([{ t: "update", ns: "builderLibraryLocal", id:
|
|
14975
|
+
const row2 = { id: id7, appId: document.source.appId, environment, revision: document.revision, document, updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
14976
|
+
await db.transact([{ t: "update", ns: "builderLibraryLocal", id: id7, attrs: row2 }], {
|
|
14497
14977
|
mutationId: `builder-library-local:${environment}:${document.revision}`,
|
|
14498
|
-
guards: current ? [{ ns: "builderLibraryLocal", id:
|
|
14978
|
+
guards: current ? [{ ns: "builderLibraryLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "builderLibraryLocal", id: id7, exists: false }]
|
|
14499
14979
|
});
|
|
14500
14980
|
return row2;
|
|
14501
14981
|
}
|
|
@@ -14710,30 +15190,30 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14710
15190
|
};
|
|
14711
15191
|
|
|
14712
15192
|
// src/membership-cancel-authority.ts
|
|
14713
|
-
var
|
|
15193
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
14714
15194
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14715
15195
|
membershipClock(now);
|
|
14716
|
-
const
|
|
15196
|
+
const id7 = await networkMembershipId(target);
|
|
14717
15197
|
const state = await loadMembershipAuthorityState(db, target);
|
|
14718
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15198
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14719
15199
|
requireMembershipScope(member, target);
|
|
14720
15200
|
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
14721
15201
|
membershipFail("membership_cancellation_unavailable");
|
|
14722
15202
|
}
|
|
14723
15203
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14724
|
-
const read2 = await (0,
|
|
15204
|
+
const read2 = await (0, import_stripe14.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
14725
15205
|
const sub = read2.body;
|
|
14726
15206
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14727
15207
|
...stripePartitionMetadata(target),
|
|
14728
15208
|
applicationId: target.applicationId
|
|
14729
15209
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14730
15210
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14731
|
-
const write = await (0,
|
|
15211
|
+
const write = await (0, import_stripe14.stripeRequest)(
|
|
14732
15212
|
{ secretKey: stripeSecret },
|
|
14733
15213
|
"POST",
|
|
14734
15214
|
path,
|
|
14735
15215
|
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
14736
|
-
{ idempotencyKey: `membership-cancel-at-term:${
|
|
15216
|
+
{ idempotencyKey: `membership-cancel-at-term:${id7}:${member.providerSubscriptionId}` }
|
|
14737
15217
|
);
|
|
14738
15218
|
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
14739
15219
|
membershipFail("membership_cancellation_pending");
|
|
@@ -14741,7 +15221,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14741
15221
|
}
|
|
14742
15222
|
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
14743
15223
|
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
14744
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
15224
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
14745
15225
|
status: status2,
|
|
14746
15226
|
autoRenew: false,
|
|
14747
15227
|
graceEndsAt: 0,
|
|
@@ -14750,12 +15230,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14750
15230
|
updatedAt: now
|
|
14751
15231
|
})], {
|
|
14752
15232
|
guards: [
|
|
14753
|
-
{ ns: "networkMembership", id:
|
|
15233
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14754
15234
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14755
15235
|
]
|
|
14756
15236
|
});
|
|
14757
15237
|
}
|
|
14758
|
-
return { membershipId:
|
|
15238
|
+
return { membershipId: id7, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
14759
15239
|
}
|
|
14760
15240
|
|
|
14761
15241
|
// src/membership-authority-handler.ts
|
|
@@ -14794,17 +15274,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
|
|
|
14794
15274
|
// src/membership-approval-authority.ts
|
|
14795
15275
|
async function approveNetworkMembership(db, input, now) {
|
|
14796
15276
|
membershipClock(now);
|
|
14797
|
-
const
|
|
15277
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
14798
15278
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14799
15279
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14800
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15280
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14801
15281
|
requireMembershipScope(member, input);
|
|
14802
15282
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14803
15283
|
requireMembershipScope(entitlement, input);
|
|
14804
|
-
if (entitlement.membershipId !==
|
|
15284
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
|
|
14805
15285
|
if (["active", "cancel_at_term_end"].includes(String(member.status)) && entitlement.status === "active" && Number(member.refundedCents) === 0 && Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && Number(member.termEndsAt) > now) {
|
|
14806
15286
|
return {
|
|
14807
|
-
membershipId:
|
|
15287
|
+
membershipId: id7,
|
|
14808
15288
|
status: String(member.status),
|
|
14809
15289
|
cohort: String(member.cohort),
|
|
14810
15290
|
memberNumber: Number(member.memberNumber),
|
|
@@ -14824,7 +15304,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14824
15304
|
const seatActivation = await namedSeatPrimaryActivation(db, member, now);
|
|
14825
15305
|
try {
|
|
14826
15306
|
await db.transact([
|
|
14827
|
-
membershipUpdate("networkMembership",
|
|
15307
|
+
membershipUpdate("networkMembership", id7, {
|
|
14828
15308
|
status: member.autoRenew ? "active" : "cancel_at_term_end",
|
|
14829
15309
|
memberNumber,
|
|
14830
15310
|
journeyApprovedAt: now,
|
|
@@ -14839,8 +15319,8 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14839
15319
|
updatedAt: now
|
|
14840
15320
|
}),
|
|
14841
15321
|
...seatActivation.ops
|
|
14842
|
-
], { mutationId: `membership-approve:${
|
|
14843
|
-
{ ns: "networkMembership", id:
|
|
15322
|
+
], { mutationId: `membership-approve:${id7}`, guards: [
|
|
15323
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
|
|
14844
15324
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
|
|
14845
15325
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
14846
15326
|
...seatActivation.guards
|
|
@@ -14855,41 +15335,41 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14855
15335
|
// src/membership-refund-authority.ts
|
|
14856
15336
|
async function refundNetworkMembership(db, input, secretKey, now) {
|
|
14857
15337
|
membershipClock(now);
|
|
14858
|
-
const
|
|
14859
|
-
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15338
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
15339
|
+
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14860
15340
|
requireMembershipScope(member, input);
|
|
14861
|
-
if (member.status === "refunded") return { membershipId:
|
|
15341
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14862
15342
|
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
|
|
14863
15343
|
if (member.status !== "refund_pending") {
|
|
14864
15344
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14865
15345
|
requireMembershipScope(entitlement, input);
|
|
14866
15346
|
await db.transact([
|
|
14867
|
-
membershipUpdate("networkMembership",
|
|
15347
|
+
membershipUpdate("networkMembership", id7, {
|
|
14868
15348
|
status: "refund_pending",
|
|
14869
15349
|
refundRequestedAt: now,
|
|
14870
15350
|
revision: Number(member.revision) + 1,
|
|
14871
15351
|
updatedAt: now
|
|
14872
15352
|
}),
|
|
14873
15353
|
membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
14874
|
-
membershipUpdate("membershipEffect", `${
|
|
14875
|
-
id: `${
|
|
15354
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, {
|
|
15355
|
+
id: `${id7}:refund`,
|
|
14876
15356
|
chapterId: input.chapterId,
|
|
14877
15357
|
environment: input.environment,
|
|
14878
15358
|
runtime: input.runtime,
|
|
14879
|
-
membershipId:
|
|
15359
|
+
membershipId: id7,
|
|
14880
15360
|
kind: "refund_membership",
|
|
14881
15361
|
status: "pending",
|
|
14882
15362
|
attempts: 0,
|
|
14883
15363
|
createdAt: now,
|
|
14884
15364
|
updatedAt: now
|
|
14885
15365
|
})
|
|
14886
|
-
], { mutationId: `membership-refund-request:${
|
|
14887
|
-
{ ns: "networkMembership", id:
|
|
15366
|
+
], { mutationId: `membership-refund-request:${id7}`, guards: [
|
|
15367
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14888
15368
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
|
|
14889
15369
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14890
15370
|
] });
|
|
14891
15371
|
}
|
|
14892
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15372
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14893
15373
|
requireMembershipScope(member, input);
|
|
14894
15374
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14895
15375
|
const result = await refundSubscriptionPayment(secretKey, {
|
|
@@ -14903,23 +15383,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14903
15383
|
if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
|
|
14904
15384
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14905
15385
|
state = await loadMembershipAuthorityState(db, input);
|
|
14906
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15386
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14907
15387
|
requireMembershipScope(member, input);
|
|
14908
|
-
if (member.status === "refunded") return { membershipId:
|
|
15388
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14909
15389
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14910
15390
|
const counted = member.founding === true && member.memberNumber !== void 0;
|
|
14911
15391
|
const counts = state.cohortCounts;
|
|
14912
15392
|
if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
|
|
14913
15393
|
try {
|
|
14914
15394
|
await db.transact([
|
|
14915
|
-
membershipUpdate("networkMembership",
|
|
15395
|
+
membershipUpdate("networkMembership", id7, {
|
|
14916
15396
|
status: "refunded",
|
|
14917
15397
|
refundedCents: result.refundedCents,
|
|
14918
15398
|
autoRenew: false,
|
|
14919
15399
|
revision: Number(member.revision) + 1,
|
|
14920
15400
|
updatedAt: now
|
|
14921
15401
|
}),
|
|
14922
|
-
membershipUpdate("membershipEffect", `${
|
|
15402
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, { status: "complete", updatedAt: now }),
|
|
14923
15403
|
...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
|
|
14924
15404
|
cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
|
|
14925
15405
|
revision: Number(state.revision) + 1,
|
|
@@ -14930,7 +15410,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14930
15410
|
chapterId: input.chapterId,
|
|
14931
15411
|
environment: input.environment,
|
|
14932
15412
|
runtime: input.runtime,
|
|
14933
|
-
membershipId:
|
|
15413
|
+
membershipId: id7,
|
|
14934
15414
|
seatId: member.namedSeatId,
|
|
14935
15415
|
kind: "refund_seat_with_primary",
|
|
14936
15416
|
status: "pending",
|
|
@@ -14938,8 +15418,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14938
15418
|
createdAt: now,
|
|
14939
15419
|
updatedAt: now
|
|
14940
15420
|
})] : []
|
|
14941
|
-
], { mutationId: `membership-refunded:${
|
|
14942
|
-
{ ns: "networkMembership", id:
|
|
15421
|
+
], { mutationId: `membership-refunded:${id7}`, guards: [
|
|
15422
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "refund_pending", revision: member.revision } },
|
|
14943
15423
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14944
15424
|
] });
|
|
14945
15425
|
} catch (error) {
|
|
@@ -14950,12 +15430,12 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14950
15430
|
}
|
|
14951
15431
|
|
|
14952
15432
|
// src/membership-provider-lifecycle.ts
|
|
14953
|
-
var
|
|
15433
|
+
var import_stripe15 = require("@odla-ai/stripe");
|
|
14954
15434
|
|
|
14955
15435
|
// src/membership-renewal-authority.ts
|
|
14956
15436
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
14957
15437
|
membershipClock(now);
|
|
14958
|
-
const
|
|
15438
|
+
const id7 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
14959
15439
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
14960
15440
|
const fact = eventRow?.fact;
|
|
14961
15441
|
if (!fact || !/^evt_[A-Za-z0-9_]+$/.test(input.eventId) || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.objectKind !== "invoice" || !["invoice.payment_succeeded", "invoice.payment_failed"].includes(fact.eventType) || fact.billingReason !== "subscription_cycle" || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || Object.entries({
|
|
@@ -14968,10 +15448,10 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14968
15448
|
return membershipFail("membership_renewal_evidence_pending");
|
|
14969
15449
|
}
|
|
14970
15450
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14971
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15451
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14972
15452
|
requireMembershipScope(member, input);
|
|
14973
15453
|
const receipt = () => ({
|
|
14974
|
-
membershipId:
|
|
15454
|
+
membershipId: id7,
|
|
14975
15455
|
status: String(member.status),
|
|
14976
15456
|
termEndsAt: Number(member.termEndsAt),
|
|
14977
15457
|
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
@@ -14996,13 +15476,13 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14996
15476
|
const paid = fact.eventType === "invoice.payment_succeeded";
|
|
14997
15477
|
if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
|
|
14998
15478
|
if (paid && (fact.status !== "paid" || fact.amountPaidCents !== amount || !Number.isSafeInteger(fact.invoicePaidAt) || Number(fact.invoicePaidAt) < start || Number(fact.invoicePaidAt) > now + 3e4)) membershipFail("membership_renewal_unpaid");
|
|
14999
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
15479
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
15000
15480
|
requireMembershipScope(entitlement, input);
|
|
15001
15481
|
const graceEndsAt = paid ? 0 : start + 14 * 864e5;
|
|
15002
15482
|
const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
|
|
15003
15483
|
const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
|
|
15004
15484
|
const ops = [
|
|
15005
|
-
membershipUpdate("networkMembership",
|
|
15485
|
+
membershipUpdate("networkMembership", id7, {
|
|
15006
15486
|
status: status2,
|
|
15007
15487
|
graceEndsAt,
|
|
15008
15488
|
...paid ? {
|
|
@@ -15013,7 +15493,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15013
15493
|
revision: Number(member.revision) + 1,
|
|
15014
15494
|
updatedAt: now
|
|
15015
15495
|
}),
|
|
15016
|
-
membershipUpdate("networkEntitlement", `${
|
|
15496
|
+
membershipUpdate("networkEntitlement", `${id7}:primary`, {
|
|
15017
15497
|
status: entitlementStatus,
|
|
15018
15498
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
15019
15499
|
sourceEventId: input.eventId,
|
|
@@ -15022,8 +15502,8 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15022
15502
|
})
|
|
15023
15503
|
];
|
|
15024
15504
|
const guards = [
|
|
15025
|
-
{ ns: "networkMembership", id:
|
|
15026
|
-
{ ns: "networkEntitlement", id: `${
|
|
15505
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15506
|
+
{ ns: "networkEntitlement", id: `${id7}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
15027
15507
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
15028
15508
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
15029
15509
|
evidenceGuard
|
|
@@ -15048,15 +15528,15 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15048
15528
|
);
|
|
15049
15529
|
}
|
|
15050
15530
|
try {
|
|
15051
|
-
await db.transact(ops, { mutationId: `membership-renewal:${
|
|
15531
|
+
await db.transact(ops, { mutationId: `membership-renewal:${id7}:${input.eventId}`, guards });
|
|
15052
15532
|
} catch (error) {
|
|
15053
15533
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15054
15534
|
continue;
|
|
15055
15535
|
}
|
|
15056
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15536
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15057
15537
|
requireMembershipScope(saved, input);
|
|
15058
15538
|
return {
|
|
15059
|
-
membershipId:
|
|
15539
|
+
membershipId: id7,
|
|
15060
15540
|
status: String(saved.status),
|
|
15061
15541
|
termEndsAt: Number(saved.termEndsAt),
|
|
15062
15542
|
graceEndsAt: Number(saved.graceEndsAt ?? 0),
|
|
@@ -15082,12 +15562,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15082
15562
|
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
15083
15563
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
15084
15564
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
15085
|
-
const
|
|
15565
|
+
const id7 = await networkMembershipId(input);
|
|
15086
15566
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
15087
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15567
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
15088
15568
|
requireMembershipScope(member, input);
|
|
15089
15569
|
const receipt = (value = member) => ({
|
|
15090
|
-
membershipId:
|
|
15570
|
+
membershipId: id7,
|
|
15091
15571
|
status: String(value.status),
|
|
15092
15572
|
termEndsAt: Number(value.termEndsAt),
|
|
15093
15573
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
@@ -15108,13 +15588,13 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15108
15588
|
});
|
|
15109
15589
|
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15110
15590
|
await refundNetworkMembership(db, input, sk, now);
|
|
15111
|
-
const saved2 = await membershipLoad(db, "networkMembership",
|
|
15591
|
+
const saved2 = await membershipLoad(db, "networkMembership", id7);
|
|
15112
15592
|
requireMembershipScope(saved2, input);
|
|
15113
15593
|
return receipt(saved2);
|
|
15114
15594
|
}
|
|
15115
15595
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
15116
15596
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
15117
|
-
const result = await (0,
|
|
15597
|
+
const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
15118
15598
|
const sub = result.body;
|
|
15119
15599
|
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
|
|
15120
15600
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -15123,18 +15603,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15123
15603
|
const status2 = autoRenew ? member.status === "cancel_at_term_end" ? "active" : String(member.status) : now >= Number(member.termEndsAt) ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
15124
15604
|
const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
|
|
15125
15605
|
if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
|
|
15126
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
15606
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
15127
15607
|
requireMembershipScope(entitlement, input);
|
|
15128
15608
|
try {
|
|
15129
15609
|
await db.transact([
|
|
15130
|
-
membershipUpdate("networkMembership",
|
|
15610
|
+
membershipUpdate("networkMembership", id7, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
|
|
15131
15611
|
membershipUpdate("networkEntitlement", String(entitlement.id), {
|
|
15132
15612
|
status: status2 === "lapsed" ? "closed" : entitlement.status,
|
|
15133
15613
|
revision: Number(entitlement.revision) + 1,
|
|
15134
15614
|
updatedAt: now
|
|
15135
15615
|
})
|
|
15136
15616
|
], { guards: [
|
|
15137
|
-
{ ns: "networkMembership", id:
|
|
15617
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15138
15618
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
15139
15619
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
15140
15620
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
@@ -15144,7 +15624,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15144
15624
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15145
15625
|
continue;
|
|
15146
15626
|
}
|
|
15147
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15627
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15148
15628
|
requireMembershipScope(saved, input);
|
|
15149
15629
|
return receipt(saved);
|
|
15150
15630
|
}
|
|
@@ -15152,7 +15632,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15152
15632
|
}
|
|
15153
15633
|
|
|
15154
15634
|
// src/membership-seat-stripe.ts
|
|
15155
|
-
var
|
|
15635
|
+
var import_stripe16 = require("@odla-ai/stripe");
|
|
15156
15636
|
var reject7 = () => {
|
|
15157
15637
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
15158
15638
|
};
|
|
@@ -15164,7 +15644,7 @@ var metadataFor = (seat) => ({
|
|
|
15164
15644
|
});
|
|
15165
15645
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
15166
15646
|
async function read(sk, path, params) {
|
|
15167
|
-
const response = await (0,
|
|
15647
|
+
const response = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
15168
15648
|
if (!response.ok) return reject7();
|
|
15169
15649
|
return response.body;
|
|
15170
15650
|
}
|
|
@@ -15175,7 +15655,7 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
15175
15655
|
currency: seat.currency,
|
|
15176
15656
|
interval: seat.interval,
|
|
15177
15657
|
environment: seat.environment
|
|
15178
|
-
}, Boolean(seat.providerPaymentIntentId));
|
|
15658
|
+
}, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
|
|
15179
15659
|
if (!productId) reject7();
|
|
15180
15660
|
const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
|
|
15181
15661
|
const items = sub.items?.data;
|
|
@@ -15201,11 +15681,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15201
15681
|
}
|
|
15202
15682
|
await subscriptionForSeat(sk, seat);
|
|
15203
15683
|
const metadata = metadataFor(seat);
|
|
15204
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
15684
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe16.stripeRequest)(
|
|
15205
15685
|
{ secretKey: sk },
|
|
15206
15686
|
"GET",
|
|
15207
15687
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
15208
|
-
) : await (0,
|
|
15688
|
+
) : await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
15209
15689
|
amount: seat.amountCents,
|
|
15210
15690
|
currency: seat.currency,
|
|
15211
15691
|
customer: seat.providerCustomerId,
|
|
@@ -15219,11 +15699,15 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15219
15699
|
return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
|
|
15220
15700
|
}
|
|
15221
15701
|
async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
15222
|
-
if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15702
|
+
if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15223
15703
|
const sub = await subscriptionForSeat(sk, seat);
|
|
15224
15704
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
15225
|
-
if (existing)
|
|
15226
|
-
|
|
15705
|
+
if (existing) {
|
|
15706
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject7();
|
|
15707
|
+
return String(existing.id);
|
|
15708
|
+
}
|
|
15709
|
+
if (seat.combinedInvoice) reject7();
|
|
15710
|
+
const result = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
15227
15711
|
subscription: seat.providerSubscriptionId,
|
|
15228
15712
|
price: seat.priceId,
|
|
15229
15713
|
quantity: 1,
|
|
@@ -15440,7 +15924,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
15440
15924
|
const state = await loadMembershipAuthorityState(db, input);
|
|
15441
15925
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
15442
15926
|
requireMembershipScope(seat, input);
|
|
15443
|
-
if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15927
|
+
if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15444
15928
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
15445
15929
|
requireMembershipScope(member, input);
|
|
15446
15930
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
@@ -15760,9 +16244,14 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15760
16244
|
try {
|
|
15761
16245
|
input = JSON.parse(verified.body);
|
|
15762
16246
|
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
15763
|
-
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE2.test(input[key3])))) {
|
|
16247
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3) && !(action === "checkouts" && key3 === "namedSeat")) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE2.test(input[key3])))) {
|
|
15764
16248
|
throw new TypeError("invalid request");
|
|
15765
16249
|
}
|
|
16250
|
+
if (action === "checkouts" && input.namedSeat !== void 0) {
|
|
16251
|
+
const seat = input.namedSeat;
|
|
16252
|
+
if (!seat || Object.keys(seat).some((key3) => !["recipientName", "recipientEmail", "purchaserEmail"].includes(key3)) || typeof seat.purchaserEmail !== "string" || seat.purchaserEmail.length > 254) throw new TypeError("invalid seat");
|
|
16253
|
+
checkoutNamedSeatSelection(seat, seat.purchaserEmail);
|
|
16254
|
+
}
|
|
15766
16255
|
} catch {
|
|
15767
16256
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
15768
16257
|
}
|
|
@@ -15835,8 +16324,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15835
16324
|
offerId: String(input.offerId),
|
|
15836
16325
|
offerDigest: String(input.offerDigest),
|
|
15837
16326
|
quoteDigest: String(input.quoteDigest),
|
|
15838
|
-
priceId: String(input.priceId)
|
|
15839
|
-
|
|
16327
|
+
priceId: String(input.priceId),
|
|
16328
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {}
|
|
16329
|
+
}, options.now?.() ?? Date.now(), peer.seatPriceId);
|
|
15840
16330
|
return respond({ ok: true, binding });
|
|
15841
16331
|
}
|
|
15842
16332
|
const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
|
|
@@ -15864,13 +16354,13 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15864
16354
|
|
|
15865
16355
|
// src/schema-membership-authority.ts
|
|
15866
16356
|
var attr = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
15867
|
-
var
|
|
16357
|
+
var id5 = () => attr("string", false, true, true);
|
|
15868
16358
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
15869
16359
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
15870
16360
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
15871
16361
|
var membershipAuthorityEntities = {
|
|
15872
16362
|
networkMembership: { attrs: {
|
|
15873
|
-
id:
|
|
16363
|
+
id: id5(),
|
|
15874
16364
|
membershipId: key(),
|
|
15875
16365
|
...scope(),
|
|
15876
16366
|
personId: key(true),
|
|
@@ -15910,7 +16400,7 @@ var membershipAuthorityEntities = {
|
|
|
15910
16400
|
restartId: key(true)
|
|
15911
16401
|
} },
|
|
15912
16402
|
membershipRestart: { attrs: {
|
|
15913
|
-
id:
|
|
16403
|
+
id: id5(),
|
|
15914
16404
|
...scope(),
|
|
15915
16405
|
membershipId: key(),
|
|
15916
16406
|
restartId: key(),
|
|
@@ -15920,7 +16410,7 @@ var membershipAuthorityEntities = {
|
|
|
15920
16410
|
createdAt: attr("number")
|
|
15921
16411
|
} },
|
|
15922
16412
|
membershipPurchaseHistory: { attrs: {
|
|
15923
|
-
id:
|
|
16413
|
+
id: id5(),
|
|
15924
16414
|
...scope(),
|
|
15925
16415
|
membershipId: key(),
|
|
15926
16416
|
providerSubscriptionId: key(),
|
|
@@ -15929,7 +16419,7 @@ var membershipAuthorityEntities = {
|
|
|
15929
16419
|
archivedAt: attr("number")
|
|
15930
16420
|
} },
|
|
15931
16421
|
networkEntitlement: { attrs: {
|
|
15932
|
-
id:
|
|
16422
|
+
id: id5(),
|
|
15933
16423
|
...scope(),
|
|
15934
16424
|
membershipId: key(),
|
|
15935
16425
|
seatId: key(true),
|
|
@@ -15946,10 +16436,12 @@ var membershipAuthorityEntities = {
|
|
|
15946
16436
|
journeyApprovedAt: attr("number", true)
|
|
15947
16437
|
} },
|
|
15948
16438
|
networkSeat: { attrs: {
|
|
15949
|
-
id:
|
|
16439
|
+
id: id5(),
|
|
15950
16440
|
...scope(),
|
|
15951
16441
|
primaryMembershipId: key(),
|
|
15952
|
-
purchaserUserId: key(),
|
|
16442
|
+
purchaserUserId: key(true),
|
|
16443
|
+
purchaserEmailNormalized: key(true),
|
|
16444
|
+
combinedInvoice: attr("json", true),
|
|
15953
16445
|
recipientName: attr("string"),
|
|
15954
16446
|
recipientEmailNormalized: key(),
|
|
15955
16447
|
recipientClaimId: key(),
|
|
@@ -15981,9 +16473,9 @@ var membershipAuthorityEntities = {
|
|
|
15981
16473
|
refundedCents: attr("number", true),
|
|
15982
16474
|
providerRefundIds: attr("json", true)
|
|
15983
16475
|
} },
|
|
15984
|
-
networkSeatRecipient: { attrs: { id:
|
|
16476
|
+
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
15985
16477
|
membershipEffect: { attrs: {
|
|
15986
|
-
id:
|
|
16478
|
+
id: id5(),
|
|
15987
16479
|
chapterId: key(),
|
|
15988
16480
|
environment: key(),
|
|
15989
16481
|
runtime: key(),
|
|
@@ -16002,11 +16494,11 @@ var membershipAuthorityRules = Object.fromEntries(Object.keys(membershipAuthorit
|
|
|
16002
16494
|
|
|
16003
16495
|
// src/schema-membership-offers.ts
|
|
16004
16496
|
var attr2 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
16005
|
-
var
|
|
16497
|
+
var id6 = () => attr2("string", false, true, true);
|
|
16006
16498
|
var key2 = () => attr2("string", false, false, true);
|
|
16007
16499
|
var membershipOfferEntities = {
|
|
16008
16500
|
membershipAuthorityState: { attrs: {
|
|
16009
|
-
id:
|
|
16501
|
+
id: id6(),
|
|
16010
16502
|
chapterId: key2(),
|
|
16011
16503
|
environment: key2(),
|
|
16012
16504
|
mode: key2(),
|
|
@@ -16016,7 +16508,7 @@ var membershipOfferEntities = {
|
|
|
16016
16508
|
updatedAt: attr2("number")
|
|
16017
16509
|
} },
|
|
16018
16510
|
membershipCheckoutOffer: { attrs: {
|
|
16019
|
-
id:
|
|
16511
|
+
id: id6(),
|
|
16020
16512
|
applicationId: key2(),
|
|
16021
16513
|
appId: key2(),
|
|
16022
16514
|
chapterId: key2(),
|
|
@@ -16031,7 +16523,7 @@ var membershipOfferEntities = {
|
|
|
16031
16523
|
authorityRevision: attr2("number")
|
|
16032
16524
|
} },
|
|
16033
16525
|
membershipOfferCheckout: { attrs: {
|
|
16034
|
-
id:
|
|
16526
|
+
id: id6(),
|
|
16035
16527
|
appId: key2(),
|
|
16036
16528
|
chapterId: key2(),
|
|
16037
16529
|
environment: key2(),
|
|
@@ -16042,7 +16534,8 @@ var membershipOfferEntities = {
|
|
|
16042
16534
|
offerDigest: attr2("string"),
|
|
16043
16535
|
quoteDigest: attr2("string"),
|
|
16044
16536
|
priceId: attr2("string"),
|
|
16045
|
-
boundAt: attr2("number")
|
|
16537
|
+
boundAt: attr2("number"),
|
|
16538
|
+
namedSeat: attr2("json", true)
|
|
16046
16539
|
} }
|
|
16047
16540
|
};
|
|
16048
16541
|
var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
|
|
@@ -16097,7 +16590,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
16097
16590
|
}
|
|
16098
16591
|
|
|
16099
16592
|
// src/membership-seat-cancel.ts
|
|
16100
|
-
var
|
|
16593
|
+
var import_stripe17 = require("@odla-ai/stripe");
|
|
16101
16594
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
16102
16595
|
membershipClock(now);
|
|
16103
16596
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -16125,7 +16618,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16125
16618
|
}
|
|
16126
16619
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
16127
16620
|
const request = async (method, path, params) => {
|
|
16128
|
-
const result = await (0,
|
|
16621
|
+
const result = await (0, import_stripe17.stripeRequest)(
|
|
16129
16622
|
{ secretKey },
|
|
16130
16623
|
method,
|
|
16131
16624
|
path,
|
|
@@ -16138,7 +16631,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16138
16631
|
let payment;
|
|
16139
16632
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
16140
16633
|
else {
|
|
16141
|
-
const snapshot = await listStripeObjects((0,
|
|
16634
|
+
const snapshot = await listStripeObjects((0, import_stripe17.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
16142
16635
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
16143
16636
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
16144
16637
|
payment = matches2[0];
|
|
@@ -16151,9 +16644,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16151
16644
|
if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
|
|
16152
16645
|
if (payment.status !== "canceled") {
|
|
16153
16646
|
if (!["requires_payment_method", "requires_confirmation", "requires_action", "processing"].includes(payment.status)) throw new Error("seat payment outcome unresolved");
|
|
16154
|
-
const
|
|
16155
|
-
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(
|
|
16156
|
-
if (payment.id !==
|
|
16647
|
+
const id7 = payment.id;
|
|
16648
|
+
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id7)}/cancel`, { cancellation_reason: "abandoned" });
|
|
16649
|
+
if (payment.id !== id7) throw new Error("seat cancellation receipt mismatch");
|
|
16157
16650
|
}
|
|
16158
16651
|
if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
|
|
16159
16652
|
await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
|
|
@@ -16475,7 +16968,7 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
16475
16968
|
async function requestMembershipOfferPreview(options) {
|
|
16476
16969
|
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
16477
16970
|
const preview = result.preview;
|
|
16478
|
-
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
16971
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.seatPriceCents !== void 0 && (!Number.isSafeInteger(preview.seatPriceCents) || preview.seatPriceCents < 0) || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
16479
16972
|
return preview;
|
|
16480
16973
|
}
|
|
16481
16974
|
async function requestMembershipCheckoutBinding(options) {
|
|
@@ -16485,7 +16978,8 @@ async function requestMembershipCheckoutBinding(options) {
|
|
|
16485
16978
|
offerId: options.offerId,
|
|
16486
16979
|
offerDigest: options.offerDigest,
|
|
16487
16980
|
quoteDigest: options.quoteDigest,
|
|
16488
|
-
priceId: options.priceId
|
|
16981
|
+
priceId: options.priceId,
|
|
16982
|
+
...options.namedSeat ? { namedSeat: options.namedSeat } : {}
|
|
16489
16983
|
});
|
|
16490
16984
|
return verifyMembershipCheckoutBinding(result.binding, options);
|
|
16491
16985
|
}
|