@odla-ai/chapter 0.54.2 → 0.55.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-VEOGTSVX.js} +507 -359
- package/dist/chunk-VEOGTSVX.js.map +1 -0
- package/dist/index.cjs +515 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +511 -258
- package/dist/index.js.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +7 -0
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +1000 -609
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +20 -1
- package/dist/worker/index.d.ts +20 -1
- package/dist/worker/index.js +993 -602
- 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
|
|
@@ -1950,11 +1953,11 @@ async function sendTemplated(deps, input) {
|
|
|
1950
1953
|
const previous = prior[0];
|
|
1951
1954
|
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
1955
|
const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
|
|
1953
|
-
const
|
|
1956
|
+
const id7 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1954
1957
|
const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
|
|
1955
1958
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
|
|
1956
1959
|
const row2 = {
|
|
1957
|
-
id:
|
|
1960
|
+
id: id7,
|
|
1958
1961
|
groupId: input.group.id,
|
|
1959
1962
|
to: decision.to,
|
|
1960
1963
|
template: input.template,
|
|
@@ -1972,15 +1975,15 @@ async function sendTemplated(deps, input) {
|
|
|
1972
1975
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
1973
1976
|
if ((0, import_crm7.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
1974
1977
|
try {
|
|
1975
|
-
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1976
|
-
mutationId: `email-claim:${
|
|
1978
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
1979
|
+
mutationId: `email-claim:${id7}:${attempt}`,
|
|
1977
1980
|
guards: [previous ? {
|
|
1978
1981
|
ns: "emailLog",
|
|
1979
|
-
id:
|
|
1982
|
+
id: id7,
|
|
1980
1983
|
exists: true,
|
|
1981
1984
|
equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
|
|
1982
1985
|
...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
|
|
1983
|
-
} : { ns: "emailLog", id:
|
|
1986
|
+
} : { ns: "emailLog", id: id7, exists: false }]
|
|
1984
1987
|
});
|
|
1985
1988
|
if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
|
|
1986
1989
|
} catch (error) {
|
|
@@ -2000,16 +2003,16 @@ async function sendTemplated(deps, input) {
|
|
|
2000
2003
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
2001
2004
|
messageId = response.messageId;
|
|
2002
2005
|
} catch {
|
|
2003
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2004
|
-
guards: [{ ns: "emailLog", id:
|
|
2006
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
2007
|
+
guards: [{ ns: "emailLog", id: id7, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
2005
2008
|
});
|
|
2006
2009
|
return { sent: false, reason: "delivery-uncertain" };
|
|
2007
2010
|
}
|
|
2008
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2009
|
-
mutationId: `email-confirmed:${
|
|
2011
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
2012
|
+
mutationId: `email-confirmed:${id7}:${attempt}`,
|
|
2010
2013
|
guards: [{
|
|
2011
2014
|
ns: "emailLog",
|
|
2012
|
-
id:
|
|
2015
|
+
id: id7,
|
|
2013
2016
|
exists: true,
|
|
2014
2017
|
equals: { deliveryState: "sending", deliveryAttempt: attempt }
|
|
2015
2018
|
}]
|
|
@@ -2194,8 +2197,8 @@ async function hash(value) {
|
|
|
2194
2197
|
async function membershipDiscountCouponId(input, policyId) {
|
|
2195
2198
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
2196
2199
|
}
|
|
2197
|
-
async function load(db, ns,
|
|
2198
|
-
return (await db.query({ [ns]: { $: { where: { id:
|
|
2200
|
+
async function load(db, ns, id7) {
|
|
2201
|
+
return (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2199
2202
|
}
|
|
2200
2203
|
async function membershipOfferIdForRequest(input) {
|
|
2201
2204
|
return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
|
|
@@ -2329,20 +2332,20 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2329
2332
|
|
|
2330
2333
|
// src/commercial-evidence-guard.ts
|
|
2331
2334
|
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:
|
|
2335
|
+
const ns = "networkCommercialExceptions", id7 = `network-commercial-exception:${eventId}:collision`;
|
|
2336
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2337
|
+
if (!row2) return { ns, id: id7, exists: false };
|
|
2335
2338
|
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
2336
2339
|
throw Object.assign(new Error(code), { code });
|
|
2337
2340
|
}
|
|
2338
|
-
return { ns, id:
|
|
2341
|
+
return { ns, id: id7, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
2339
2342
|
}
|
|
2340
2343
|
|
|
2341
2344
|
// src/membership-authority-state.ts
|
|
2342
2345
|
var membershipFail = (code) => {
|
|
2343
2346
|
throw Object.assign(new Error(code), { code });
|
|
2344
2347
|
};
|
|
2345
|
-
var membershipLoad = async (db, ns,
|
|
2348
|
+
var membershipLoad = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2346
2349
|
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
2347
2350
|
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
2348
2351
|
function requireMembershipScope(row2, input) {
|
|
@@ -2353,18 +2356,18 @@ async function loadMembershipAuthorityState(db, input) {
|
|
|
2353
2356
|
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
2357
|
return row2;
|
|
2355
2358
|
}
|
|
2356
|
-
var membershipUpdate = (ns,
|
|
2359
|
+
var membershipUpdate = (ns, id7, attrs) => ({ t: "update", ns, id: id7, attrs });
|
|
2357
2360
|
var membershipClock = (now) => {
|
|
2358
2361
|
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
2359
2362
|
};
|
|
2360
2363
|
|
|
2361
2364
|
// src/membership-restart-payment.ts
|
|
2362
2365
|
async function membershipRestartPayment(db, target, member, now) {
|
|
2363
|
-
const
|
|
2366
|
+
const id7 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id7);
|
|
2364
2367
|
requireMembershipScope(restart, target);
|
|
2365
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2368
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2366
2369
|
requireMembershipScope(entitlement, target);
|
|
2367
|
-
if (entitlement.membershipId !==
|
|
2370
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
2368
2371
|
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
2369
2372
|
return {
|
|
2370
2373
|
restartId: String(restart.restartId),
|
|
@@ -2381,49 +2384,117 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
2381
2384
|
graceEndsAt: 0
|
|
2382
2385
|
},
|
|
2383
2386
|
ops: [
|
|
2384
|
-
membershipUpdate("membershipRestart",
|
|
2385
|
-
{ t: "retract", ns: "networkMembership", id:
|
|
2387
|
+
membershipUpdate("membershipRestart", id7, { status: "complete" }),
|
|
2388
|
+
{ t: "retract", ns: "networkMembership", id: id7, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
2386
2389
|
],
|
|
2387
2390
|
guards: [
|
|
2388
|
-
{ ns: "networkMembership", id:
|
|
2389
|
-
{ ns: "membershipRestart", id:
|
|
2391
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
2392
|
+
{ ns: "membershipRestart", id: id7, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
2390
2393
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
2391
2394
|
]
|
|
2392
2395
|
};
|
|
2393
2396
|
}
|
|
2394
2397
|
|
|
2398
|
+
// src/checkout-named-seat.ts
|
|
2399
|
+
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
2400
|
+
const row2 = value;
|
|
2401
|
+
const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
2402
|
+
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
2403
|
+
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
2404
|
+
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) {
|
|
2405
|
+
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
2406
|
+
}
|
|
2407
|
+
return { recipientName: name, recipientEmail: email };
|
|
2408
|
+
}
|
|
2409
|
+
async function checkoutNamedSeatIdentity(target, email) {
|
|
2410
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
2411
|
+
target.appId,
|
|
2412
|
+
target.chapterId,
|
|
2413
|
+
target.environment,
|
|
2414
|
+
target.runtime,
|
|
2415
|
+
email
|
|
2416
|
+
])));
|
|
2417
|
+
return {
|
|
2418
|
+
seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
|
|
2419
|
+
membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
|
|
2420
|
+
recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2395
2424
|
// src/membership-checkout-binding.ts
|
|
2396
2425
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
2397
2426
|
var reject2 = (code) => {
|
|
2398
2427
|
throw Object.assign(new Error(code), { code });
|
|
2399
2428
|
};
|
|
2400
|
-
var load2 = async (db, ns,
|
|
2429
|
+
var load2 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2401
2430
|
async function membershipCheckoutBindingId(input) {
|
|
2402
2431
|
return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
|
|
2403
2432
|
}
|
|
2404
2433
|
async function verifyMembershipCheckoutBinding(value, input) {
|
|
2405
2434
|
const row2 = value;
|
|
2406
2435
|
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
|
-
|
|
2436
|
+
if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
|
|
2437
|
+
if (input.namedSeat) {
|
|
2438
|
+
const expected = {
|
|
2439
|
+
...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
|
|
2440
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
|
|
2441
|
+
};
|
|
2442
|
+
const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
|
|
2443
|
+
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");
|
|
2444
|
+
}
|
|
2445
|
+
return {
|
|
2446
|
+
id: row2.id,
|
|
2447
|
+
...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
|
|
2448
|
+
boundAt: row2.boundAt,
|
|
2449
|
+
...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
|
|
2450
|
+
};
|
|
2408
2451
|
}
|
|
2409
|
-
async function bindMembershipCheckout(db, input, now) {
|
|
2452
|
+
async function bindMembershipCheckout(db, input, now, seatPriceId) {
|
|
2410
2453
|
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
2454
|
const stateId = `${input.chapterId}:${input.environment}`;
|
|
2412
2455
|
const state = await load2(db, "membershipAuthorityState", stateId);
|
|
2413
2456
|
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",
|
|
2457
|
+
const id7 = await membershipCheckoutBindingId(input);
|
|
2458
|
+
const existing = await load2(db, "membershipOfferCheckout", id7);
|
|
2416
2459
|
if (existing) return verifyMembershipCheckoutBinding(existing, input);
|
|
2417
2460
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
2418
2461
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
2419
2462
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
2420
|
-
const restart = await load2(db, "membershipRestart",
|
|
2421
|
-
|
|
2463
|
+
const restart = await load2(db, "membershipRestart", id7.replace(/^checkout_/, "membership_"));
|
|
2464
|
+
let namedSeat;
|
|
2465
|
+
if (input.namedSeat) {
|
|
2466
|
+
if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
|
|
2467
|
+
const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
|
|
2468
|
+
namedSeat = {
|
|
2469
|
+
...selection,
|
|
2470
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
|
|
2471
|
+
...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
|
|
2472
|
+
priceId: seatPriceId,
|
|
2473
|
+
amountCents: offer.contract.offering.seatPriceCents
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
const receipt = { id: id7, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
|
|
2422
2477
|
try {
|
|
2423
|
-
await db.transact([
|
|
2424
|
-
|
|
2478
|
+
await db.transact([
|
|
2479
|
+
{ t: "update", ns: "membershipOfferCheckout", id: id7, attrs: receipt },
|
|
2480
|
+
...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
|
|
2481
|
+
id: namedSeat.recipientClaimId,
|
|
2482
|
+
appId: input.appId,
|
|
2483
|
+
chapterId: input.chapterId,
|
|
2484
|
+
environment: input.environment,
|
|
2485
|
+
runtime: input.runtime,
|
|
2486
|
+
seatId: namedSeat.seatId,
|
|
2487
|
+
recipientEmailNormalized: namedSeat.recipientEmail,
|
|
2488
|
+
createdAt: now
|
|
2489
|
+
} }] : []
|
|
2490
|
+
], {
|
|
2491
|
+
mutationId: `membership-checkout:${id7}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
2425
2492
|
guards: [
|
|
2426
|
-
{ ns: "membershipOfferCheckout", id:
|
|
2493
|
+
{ ns: "membershipOfferCheckout", id: id7, exists: false },
|
|
2494
|
+
...namedSeat ? [
|
|
2495
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2496
|
+
{ ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
|
|
2497
|
+
] : [],
|
|
2427
2498
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
2428
2499
|
{ ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2429
2500
|
]
|
|
@@ -2431,11 +2502,78 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
2431
2502
|
} catch (error) {
|
|
2432
2503
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2433
2504
|
}
|
|
2434
|
-
const saved = await load2(db, "membershipOfferCheckout",
|
|
2505
|
+
const saved = await load2(db, "membershipOfferCheckout", id7);
|
|
2435
2506
|
if (!saved) return reject2("membership_binding_unconfirmed");
|
|
2436
2507
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
2437
2508
|
}
|
|
2438
2509
|
|
|
2510
|
+
// src/membership-checkout-seat-payment.ts
|
|
2511
|
+
function checkoutSeatPaymentOperations(target, seat, input) {
|
|
2512
|
+
const { now, paidAt, endsAt } = input;
|
|
2513
|
+
const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
|
|
2514
|
+
const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
|
|
2515
|
+
return [
|
|
2516
|
+
membershipUpdate("networkSeat", seat.seatId, {
|
|
2517
|
+
id: seat.seatId,
|
|
2518
|
+
...membershipScope(target),
|
|
2519
|
+
primaryMembershipId: seat.membershipId,
|
|
2520
|
+
purchaserEmailNormalized: seat.purchaserEmail,
|
|
2521
|
+
recipientName: seat.recipientName,
|
|
2522
|
+
recipientEmailNormalized: seat.recipientEmail,
|
|
2523
|
+
recipientClaimId: seat.recipientClaimId,
|
|
2524
|
+
priceId: seat.priceId,
|
|
2525
|
+
primaryPriceId: input.primaryPriceId,
|
|
2526
|
+
amountCents: seat.amountCents,
|
|
2527
|
+
currency: input.offering.currency,
|
|
2528
|
+
interval: input.offering.interval,
|
|
2529
|
+
providerCustomerId: input.customerId,
|
|
2530
|
+
providerSubscriptionId: input.subscriptionId,
|
|
2531
|
+
combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
|
|
2532
|
+
status: included ? "pending_acceptance" : "renewal_pending",
|
|
2533
|
+
preparedAt: input.boundAt,
|
|
2534
|
+
purchasedAt: paidAt,
|
|
2535
|
+
termStartedAt: paidAt,
|
|
2536
|
+
termEndsAt: endsAt,
|
|
2537
|
+
acceptanceEndsAt: endsAt,
|
|
2538
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2539
|
+
offeringSnapshot: input.offering,
|
|
2540
|
+
paymentEventId: input.eventId,
|
|
2541
|
+
...included ? { entitlementId } : {},
|
|
2542
|
+
revision: 1,
|
|
2543
|
+
createdAt: now,
|
|
2544
|
+
updatedAt: now
|
|
2545
|
+
}),
|
|
2546
|
+
...included ? [membershipUpdate("networkEntitlement", entitlementId, {
|
|
2547
|
+
id: entitlementId,
|
|
2548
|
+
...membershipScope(target),
|
|
2549
|
+
membershipId: seat.membershipId,
|
|
2550
|
+
seatId: seat.seatId,
|
|
2551
|
+
kind: "named_seat",
|
|
2552
|
+
status: "pending_acceptance",
|
|
2553
|
+
termStartedAt: paidAt,
|
|
2554
|
+
termEndsAt: endsAt,
|
|
2555
|
+
offeringSnapshot: input.offering,
|
|
2556
|
+
sourceEventId: input.eventId,
|
|
2557
|
+
revision: 1,
|
|
2558
|
+
createdAt: now,
|
|
2559
|
+
updatedAt: now
|
|
2560
|
+
})] : [],
|
|
2561
|
+
membershipUpdate("membershipEffect", effectId, {
|
|
2562
|
+
id: effectId,
|
|
2563
|
+
chapterId: target.chapterId,
|
|
2564
|
+
environment: target.environment,
|
|
2565
|
+
runtime: target.runtime,
|
|
2566
|
+
membershipId: seat.membershipId,
|
|
2567
|
+
seatId: seat.seatId,
|
|
2568
|
+
kind: included ? "invite_named_recipient" : "attach_seat_renewal",
|
|
2569
|
+
status: "pending",
|
|
2570
|
+
attempts: 0,
|
|
2571
|
+
createdAt: now,
|
|
2572
|
+
updatedAt: now
|
|
2573
|
+
})
|
|
2574
|
+
];
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2439
2577
|
// src/membership-payment-authority.ts
|
|
2440
2578
|
async function networkMembershipId(input) {
|
|
2441
2579
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -2451,11 +2589,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2451
2589
|
{ ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
|
|
2452
2590
|
);
|
|
2453
2591
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
2592
|
+
const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
|
|
2593
|
+
if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
|
|
2594
|
+
const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
|
|
2454
2595
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
2455
2596
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
2456
2597
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
2457
2598
|
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 !==
|
|
2599
|
+
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
2600
|
appId: input.appId,
|
|
2460
2601
|
chapterId: input.chapterId,
|
|
2461
2602
|
environment: input.environment,
|
|
@@ -2467,14 +2608,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2467
2608
|
const paidAt = fact.invoicePaidAt;
|
|
2468
2609
|
const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
|
|
2469
2610
|
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",
|
|
2611
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
2612
|
+
let member = await membershipLoad(db, "networkMembership", id7);
|
|
2472
2613
|
if (!member || member.status === "restarting") {
|
|
2473
2614
|
const offering = offer.contract.offering;
|
|
2474
2615
|
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
2475
2616
|
const attrs = {
|
|
2476
|
-
id:
|
|
2477
|
-
membershipId:
|
|
2617
|
+
id: id7,
|
|
2618
|
+
membershipId: id7,
|
|
2478
2619
|
...membershipScope(input),
|
|
2479
2620
|
baseSkuId: input.skuId,
|
|
2480
2621
|
status: "paid_pending_approval",
|
|
@@ -2490,8 +2631,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2490
2631
|
founding: offer.contract.founding,
|
|
2491
2632
|
cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
|
|
2492
2633
|
baseAmountCents: offering.basePriceCents,
|
|
2493
|
-
amountCents:
|
|
2634
|
+
amountCents: totalCents,
|
|
2494
2635
|
renewalAmountCents: offer.contract.renewalCents,
|
|
2636
|
+
...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
|
|
2495
2637
|
currency: offering.currency,
|
|
2496
2638
|
interval: offering.interval,
|
|
2497
2639
|
initialTermStartedAt: paidAt,
|
|
@@ -2508,11 +2650,11 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2508
2650
|
};
|
|
2509
2651
|
try {
|
|
2510
2652
|
await db.transact([
|
|
2511
|
-
membershipUpdate("networkMembership",
|
|
2653
|
+
membershipUpdate("networkMembership", id7, attrs),
|
|
2512
2654
|
membershipUpdate("networkEntitlement", entitlementId, {
|
|
2513
2655
|
id: entitlementId,
|
|
2514
2656
|
...membershipScope(input),
|
|
2515
|
-
membershipId:
|
|
2657
|
+
membershipId: id7,
|
|
2516
2658
|
kind: "primary",
|
|
2517
2659
|
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
2518
2660
|
termStartedAt: paidAt,
|
|
@@ -2524,23 +2666,45 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2524
2666
|
updatedAt: now,
|
|
2525
2667
|
...restart?.entitlementAttrs
|
|
2526
2668
|
}),
|
|
2527
|
-
...restart?.ops ?? []
|
|
2528
|
-
|
|
2529
|
-
|
|
2669
|
+
...restart?.ops ?? [],
|
|
2670
|
+
...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
|
|
2671
|
+
offering,
|
|
2672
|
+
primaryPriceId: String(binding.priceId),
|
|
2673
|
+
invoiceId: fact.invoiceId,
|
|
2674
|
+
invoiceTotalCents: totalCents,
|
|
2675
|
+
subscriptionId: input.providerSubscriptionId,
|
|
2676
|
+
customerId: input.providerCustomerId,
|
|
2677
|
+
eventId: input.eventId,
|
|
2678
|
+
paidAt: Number(paidAt),
|
|
2679
|
+
endsAt: Number(endsAt),
|
|
2680
|
+
boundAt: Number(binding.boundAt),
|
|
2681
|
+
now
|
|
2682
|
+
}) : []
|
|
2683
|
+
], { mutationId: `membership-paid:${id7}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
2684
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id7, exists: false }],
|
|
2685
|
+
...namedSeat ? [
|
|
2686
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2687
|
+
{
|
|
2688
|
+
ns: "networkSeatRecipient",
|
|
2689
|
+
id: namedSeat.recipientClaimId,
|
|
2690
|
+
exists: true,
|
|
2691
|
+
equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
|
|
2692
|
+
}
|
|
2693
|
+
] : [],
|
|
2530
2694
|
{ 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 } },
|
|
2695
|
+
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
|
|
2532
2696
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
2533
2697
|
evidenceGuard
|
|
2534
2698
|
] });
|
|
2535
2699
|
} catch (error) {
|
|
2536
2700
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2537
2701
|
}
|
|
2538
|
-
member = await membershipLoad(db, "networkMembership",
|
|
2702
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
2539
2703
|
}
|
|
2540
2704
|
requireMembershipScope(member, input);
|
|
2541
2705
|
if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
|
|
2542
2706
|
return {
|
|
2543
|
-
membershipId:
|
|
2707
|
+
membershipId: id7,
|
|
2544
2708
|
status: String(member.status),
|
|
2545
2709
|
cohort: String(member.cohort),
|
|
2546
2710
|
initialTermStartedAt: Number(member.initialTermStartedAt),
|
|
@@ -2611,19 +2775,19 @@ async function stopPreviousSubscription(member, target, secret) {
|
|
|
2611
2775
|
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
2612
2776
|
membershipClock(now);
|
|
2613
2777
|
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
2614
|
-
const
|
|
2615
|
-
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([
|
|
2778
|
+
const id7 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
2779
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id7, input.previousSubscriptionId]))}`;
|
|
2616
2780
|
const state = await loadMembershipAuthorityState(db, input);
|
|
2617
|
-
const previous = await membershipLoad(db, "networkMembership",
|
|
2781
|
+
const previous = await membershipLoad(db, "networkMembership", id7);
|
|
2618
2782
|
requireMembershipScope(previous, input);
|
|
2619
2783
|
const receipt = {
|
|
2620
2784
|
applicationId: input.applicationId,
|
|
2621
|
-
membershipId:
|
|
2785
|
+
membershipId: id7,
|
|
2622
2786
|
restartId,
|
|
2623
2787
|
memberNumber: Number(previous.memberNumber),
|
|
2624
2788
|
status: "ready"
|
|
2625
2789
|
};
|
|
2626
|
-
const existing = await membershipLoad(db, "membershipRestart",
|
|
2790
|
+
const existing = await membershipLoad(db, "membershipRestart", id7);
|
|
2627
2791
|
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
2628
2792
|
requireMembershipScope(existing, input);
|
|
2629
2793
|
return receipt;
|
|
@@ -2639,33 +2803,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2639
2803
|
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
2640
2804
|
id: restartId,
|
|
2641
2805
|
...membershipScope(input),
|
|
2642
|
-
membershipId:
|
|
2806
|
+
membershipId: id7,
|
|
2643
2807
|
providerSubscriptionId: input.previousSubscriptionId,
|
|
2644
2808
|
memberSnapshot: previous,
|
|
2645
2809
|
bindingSnapshot: binding2,
|
|
2646
2810
|
archivedAt: now
|
|
2647
2811
|
}),
|
|
2648
|
-
membershipUpdate("networkMembership",
|
|
2812
|
+
membershipUpdate("networkMembership", id7, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
2649
2813
|
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
2650
2814
|
...seats.guards,
|
|
2651
|
-
{ ns: "networkMembership", id:
|
|
2815
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
2652
2816
|
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
2653
2817
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
2654
2818
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2655
2819
|
] });
|
|
2656
2820
|
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2657
|
-
const reserved = await membershipLoad(db, "networkMembership",
|
|
2821
|
+
const reserved = await membershipLoad(db, "networkMembership", id7);
|
|
2658
2822
|
requireMembershipScope(reserved, input);
|
|
2659
2823
|
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2660
2824
|
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
2661
2825
|
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
2662
2826
|
requireMembershipScope(binding, input);
|
|
2663
2827
|
await db.transact([
|
|
2664
|
-
membershipUpdate("membershipRestart",
|
|
2828
|
+
membershipUpdate("membershipRestart", id7, { id: id7, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
2665
2829
|
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
2666
2830
|
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
2667
|
-
{ ns: "membershipRestart", id:
|
|
2668
|
-
{ ns: "networkMembership", id:
|
|
2831
|
+
{ ns: "membershipRestart", id: id7, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
2832
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
2669
2833
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
2670
2834
|
] });
|
|
2671
2835
|
return receipt;
|
|
@@ -2675,15 +2839,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2675
2839
|
async function readMembershipAccess(db, input, now) {
|
|
2676
2840
|
membershipClock(now);
|
|
2677
2841
|
await loadMembershipAuthorityState(db, input);
|
|
2678
|
-
const
|
|
2679
|
-
if (!member) return { membershipId:
|
|
2842
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
2843
|
+
if (!member) return { membershipId: id7, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
|
|
2680
2844
|
requireMembershipScope(member, input);
|
|
2681
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2845
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2682
2846
|
requireMembershipScope(entitlement, input);
|
|
2683
2847
|
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 ===
|
|
2848
|
+
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
2849
|
return {
|
|
2686
|
-
membershipId:
|
|
2850
|
+
membershipId: id7,
|
|
2687
2851
|
allowed: allowed2,
|
|
2688
2852
|
status: String(member.status),
|
|
2689
2853
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
@@ -2801,7 +2965,7 @@ async function refundChapterNetworkMembership(db, env, ctx, app) {
|
|
|
2801
2965
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2802
2966
|
const receipt = await ctx.membershipAuthority.refundMembership(target, env);
|
|
2803
2967
|
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");
|
|
2968
|
+
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
2969
|
return receipt;
|
|
2806
2970
|
}
|
|
2807
2971
|
async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
@@ -2852,14 +3016,14 @@ function facts(snapshots) {
|
|
|
2852
3016
|
["payment_intent.updated", snapshots.payments?.objects ?? []]
|
|
2853
3017
|
];
|
|
2854
3018
|
return groups.flatMap(([eventType, objects]) => objects.map((object2) => {
|
|
2855
|
-
const
|
|
3019
|
+
const id7 = typeof object2.id === "string" ? object2.id : "invalid";
|
|
2856
3020
|
return normalizeNetworkCommercialStripeEvent({
|
|
2857
|
-
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${
|
|
3021
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id7}`,
|
|
2858
3022
|
type: eventType,
|
|
2859
3023
|
created: typeof object2.created === "number" ? object2.created : 0,
|
|
2860
3024
|
livemode: object2.livemode === true,
|
|
2861
3025
|
data: { object: object2 }
|
|
2862
|
-
}, `stripe-snapshot:${eventType}:${
|
|
3026
|
+
}, `stripe-snapshot:${eventType}:${id7}`);
|
|
2863
3027
|
}));
|
|
2864
3028
|
}
|
|
2865
3029
|
var selected = (row2) => ({
|
|
@@ -2934,9 +3098,9 @@ async function chapterBillingSubscriptions(secretKey, chapterId, env) {
|
|
|
2934
3098
|
// src/approve-application.ts
|
|
2935
3099
|
async function approveApplication(input) {
|
|
2936
3100
|
const { db, env, ctx } = input;
|
|
2937
|
-
const
|
|
2938
|
-
if (!
|
|
2939
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
3101
|
+
const id7 = String(input.app.id ?? "");
|
|
3102
|
+
if (!id7) return { ok: false, status: 404, error: "not found" };
|
|
3103
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2940
3104
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) {
|
|
2941
3105
|
return { ok: false, status: 404, error: "not found" };
|
|
2942
3106
|
}
|
|
@@ -2970,10 +3134,10 @@ async function approveApplication(input) {
|
|
|
2970
3134
|
try {
|
|
2971
3135
|
if (!duplicate) {
|
|
2972
3136
|
const write = await db.transact(
|
|
2973
|
-
[{ t: "update", ns: "applications", id:
|
|
2974
|
-
{ mutationId: `approve:${
|
|
3137
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3138
|
+
{ mutationId: `approve:${id7}`, guards: [{
|
|
2975
3139
|
ns: "applications",
|
|
2976
|
-
id:
|
|
3140
|
+
id: id7,
|
|
2977
3141
|
exists: true,
|
|
2978
3142
|
equals: Object.fromEntries(guardFields.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
2979
3143
|
absent: guardFields.filter((key3) => app[key3] === void 0)
|
|
@@ -2985,7 +3149,7 @@ async function approveApplication(input) {
|
|
|
2985
3149
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2986
3150
|
return { ok: false, status: 409, error: "application changed; reload before approving" };
|
|
2987
3151
|
}
|
|
2988
|
-
const current = (await db.query({ applications: { $: { where: { id:
|
|
3152
|
+
const current = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2989
3153
|
if (!current || current.status !== "approved" || current.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => current[key3] !== app[key3])) {
|
|
2990
3154
|
return { ok: false, status: 409, error: "application changed; approval effects were not applied" };
|
|
2991
3155
|
}
|
|
@@ -2995,7 +3159,7 @@ async function approveApplication(input) {
|
|
|
2995
3159
|
} catch {
|
|
2996
3160
|
return { ok: false, status: 503, error: "network membership approval reconciliation pending" };
|
|
2997
3161
|
}
|
|
2998
|
-
const afterAuthority = (await db.query({ applications: { $: { where: { id:
|
|
3162
|
+
const afterAuthority = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2999
3163
|
if (!afterAuthority || afterAuthority.status !== "approved" || afterAuthority.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => afterAuthority[key3] !== app[key3])) {
|
|
3000
3164
|
return { ok: false, status: 409, error: "application changed during network approval" };
|
|
3001
3165
|
}
|
|
@@ -3008,7 +3172,7 @@ async function approveApplication(input) {
|
|
|
3008
3172
|
if (!userId && typeof app.email === "string") {
|
|
3009
3173
|
const found = await clerkGetUserByEmail(secret, app.email, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
3010
3174
|
userId = found?.id ?? null;
|
|
3011
|
-
if (userId) await db.transact([{ t: "update", ns: "applications", id:
|
|
3175
|
+
if (userId) await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { clerkUserId: userId } }]);
|
|
3012
3176
|
}
|
|
3013
3177
|
if (userId) {
|
|
3014
3178
|
const user = await clerkGetUser(secret, userId, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
@@ -3030,8 +3194,8 @@ async function approveApplication(input) {
|
|
|
3030
3194
|
template: send,
|
|
3031
3195
|
to: app.email,
|
|
3032
3196
|
vars: { firstName: String(app.firstName ?? ""), membersUrl: `${input.origin}/members/` },
|
|
3033
|
-
applicationId:
|
|
3034
|
-
dedupeKey: `approve:${
|
|
3197
|
+
applicationId: id7,
|
|
3198
|
+
dedupeKey: `approve:${id7}`
|
|
3035
3199
|
}
|
|
3036
3200
|
);
|
|
3037
3201
|
emailLogged = result.sent;
|
|
@@ -3039,10 +3203,10 @@ async function approveApplication(input) {
|
|
|
3039
3203
|
return { ok: false, status: 503, error: "approval notice reconciliation pending", rolePromoted, emailLogged };
|
|
3040
3204
|
}
|
|
3041
3205
|
}
|
|
3042
|
-
await db.transact([{ t: "update", ns: "applications", id:
|
|
3206
|
+
await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { approvalEffectsPending: false } }], {
|
|
3043
3207
|
guards: [{
|
|
3044
3208
|
ns: "applications",
|
|
3045
|
-
id:
|
|
3209
|
+
id: id7,
|
|
3046
3210
|
exists: true,
|
|
3047
3211
|
equals: { status: "approved", ...app.canceled !== void 0 ? { canceled: app.canceled } : {} },
|
|
3048
3212
|
...app.canceled === void 0 ? { absent: ["canceled"] } : {}
|
|
@@ -3264,10 +3428,10 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3264
3428
|
tierId: target.tierId,
|
|
3265
3429
|
...typeof existing?.clerkUserId === "string" ? { clerkUserId: existing.clerkUserId } : {}
|
|
3266
3430
|
};
|
|
3267
|
-
const
|
|
3431
|
+
const id7 = existing ? String(existing.id) : `gift-application:${String(claim.id)}`;
|
|
3268
3432
|
const status2 = existing && existing.status !== "submitted" ? String(existing.status) : "paid_pending_vetting";
|
|
3269
|
-
ops.push({ t: "update", ns: "applications", id:
|
|
3270
|
-
id:
|
|
3433
|
+
ops.push({ t: "update", ns: "applications", id: id7, attrs: {
|
|
3434
|
+
id: id7,
|
|
3271
3435
|
groupId: chapterId,
|
|
3272
3436
|
email: beneficiary.email,
|
|
3273
3437
|
emailNormalized,
|
|
@@ -3278,12 +3442,12 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3278
3442
|
clerkUserId: beneficiary.userId,
|
|
3279
3443
|
...existing ? {} : { createdAt: now }
|
|
3280
3444
|
} });
|
|
3281
|
-
result = { kind: "application", id:
|
|
3445
|
+
result = { kind: "application", id: id7 };
|
|
3282
3446
|
} else {
|
|
3283
3447
|
if (beneficiary.applicationId) return null;
|
|
3284
|
-
const
|
|
3285
|
-
ops.push({ t: "update", ns: "giftEntitlements", id:
|
|
3286
|
-
id:
|
|
3448
|
+
const id7 = `gift-entitlement:${String(claim.id)}`;
|
|
3449
|
+
ops.push({ t: "update", ns: "giftEntitlements", id: id7, attrs: {
|
|
3450
|
+
id: id7,
|
|
3287
3451
|
claimId: String(claim.id),
|
|
3288
3452
|
orderId: String(claim.orderId),
|
|
3289
3453
|
chapterId,
|
|
@@ -3293,7 +3457,7 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3293
3457
|
active: true,
|
|
3294
3458
|
createdAt: now
|
|
3295
3459
|
} });
|
|
3296
|
-
result = { kind: "entitlement", id:
|
|
3460
|
+
result = { kind: "entitlement", id: id7 };
|
|
3297
3461
|
}
|
|
3298
3462
|
ops.unshift({ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
|
|
3299
3463
|
status: "redeemed",
|
|
@@ -3467,9 +3631,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
3467
3631
|
return "stored";
|
|
3468
3632
|
}
|
|
3469
3633
|
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:
|
|
3634
|
+
const id7 = `stripe-event:${input.eventId}`;
|
|
3635
|
+
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id: id7, attrs: {
|
|
3636
|
+
id: id7,
|
|
3473
3637
|
eventId: input.eventId,
|
|
3474
3638
|
digest: input.digest,
|
|
3475
3639
|
disposition: input.disposition,
|
|
@@ -3701,9 +3865,9 @@ var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
|
|
|
3701
3865
|
var fail3 = (message, code) => Object.assign(new Error(message), { code });
|
|
3702
3866
|
async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
|
|
3703
3867
|
if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3704
|
-
const
|
|
3868
|
+
const id7 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
|
|
3705
3869
|
const found = await db.query({
|
|
3706
|
-
networkCommercialExceptions: { $: { where: { id:
|
|
3870
|
+
networkCommercialExceptions: { $: { where: { id: id7 }, limit: 1 } },
|
|
3707
3871
|
networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
|
|
3708
3872
|
});
|
|
3709
3873
|
const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
|
|
@@ -3726,7 +3890,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3726
3890
|
action: "resolve",
|
|
3727
3891
|
reason: input.reason
|
|
3728
3892
|
} },
|
|
3729
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
3893
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
3730
3894
|
status: "resolved",
|
|
3731
3895
|
resolvedDigest: stored.digest,
|
|
3732
3896
|
...dismissed ? { reviewedDigest: dismissed } : {},
|
|
@@ -3735,7 +3899,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3735
3899
|
updatedAt: now
|
|
3736
3900
|
} }
|
|
3737
3901
|
], { mutationId: input.mutationId, guards: [
|
|
3738
|
-
{ ns: "networkCommercialExceptions", id:
|
|
3902
|
+
{ ns: "networkCommercialExceptions", id: id7, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
3739
3903
|
] });
|
|
3740
3904
|
if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
|
|
3741
3905
|
const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
|
|
@@ -3850,10 +4014,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3850
4014
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3851
4015
|
const event2 = normalizeWebhookEvent(provider.event, { requireProviderPayment: Boolean(ctx.membershipAuthority) });
|
|
3852
4016
|
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
|
|
4017
|
+
const id7 = crypto.randomUUID();
|
|
3854
4018
|
await db.transact([
|
|
3855
|
-
{ t: "update", ns: "paymentRecoveryReviews", id:
|
|
3856
|
-
id:
|
|
4019
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: id7, attrs: {
|
|
4020
|
+
id: id7,
|
|
3857
4021
|
eventId,
|
|
3858
4022
|
actorUserId: user.userId,
|
|
3859
4023
|
reviewedAt: Date.now(),
|
|
@@ -3862,8 +4026,8 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3862
4026
|
scope: `${target.appId}:${target.environment}:${target.runtime}`,
|
|
3863
4027
|
previousState: row2
|
|
3864
4028
|
} },
|
|
3865
|
-
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId:
|
|
3866
|
-
], { mutationId: `payment-recovery:${
|
|
4029
|
+
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId: id7 } }
|
|
4030
|
+
], { mutationId: `payment-recovery:${id7}`, guards: [{
|
|
3867
4031
|
ns: "stripeEventReceipts",
|
|
3868
4032
|
id: String(row2.id),
|
|
3869
4033
|
exists: true,
|
|
@@ -3932,43 +4096,121 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3932
4096
|
}
|
|
3933
4097
|
|
|
3934
4098
|
// src/membership-seat-refund-stripe.ts
|
|
4099
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4100
|
+
|
|
4101
|
+
// src/membership-checkout-seat-refund.ts
|
|
3935
4102
|
var import_stripe6 = require("@odla-ai/stripe");
|
|
3936
4103
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3937
4104
|
throw Object.assign(new Error(code), { code });
|
|
3938
4105
|
};
|
|
4106
|
+
var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
4107
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
4108
|
+
const invoiceRef = seat.combinedInvoice;
|
|
4109
|
+
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();
|
|
4110
|
+
const call = async (method, path, params, key3) => {
|
|
4111
|
+
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4112
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4113
|
+
return result.body;
|
|
4114
|
+
};
|
|
4115
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
4116
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4117
|
+
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();
|
|
4118
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
4119
|
+
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();
|
|
4120
|
+
let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
|
|
4121
|
+
if (!chargeId && !paymentId) {
|
|
4122
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
4123
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
4124
|
+
const payment = payments.data[0];
|
|
4125
|
+
if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
4126
|
+
paymentId = id2(payment.payment.payment_intent);
|
|
4127
|
+
}
|
|
4128
|
+
if (!chargeId) {
|
|
4129
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
4130
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
4131
|
+
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();
|
|
4132
|
+
chargeId = id2(payment.latest_charge);
|
|
4133
|
+
}
|
|
4134
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
4135
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
4136
|
+
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();
|
|
4137
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4138
|
+
if (items.length > 1) fail4();
|
|
4139
|
+
if (items.length && sub.status !== "canceled") {
|
|
4140
|
+
const item = items[0];
|
|
4141
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
4142
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4143
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
4144
|
+
}
|
|
4145
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
4146
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
4147
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
4148
|
+
let refundedCents = 0;
|
|
4149
|
+
const refundIds = [];
|
|
4150
|
+
for (const refund of list.data) {
|
|
4151
|
+
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();
|
|
4152
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
4153
|
+
if (refund.status === "succeeded") {
|
|
4154
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
4155
|
+
refundedCents += refund.amount;
|
|
4156
|
+
refundIds.push(refund.id);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
4160
|
+
if (refundedCents < seat.amountCents) {
|
|
4161
|
+
const remaining = seat.amountCents - refundedCents;
|
|
4162
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
4163
|
+
charge: chargeId,
|
|
4164
|
+
amount: remaining,
|
|
4165
|
+
reason: "requested_by_customer",
|
|
4166
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
4167
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
4168
|
+
if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
4169
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
4170
|
+
refundedCents += remaining;
|
|
4171
|
+
refundIds.push(refund.id);
|
|
4172
|
+
}
|
|
4173
|
+
return { refundedCents, refundIds };
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// src/membership-seat-refund-stripe.ts
|
|
4177
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
4178
|
+
throw Object.assign(new Error(code), { code });
|
|
4179
|
+
};
|
|
3939
4180
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3940
|
-
if (
|
|
4181
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4182
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
3941
4183
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3942
|
-
const result = await (0,
|
|
3943
|
-
if (!result.ok || !result.body) return
|
|
4184
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4185
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
3944
4186
|
return result.body;
|
|
3945
4187
|
};
|
|
3946
4188
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3947
4189
|
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))
|
|
4190
|
+
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
4191
|
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))
|
|
4192
|
+
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
4193
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3952
|
-
if (items.length > 1)
|
|
4194
|
+
if (items.length > 1) fail5();
|
|
3953
4195
|
if (items.length && sub.status !== "canceled") {
|
|
3954
4196
|
const item = items[0];
|
|
3955
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
4197
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
3956
4198
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3957
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
4199
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
3958
4200
|
}
|
|
3959
4201
|
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)
|
|
4202
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
3961
4203
|
let refundedCents = 0, pendingCents = 0;
|
|
3962
4204
|
const refundIds = [];
|
|
3963
4205
|
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))
|
|
4206
|
+
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
4207
|
if (refund.status === "succeeded") {
|
|
3966
4208
|
refundedCents += refund.amount;
|
|
3967
4209
|
refundIds.push(refund.id);
|
|
3968
4210
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3969
4211
|
}
|
|
3970
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3971
|
-
if (pendingCents)
|
|
4212
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
4213
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
3972
4214
|
if (refundedCents < seat.amountCents) {
|
|
3973
4215
|
const remaining = seat.amountCents - refundedCents;
|
|
3974
4216
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3977,8 +4219,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3977
4219
|
reason: "requested_by_customer",
|
|
3978
4220
|
metadata
|
|
3979
4221
|
}, `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")
|
|
4222
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
4223
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
3982
4224
|
refundedCents += remaining;
|
|
3983
4225
|
refundIds.push(refund.id);
|
|
3984
4226
|
}
|
|
@@ -4000,8 +4242,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4000
4242
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4001
4243
|
requireMembershipScope(member, input);
|
|
4002
4244
|
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",
|
|
4245
|
+
const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4246
|
+
let seat = await membershipLoad(db, "networkSeat", id7);
|
|
4005
4247
|
if (!seat) {
|
|
4006
4248
|
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4007
4249
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
@@ -4009,7 +4251,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4009
4251
|
membershipFail("named_seat_purchase_unavailable");
|
|
4010
4252
|
}
|
|
4011
4253
|
const attrs = {
|
|
4012
|
-
id:
|
|
4254
|
+
id: id7,
|
|
4013
4255
|
...membershipScope(input),
|
|
4014
4256
|
primaryMembershipId: memberId,
|
|
4015
4257
|
recipientName: name,
|
|
@@ -4034,20 +4276,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4034
4276
|
};
|
|
4035
4277
|
try {
|
|
4036
4278
|
await db.transact([
|
|
4037
|
-
membershipUpdate("networkSeat",
|
|
4279
|
+
membershipUpdate("networkSeat", id7, attrs),
|
|
4038
4280
|
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4039
4281
|
id: claimId,
|
|
4040
4282
|
appId: input.appId,
|
|
4041
4283
|
chapterId: input.chapterId,
|
|
4042
4284
|
environment: input.environment,
|
|
4043
4285
|
runtime: input.runtime,
|
|
4044
|
-
seatId:
|
|
4286
|
+
seatId: id7,
|
|
4045
4287
|
recipientEmailNormalized: email,
|
|
4046
4288
|
createdAt: now
|
|
4047
4289
|
}),
|
|
4048
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId:
|
|
4049
|
-
], { mutationId: `named-seat-reserve:${
|
|
4050
|
-
{ ns: "networkSeat", id:
|
|
4290
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4291
|
+
], { mutationId: `named-seat-reserve:${id7}`, guards: [
|
|
4292
|
+
{ ns: "networkSeat", id: id7, exists: false },
|
|
4051
4293
|
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4052
4294
|
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4053
4295
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
@@ -4055,7 +4297,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4055
4297
|
} catch (error) {
|
|
4056
4298
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4057
4299
|
}
|
|
4058
|
-
seat = await membershipLoad(db, "networkSeat",
|
|
4300
|
+
seat = await membershipLoad(db, "networkSeat", id7);
|
|
4059
4301
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4060
4302
|
}
|
|
4061
4303
|
requireMembershipScope(seat, input);
|
|
@@ -4065,7 +4307,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4065
4307
|
|
|
4066
4308
|
// src/membership-seat-included.ts
|
|
4067
4309
|
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4068
|
-
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4310
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4069
4311
|
function namedSeatRecipientState(seat) {
|
|
4070
4312
|
switch (String(seat.status)) {
|
|
4071
4313
|
case "renewal_pending":
|
|
@@ -4169,7 +4411,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4169
4411
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4170
4412
|
requireMembershipScope(member, input);
|
|
4171
4413
|
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");
|
|
4414
|
+
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
4415
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4174
4416
|
await db.transact([
|
|
4175
4417
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4282,8 +4524,8 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4282
4524
|
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4283
4525
|
for (const member of rows5.slice(0, limit)) {
|
|
4284
4526
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4285
|
-
const
|
|
4286
|
-
receipt.skipped.push({ membershipId:
|
|
4527
|
+
const id7 = String(member.id), skip = (reason) => {
|
|
4528
|
+
receipt.skipped.push({ membershipId: id7, reason });
|
|
4287
4529
|
};
|
|
4288
4530
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4289
4531
|
if (offering.seatPriceCents <= target) {
|
|
@@ -4299,16 +4541,16 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4299
4541
|
skip(`seat is ${String(seat.status)}`);
|
|
4300
4542
|
continue;
|
|
4301
4543
|
}
|
|
4302
|
-
const ops = [membershipUpdate("networkMembership",
|
|
4544
|
+
const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4303
4545
|
const guards = [
|
|
4304
|
-
{ ns: "networkMembership", id:
|
|
4546
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
4305
4547
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4306
4548
|
];
|
|
4307
4549
|
if (seat) {
|
|
4308
4550
|
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4309
4551
|
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4310
4552
|
}
|
|
4311
|
-
await db.transact(ops, { mutationId: `seat-reprice:${
|
|
4553
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
|
|
4312
4554
|
receipt.repriced++;
|
|
4313
4555
|
}
|
|
4314
4556
|
return receipt;
|
|
@@ -4509,25 +4751,28 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4509
4751
|
};
|
|
4510
4752
|
|
|
4511
4753
|
// src/membership-discount-reterm.ts
|
|
4512
|
-
var
|
|
4754
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
4513
4755
|
|
|
4514
4756
|
// src/subscription-provider-contract.ts
|
|
4515
|
-
var
|
|
4757
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4516
4758
|
function subscriptionContractMismatch() {
|
|
4517
4759
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4518
4760
|
}
|
|
4519
4761
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4520
4762
|
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,
|
|
4763
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4522
4764
|
const price = result.body;
|
|
4523
4765
|
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
4766
|
return typeof price.product === "string" ? price.product : void 0;
|
|
4525
4767
|
}
|
|
4526
4768
|
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4527
4769
|
const items = sub?.items;
|
|
4528
|
-
|
|
4770
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
4771
|
+
const primary = items?.data?.find((item) => item.price?.id === input.priceId);
|
|
4772
|
+
const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
|
|
4773
|
+
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
4774
|
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();
|
|
4775
|
+
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
4776
|
verifySubscriptionBillingResult(sub, input);
|
|
4532
4777
|
}
|
|
4533
4778
|
|
|
@@ -4539,8 +4784,8 @@ var couponOf = (discount) => {
|
|
|
4539
4784
|
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4540
4785
|
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4541
4786
|
};
|
|
4542
|
-
async function retermPolicy(db, scope2,
|
|
4543
|
-
const row2 = await membershipLoad(db, "discountPolicyVersion",
|
|
4787
|
+
async function retermPolicy(db, scope2, id7) {
|
|
4788
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
|
|
4544
4789
|
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4545
4790
|
let policy2;
|
|
4546
4791
|
try {
|
|
@@ -4548,12 +4793,12 @@ async function retermPolicy(db, scope2, id6) {
|
|
|
4548
4793
|
} catch {
|
|
4549
4794
|
return membershipFail("membership_reterm_policy_ineligible");
|
|
4550
4795
|
}
|
|
4551
|
-
if (policy2.id !==
|
|
4796
|
+
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
4797
|
return policy2;
|
|
4553
4798
|
}
|
|
4554
4799
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
4555
4800
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4556
|
-
const read2 = await (0,
|
|
4801
|
+
const read2 = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4557
4802
|
const sub = read2.body, items = sub?.items?.data;
|
|
4558
4803
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4559
4804
|
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 +4809,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
4564
4809
|
const present = discounts.map(couponOf);
|
|
4565
4810
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
4566
4811
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4567
|
-
const written = await (0,
|
|
4812
|
+
const written = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "POST", path, {
|
|
4568
4813
|
"discounts[0][coupon]": couponId,
|
|
4569
4814
|
"expand[0]": "discounts",
|
|
4570
4815
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -4580,7 +4825,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4580
4825
|
const limit = input.limit ?? 100;
|
|
4581
4826
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4582
4827
|
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((
|
|
4828
|
+
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
4829
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4585
4830
|
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4586
4831
|
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
@@ -4590,10 +4835,10 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4590
4835
|
let rows5;
|
|
4591
4836
|
if (input.membershipIds) {
|
|
4592
4837
|
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(
|
|
4838
|
+
for (const id7 of input.membershipIds) {
|
|
4839
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
4840
|
+
if (!member) skip(id7, "not_found");
|
|
4841
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
4597
4842
|
else rows5.push(member);
|
|
4598
4843
|
}
|
|
4599
4844
|
} else {
|
|
@@ -4603,32 +4848,32 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4603
4848
|
rows5 = found.slice(0, limit);
|
|
4604
4849
|
}
|
|
4605
4850
|
for (const member of rows5) {
|
|
4606
|
-
const
|
|
4851
|
+
const id7 = String(member.id);
|
|
4607
4852
|
if (member.discountPolicyVersionId === policy2.id) {
|
|
4608
|
-
receipt.unchanged.push(
|
|
4853
|
+
receipt.unchanged.push(id7);
|
|
4609
4854
|
continue;
|
|
4610
4855
|
}
|
|
4611
4856
|
if (!RETERMABLE.has(String(member.status))) {
|
|
4612
|
-
skip(
|
|
4857
|
+
skip(id7, "status");
|
|
4613
4858
|
continue;
|
|
4614
4859
|
}
|
|
4615
4860
|
if (member.founding !== true) {
|
|
4616
|
-
skip(
|
|
4861
|
+
skip(id7, "not_founding");
|
|
4617
4862
|
continue;
|
|
4618
4863
|
}
|
|
4619
4864
|
if (member.cohort !== policy2.cohortLabel) {
|
|
4620
|
-
skip(
|
|
4865
|
+
skip(id7, "cohort_mismatch");
|
|
4621
4866
|
continue;
|
|
4622
4867
|
}
|
|
4623
4868
|
const skuId = String(member.baseSkuId);
|
|
4624
4869
|
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
4625
|
-
skip(
|
|
4870
|
+
skip(id7, "sku_not_discounted");
|
|
4626
4871
|
continue;
|
|
4627
4872
|
}
|
|
4628
4873
|
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4629
4874
|
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
4630
4875
|
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4631
|
-
skip(
|
|
4876
|
+
skip(id7, "would_not_widen");
|
|
4632
4877
|
continue;
|
|
4633
4878
|
}
|
|
4634
4879
|
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
@@ -4656,22 +4901,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4656
4901
|
} catch (error) {
|
|
4657
4902
|
const code = error.code;
|
|
4658
4903
|
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4659
|
-
skip(
|
|
4904
|
+
skip(id7, "provider_mismatch");
|
|
4660
4905
|
continue;
|
|
4661
4906
|
}
|
|
4662
4907
|
throw error;
|
|
4663
4908
|
}
|
|
4664
4909
|
try {
|
|
4665
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
4910
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
4666
4911
|
discountPolicyVersionId: policy2.id,
|
|
4667
4912
|
discountSnapshot: policy2,
|
|
4668
4913
|
renewalAmountCents: renewal,
|
|
4669
4914
|
revision: Number(member.revision) + 1,
|
|
4670
4915
|
updatedAt: now
|
|
4671
4916
|
})], {
|
|
4672
|
-
mutationId: `membership-reterm:${
|
|
4917
|
+
mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
|
|
4673
4918
|
guards: [
|
|
4674
|
-
{ ns: "networkMembership", id:
|
|
4919
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4675
4920
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4676
4921
|
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4677
4922
|
]
|
|
@@ -4681,7 +4926,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4681
4926
|
return membershipFail("membership_reterm_retry_required");
|
|
4682
4927
|
}
|
|
4683
4928
|
receipt.retermed.push({
|
|
4684
|
-
membershipId:
|
|
4929
|
+
membershipId: id7,
|
|
4685
4930
|
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4686
4931
|
previousRenewalAmountCents: current,
|
|
4687
4932
|
renewalAmountCents: renewal,
|
|
@@ -4717,7 +4962,7 @@ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
|
4717
4962
|
return json({ error: BAD }, 400);
|
|
4718
4963
|
}
|
|
4719
4964
|
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((
|
|
4965
|
+
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
4966
|
return json({ error: BAD }, 400);
|
|
4722
4967
|
}
|
|
4723
4968
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
@@ -4992,12 +5237,12 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4992
5237
|
if (!app || app.clerkUserId !== user.userId || app.namedSeatId || app.giftClaimId) return json({ error: "membership is not eligible to restart" }, 409);
|
|
4993
5238
|
if (app.membershipRestartId && app.status === "submitted") return json({ applicationId: app.id });
|
|
4994
5239
|
try {
|
|
4995
|
-
const
|
|
5240
|
+
const id7 = String(app.id), intent = await membershipLoad(db, "subscriptionCheckoutIntents", id7);
|
|
4996
5241
|
if (!intent || !app.stripeSubscriptionId || intent.providerSubscriptionId !== app.stripeSubscriptionId) return json({ error: "membership history needs reconciliation" }, 409);
|
|
4997
5242
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
4998
5243
|
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",
|
|
5244
|
+
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");
|
|
5245
|
+
const now = Date.now(), quote = await membershipLoad(db, "membershipQuoteProjections", id7);
|
|
5001
5246
|
const attrs = {
|
|
5002
5247
|
status: "submitted",
|
|
5003
5248
|
membershipRestartId: restart.restartId,
|
|
@@ -5017,8 +5262,8 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
5017
5262
|
quoteSnapshot: quote ?? {},
|
|
5018
5263
|
archivedAt: now
|
|
5019
5264
|
} },
|
|
5020
|
-
{ t: "delete", ns: "subscriptionCheckoutIntents", id:
|
|
5021
|
-
{ t: "retract", ns: "applications", id:
|
|
5265
|
+
{ t: "delete", ns: "subscriptionCheckoutIntents", id: id7 },
|
|
5266
|
+
{ t: "retract", ns: "applications", id: id7, attrs: [
|
|
5022
5267
|
"stripeSubscriptionId",
|
|
5023
5268
|
"canceled",
|
|
5024
5269
|
"renewalAt",
|
|
@@ -5029,28 +5274,28 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
5029
5274
|
"approvalEffectsOrigin",
|
|
5030
5275
|
"approvalEffectsLastAttemptAt"
|
|
5031
5276
|
] },
|
|
5032
|
-
{ t: "update", ns: "applications", id:
|
|
5033
|
-
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id:
|
|
5277
|
+
{ t: "update", ns: "applications", id: id7, attrs },
|
|
5278
|
+
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id: id7, attrs: { status: "restart", updatedAt: now } }] : []
|
|
5034
5279
|
];
|
|
5035
5280
|
try {
|
|
5036
5281
|
await db.transact(ops, { mutationId: `membership-restart-application:${restart.restartId}`, guards: [
|
|
5037
|
-
{ ns: "applications", id:
|
|
5282
|
+
{ ns: "applications", id: id7, exists: true, equals: {
|
|
5038
5283
|
clerkUserId: user.userId,
|
|
5039
5284
|
groupId: ctx.chapter.id,
|
|
5040
5285
|
status: app.status,
|
|
5041
5286
|
stripeSubscriptionId: app.stripeSubscriptionId
|
|
5042
5287
|
} },
|
|
5043
|
-
{ ns: "subscriptionCheckoutIntents", id:
|
|
5288
|
+
{ ns: "subscriptionCheckoutIntents", id: id7, exists: true, equals: { providerSubscriptionId: app.stripeSubscriptionId } },
|
|
5044
5289
|
{ ns: "membershipCheckoutHistory", id: restart.restartId, exists: false },
|
|
5045
|
-
{ ns: "membershipQuoteProjections", id:
|
|
5290
|
+
{ ns: "membershipQuoteProjections", id: id7, exists: Boolean(quote), ...quote ? { equals: { revision: quote.revision, status: quote.status } } : {} }
|
|
5046
5291
|
] });
|
|
5047
5292
|
} catch (error) {
|
|
5048
5293
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5049
5294
|
}
|
|
5050
|
-
const saved = await membershipLoad(db, "applications",
|
|
5295
|
+
const saved = await membershipLoad(db, "applications", id7);
|
|
5051
5296
|
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
5297
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: saved, stage: String(saved.status) }).catch(() => void 0);
|
|
5053
|
-
return json({ applicationId:
|
|
5298
|
+
return json({ applicationId: id7 });
|
|
5054
5299
|
} catch {
|
|
5055
5300
|
return json({ error: "We could not prepare your new subscription. Your account is saved. Please retry." }, 503);
|
|
5056
5301
|
}
|
|
@@ -5181,8 +5426,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5181
5426
|
if (app.requireDisclaimerAck && !acked) {
|
|
5182
5427
|
return { ok: false, error: "disclaimerAck is required" };
|
|
5183
5428
|
}
|
|
5184
|
-
const
|
|
5185
|
-
const row2 = { id:
|
|
5429
|
+
const id7 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
5430
|
+
const row2 = { id: id7, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
5186
5431
|
for (const f of [...app.required, ...app.optional]) {
|
|
5187
5432
|
if (fieldStates[f]?.visible === false) continue;
|
|
5188
5433
|
if (typeof fields[f] === "string") row2[f] = fields[f].trim();
|
|
@@ -5216,12 +5461,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5216
5461
|
}
|
|
5217
5462
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
5218
5463
|
const ops = [
|
|
5219
|
-
{ t: "update", ns: "applications", id:
|
|
5464
|
+
{ t: "update", ns: "applications", id: id7, attrs: row2 }
|
|
5220
5465
|
];
|
|
5221
5466
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
5222
5467
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
5223
5468
|
consumedAt: opts.now,
|
|
5224
|
-
consumedApplicationId:
|
|
5469
|
+
consumedApplicationId: id7
|
|
5225
5470
|
} });
|
|
5226
5471
|
}
|
|
5227
5472
|
try {
|
|
@@ -5229,7 +5474,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5229
5474
|
ops,
|
|
5230
5475
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
5231
5476
|
);
|
|
5232
|
-
return { ok: true, id:
|
|
5477
|
+
return { ok: true, id: id7, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
5233
5478
|
} catch (error) {
|
|
5234
5479
|
if (error.code === "unique_violation") return {
|
|
5235
5480
|
ok: false,
|
|
@@ -5435,14 +5680,14 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5435
5680
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
5436
5681
|
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
5437
5682
|
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
5438
|
-
const
|
|
5683
|
+
const id7 = typeof grant.id === "string" ? grant.id : "";
|
|
5439
5684
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
5440
5685
|
const normalized = normalizeAdmissionEmail(email);
|
|
5441
5686
|
const decision = grant.decision;
|
|
5442
5687
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
5443
5688
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
5444
|
-
if (!ID2.test(
|
|
5445
|
-
if (seenIds.has(
|
|
5689
|
+
if (!ID2.test(id7)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
5690
|
+
if (seenIds.has(id7)) errors.push(`document.grants repeats id "${id7}"`);
|
|
5446
5691
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
5447
5692
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
5448
5693
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -5452,10 +5697,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5452
5697
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
5453
5698
|
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
5454
5699
|
}
|
|
5455
|
-
seenIds.add(
|
|
5700
|
+
seenIds.add(id7);
|
|
5456
5701
|
seenEmails.add(normalized);
|
|
5457
5702
|
grants.push({
|
|
5458
|
-
id:
|
|
5703
|
+
id: id7,
|
|
5459
5704
|
email,
|
|
5460
5705
|
decision,
|
|
5461
5706
|
...note ? { note } : {},
|
|
@@ -5527,8 +5772,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5527
5772
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
5528
5773
|
const ops = [];
|
|
5529
5774
|
for (const row2 of current) {
|
|
5530
|
-
const
|
|
5531
|
-
if (
|
|
5775
|
+
const id7 = String(row2.id ?? "");
|
|
5776
|
+
if (id7 && !incomingIds.has(id7)) ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5532
5777
|
active: false,
|
|
5533
5778
|
revokedAt: receivedAt,
|
|
5534
5779
|
revokedBy: document.source.actor,
|
|
@@ -5537,10 +5782,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5537
5782
|
} });
|
|
5538
5783
|
}
|
|
5539
5784
|
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:
|
|
5785
|
+
const id7 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
5786
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
5787
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5788
|
+
id: id7,
|
|
5544
5789
|
sourceGrantId: grant.id,
|
|
5545
5790
|
sourceScope: "parent",
|
|
5546
5791
|
sourceId: document.source.appId,
|
|
@@ -5561,7 +5806,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5561
5806
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
5562
5807
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
5563
5808
|
];
|
|
5564
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
5809
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id7, attrs: retract });
|
|
5565
5810
|
}
|
|
5566
5811
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
5567
5812
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -5592,9 +5837,9 @@ var APPLICATION_LABELS = {
|
|
|
5592
5837
|
phone: "Phone",
|
|
5593
5838
|
state: "State"
|
|
5594
5839
|
};
|
|
5595
|
-
function humanize(
|
|
5596
|
-
const text8 =
|
|
5597
|
-
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` :
|
|
5840
|
+
function humanize(id7) {
|
|
5841
|
+
const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5842
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
|
|
5598
5843
|
}
|
|
5599
5844
|
function applicationDefs(chapter) {
|
|
5600
5845
|
try {
|
|
@@ -5603,23 +5848,23 @@ function applicationDefs(chapter) {
|
|
|
5603
5848
|
return {};
|
|
5604
5849
|
}
|
|
5605
5850
|
}
|
|
5606
|
-
function applicationType(
|
|
5851
|
+
function applicationType(id7, def) {
|
|
5607
5852
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
5608
|
-
return
|
|
5853
|
+
return id7 === "email" ? "email" : id7 === "linkedin" ? "url" : "string";
|
|
5609
5854
|
}
|
|
5610
5855
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
5611
5856
|
const defs = applicationDefs(chapter);
|
|
5612
|
-
return [...application.required, ...application.optional].map((
|
|
5613
|
-
const def = defs[
|
|
5614
|
-
const conditions = application.conditions[
|
|
5615
|
-
const type = applicationType(
|
|
5857
|
+
return [...application.required, ...application.optional].map((id7, order2) => {
|
|
5858
|
+
const def = defs[id7];
|
|
5859
|
+
const conditions = application.conditions[id7];
|
|
5860
|
+
const type = applicationType(id7, def);
|
|
5616
5861
|
return {
|
|
5617
|
-
id:
|
|
5618
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
5862
|
+
id: id7,
|
|
5863
|
+
label: def?.label ?? APPLICATION_LABELS[id7] ?? humanize(id7),
|
|
5619
5864
|
type,
|
|
5620
|
-
required: application.required.includes(
|
|
5865
|
+
required: application.required.includes(id7),
|
|
5621
5866
|
order: order2,
|
|
5622
|
-
maxLength: application.maxLen[
|
|
5867
|
+
maxLength: application.maxLen[id7] ?? application.defaultMaxLen,
|
|
5623
5868
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
5624
5869
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
5625
5870
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -5629,17 +5874,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
5629
5874
|
function resolveFormationFlowFields(crm, formation) {
|
|
5630
5875
|
if (!formation.enabled) return [];
|
|
5631
5876
|
const def = crm.type(formation.type);
|
|
5632
|
-
return [...formation.required, ...formation.optional].map((
|
|
5633
|
-
const field = def.fields[
|
|
5877
|
+
return [...formation.required, ...formation.optional].map((id7, order2) => {
|
|
5878
|
+
const field = def.fields[id7];
|
|
5634
5879
|
return {
|
|
5635
|
-
id:
|
|
5636
|
-
label: field.label ?? humanize(
|
|
5880
|
+
id: id7,
|
|
5881
|
+
label: field.label ?? humanize(id7),
|
|
5637
5882
|
type: field.type,
|
|
5638
|
-
required: formation.required.includes(
|
|
5883
|
+
required: formation.required.includes(id7),
|
|
5639
5884
|
order: order2,
|
|
5640
|
-
maxLength: formation.maxLen[
|
|
5885
|
+
maxLength: formation.maxLen[id7] ?? formation.defaultMaxLen,
|
|
5641
5886
|
...field.options ? { options: [...field.options] } : {},
|
|
5642
|
-
...
|
|
5887
|
+
...id7 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
5643
5888
|
};
|
|
5644
5889
|
});
|
|
5645
5890
|
}
|
|
@@ -5689,10 +5934,10 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5689
5934
|
}
|
|
5690
5935
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5691
5936
|
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(
|
|
5937
|
+
const id7 = text3(item.id, `${path}.id`, errors, 80);
|
|
5938
|
+
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5939
|
+
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
5940
|
+
ids2.add(id7);
|
|
5696
5941
|
const targetRaw = record(item.target);
|
|
5697
5942
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
5698
5943
|
if (targetRaw?.type === "tier") {
|
|
@@ -5718,7 +5963,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5718
5963
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
5719
5964
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
5720
5965
|
offers.push({
|
|
5721
|
-
id:
|
|
5966
|
+
id: id7,
|
|
5722
5967
|
target,
|
|
5723
5968
|
billingMode: item.billingMode,
|
|
5724
5969
|
stripePriceId,
|
|
@@ -5818,9 +6063,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5818
6063
|
const ops = [];
|
|
5819
6064
|
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
5820
6065
|
for (const row2 of currentRows) {
|
|
5821
|
-
const
|
|
5822
|
-
if (
|
|
5823
|
-
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers",
|
|
6066
|
+
const id8 = String(row2.id ?? "");
|
|
6067
|
+
if (id8 && !incoming.has(id8)) {
|
|
6068
|
+
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers", id8), attrs: { active: false } });
|
|
5824
6069
|
}
|
|
5825
6070
|
}
|
|
5826
6071
|
for (const tier of document.tiers) {
|
|
@@ -5855,9 +6100,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5855
6100
|
if (!("stripePriceId" in attrs)) {
|
|
5856
6101
|
ops.push({ t: "retract", ns: "groups", id: groupRef, attrs: ["stripePriceId"] });
|
|
5857
6102
|
}
|
|
5858
|
-
const
|
|
5859
|
-
ops.push({ t: "update", ns: "signupControls", id:
|
|
5860
|
-
id:
|
|
6103
|
+
const id7 = `signup-control:${document.target.environment}:${document.revision}`;
|
|
6104
|
+
ops.push({ t: "update", ns: "signupControls", id: id7, attrs: {
|
|
6105
|
+
id: id7,
|
|
5861
6106
|
sourceId: document.source.appId,
|
|
5862
6107
|
targetId: document.target.chapterId,
|
|
5863
6108
|
environment: document.target.environment,
|
|
@@ -5896,6 +6141,8 @@ function foundingOf(offer) {
|
|
|
5896
6141
|
async function createPaymentQuote(input) {
|
|
5897
6142
|
const { group, tier, policy: policy2 } = input;
|
|
5898
6143
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
6144
|
+
if (input.seatOffer && (!terms2 || !Number.isSafeInteger(input.seatOffer.amountCents) || input.seatOffer.amountCents < 0) || input.namedSeat && !input.seatOffer) throw new Error("invalid checkout seat offer");
|
|
6145
|
+
const seatCents = input.namedSeat ? input.seatOffer.amountCents : 0;
|
|
5899
6146
|
const compareAt = input.presentation?.compareAtPriceCents;
|
|
5900
6147
|
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
5901
6148
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
@@ -5904,6 +6151,7 @@ async function createPaymentQuote(input) {
|
|
|
5904
6151
|
discountCents: tier.priceCents - dueToday,
|
|
5905
6152
|
dueTodayCents: dueToday
|
|
5906
6153
|
};
|
|
6154
|
+
if (!Number.isSafeInteger(lines.dueTodayCents + seatCents) || !Number.isSafeInteger(tier.priceCents + seatCents)) throw new Error("invalid checkout total");
|
|
5907
6155
|
const quote = {
|
|
5908
6156
|
applicationId: input.applicationId,
|
|
5909
6157
|
chapterId: input.chapterId,
|
|
@@ -5911,11 +6159,17 @@ async function createPaymentQuote(input) {
|
|
|
5911
6159
|
tierName: tier.name,
|
|
5912
6160
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
5913
6161
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
5914
|
-
lineItems:
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
6162
|
+
lineItems: {
|
|
6163
|
+
...presentation ? {
|
|
6164
|
+
standardCents: presentation.compareAtPriceCents,
|
|
6165
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
6166
|
+
dueTodayCents: lines.dueTodayCents
|
|
6167
|
+
} : lines,
|
|
6168
|
+
dueTodayCents: lines.dueTodayCents + seatCents,
|
|
6169
|
+
...input.namedSeat ? { namedSeatCents: seatCents } : {}
|
|
6170
|
+
},
|
|
6171
|
+
...input.seatOffer ? { seatOffer: input.seatOffer } : {},
|
|
6172
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {},
|
|
5919
6173
|
...presentation ? { presentation } : {},
|
|
5920
6174
|
refundPolicyText: policy2?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
5921
6175
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
@@ -5927,7 +6181,7 @@ async function createPaymentQuote(input) {
|
|
|
5927
6181
|
discountDuration: terms2.discount.duration === "once" ? "first_term" : "while_active"
|
|
5928
6182
|
} : {},
|
|
5929
6183
|
...terms2.initialTermEndsAt !== void 0 ? { initialTermEndsAt: terms2.initialTermEndsAt } : {},
|
|
5930
|
-
renewalCents: terms2.discount?.duration === "forever" ? dueToday : tier.priceCents,
|
|
6184
|
+
renewalCents: (terms2.discount?.duration === "forever" ? dueToday : tier.priceCents) + seatCents,
|
|
5931
6185
|
autoRenew: true
|
|
5932
6186
|
} } : {}
|
|
5933
6187
|
};
|
|
@@ -5947,22 +6201,23 @@ async function createPaymentQuote(input) {
|
|
|
5947
6201
|
// src/membership-quote-projection.ts
|
|
5948
6202
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
5949
6203
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
5950
|
-
var
|
|
6204
|
+
var fail6 = (code) => {
|
|
5951
6205
|
throw Object.assign(new Error(code), { code });
|
|
5952
6206
|
};
|
|
5953
|
-
var load3 = async (db, ns,
|
|
6207
|
+
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
5954
6208
|
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
|
|
6209
|
+
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");
|
|
6210
|
+
if (row2.status !== "ready") return fail6("membership_quote_pending");
|
|
5957
6211
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
5958
6212
|
...target,
|
|
5959
6213
|
requestId: row2.requestId
|
|
5960
6214
|
}), now, lifetimeMs: 6e4 });
|
|
5961
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6215
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
|
|
5962
6216
|
const tier = row2.tier, quote = row2.quote;
|
|
5963
6217
|
const offering = offer.contract.offering;
|
|
6218
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
|
|
5964
6219
|
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
|
|
6220
|
+
return fail6("membership_catalog_mismatch");
|
|
5966
6221
|
}
|
|
5967
6222
|
const expected = await createPaymentQuote({
|
|
5968
6223
|
...target,
|
|
@@ -5971,19 +6226,21 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
5971
6226
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
5972
6227
|
billingTerms: offer.contract.billingTerms,
|
|
5973
6228
|
presentation: quote.presentation,
|
|
5974
|
-
founding: foundingOf(offer)
|
|
6229
|
+
founding: foundingOf(offer),
|
|
6230
|
+
seatOffer: quote.seatOffer,
|
|
6231
|
+
namedSeat: quote.namedSeat
|
|
5975
6232
|
});
|
|
5976
|
-
if (quote.digest !== expected.digest) return
|
|
6233
|
+
if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
|
|
5977
6234
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
5978
6235
|
}
|
|
5979
6236
|
async function readMembershipQuote(db, target, now) {
|
|
5980
6237
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, now);
|
|
5981
6238
|
}
|
|
5982
6239
|
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
|
|
6240
|
+
const { applicationId: id7, now, app } = input;
|
|
6241
|
+
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");
|
|
6242
|
+
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6243
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
|
|
5987
6244
|
if (row2?.status === "ready") {
|
|
5988
6245
|
try {
|
|
5989
6246
|
return await restore2(row2, input, now);
|
|
@@ -5994,15 +6251,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5994
6251
|
const identity2 = ["groupId", "tierId", "stripeRuntime", "email", "emailNormalized", "status", "canceled", "membershipRestartId", "namedSeatId", "namedSeatClaimPending", "giftClaimId", "giftOfferId"];
|
|
5995
6252
|
const guards = [{
|
|
5996
6253
|
ns: "applications",
|
|
5997
|
-
id:
|
|
6254
|
+
id: id7,
|
|
5998
6255
|
exists: true,
|
|
5999
6256
|
equals: Object.fromEntries(identity2.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
6000
6257
|
absent: identity2.filter((key3) => app[key3] === void 0)
|
|
6001
|
-
}, { ns: "subscriptionCheckoutIntents", id:
|
|
6258
|
+
}, { ns: "subscriptionCheckoutIntents", id: id7, exists: false }];
|
|
6002
6259
|
if (!row2 || row2.status === "ready" || row2.status === "restart") {
|
|
6003
6260
|
const revision = Number(row2?.revision ?? 0) + 1;
|
|
6004
6261
|
const pending = {
|
|
6005
|
-
id:
|
|
6262
|
+
id: id7,
|
|
6006
6263
|
...Object.fromEntries(keys2.map((key3) => [key3, input[key3]])),
|
|
6007
6264
|
revision,
|
|
6008
6265
|
status: "pending",
|
|
@@ -6010,25 +6267,25 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6010
6267
|
updatedAt: now
|
|
6011
6268
|
};
|
|
6012
6269
|
try {
|
|
6013
|
-
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id:
|
|
6014
|
-
mutationId: `membership-quote:${
|
|
6270
|
+
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id: id7, attrs: pending }], {
|
|
6271
|
+
mutationId: `membership-quote:${id7}:${revision}`,
|
|
6015
6272
|
guards: [...guards, {
|
|
6016
6273
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6017
|
-
id:
|
|
6274
|
+
id: id7,
|
|
6018
6275
|
exists: Boolean(row2),
|
|
6019
6276
|
...row2 ? { equals: { revision: row2.revision, status: row2.status } } : {}
|
|
6020
6277
|
}]
|
|
6021
6278
|
});
|
|
6022
6279
|
} catch (error) {
|
|
6023
6280
|
if (error.code !== "transact_guard_failed") throw error;
|
|
6024
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6281
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6025
6282
|
if (!winner || winner.revision !== revision) throw error;
|
|
6026
6283
|
}
|
|
6027
|
-
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6284
|
+
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6028
6285
|
}
|
|
6029
6286
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
6030
6287
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
6031
|
-
return
|
|
6288
|
+
return fail6("membership_quote_pending");
|
|
6032
6289
|
}
|
|
6033
6290
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
6034
6291
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6049,7 +6306,8 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6049
6306
|
policy: input.policy,
|
|
6050
6307
|
billingTerms: offer.contract.billingTerms,
|
|
6051
6308
|
presentation: input.presentation,
|
|
6052
|
-
founding: foundingOf(offer)
|
|
6309
|
+
founding: foundingOf(offer),
|
|
6310
|
+
...input.namedSeatsEnabled ? { seatOffer: { amountCents: offer.contract.offering.seatPriceCents } } : {}
|
|
6053
6311
|
});
|
|
6054
6312
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
6055
6313
|
await restore2(ready, target, now, true);
|
|
@@ -6057,23 +6315,100 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6057
6315
|
await db.transact([{
|
|
6058
6316
|
t: "update",
|
|
6059
6317
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6060
|
-
id:
|
|
6318
|
+
id: id7,
|
|
6061
6319
|
attrs: { offer, offerDigest: offer.digest, tier, quote, status: "ready", updatedAt: now }
|
|
6062
6320
|
}], {
|
|
6063
|
-
mutationId: `membership-quote-ready:${
|
|
6321
|
+
mutationId: `membership-quote-ready:${id7}:${row2.revision}`,
|
|
6064
6322
|
guards: [...guards, {
|
|
6065
6323
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6066
|
-
id:
|
|
6324
|
+
id: id7,
|
|
6067
6325
|
exists: true,
|
|
6068
6326
|
equals: { status: "pending", revision: row2.revision, requestId: row2.requestId }
|
|
6069
6327
|
}]
|
|
6070
6328
|
});
|
|
6071
6329
|
} catch (error) {
|
|
6072
6330
|
if (error.code !== "transact_guard_failed") throw error;
|
|
6073
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6331
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6074
6332
|
if (winner?.status !== "ready" || winner.requestId !== row2.requestId || winner.offerDigest !== offer.digest) throw error;
|
|
6075
6333
|
}
|
|
6076
|
-
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6334
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6335
|
+
}
|
|
6336
|
+
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6337
|
+
if (!input.enabled) return fail6("named_seat_purchase_unavailable");
|
|
6338
|
+
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6339
|
+
const current = await restore2(row2, target, input.now);
|
|
6340
|
+
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
6341
|
+
const quote = await createPaymentQuote({
|
|
6342
|
+
...target,
|
|
6343
|
+
tier: current.tier,
|
|
6344
|
+
group: { merchantDisclosureText: current.quote.merchantDisclosureText },
|
|
6345
|
+
policy: { refundPolicyText: current.quote.refundPolicyText, trustCopy: current.quote.trustCopy },
|
|
6346
|
+
billingTerms: current.billingTerms,
|
|
6347
|
+
presentation: current.quote.presentation,
|
|
6348
|
+
founding: current.quote.founding,
|
|
6349
|
+
seatOffer: { amountCents: current.membershipOffer.contract.offering.seatPriceCents },
|
|
6350
|
+
namedSeat
|
|
6351
|
+
});
|
|
6352
|
+
if (quote.digest === current.quote.digest) return current;
|
|
6353
|
+
await db.transact([{
|
|
6354
|
+
t: "update",
|
|
6355
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6356
|
+
id: target.applicationId,
|
|
6357
|
+
attrs: { quote, revision: current.projectionRevision + 1, updatedAt: input.now }
|
|
6358
|
+
}], {
|
|
6359
|
+
mutationId: `membership-quote-selection:${target.applicationId}:${current.projectionRevision}:${quote.digest}`,
|
|
6360
|
+
guards: [
|
|
6361
|
+
{
|
|
6362
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6363
|
+
id: target.applicationId,
|
|
6364
|
+
exists: true,
|
|
6365
|
+
equals: { revision: current.projectionRevision, status: "ready" }
|
|
6366
|
+
},
|
|
6367
|
+
{ ns: "subscriptionCheckoutIntents", id: target.applicationId, exists: false }
|
|
6368
|
+
]
|
|
6369
|
+
});
|
|
6370
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, input.now);
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
// src/bounded-request-text.ts
|
|
6374
|
+
async function readBoundedRequestText(req, maxBytes) {
|
|
6375
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
6376
|
+
const reader = req.body?.getReader();
|
|
6377
|
+
const cancel = () => {
|
|
6378
|
+
void reader?.cancel().catch(() => {
|
|
6379
|
+
});
|
|
6380
|
+
};
|
|
6381
|
+
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
6382
|
+
let complete = false;
|
|
6383
|
+
req.signal.addEventListener("abort", cancel, { once: true });
|
|
6384
|
+
try {
|
|
6385
|
+
if (req.signal.aborted) return invalid2();
|
|
6386
|
+
const declared = req.headers.get("content-length");
|
|
6387
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
6388
|
+
return json({ error: "request body too large" }, 413);
|
|
6389
|
+
}
|
|
6390
|
+
if (!reader) return "";
|
|
6391
|
+
const decoder = new TextDecoder();
|
|
6392
|
+
let bytes = 0;
|
|
6393
|
+
let text8 = "";
|
|
6394
|
+
for (; ; ) {
|
|
6395
|
+
const { done, value } = await reader.read();
|
|
6396
|
+
if (req.signal.aborted) return invalid2();
|
|
6397
|
+
if (done) {
|
|
6398
|
+
complete = true;
|
|
6399
|
+
return text8 + decoder.decode();
|
|
6400
|
+
}
|
|
6401
|
+
bytes += value.byteLength;
|
|
6402
|
+
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6403
|
+
text8 += decoder.decode(value, { stream: true });
|
|
6404
|
+
}
|
|
6405
|
+
} catch {
|
|
6406
|
+
return invalid2();
|
|
6407
|
+
} finally {
|
|
6408
|
+
req.signal.removeEventListener("abort", cancel);
|
|
6409
|
+
if (!complete) cancel();
|
|
6410
|
+
reader?.releaseLock();
|
|
6411
|
+
}
|
|
6077
6412
|
}
|
|
6078
6413
|
|
|
6079
6414
|
// src/payment-quote-route.ts
|
|
@@ -6107,8 +6442,11 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6107
6442
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
6108
6443
|
billingTerms,
|
|
6109
6444
|
presentation: quote2.presentation,
|
|
6110
|
-
founding: foundingOf(intent.membershipOffer)
|
|
6445
|
+
founding: foundingOf(intent.membershipOffer),
|
|
6446
|
+
seatOffer: quote2.seatOffer,
|
|
6447
|
+
namedSeat: quote2.namedSeat
|
|
6111
6448
|
});
|
|
6449
|
+
if (quote2.seatOffer && quote2.seatOffer.amountCents !== intent.membershipOffer?.contract.offering.seatPriceCents) throw new Error("invalid seat snapshot");
|
|
6112
6450
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
6113
6451
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
6114
6452
|
}
|
|
@@ -6137,7 +6475,16 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6137
6475
|
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
6138
6476
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
6139
6477
|
db,
|
|
6140
|
-
{
|
|
6478
|
+
{
|
|
6479
|
+
...target,
|
|
6480
|
+
app,
|
|
6481
|
+
group,
|
|
6482
|
+
tier,
|
|
6483
|
+
policy: policy2,
|
|
6484
|
+
presentation,
|
|
6485
|
+
namedSeatsEnabled: Boolean(ctx.membershipAuthority.namedSeats) && !app.membershipRestartId,
|
|
6486
|
+
now: Date.now()
|
|
6487
|
+
},
|
|
6141
6488
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
6142
6489
|
);
|
|
6143
6490
|
const quote = await createPaymentQuote({
|
|
@@ -6153,20 +6500,54 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6153
6500
|
});
|
|
6154
6501
|
return { tier, quote, billingTerms: void 0 };
|
|
6155
6502
|
}
|
|
6156
|
-
async function paymentQuoteRoute(url, env, ctx, prepare = false) {
|
|
6157
|
-
|
|
6158
|
-
if (
|
|
6503
|
+
async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
6504
|
+
let selection;
|
|
6505
|
+
if (request?.body) {
|
|
6506
|
+
try {
|
|
6507
|
+
const text8 = await readBoundedRequestText(request, 2048);
|
|
6508
|
+
if (text8 instanceof Response) return text8;
|
|
6509
|
+
const body = JSON.parse(text8);
|
|
6510
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6511
|
+
selection = body.namedSeat;
|
|
6512
|
+
} catch {
|
|
6513
|
+
return json({ error: "invalid seat selection" }, 400);
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
const id7 = url.searchParams.get("application");
|
|
6517
|
+
if (!id7 || id7.length > 160) return json({ error: "application required" }, 400);
|
|
6159
6518
|
const db = ctx.makeDb(env);
|
|
6160
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
6519
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
6161
6520
|
if (!app || String(app.groupId ?? ctx.chapter.id) !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
6162
6521
|
if (typeof app.stripeRuntime === "string" && app.stripeRuntime !== stripeRuntimeIdentity(env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
6163
6522
|
if (app.status !== "submitted" || app.giftOfferId || app.namedSeatId || app.namedSeatClaimPending || app.giftClaimId || app.canceled === true) return json({ error: "membership checkout unavailable" }, 409);
|
|
6164
6523
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
6165
6524
|
if (!group) return json({ error: "payments not configured" }, 503);
|
|
6166
6525
|
try {
|
|
6167
|
-
|
|
6168
|
-
|
|
6526
|
+
let { quote } = await checkoutQuoteForApplication(db, app, group, env, ctx, prepare);
|
|
6527
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
6528
|
+
if (prepare && !intent && !quote.seatOffer && ctx.membershipAuthority?.namedSeats && !app.membershipRestartId && selection === void 0) selection = null;
|
|
6529
|
+
if (selection !== void 0) {
|
|
6530
|
+
if (intent) return json({ error: "checkout has already started", code: "checkout_intent_mismatch" }, 409);
|
|
6531
|
+
({ quote } = await selectCheckoutNamedSeat(
|
|
6532
|
+
db,
|
|
6533
|
+
{
|
|
6534
|
+
appId: env.ODLA_APP_ID,
|
|
6535
|
+
chapterId: ctx.chapter.id,
|
|
6536
|
+
environment: env.ODLA_ENV,
|
|
6537
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
6538
|
+
applicationId: id7,
|
|
6539
|
+
skuId: String(app.tierId)
|
|
6540
|
+
},
|
|
6541
|
+
{ selection, purchaserEmail: String(app.email ?? ""), now: Date.now(), enabled: Boolean(ctx.membershipAuthority?.namedSeats) && !app.membershipRestartId }
|
|
6542
|
+
));
|
|
6543
|
+
}
|
|
6544
|
+
return new Response(JSON.stringify(quote), { headers: {
|
|
6545
|
+
"content-type": "application/json",
|
|
6546
|
+
"cache-control": "no-store",
|
|
6547
|
+
"x-chapter-checkout-started": intent ? "true" : "false"
|
|
6548
|
+
} });
|
|
6169
6549
|
} catch (error) {
|
|
6550
|
+
if (error.code === "invalid_named_seat_recipient") return json({ error: "Enter a different recipient's name and email." }, 400);
|
|
6170
6551
|
const expired = error.code === "membership_quote_expired";
|
|
6171
6552
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_expired" } : {} }, expired ? 409 : 503);
|
|
6172
6553
|
}
|
|
@@ -6272,47 +6653,6 @@ async function resumeJoin(url, env, ctx) {
|
|
|
6272
6653
|
return json({ error: "Application continuation is unavailable. Retry when its payment or membership configuration is restored." }, 409);
|
|
6273
6654
|
}
|
|
6274
6655
|
|
|
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
6656
|
// src/named-seat-application.ts
|
|
6317
6657
|
async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
|
|
6318
6658
|
const app = await membershipLoad(db, "applications", applicationId);
|
|
@@ -7207,10 +7547,10 @@ function tiersOf(value, errors) {
|
|
|
7207
7547
|
return [];
|
|
7208
7548
|
}
|
|
7209
7549
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7210
|
-
const
|
|
7211
|
-
if (
|
|
7212
|
-
if (seen.has(
|
|
7213
|
-
seen.add(
|
|
7550
|
+
const id7 = text4(item.id, `${path}.id`, errors, 80);
|
|
7551
|
+
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7552
|
+
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7553
|
+
seen.add(id7);
|
|
7214
7554
|
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
7215
7555
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7216
7556
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
@@ -7231,7 +7571,7 @@ function tiersOf(value, errors) {
|
|
|
7231
7571
|
}
|
|
7232
7572
|
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7233
7573
|
return [{
|
|
7234
|
-
id:
|
|
7574
|
+
id: id7,
|
|
7235
7575
|
name,
|
|
7236
7576
|
available: item.available === true,
|
|
7237
7577
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -7293,15 +7633,15 @@ function policyOf(value, errors, tiers) {
|
|
|
7293
7633
|
if (input.tierPolicies !== void 0) {
|
|
7294
7634
|
const overrides = recordOf(input.tierPolicies);
|
|
7295
7635
|
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.${
|
|
7636
|
+
else for (const [id7, raw2] of Object.entries(overrides)) {
|
|
7637
|
+
const path = `document.policy.tierPolicies.${id7}`;
|
|
7298
7638
|
const policy2 = recordOf(raw2);
|
|
7299
|
-
if (!tiers.some((tier) => tier.id ===
|
|
7639
|
+
if (!tiers.some((tier) => tier.id === id7) || !policy2) {
|
|
7300
7640
|
errors.push(`${path} must name a declared tier and policy`);
|
|
7301
7641
|
continue;
|
|
7302
7642
|
}
|
|
7303
7643
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7304
|
-
tierPolicies[
|
|
7644
|
+
tierPolicies[id7] = {
|
|
7305
7645
|
refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
7306
7646
|
...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7307
7647
|
};
|
|
@@ -7594,14 +7934,14 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7594
7934
|
continue;
|
|
7595
7935
|
}
|
|
7596
7936
|
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
7597
|
-
const
|
|
7937
|
+
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7598
7938
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7599
7939
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7600
|
-
if (
|
|
7940
|
+
if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7601
7941
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7602
|
-
if (ids2.has(
|
|
7942
|
+
if (ids2.has(id7) || knownParentIds.has(id7)) errors.push(`${path}.id duplicates page "${id7}"`);
|
|
7603
7943
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
7604
|
-
ids2.add(
|
|
7944
|
+
ids2.add(id7);
|
|
7605
7945
|
slugs.add(slug);
|
|
7606
7946
|
const navRaw = record2(raw2.navigation);
|
|
7607
7947
|
let navigation = { section: "General", order: index2 };
|
|
@@ -7657,7 +7997,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7657
7997
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
7658
7998
|
}
|
|
7659
7999
|
}
|
|
7660
|
-
pages.push({ id:
|
|
8000
|
+
pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
7661
8001
|
}
|
|
7662
8002
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
7663
8003
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -7800,20 +8140,20 @@ async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
|
7800
8140
|
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7801
8141
|
}
|
|
7802
8142
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
7803
|
-
const
|
|
7804
|
-
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id:
|
|
8143
|
+
const id7 = `member-content-local:${environment}`;
|
|
8144
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
|
|
7805
8145
|
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
7806
8146
|
}
|
|
7807
8147
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
7808
8148
|
const { document, digest } = envelope;
|
|
7809
8149
|
const target = document.target;
|
|
7810
|
-
const
|
|
8150
|
+
const id7 = `member-content-control:${target.environment}:${document.revision}`;
|
|
7811
8151
|
const ops = [];
|
|
7812
8152
|
if (previous?.active) {
|
|
7813
8153
|
ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
|
|
7814
8154
|
}
|
|
7815
|
-
ops.push({ t: "update", ns: "memberContentControls", id:
|
|
7816
|
-
id:
|
|
8155
|
+
ops.push({ t: "update", ns: "memberContentControls", id: id7, attrs: {
|
|
8156
|
+
id: id7,
|
|
7817
8157
|
sourceId: document.source.appId,
|
|
7818
8158
|
targetId: target.chapterId,
|
|
7819
8159
|
environment: target.environment,
|
|
@@ -7826,11 +8166,11 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
7826
8166
|
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
|
|
7827
8167
|
}
|
|
7828
8168
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7829
|
-
const
|
|
8169
|
+
const id7 = `member-content-local:${environment}`;
|
|
7830
8170
|
const current = await loadLocalMemberContent(db, document.source.appId, environment);
|
|
7831
8171
|
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
|
|
7832
8172
|
const row2 = {
|
|
7833
|
-
id:
|
|
8173
|
+
id: id7,
|
|
7834
8174
|
chapterId: document.source.appId,
|
|
7835
8175
|
environment,
|
|
7836
8176
|
revision: document.revision,
|
|
@@ -7838,9 +8178,9 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
|
|
|
7838
8178
|
updatedAt,
|
|
7839
8179
|
...updatedBy ? { updatedBy } : {}
|
|
7840
8180
|
};
|
|
7841
|
-
await db.transact([{ t: "update", ns: "memberContentLocal", id:
|
|
8181
|
+
await db.transact([{ t: "update", ns: "memberContentLocal", id: id7, attrs: row2 }], {
|
|
7842
8182
|
mutationId: `member-content-local:${environment}:${document.revision}`,
|
|
7843
|
-
guards: current ? [{ ns: "memberContentLocal", id:
|
|
8183
|
+
guards: current ? [{ ns: "memberContentLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id: id7, exists: false }]
|
|
7844
8184
|
});
|
|
7845
8185
|
return row2;
|
|
7846
8186
|
}
|
|
@@ -7977,7 +8317,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
7977
8317
|
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
7978
8318
|
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
7979
8319
|
effective,
|
|
7980
|
-
tiers: tiers.map(({ id:
|
|
8320
|
+
tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
|
|
7981
8321
|
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
7982
8322
|
});
|
|
7983
8323
|
}
|
|
@@ -8135,11 +8475,11 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8135
8475
|
return json({ error: "expiresAt must be a future epoch timestamp" }, 400);
|
|
8136
8476
|
}
|
|
8137
8477
|
const sourceGrantId = crypto.randomUUID();
|
|
8138
|
-
const
|
|
8478
|
+
const id7 = `admission:local:${ctx.chapter.id}:${sourceGrantId}`;
|
|
8139
8479
|
const createdAt = Date.now();
|
|
8140
8480
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8141
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8142
|
-
id:
|
|
8481
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8482
|
+
id: id7,
|
|
8143
8483
|
sourceGrantId,
|
|
8144
8484
|
sourceScope: "local",
|
|
8145
8485
|
sourceId: ctx.chapter.id,
|
|
@@ -8155,7 +8495,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8155
8495
|
updatedAt: createdAt,
|
|
8156
8496
|
updatedBy: actor
|
|
8157
8497
|
} }], { mutationId: `admission-local-create:${sourceGrantId}` });
|
|
8158
|
-
return json({ grant: { id:
|
|
8498
|
+
return json({ grant: { id: id7, sourceGrantId, sourceScope: "local", email, decision, active: true, createdAt, ...note ? { note } : {}, ...expiresAt ? { expiresAt } : {} } }, 201);
|
|
8159
8499
|
}
|
|
8160
8500
|
const prefix = "/api/admin/admission-grants/";
|
|
8161
8501
|
let revokeId = "";
|
|
@@ -8169,20 +8509,20 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8169
8509
|
if (/^admission:local:[a-z0-9-]+:[0-9a-f-]+$/.test(revokeId) && req.method === "DELETE") {
|
|
8170
8510
|
const auth = await gate(req, env, ctx);
|
|
8171
8511
|
if (auth instanceof Response) return auth;
|
|
8172
|
-
const
|
|
8173
|
-
const found = await auth.db.query({ admissionGrants: { $: { where: { id:
|
|
8512
|
+
const id7 = revokeId;
|
|
8513
|
+
const found = await auth.db.query({ admissionGrants: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8174
8514
|
const row2 = Array.isArray(found.admissionGrants) ? found.admissionGrants[0] : void 0;
|
|
8175
8515
|
if (!row2 || row2.targetId !== ctx.chapter.id || row2.sourceScope !== "local") return json({ error: "not found" }, 404);
|
|
8176
8516
|
if (row2.active !== true) return json({ ok: true, replayed: true });
|
|
8177
8517
|
const revokedAt = Date.now();
|
|
8178
8518
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8179
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8519
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8180
8520
|
active: false,
|
|
8181
8521
|
revokedAt,
|
|
8182
8522
|
revokedBy: actor,
|
|
8183
8523
|
updatedAt: revokedAt,
|
|
8184
8524
|
updatedBy: actor
|
|
8185
|
-
} }], { mutationId: `admission-local-revoke:${
|
|
8525
|
+
} }], { mutationId: `admission-local-revoke:${id7}` });
|
|
8186
8526
|
return json({ ok: true, replayed: false });
|
|
8187
8527
|
}
|
|
8188
8528
|
return null;
|
|
@@ -8244,8 +8584,8 @@ async function loadLeaderAdmissionAssignments(db, targetId) {
|
|
|
8244
8584
|
return await readAdmissionRows(db, "leaderAdmissionAssignments", { targetId }, MAX_ASSIGNMENTS);
|
|
8245
8585
|
}
|
|
8246
8586
|
async function loadLeaderAdmissionDelivery(db, targetId, environment) {
|
|
8247
|
-
const
|
|
8248
|
-
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id:
|
|
8587
|
+
const id7 = deliveryId(targetId, environment);
|
|
8588
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8249
8589
|
return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
|
|
8250
8590
|
}
|
|
8251
8591
|
function personAdmissionEmail(ctx, record4) {
|
|
@@ -8273,8 +8613,8 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8273
8613
|
const now = Date.now();
|
|
8274
8614
|
const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
8275
8615
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
8276
|
-
const
|
|
8277
|
-
const existing = current.find((row2) => row2.id ===
|
|
8616
|
+
const id7 = assignmentId(opts.target.id, opts.personId);
|
|
8617
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
8278
8618
|
const digest = await signupControlDigest({
|
|
8279
8619
|
targetId: opts.target.id,
|
|
8280
8620
|
personId: opts.personId,
|
|
@@ -8304,7 +8644,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8304
8644
|
return { delivery: saved, duplicate: true };
|
|
8305
8645
|
}
|
|
8306
8646
|
const assignment = {
|
|
8307
|
-
id:
|
|
8647
|
+
id: id7,
|
|
8308
8648
|
targetId: opts.target.id,
|
|
8309
8649
|
personId: opts.personId,
|
|
8310
8650
|
grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
@@ -8319,7 +8659,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8319
8659
|
updatedAt: now,
|
|
8320
8660
|
updatedBy: opts.actor
|
|
8321
8661
|
};
|
|
8322
|
-
const desired = current.filter((row2) => row2.id !==
|
|
8662
|
+
const desired = current.filter((row2) => row2.id !== id7 && row2.active).concat(assignment.active ? [assignment] : []);
|
|
8323
8663
|
const grants = await Promise.all(desired.map((row2) => grantInput(opts.db, opts.ctx, row2, opts.resolveEmail)));
|
|
8324
8664
|
const revision = (previous?.revision ?? 0) + 1;
|
|
8325
8665
|
const envelope = await createAdmissionGrantEnvelope({
|
|
@@ -8342,7 +8682,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8342
8682
|
updatedAt: now
|
|
8343
8683
|
};
|
|
8344
8684
|
const ops = [
|
|
8345
|
-
{ t: "update", ns: "leaderAdmissionAssignments", id:
|
|
8685
|
+
{ t: "update", ns: "leaderAdmissionAssignments", id: id7, attrs: assignment },
|
|
8346
8686
|
{ t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
|
|
8347
8687
|
{ t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
|
|
8348
8688
|
id: mutationReceiptId,
|
|
@@ -8357,7 +8697,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8357
8697
|
...!opts.note && existing?.note !== void 0 ? ["note"] : [],
|
|
8358
8698
|
...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
8359
8699
|
];
|
|
8360
|
-
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id:
|
|
8700
|
+
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id: id7, attrs: retract });
|
|
8361
8701
|
const tx = await opts.db.transact(ops, {
|
|
8362
8702
|
mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
|
|
8363
8703
|
guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
|
|
@@ -8417,11 +8757,11 @@ function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
|
|
|
8417
8757
|
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
8418
8758
|
);
|
|
8419
8759
|
const def = crm.type(record4.type);
|
|
8420
|
-
const editFields = editable.map((
|
|
8421
|
-
const field = def.fields[
|
|
8760
|
+
const editFields = editable.map((id7) => {
|
|
8761
|
+
const field = def.fields[id7];
|
|
8422
8762
|
return {
|
|
8423
|
-
id:
|
|
8424
|
-
label: field.label ??
|
|
8763
|
+
id: id7,
|
|
8764
|
+
label: field.label ?? id7,
|
|
8425
8765
|
type: field.type,
|
|
8426
8766
|
required: field.required ?? false,
|
|
8427
8767
|
...field.options ? { options: field.options } : {}
|
|
@@ -8538,8 +8878,8 @@ async function gate2(req, env, ctx) {
|
|
|
8538
8878
|
if (!await networkWriteAllowed(req, db, user, ctx, true)) return { response: reject6(403, "super_admin_required", "super-admin required") };
|
|
8539
8879
|
return { db, user };
|
|
8540
8880
|
}
|
|
8541
|
-
function targetFor(ctx,
|
|
8542
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
8881
|
+
function targetFor(ctx, id7) {
|
|
8882
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7 && target.admissionGrants);
|
|
8543
8883
|
}
|
|
8544
8884
|
async function deliverSaved(db, env, ctx, target, delivery) {
|
|
8545
8885
|
if (delivery.status === "acknowledged") return delivery;
|
|
@@ -8643,7 +8983,7 @@ var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
|
8643
8983
|
if (!personId || !MUTATION_ID2.test(mutationId)) {
|
|
8644
8984
|
return reject6(422, "invalid_request", "personId and a stable mutationId are required");
|
|
8645
8985
|
}
|
|
8646
|
-
const resolveEmail = (
|
|
8986
|
+
const resolveEmail = (id7) => resolveAdmissionPersonEmail(db, env, ctx, target, id7);
|
|
8647
8987
|
let email;
|
|
8648
8988
|
try {
|
|
8649
8989
|
email = await resolveEmail(personId);
|
|
@@ -8882,28 +9222,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8882
9222
|
};
|
|
8883
9223
|
|
|
8884
9224
|
// src/payments-stripe.ts
|
|
8885
|
-
var
|
|
9225
|
+
var import_stripe12 = require("@odla-ai/stripe");
|
|
8886
9226
|
|
|
8887
9227
|
// src/payments-refund.ts
|
|
8888
|
-
var
|
|
9228
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
8889
9229
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8890
|
-
var
|
|
9230
|
+
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8891
9231
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
8892
|
-
function
|
|
9232
|
+
function fail7(code = "refund_target_mismatch") {
|
|
8893
9233
|
throw Object.assign(new Error(code), { code });
|
|
8894
9234
|
}
|
|
8895
9235
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8896
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9236
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
|
|
8897
9237
|
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))
|
|
9238
|
+
const result = await (0, import_stripe10.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9239
|
+
if (!result.ok) fail7("stripe_error");
|
|
9240
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
|
|
8901
9241
|
return result.body;
|
|
8902
9242
|
};
|
|
8903
9243
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8904
|
-
if (
|
|
9244
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
|
|
8905
9245
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
8906
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9246
|
+
if (row(sub.metadata)[key3] !== value) fail7();
|
|
8907
9247
|
}
|
|
8908
9248
|
let cursor;
|
|
8909
9249
|
let earliest;
|
|
@@ -8916,79 +9256,79 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8916
9256
|
limit: 100,
|
|
8917
9257
|
...cursor ? { starting_after: cursor } : {}
|
|
8918
9258
|
});
|
|
8919
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9259
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
|
|
8920
9260
|
for (const value of list.data) {
|
|
8921
9261
|
const invoice2 = row(value);
|
|
8922
9262
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
8923
|
-
if (!providerId2(
|
|
8924
|
-
seen.add(
|
|
9263
|
+
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();
|
|
9264
|
+
seen.add(id3(invoice2));
|
|
8925
9265
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
8926
9266
|
}
|
|
8927
9267
|
if (!list.has_more) break;
|
|
8928
|
-
if (!list.data.length || page === 9)
|
|
8929
|
-
cursor =
|
|
9268
|
+
if (!list.data.length || page === 9) fail7("refund_history_incomplete");
|
|
9269
|
+
cursor = id3(list.data[list.data.length - 1]);
|
|
8930
9270
|
}
|
|
8931
|
-
if (!earliest)
|
|
9271
|
+
if (!earliest) fail7("no_charge");
|
|
8932
9272
|
const invoice = earliest;
|
|
8933
|
-
if (input.expectedInvoiceId !== void 0 &&
|
|
8934
|
-
let chargeId =
|
|
8935
|
-
let paymentIntentId =
|
|
9273
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
|
|
9274
|
+
let chargeId = id3(invoice.charge);
|
|
9275
|
+
let paymentIntentId = id3(invoice.payment_intent);
|
|
8936
9276
|
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)
|
|
9277
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
|
|
9278
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
|
|
8939
9279
|
const payment = row(payments.data[0]);
|
|
8940
|
-
if (
|
|
8941
|
-
paymentIntentId =
|
|
9280
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
|
|
9281
|
+
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
8942
9282
|
}
|
|
8943
9283
|
if (!chargeId) {
|
|
8944
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9284
|
+
if (!providerId2(paymentIntentId, "pi")) fail7();
|
|
8945
9285
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
8946
|
-
if (
|
|
8947
|
-
chargeId =
|
|
9286
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
|
|
9287
|
+
chargeId = id3(payment.latest_charge);
|
|
8948
9288
|
}
|
|
8949
|
-
if (!providerId2(chargeId, "ch"))
|
|
9289
|
+
if (!providerId2(chargeId, "ch")) fail7();
|
|
8950
9290
|
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))
|
|
9291
|
+
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();
|
|
9292
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
|
|
8953
9293
|
if (charge.amount_refunded !== charge.amount) {
|
|
8954
9294
|
const refund = await call("POST", "/v1/refunds", {
|
|
8955
9295
|
charge: chargeId,
|
|
8956
9296
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
8957
9297
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
8958
|
-
if (
|
|
9298
|
+
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
9299
|
}
|
|
8960
9300
|
let subscriptionCanceled = sub.status === "canceled";
|
|
8961
9301
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
8962
9302
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8963
|
-
if (
|
|
9303
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
|
|
8964
9304
|
subscriptionCanceled = true;
|
|
8965
9305
|
}
|
|
8966
9306
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
8967
|
-
if (
|
|
9307
|
+
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
9308
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
8969
9309
|
}
|
|
8970
9310
|
|
|
8971
9311
|
// src/subscription-payment-identity.ts
|
|
8972
|
-
var
|
|
8973
|
-
var
|
|
9312
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
9313
|
+
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8974
9314
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8975
|
-
let paymentId =
|
|
9315
|
+
let paymentId = id4(invoice.payment_intent);
|
|
8976
9316
|
if (!paymentId) {
|
|
8977
|
-
if (!/^in_[A-Za-z0-9]+$/.test(
|
|
8978
|
-
const result2 = await (0,
|
|
9317
|
+
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9318
|
+
const result2 = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
8979
9319
|
const list = result2.body;
|
|
8980
9320
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8981
9321
|
const link = list.data[0];
|
|
8982
|
-
if (
|
|
8983
|
-
paymentId =
|
|
9322
|
+
if (id4(link.invoice) !== id4(invoice) || !["open", "paid"].includes(link.status) || link.payment?.type !== "payment_intent") subscriptionContractMismatch();
|
|
9323
|
+
paymentId = id4(link.payment.payment_intent);
|
|
8984
9324
|
}
|
|
8985
9325
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8986
9326
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8987
|
-
const result = await (0,
|
|
9327
|
+
const result = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", path);
|
|
8988
9328
|
const payment = result.body;
|
|
8989
|
-
if (!result.ok || payment?.id !== paymentId ||
|
|
9329
|
+
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
9330
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8991
|
-
const tagged = await (0,
|
|
9331
|
+
const tagged = await (0, import_stripe11.stripeRequest)(
|
|
8992
9332
|
{ secretKey },
|
|
8993
9333
|
"POST",
|
|
8994
9334
|
path,
|
|
@@ -9000,7 +9340,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
9000
9340
|
|
|
9001
9341
|
// src/payments-stripe.ts
|
|
9002
9342
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
9003
|
-
return (0,
|
|
9343
|
+
return (0, import_stripe12.stripeRequest)(
|
|
9004
9344
|
{ secretKey: sk },
|
|
9005
9345
|
method,
|
|
9006
9346
|
path,
|
|
@@ -9008,7 +9348,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
9008
9348
|
idempotencyKey ? { idempotencyKey } : {}
|
|
9009
9349
|
);
|
|
9010
9350
|
}
|
|
9011
|
-
function
|
|
9351
|
+
function fail8(op, r) {
|
|
9012
9352
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
9013
9353
|
err.code = "stripe_error";
|
|
9014
9354
|
throw err;
|
|
@@ -9047,6 +9387,15 @@ function createStripeProvider(config) {
|
|
|
9047
9387
|
let customerId = input.existingCustomerId;
|
|
9048
9388
|
if (customerId && !/^cus_[A-Za-z0-9]+$/.test(customerId) || input.existingSubscriptionId && (!customerId || !/^sub_[A-Za-z0-9]+$/.test(input.existingSubscriptionId))) subscriptionContractMismatch();
|
|
9049
9389
|
const productId = await verifySubscriptionPrice(sk, input, Boolean(input.existingSubscriptionId));
|
|
9390
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
9391
|
+
const seatProductId = seat ? await verifySubscriptionPrice(sk, {
|
|
9392
|
+
...input,
|
|
9393
|
+
priceId: seat.priceId,
|
|
9394
|
+
priceCents: seat.amountCents,
|
|
9395
|
+
billingTerms: void 0,
|
|
9396
|
+
namedSeat: void 0
|
|
9397
|
+
}, Boolean(input.existingSubscriptionId)) : void 0;
|
|
9398
|
+
if (seat && (!seatProductId || !productId || seatProductId === productId)) subscriptionContractMismatch();
|
|
9050
9399
|
if (input.billingTerms?.discount && !productId) subscriptionContractMismatch();
|
|
9051
9400
|
await prepareSubscriptionBillingTerms(sk, { ...input, discountProductId: productId }, Boolean(input.existingSubscriptionId));
|
|
9052
9401
|
const billingParams = subscriptionBillingParams(input, productId);
|
|
@@ -9058,7 +9407,7 @@ function createStripeProvider(config) {
|
|
|
9058
9407
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9059
9408
|
`cus:${input.applicationId}`
|
|
9060
9409
|
);
|
|
9061
|
-
if (!cust.ok)
|
|
9410
|
+
if (!cust.ok) fail8("customer create", cust);
|
|
9062
9411
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9063
9412
|
customerId = cust.body.id;
|
|
9064
9413
|
}
|
|
@@ -9078,19 +9427,32 @@ function createStripeProvider(config) {
|
|
|
9078
9427
|
{
|
|
9079
9428
|
customer: customerId,
|
|
9080
9429
|
"items[0][price]": input.priceId,
|
|
9430
|
+
...seat ? {
|
|
9431
|
+
"items[1][price]": seat.priceId,
|
|
9432
|
+
"items[1][quantity]": 1,
|
|
9433
|
+
"items[1][metadata][seatId]": seat.seatId,
|
|
9434
|
+
"items[1][metadata][membershipId]": seat.membershipId
|
|
9435
|
+
} : {},
|
|
9081
9436
|
payment_behavior: "default_incomplete",
|
|
9082
9437
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
9083
9438
|
"payment_settings[payment_method_types][0]": "card",
|
|
9084
9439
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
9085
9440
|
"expand[1]": "latest_invoice.payment_intent",
|
|
9086
9441
|
metadata: meta,
|
|
9087
|
-
...billingParams
|
|
9442
|
+
...billingParams,
|
|
9443
|
+
...seat && input.billingTerms?.initialTermEndsAt ? {
|
|
9444
|
+
"add_invoice_items[1][price_data][product]": seatProductId,
|
|
9445
|
+
"add_invoice_items[1][price_data][currency]": input.currency,
|
|
9446
|
+
"add_invoice_items[1][price_data][unit_amount]": seat.amountCents,
|
|
9447
|
+
"add_invoice_items[1][quantity]": 1,
|
|
9448
|
+
"add_invoice_items[1][discountable]": false
|
|
9449
|
+
} : {}
|
|
9088
9450
|
},
|
|
9089
9451
|
// Hardening: one subscription per application, so a client
|
|
9090
9452
|
// retry between create and the db write can't orphan a second one.
|
|
9091
9453
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9092
9454
|
);
|
|
9093
|
-
if (!sub.ok)
|
|
9455
|
+
if (!sub.ok) fail8("subscription create", sub);
|
|
9094
9456
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9095
9457
|
const subscriptionId = sub.body.id;
|
|
9096
9458
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9100,14 +9462,14 @@ function createStripeProvider(config) {
|
|
|
9100
9462
|
"expand[2]": "discounts",
|
|
9101
9463
|
"expand[3]": "latest_invoice.discounts"
|
|
9102
9464
|
});
|
|
9103
|
-
if (!expanded.ok)
|
|
9465
|
+
if (!expanded.ok) fail8("subscription invoice evidence", expanded);
|
|
9104
9466
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9105
9467
|
sub = expanded;
|
|
9106
9468
|
}
|
|
9107
9469
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9108
9470
|
const invoice = sub.body.latest_invoice;
|
|
9109
9471
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9110
|
-
if (!clientSecret2)
|
|
9472
|
+
if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
|
|
9111
9473
|
await tagSubscriptionPayment(
|
|
9112
9474
|
sk,
|
|
9113
9475
|
invoice,
|
|
@@ -9119,7 +9481,7 @@ function createStripeProvider(config) {
|
|
|
9119
9481
|
return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
|
|
9120
9482
|
},
|
|
9121
9483
|
async ingestWebhook(rawBody, sigHeader) {
|
|
9122
|
-
if (!webhookSecret || !await (0,
|
|
9484
|
+
if (!webhookSecret || !await (0, import_stripe12.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
|
|
9123
9485
|
return { ok: false, reason: "bad_signature" };
|
|
9124
9486
|
}
|
|
9125
9487
|
const event2 = JSON.parse(rawBody);
|
|
@@ -9319,9 +9681,9 @@ function matchesIntent(stored, expected) {
|
|
|
9319
9681
|
"purchaseAttemptId"
|
|
9320
9682
|
].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
9683
|
}
|
|
9322
|
-
async function loadIntent(db,
|
|
9684
|
+
async function loadIntent(db, id7) {
|
|
9323
9685
|
return first3((await db.query({
|
|
9324
|
-
subscriptionCheckoutIntents: { $: { where: { id:
|
|
9686
|
+
subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } }
|
|
9325
9687
|
})).subscriptionCheckoutIntents);
|
|
9326
9688
|
}
|
|
9327
9689
|
async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCheckout) {
|
|
@@ -9342,16 +9704,22 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9342
9704
|
if (input.purchaseAttemptId !== application?.membershipRestartId) mismatch();
|
|
9343
9705
|
if (application?.namedSeatId || application?.namedSeatClaimPending || application?.giftClaimId) mismatch();
|
|
9344
9706
|
if (input.membershipOffer && (!application || application.canceled === true || application.status !== "submitted")) mismatch();
|
|
9707
|
+
if (input.paymentQuote?.namedSeat && application?.email !== input.email) mismatch();
|
|
9345
9708
|
const applicationGuard = {
|
|
9346
9709
|
ns: "applications",
|
|
9347
9710
|
id: input.applicationId,
|
|
9348
9711
|
exists: true,
|
|
9349
|
-
equals: {
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
...
|
|
9354
|
-
|
|
9712
|
+
equals: {
|
|
9713
|
+
status: "submitted",
|
|
9714
|
+
...input.paymentQuote?.namedSeat ? { email: input.email } : {},
|
|
9715
|
+
...input.purchaseAttemptId ? { membershipRestartId: input.purchaseAttemptId } : {},
|
|
9716
|
+
...input.membershipOffer ? {
|
|
9717
|
+
groupId: input.groupId,
|
|
9718
|
+
tierId: input.tierId,
|
|
9719
|
+
stripeRuntime: stripeRuntimeIdentity(input.runtime),
|
|
9720
|
+
...application?.canceled !== void 0 ? { canceled: application.canceled } : {}
|
|
9721
|
+
} : {}
|
|
9722
|
+
},
|
|
9355
9723
|
absent: ["namedSeatId", "namedSeatClaimPending", "giftClaimId", ...!input.purchaseAttemptId ? ["membershipRestartId"] : [], ...input.membershipOffer && application?.canceled === void 0 ? ["canceled"] : []]
|
|
9356
9724
|
};
|
|
9357
9725
|
let stored = await loadIntent(db, input.applicationId);
|
|
@@ -9375,7 +9743,11 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9375
9743
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
9376
9744
|
id: input.applicationId,
|
|
9377
9745
|
exists: true,
|
|
9378
|
-
equals: {
|
|
9746
|
+
equals: {
|
|
9747
|
+
status: "ready",
|
|
9748
|
+
offerDigest: input.membershipOffer.digest,
|
|
9749
|
+
...input.projectionRevision !== void 0 ? { revision: input.projectionRevision } : {}
|
|
9750
|
+
}
|
|
9379
9751
|
}
|
|
9380
9752
|
] : []
|
|
9381
9753
|
]
|
|
@@ -9390,7 +9762,8 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9390
9762
|
if (!stored.providerSubscriptionId && input.now - Number(stored.createdAt) >= 23 * 60 * 6e4) {
|
|
9391
9763
|
throw Object.assign(new Error("checkout requires provider reconciliation"), { code: "checkout_recovery_required" });
|
|
9392
9764
|
}
|
|
9393
|
-
|
|
9765
|
+
const binding = input.membershipOffer ? await authorizeNetworkCheckout() : void 0;
|
|
9766
|
+
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
9767
|
const result = await provider.createSubscription({
|
|
9395
9768
|
appId: input.appId,
|
|
9396
9769
|
chapterId: input.chapterId,
|
|
@@ -9405,6 +9778,7 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9405
9778
|
priceCents: input.priceCents,
|
|
9406
9779
|
currency: input.currency,
|
|
9407
9780
|
interval: input.interval,
|
|
9781
|
+
...binding?.namedSeat ? { namedSeat: binding.namedSeat } : {},
|
|
9408
9782
|
...stored.billingTerms ? { billingTerms: stored.billingTerms } : {},
|
|
9409
9783
|
...typeof stored.providerSubscriptionId === "string" ? { existingSubscriptionId: stored.providerSubscriptionId } : {},
|
|
9410
9784
|
existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
|
|
@@ -9521,6 +9895,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9521
9895
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_changed" } : {} }, expired ? 409 : 503);
|
|
9522
9896
|
}
|
|
9523
9897
|
const { tier, quote, billingTerms } = checkout2;
|
|
9898
|
+
if (quote.namedSeat && body.seatTermsAck !== true) return json({ error: "seatTermsAck required" }, 400);
|
|
9524
9899
|
const membershipOffer = "membershipOffer" in checkout2 ? checkout2.membershipOffer : void 0;
|
|
9525
9900
|
if (ctx.membershipAuthority && !membershipOffer) return json({ error: "checkout requires reconciliation", code: "checkout_recovery_required" }, 409);
|
|
9526
9901
|
if (!tier) return json({ error: "tier not found", code: "tier_unresolved" }, 400);
|
|
@@ -9545,6 +9920,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9545
9920
|
priceCents: billingTerms ? tier.priceCents : quote.lineItems.dueTodayCents,
|
|
9546
9921
|
...billingTerms ? { billingTerms } : {},
|
|
9547
9922
|
...membershipOffer ? { membershipOffer } : {},
|
|
9923
|
+
..."projectionRevision" in checkout2 ? { projectionRevision: checkout2.projectionRevision } : {},
|
|
9548
9924
|
...ctx.requirePaymentQuote || ctx.membershipAuthority || body.quoteDigest ? { paymentQuote: quote } : {},
|
|
9549
9925
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
9550
9926
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
@@ -9566,9 +9942,10 @@ async function startSubscription(req, env, ctx) {
|
|
|
9566
9942
|
offerId: membershipOffer.id,
|
|
9567
9943
|
offerDigest: membershipOffer.digest,
|
|
9568
9944
|
quoteDigest: quote.digest,
|
|
9569
|
-
priceId: String(priceId)
|
|
9945
|
+
priceId: String(priceId),
|
|
9946
|
+
...quote.namedSeat ? { namedSeat: { ...quote.namedSeat, purchaserEmail: normalizeChapterEmail(String(app.email ?? "")) } } : {}
|
|
9570
9947
|
};
|
|
9571
|
-
|
|
9948
|
+
return verifyMembershipCheckoutBinding(await ctx.membershipAuthority.bindCheckout(binding, env), binding);
|
|
9572
9949
|
} : void 0);
|
|
9573
9950
|
} catch (err) {
|
|
9574
9951
|
const code = paymentErrorCode(err);
|
|
@@ -9590,7 +9967,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
9590
9967
|
var handlePayments = async (req, url, env, ctx) => {
|
|
9591
9968
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
9592
9969
|
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);
|
|
9970
|
+
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true, req);
|
|
9594
9971
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
9595
9972
|
if (req.method === "POST" && url.pathname === "/api/webhooks/stripe") return ingestWebhook(req, env, ctx);
|
|
9596
9973
|
return null;
|
|
@@ -9604,16 +9981,16 @@ var userFor = async (req, env, ctx) => {
|
|
|
9604
9981
|
const user = await ctx.verifyUser(req, env);
|
|
9605
9982
|
return user?.email ? user : null;
|
|
9606
9983
|
};
|
|
9607
|
-
var loadClaim = async (db,
|
|
9984
|
+
var loadClaim = async (db, id7) => first4((await db.query({ giftClaims: { $: { where: { id: id7 }, limit: 1 } } })).giftClaims);
|
|
9608
9985
|
async function consumeRateLimit(db, chapterId, requester, now) {
|
|
9609
9986
|
const bucket = await giftRequesterBucket(chapterId, requester);
|
|
9610
|
-
const
|
|
9611
|
-
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id:
|
|
9987
|
+
const id7 = `gift-rate:${chapterId}:${bucket}`;
|
|
9988
|
+
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id: id7 }, limit: 1 } } })).giftRedemptionAttempts);
|
|
9612
9989
|
const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
|
|
9613
9990
|
const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
|
|
9614
9991
|
if (attemptCount > 10) return false;
|
|
9615
|
-
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id:
|
|
9616
|
-
id:
|
|
9992
|
+
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id: id7, attrs: {
|
|
9993
|
+
id: id7,
|
|
9617
9994
|
chapterId,
|
|
9618
9995
|
bucket,
|
|
9619
9996
|
attemptCount,
|
|
@@ -9787,14 +10164,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
|
9787
10164
|
async function readNamedSeatOffer(db, input, now) {
|
|
9788
10165
|
membershipClock(now);
|
|
9789
10166
|
await loadMembershipAuthorityState(db, input);
|
|
9790
|
-
const
|
|
10167
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
9791
10168
|
requireMembershipScope(member, input);
|
|
9792
10169
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
9793
10170
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
9794
10171
|
if (seat) requireMembershipScope(seat, input);
|
|
9795
10172
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
9796
10173
|
const offer = {
|
|
9797
|
-
membershipId:
|
|
10174
|
+
membershipId: id7,
|
|
9798
10175
|
amountCents: offering.seatPriceCents,
|
|
9799
10176
|
currency: offering.currency,
|
|
9800
10177
|
interval: offering.interval,
|
|
@@ -10163,18 +10540,18 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
10163
10540
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10164
10541
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10165
10542
|
try {
|
|
10166
|
-
const
|
|
10543
|
+
const id7 = decodeURIComponent(match[1]);
|
|
10167
10544
|
const body = await namedSeatRequest(req);
|
|
10168
10545
|
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
10546
|
return json({ error: "invalid delivery evidence" }, 400);
|
|
10170
10547
|
}
|
|
10171
|
-
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog",
|
|
10548
|
+
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog", id7);
|
|
10172
10549
|
if (!row2 || row2.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
10173
10550
|
if (row2.applicationId) {
|
|
10174
10551
|
const app = await membershipLoad(chapterDb, "applications", String(row2.applicationId));
|
|
10175
10552
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10176
10553
|
}
|
|
10177
|
-
await reconcileLifecycleDelivery(chapterDb, { ...body, id:
|
|
10554
|
+
await reconcileLifecycleDelivery(chapterDb, { ...body, id: id7, groupId: ctx.chapter.id, actorId: user.userId }, Date.now());
|
|
10178
10555
|
return json({ ok: true, sent: false });
|
|
10179
10556
|
} catch {
|
|
10180
10557
|
return json({ error: "delivery evidence is invalid, stale, or still in progress; reload the exact attempt" }, 409);
|
|
@@ -10277,13 +10654,13 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10277
10654
|
const user = await ctx.verifyUser(req, env);
|
|
10278
10655
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10279
10656
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10280
|
-
let
|
|
10657
|
+
let id7;
|
|
10281
10658
|
try {
|
|
10282
|
-
|
|
10659
|
+
id7 = dedicated ? url.searchParams.get("recordId") ?? "" : decodeURIComponent(match[1]);
|
|
10283
10660
|
} catch {
|
|
10284
10661
|
return json({ error: "invalid record id" }, 422);
|
|
10285
10662
|
}
|
|
10286
|
-
const record4 = await (0, import_crm13.getRecord)({ crm: ctx.chapter.crm, db },
|
|
10663
|
+
const record4 = await (0, import_crm13.getRecord)({ crm: ctx.chapter.crm, db }, id7);
|
|
10287
10664
|
if (!record4) return json({ error: "not found" }, 404);
|
|
10288
10665
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
10289
10666
|
if (match?.[2]) {
|
|
@@ -10298,9 +10675,9 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10298
10675
|
}
|
|
10299
10676
|
if (req.method === "GET" && dedicated) {
|
|
10300
10677
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
10301
|
-
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([
|
|
10302
|
-
id:
|
|
10303
|
-
label: field.label ??
|
|
10678
|
+
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id8]) => !excluded.includes(id8)).map(([id8, field]) => ({
|
|
10679
|
+
id: id8,
|
|
10680
|
+
label: field.label ?? id8,
|
|
10304
10681
|
type: field.type,
|
|
10305
10682
|
required: field.required ?? false,
|
|
10306
10683
|
...field.options ? { options: field.options } : {}
|
|
@@ -11071,12 +11448,12 @@ var readJson = async (req) => {
|
|
|
11071
11448
|
return null;
|
|
11072
11449
|
}
|
|
11073
11450
|
};
|
|
11074
|
-
async function loadApp(db,
|
|
11075
|
-
const { applications } = await db.query({ applications: { $: { where: { id:
|
|
11451
|
+
async function loadApp(db, id7) {
|
|
11452
|
+
const { applications } = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11076
11453
|
return applications?.[0] ?? null;
|
|
11077
11454
|
}
|
|
11078
|
-
async function loadGroup(db,
|
|
11079
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11455
|
+
async function loadGroup(db, id7) {
|
|
11456
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11080
11457
|
return groups?.[0] ?? null;
|
|
11081
11458
|
}
|
|
11082
11459
|
var handleAdminApplicationActions = async (req, url, env, ctx) => {
|
|
@@ -11162,8 +11539,8 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
11162
11539
|
const got = await gate4(req, env, ctx);
|
|
11163
11540
|
if (got instanceof Response) return got;
|
|
11164
11541
|
const db = got;
|
|
11165
|
-
const
|
|
11166
|
-
const app = await loadApp(db,
|
|
11542
|
+
const id7 = m[1];
|
|
11543
|
+
const app = await loadApp(db, id7);
|
|
11167
11544
|
if (!app) return json({ error: "not found" }, 404);
|
|
11168
11545
|
const result = await approveApplication({ db, env, ctx, app, origin: url.origin, reason: "manual" });
|
|
11169
11546
|
if (!result.ok) return json({ error: result.error }, result.status);
|
|
@@ -11192,10 +11569,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
11192
11569
|
const got = await gate4(req, env, ctx);
|
|
11193
11570
|
if (got instanceof Response) return got;
|
|
11194
11571
|
const db = got;
|
|
11195
|
-
const
|
|
11572
|
+
const id7 = m[1];
|
|
11196
11573
|
const body = await readJson(req);
|
|
11197
11574
|
if (!body) return json({ error: "invalid JSON body" }, 400);
|
|
11198
|
-
const app = await loadApp(db,
|
|
11575
|
+
const app = await loadApp(db, id7);
|
|
11199
11576
|
if (!app) return json({ error: "not found" }, 404);
|
|
11200
11577
|
return patchApplication(db, env, ctx, app, body);
|
|
11201
11578
|
};
|
|
@@ -11305,10 +11682,10 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
11305
11682
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
11306
11683
|
await (0, import_crm16.saveConfig)({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
11307
11684
|
} else {
|
|
11308
|
-
const
|
|
11685
|
+
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11309
11686
|
await db.transact([
|
|
11310
|
-
{ t: "merge", ns: "groups", id:
|
|
11311
|
-
{ t: "update", ns: "groups", id:
|
|
11687
|
+
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
|
|
11688
|
+
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11312
11689
|
]);
|
|
11313
11690
|
}
|
|
11314
11691
|
return json({ ok: true });
|
|
@@ -11323,8 +11700,8 @@ async function gate5(req, env, ctx) {
|
|
|
11323
11700
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
11324
11701
|
return rawDb;
|
|
11325
11702
|
}
|
|
11326
|
-
async function loadGroup2(db,
|
|
11327
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11703
|
+
async function loadGroup2(db, id7) {
|
|
11704
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11328
11705
|
return groups?.[0] ?? null;
|
|
11329
11706
|
}
|
|
11330
11707
|
var str3 = (v, d = "") => typeof v === "string" ? v : d;
|
|
@@ -11578,7 +11955,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11578
11955
|
if ("response" in got) return got.response;
|
|
11579
11956
|
return json({
|
|
11580
11957
|
targets: ctx.chapter.network.targets.map(({
|
|
11581
|
-
id:
|
|
11958
|
+
id: id7,
|
|
11582
11959
|
name,
|
|
11583
11960
|
url: targetUrl,
|
|
11584
11961
|
fields,
|
|
@@ -11587,7 +11964,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11587
11964
|
stageTransitions,
|
|
11588
11965
|
commercialParity
|
|
11589
11966
|
}) => ({
|
|
11590
|
-
id:
|
|
11967
|
+
id: id7,
|
|
11591
11968
|
name,
|
|
11592
11969
|
url: targetUrl,
|
|
11593
11970
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
@@ -11749,7 +12126,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
11749
12126
|
if (typeof body.recordId !== "string" || !Array.isArray(body.targetIds) || body.targetIds.length === 0) {
|
|
11750
12127
|
return json({ error: "recordId and a non-empty targetIds array are required" }, 400);
|
|
11751
12128
|
}
|
|
11752
|
-
const requested = new Set(body.targetIds.filter((
|
|
12129
|
+
const requested = new Set(body.targetIds.filter((id7) => typeof id7 === "string"));
|
|
11753
12130
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
11754
12131
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
11755
12132
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
@@ -11877,7 +12254,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
11877
12254
|
const appId = record4.fields.applicationId;
|
|
11878
12255
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
11879
12256
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
11880
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((
|
|
12257
|
+
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
12258
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
11882
12259
|
const meta = lock?.meta;
|
|
11883
12260
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -11982,9 +12359,9 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
11982
12359
|
// src/network-mutation-receipt.ts
|
|
11983
12360
|
var import_crm22 = require("@odla-ai/crm");
|
|
11984
12361
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
11985
|
-
async function priorMutation(db,
|
|
12362
|
+
async function priorMutation(db, id7) {
|
|
11986
12363
|
const result = await db.query({
|
|
11987
|
-
[import_crm22.CRM_NS.activity]: { $: { where: { id:
|
|
12364
|
+
[import_crm22.CRM_NS.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
11988
12365
|
});
|
|
11989
12366
|
const row2 = (result[import_crm22.CRM_NS.activity] ?? [])[0];
|
|
11990
12367
|
const meta = row2?.meta;
|
|
@@ -12244,8 +12621,8 @@ async function gate7(req, env, ctx) {
|
|
|
12244
12621
|
if (!await networkWriteAllowed(req, db, user, ctx)) return { response: json({ error: "super-admin required", code: "super_admin_required" }, 403) };
|
|
12245
12622
|
return { db, user };
|
|
12246
12623
|
}
|
|
12247
|
-
function targetFor2(ctx,
|
|
12248
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
12624
|
+
function targetFor2(ctx, id7) {
|
|
12625
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12249
12626
|
}
|
|
12250
12627
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
12251
12628
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12380,7 +12757,7 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
12380
12757
|
if (ctx.chapter.network.requireSuperAdminWrites && !await ctx.isSuperAdminEmail(got.db, got.user.email, got.user.userId)) {
|
|
12381
12758
|
return json({ ...result.page, records: result.page.records.map(({ edit: _edit, ...record4 }) => ({
|
|
12382
12759
|
...record4,
|
|
12383
|
-
fields: Object.fromEntries(Object.entries(record4.fields).filter(([
|
|
12760
|
+
fields: Object.fromEntries(Object.entries(record4.fields).filter(([id7]) => id7 === "name" || id7 === "email"))
|
|
12384
12761
|
})) });
|
|
12385
12762
|
}
|
|
12386
12763
|
return json(result.page);
|
|
@@ -12450,9 +12827,9 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12450
12827
|
}
|
|
12451
12828
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
12452
12829
|
if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
|
|
12453
|
-
const
|
|
12830
|
+
const id7 = crypto.randomUUID();
|
|
12454
12831
|
const note = {
|
|
12455
|
-
id:
|
|
12832
|
+
id: id7,
|
|
12456
12833
|
targetId: identity2.targetId,
|
|
12457
12834
|
recordType: identity2.recordType,
|
|
12458
12835
|
recordId: identity2.recordId,
|
|
@@ -12464,7 +12841,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12464
12841
|
await got.db.transact([{
|
|
12465
12842
|
t: "update",
|
|
12466
12843
|
ns: "networkNotes",
|
|
12467
|
-
id:
|
|
12844
|
+
id: id7,
|
|
12468
12845
|
attrs: { ...note, recordKey: identity2.recordKey }
|
|
12469
12846
|
}]);
|
|
12470
12847
|
return json({ note }, 201);
|
|
@@ -12564,13 +12941,13 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12564
12941
|
return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
|
|
12565
12942
|
}
|
|
12566
12943
|
const createdAt = Date.now();
|
|
12567
|
-
const
|
|
12944
|
+
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
12568
12945
|
const result = await (0, import_crm24.addActivity)(
|
|
12569
12946
|
{
|
|
12570
12947
|
crm: ctx.chapter.crm,
|
|
12571
12948
|
db,
|
|
12572
12949
|
now: () => createdAt,
|
|
12573
|
-
newId: () =>
|
|
12950
|
+
newId: () => id7
|
|
12574
12951
|
},
|
|
12575
12952
|
{
|
|
12576
12953
|
recordId,
|
|
@@ -12585,11 +12962,11 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12585
12962
|
body
|
|
12586
12963
|
}
|
|
12587
12964
|
},
|
|
12588
|
-
mutationId:
|
|
12965
|
+
mutationId: id7
|
|
12589
12966
|
}
|
|
12590
12967
|
);
|
|
12591
12968
|
const note = {
|
|
12592
|
-
id:
|
|
12969
|
+
id: id7,
|
|
12593
12970
|
sourceId: verified.sender,
|
|
12594
12971
|
recordType,
|
|
12595
12972
|
recordId,
|
|
@@ -12598,8 +12975,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12598
12975
|
};
|
|
12599
12976
|
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
12600
12977
|
};
|
|
12601
|
-
function targetFor3(ctx,
|
|
12602
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
12978
|
+
function targetFor3(ctx, id7) {
|
|
12979
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12603
12980
|
}
|
|
12604
12981
|
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
12605
12982
|
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
@@ -12868,10 +13245,10 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12868
13245
|
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
12869
13246
|
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
12870
13247
|
}
|
|
12871
|
-
const
|
|
12872
|
-
const previous = state.links.find((link2) => link2.id ===
|
|
13248
|
+
const id7 = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
13249
|
+
const previous = state.links.find((link2) => link2.id === id7);
|
|
12873
13250
|
const link = {
|
|
12874
|
-
id:
|
|
13251
|
+
id: id7,
|
|
12875
13252
|
personId: person.id,
|
|
12876
13253
|
chapterId: input.chapterId,
|
|
12877
13254
|
chapterPersonId: input.chapterPersonId,
|
|
@@ -12882,11 +13259,11 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12882
13259
|
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
12883
13260
|
await db.transact([
|
|
12884
13261
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
12885
|
-
{ t: "update", ns: "networkChapterPersonLinks", id:
|
|
13262
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { ...link } },
|
|
12886
13263
|
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
12887
13264
|
], { mutationId, guards: [
|
|
12888
13265
|
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
12889
|
-
{ ns: "networkChapterPersonLinks", id:
|
|
13266
|
+
{ ns: "networkChapterPersonLinks", id: id7, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
12890
13267
|
] });
|
|
12891
13268
|
return { person: nextPerson, link };
|
|
12892
13269
|
}
|
|
@@ -12923,9 +13300,9 @@ var defaultPolicy = (now, consent) => ({
|
|
|
12923
13300
|
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
12924
13301
|
async function linkOps(personId, links, now) {
|
|
12925
13302
|
return Promise.all(links.map(async (link) => {
|
|
12926
|
-
const
|
|
13303
|
+
const id7 = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
12927
13304
|
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
12928
|
-
return { t: "update", ns: "networkChapterPersonLinks", id:
|
|
13305
|
+
return { t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { id: id7, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
12929
13306
|
}));
|
|
12930
13307
|
}
|
|
12931
13308
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -13144,10 +13521,10 @@ async function networkPersonBulkBatchDigest(rows5) {
|
|
|
13144
13521
|
}
|
|
13145
13522
|
async function networkPersonRegistryStateDigest(state) {
|
|
13146
13523
|
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:
|
|
13524
|
+
persons: state.persons.map(({ id: id7, revision, state: personState, mergedIntoId }) => ({ id: id7, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13525
|
+
claims: state.claims.map(({ id: id7, personId }) => ({ id: id7, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13526
|
+
aliases: state.aliases.map(({ id: id7, revision, status: status2, personId }) => ({ id: id7, revision, status: status2, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13527
|
+
links: state.links.map(({ id: id7, personId, state: linkState }) => ({ id: id7, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
13151
13528
|
};
|
|
13152
13529
|
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
13153
13530
|
}
|
|
@@ -13372,7 +13749,7 @@ var safeNetworkPersonAlias = async (alias) => ({
|
|
|
13372
13749
|
recordedAt: alias.consent.recordedAt,
|
|
13373
13750
|
hasBasis: Boolean(alias.consent.basis)
|
|
13374
13751
|
},
|
|
13375
|
-
identityClaims: alias.identityClaims.map(({ id:
|
|
13752
|
+
identityClaims: alias.identityClaims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13376
13753
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
13377
13754
|
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
13378
13755
|
chapterId: link.chapterId,
|
|
@@ -13401,7 +13778,7 @@ async function projectNetworkPersonRegistryState(state) {
|
|
|
13401
13778
|
return {
|
|
13402
13779
|
...safeNetworkPerson(person, claims),
|
|
13403
13780
|
canonicalPersonId: canonicalId,
|
|
13404
|
-
identityClaims: claims.map(({ id:
|
|
13781
|
+
identityClaims: claims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13405
13782
|
chapterLinks: safeLinks,
|
|
13406
13783
|
chapterProjections: projectNetworkContactPolicy(canonical3.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
13407
13784
|
};
|
|
@@ -13656,7 +14033,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
|
|
|
13656
14033
|
};
|
|
13657
14034
|
|
|
13658
14035
|
// src/worker-routes-network-commercial.ts
|
|
13659
|
-
var
|
|
14036
|
+
var import_stripe13 = require("@odla-ai/stripe");
|
|
13660
14037
|
|
|
13661
14038
|
// src/worker-routes-network-commercial-parity.ts
|
|
13662
14039
|
var import_db13 = require("@odla-ai/db");
|
|
@@ -14032,7 +14409,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14032
14409
|
return json({ error: "commercial webhook is not configured" }, 503);
|
|
14033
14410
|
}
|
|
14034
14411
|
try {
|
|
14035
|
-
const event2 = await (0,
|
|
14412
|
+
const event2 = await (0, import_stripe13.constructStripeEvent)(body, signature, secret);
|
|
14036
14413
|
const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
|
|
14037
14414
|
if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
|
|
14038
14415
|
const result = await ingestNetworkCommercialFact(
|
|
@@ -14053,7 +14430,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
|
|
|
14053
14430
|
...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
|
|
14054
14431
|
});
|
|
14055
14432
|
} catch (error) {
|
|
14056
|
-
if (error instanceof
|
|
14433
|
+
if (error instanceof import_stripe13.StripeError) return json({ error: error.message, code: error.code }, 400);
|
|
14057
14434
|
throw error;
|
|
14058
14435
|
}
|
|
14059
14436
|
};
|
|
@@ -14116,7 +14493,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
14116
14493
|
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
14117
14494
|
if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
|
|
14118
14495
|
try {
|
|
14119
|
-
const stripe = (0,
|
|
14496
|
+
const stripe = (0, import_stripe13.initStripe)({ secretKey });
|
|
14120
14497
|
const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
|
|
14121
14498
|
listStripeObjects(stripe, "/v1/customers"),
|
|
14122
14499
|
listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
|
|
@@ -14130,7 +14507,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
|
|
|
14130
14507
|
...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
|
|
14131
14508
|
});
|
|
14132
14509
|
} catch (error) {
|
|
14133
|
-
if (error instanceof
|
|
14510
|
+
if (error instanceof import_stripe13.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
|
|
14134
14511
|
return json({ error: "Stripe reconciliation failed" }, 502);
|
|
14135
14512
|
}
|
|
14136
14513
|
}
|
|
@@ -14194,14 +14571,14 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14194
14571
|
}
|
|
14195
14572
|
}
|
|
14196
14573
|
const db = ctx.makeDb(env);
|
|
14197
|
-
const
|
|
14574
|
+
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
14198
14575
|
try {
|
|
14199
14576
|
const created = await (0, import_crm25.createRecord)(
|
|
14200
14577
|
{
|
|
14201
14578
|
crm: ctx.chapter.crm,
|
|
14202
14579
|
db,
|
|
14203
14580
|
now: () => Date.now(),
|
|
14204
|
-
newId: () =>
|
|
14581
|
+
newId: () => id7
|
|
14205
14582
|
},
|
|
14206
14583
|
{
|
|
14207
14584
|
type: formation.type,
|
|
@@ -14210,7 +14587,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14210
14587
|
}
|
|
14211
14588
|
);
|
|
14212
14589
|
return json({
|
|
14213
|
-
id:
|
|
14590
|
+
id: id7,
|
|
14214
14591
|
duplicate: created.duplicate,
|
|
14215
14592
|
status: created.record.stage ?? null
|
|
14216
14593
|
}, created.duplicate ? 200 : 201);
|
|
@@ -14287,8 +14664,8 @@ function resourceValue(value, path, errors) {
|
|
|
14287
14664
|
return null;
|
|
14288
14665
|
}
|
|
14289
14666
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
14290
|
-
const
|
|
14291
|
-
if (
|
|
14667
|
+
const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
14668
|
+
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
14292
14669
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
14293
14670
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
14294
14671
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14345,7 +14722,7 @@ function resourceValue(value, path, errors) {
|
|
|
14345
14722
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
14346
14723
|
...promotedFrom ? { promotedFrom } : {}
|
|
14347
14724
|
};
|
|
14348
|
-
return { id:
|
|
14725
|
+
return { id: id7, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
|
|
14349
14726
|
}
|
|
14350
14727
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
14351
14728
|
const errors = [];
|
|
@@ -14376,7 +14753,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
14376
14753
|
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
14377
14754
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
14378
14755
|
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
14379
|
-
const ids2 = resources.map(({ id:
|
|
14756
|
+
const ids2 = resources.map(({ id: id7 }) => id7);
|
|
14380
14757
|
if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
|
|
14381
14758
|
for (const resource of resources) {
|
|
14382
14759
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -14465,18 +14842,18 @@ async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environme
|
|
|
14465
14842
|
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
14466
14843
|
}
|
|
14467
14844
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
14468
|
-
const
|
|
14469
|
-
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id:
|
|
14845
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14846
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
|
|
14470
14847
|
return local3(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
14471
14848
|
}
|
|
14472
14849
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
14473
14850
|
const { document, digest } = envelope;
|
|
14474
14851
|
const target = document.target;
|
|
14475
|
-
const
|
|
14852
|
+
const id7 = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
14476
14853
|
const ops = [];
|
|
14477
14854
|
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:
|
|
14855
|
+
ops.push({ t: "update", ns: "builderLibraryControls", id: id7, attrs: {
|
|
14856
|
+
id: id7,
|
|
14480
14857
|
sourceId: document.source.appId,
|
|
14481
14858
|
targetId: target.appId,
|
|
14482
14859
|
environment: target.environment,
|
|
@@ -14489,13 +14866,13 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
14489
14866
|
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest}` });
|
|
14490
14867
|
}
|
|
14491
14868
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
14492
|
-
const
|
|
14869
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14493
14870
|
const current = await loadLocalBuilderLibrary(db, document.source.appId, environment);
|
|
14494
14871
|
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:
|
|
14872
|
+
const row2 = { id: id7, appId: document.source.appId, environment, revision: document.revision, document, updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
14873
|
+
await db.transact([{ t: "update", ns: "builderLibraryLocal", id: id7, attrs: row2 }], {
|
|
14497
14874
|
mutationId: `builder-library-local:${environment}:${document.revision}`,
|
|
14498
|
-
guards: current ? [{ ns: "builderLibraryLocal", id:
|
|
14875
|
+
guards: current ? [{ ns: "builderLibraryLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "builderLibraryLocal", id: id7, exists: false }]
|
|
14499
14876
|
});
|
|
14500
14877
|
return row2;
|
|
14501
14878
|
}
|
|
@@ -14710,30 +15087,30 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14710
15087
|
};
|
|
14711
15088
|
|
|
14712
15089
|
// src/membership-cancel-authority.ts
|
|
14713
|
-
var
|
|
15090
|
+
var import_stripe14 = require("@odla-ai/stripe");
|
|
14714
15091
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14715
15092
|
membershipClock(now);
|
|
14716
|
-
const
|
|
15093
|
+
const id7 = await networkMembershipId(target);
|
|
14717
15094
|
const state = await loadMembershipAuthorityState(db, target);
|
|
14718
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15095
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14719
15096
|
requireMembershipScope(member, target);
|
|
14720
15097
|
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
14721
15098
|
membershipFail("membership_cancellation_unavailable");
|
|
14722
15099
|
}
|
|
14723
15100
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14724
|
-
const read2 = await (0,
|
|
15101
|
+
const read2 = await (0, import_stripe14.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
14725
15102
|
const sub = read2.body;
|
|
14726
15103
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14727
15104
|
...stripePartitionMetadata(target),
|
|
14728
15105
|
applicationId: target.applicationId
|
|
14729
15106
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14730
15107
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14731
|
-
const write = await (0,
|
|
15108
|
+
const write = await (0, import_stripe14.stripeRequest)(
|
|
14732
15109
|
{ secretKey: stripeSecret },
|
|
14733
15110
|
"POST",
|
|
14734
15111
|
path,
|
|
14735
15112
|
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
14736
|
-
{ idempotencyKey: `membership-cancel-at-term:${
|
|
15113
|
+
{ idempotencyKey: `membership-cancel-at-term:${id7}:${member.providerSubscriptionId}` }
|
|
14737
15114
|
);
|
|
14738
15115
|
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
14739
15116
|
membershipFail("membership_cancellation_pending");
|
|
@@ -14741,7 +15118,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14741
15118
|
}
|
|
14742
15119
|
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
14743
15120
|
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
14744
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
15121
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
14745
15122
|
status: status2,
|
|
14746
15123
|
autoRenew: false,
|
|
14747
15124
|
graceEndsAt: 0,
|
|
@@ -14750,12 +15127,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14750
15127
|
updatedAt: now
|
|
14751
15128
|
})], {
|
|
14752
15129
|
guards: [
|
|
14753
|
-
{ ns: "networkMembership", id:
|
|
15130
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14754
15131
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14755
15132
|
]
|
|
14756
15133
|
});
|
|
14757
15134
|
}
|
|
14758
|
-
return { membershipId:
|
|
15135
|
+
return { membershipId: id7, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
14759
15136
|
}
|
|
14760
15137
|
|
|
14761
15138
|
// src/membership-authority-handler.ts
|
|
@@ -14794,17 +15171,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
|
|
|
14794
15171
|
// src/membership-approval-authority.ts
|
|
14795
15172
|
async function approveNetworkMembership(db, input, now) {
|
|
14796
15173
|
membershipClock(now);
|
|
14797
|
-
const
|
|
15174
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
14798
15175
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14799
15176
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14800
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15177
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14801
15178
|
requireMembershipScope(member, input);
|
|
14802
15179
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14803
15180
|
requireMembershipScope(entitlement, input);
|
|
14804
|
-
if (entitlement.membershipId !==
|
|
15181
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
|
|
14805
15182
|
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
15183
|
return {
|
|
14807
|
-
membershipId:
|
|
15184
|
+
membershipId: id7,
|
|
14808
15185
|
status: String(member.status),
|
|
14809
15186
|
cohort: String(member.cohort),
|
|
14810
15187
|
memberNumber: Number(member.memberNumber),
|
|
@@ -14824,7 +15201,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14824
15201
|
const seatActivation = await namedSeatPrimaryActivation(db, member, now);
|
|
14825
15202
|
try {
|
|
14826
15203
|
await db.transact([
|
|
14827
|
-
membershipUpdate("networkMembership",
|
|
15204
|
+
membershipUpdate("networkMembership", id7, {
|
|
14828
15205
|
status: member.autoRenew ? "active" : "cancel_at_term_end",
|
|
14829
15206
|
memberNumber,
|
|
14830
15207
|
journeyApprovedAt: now,
|
|
@@ -14839,8 +15216,8 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14839
15216
|
updatedAt: now
|
|
14840
15217
|
}),
|
|
14841
15218
|
...seatActivation.ops
|
|
14842
|
-
], { mutationId: `membership-approve:${
|
|
14843
|
-
{ ns: "networkMembership", id:
|
|
15219
|
+
], { mutationId: `membership-approve:${id7}`, guards: [
|
|
15220
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
|
|
14844
15221
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
|
|
14845
15222
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
14846
15223
|
...seatActivation.guards
|
|
@@ -14855,41 +15232,41 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14855
15232
|
// src/membership-refund-authority.ts
|
|
14856
15233
|
async function refundNetworkMembership(db, input, secretKey, now) {
|
|
14857
15234
|
membershipClock(now);
|
|
14858
|
-
const
|
|
14859
|
-
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15235
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
15236
|
+
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14860
15237
|
requireMembershipScope(member, input);
|
|
14861
|
-
if (member.status === "refunded") return { membershipId:
|
|
15238
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14862
15239
|
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
|
|
14863
15240
|
if (member.status !== "refund_pending") {
|
|
14864
15241
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14865
15242
|
requireMembershipScope(entitlement, input);
|
|
14866
15243
|
await db.transact([
|
|
14867
|
-
membershipUpdate("networkMembership",
|
|
15244
|
+
membershipUpdate("networkMembership", id7, {
|
|
14868
15245
|
status: "refund_pending",
|
|
14869
15246
|
refundRequestedAt: now,
|
|
14870
15247
|
revision: Number(member.revision) + 1,
|
|
14871
15248
|
updatedAt: now
|
|
14872
15249
|
}),
|
|
14873
15250
|
membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
14874
|
-
membershipUpdate("membershipEffect", `${
|
|
14875
|
-
id: `${
|
|
15251
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, {
|
|
15252
|
+
id: `${id7}:refund`,
|
|
14876
15253
|
chapterId: input.chapterId,
|
|
14877
15254
|
environment: input.environment,
|
|
14878
15255
|
runtime: input.runtime,
|
|
14879
|
-
membershipId:
|
|
15256
|
+
membershipId: id7,
|
|
14880
15257
|
kind: "refund_membership",
|
|
14881
15258
|
status: "pending",
|
|
14882
15259
|
attempts: 0,
|
|
14883
15260
|
createdAt: now,
|
|
14884
15261
|
updatedAt: now
|
|
14885
15262
|
})
|
|
14886
|
-
], { mutationId: `membership-refund-request:${
|
|
14887
|
-
{ ns: "networkMembership", id:
|
|
15263
|
+
], { mutationId: `membership-refund-request:${id7}`, guards: [
|
|
15264
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14888
15265
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
|
|
14889
15266
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14890
15267
|
] });
|
|
14891
15268
|
}
|
|
14892
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15269
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14893
15270
|
requireMembershipScope(member, input);
|
|
14894
15271
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14895
15272
|
const result = await refundSubscriptionPayment(secretKey, {
|
|
@@ -14903,23 +15280,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14903
15280
|
if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
|
|
14904
15281
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14905
15282
|
state = await loadMembershipAuthorityState(db, input);
|
|
14906
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15283
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14907
15284
|
requireMembershipScope(member, input);
|
|
14908
|
-
if (member.status === "refunded") return { membershipId:
|
|
15285
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14909
15286
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14910
15287
|
const counted = member.founding === true && member.memberNumber !== void 0;
|
|
14911
15288
|
const counts = state.cohortCounts;
|
|
14912
15289
|
if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
|
|
14913
15290
|
try {
|
|
14914
15291
|
await db.transact([
|
|
14915
|
-
membershipUpdate("networkMembership",
|
|
15292
|
+
membershipUpdate("networkMembership", id7, {
|
|
14916
15293
|
status: "refunded",
|
|
14917
15294
|
refundedCents: result.refundedCents,
|
|
14918
15295
|
autoRenew: false,
|
|
14919
15296
|
revision: Number(member.revision) + 1,
|
|
14920
15297
|
updatedAt: now
|
|
14921
15298
|
}),
|
|
14922
|
-
membershipUpdate("membershipEffect", `${
|
|
15299
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, { status: "complete", updatedAt: now }),
|
|
14923
15300
|
...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
|
|
14924
15301
|
cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
|
|
14925
15302
|
revision: Number(state.revision) + 1,
|
|
@@ -14930,7 +15307,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14930
15307
|
chapterId: input.chapterId,
|
|
14931
15308
|
environment: input.environment,
|
|
14932
15309
|
runtime: input.runtime,
|
|
14933
|
-
membershipId:
|
|
15310
|
+
membershipId: id7,
|
|
14934
15311
|
seatId: member.namedSeatId,
|
|
14935
15312
|
kind: "refund_seat_with_primary",
|
|
14936
15313
|
status: "pending",
|
|
@@ -14938,8 +15315,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14938
15315
|
createdAt: now,
|
|
14939
15316
|
updatedAt: now
|
|
14940
15317
|
})] : []
|
|
14941
|
-
], { mutationId: `membership-refunded:${
|
|
14942
|
-
{ ns: "networkMembership", id:
|
|
15318
|
+
], { mutationId: `membership-refunded:${id7}`, guards: [
|
|
15319
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "refund_pending", revision: member.revision } },
|
|
14943
15320
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14944
15321
|
] });
|
|
14945
15322
|
} catch (error) {
|
|
@@ -14950,12 +15327,12 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14950
15327
|
}
|
|
14951
15328
|
|
|
14952
15329
|
// src/membership-provider-lifecycle.ts
|
|
14953
|
-
var
|
|
15330
|
+
var import_stripe15 = require("@odla-ai/stripe");
|
|
14954
15331
|
|
|
14955
15332
|
// src/membership-renewal-authority.ts
|
|
14956
15333
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
14957
15334
|
membershipClock(now);
|
|
14958
|
-
const
|
|
15335
|
+
const id7 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
14959
15336
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
14960
15337
|
const fact = eventRow?.fact;
|
|
14961
15338
|
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 +15345,10 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14968
15345
|
return membershipFail("membership_renewal_evidence_pending");
|
|
14969
15346
|
}
|
|
14970
15347
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14971
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15348
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14972
15349
|
requireMembershipScope(member, input);
|
|
14973
15350
|
const receipt = () => ({
|
|
14974
|
-
membershipId:
|
|
15351
|
+
membershipId: id7,
|
|
14975
15352
|
status: String(member.status),
|
|
14976
15353
|
termEndsAt: Number(member.termEndsAt),
|
|
14977
15354
|
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
@@ -14996,13 +15373,13 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14996
15373
|
const paid = fact.eventType === "invoice.payment_succeeded";
|
|
14997
15374
|
if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
|
|
14998
15375
|
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", `${
|
|
15376
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
15000
15377
|
requireMembershipScope(entitlement, input);
|
|
15001
15378
|
const graceEndsAt = paid ? 0 : start + 14 * 864e5;
|
|
15002
15379
|
const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
|
|
15003
15380
|
const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
|
|
15004
15381
|
const ops = [
|
|
15005
|
-
membershipUpdate("networkMembership",
|
|
15382
|
+
membershipUpdate("networkMembership", id7, {
|
|
15006
15383
|
status: status2,
|
|
15007
15384
|
graceEndsAt,
|
|
15008
15385
|
...paid ? {
|
|
@@ -15013,7 +15390,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15013
15390
|
revision: Number(member.revision) + 1,
|
|
15014
15391
|
updatedAt: now
|
|
15015
15392
|
}),
|
|
15016
|
-
membershipUpdate("networkEntitlement", `${
|
|
15393
|
+
membershipUpdate("networkEntitlement", `${id7}:primary`, {
|
|
15017
15394
|
status: entitlementStatus,
|
|
15018
15395
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
15019
15396
|
sourceEventId: input.eventId,
|
|
@@ -15022,8 +15399,8 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15022
15399
|
})
|
|
15023
15400
|
];
|
|
15024
15401
|
const guards = [
|
|
15025
|
-
{ ns: "networkMembership", id:
|
|
15026
|
-
{ ns: "networkEntitlement", id: `${
|
|
15402
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15403
|
+
{ ns: "networkEntitlement", id: `${id7}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
15027
15404
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
15028
15405
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
15029
15406
|
evidenceGuard
|
|
@@ -15048,15 +15425,15 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15048
15425
|
);
|
|
15049
15426
|
}
|
|
15050
15427
|
try {
|
|
15051
|
-
await db.transact(ops, { mutationId: `membership-renewal:${
|
|
15428
|
+
await db.transact(ops, { mutationId: `membership-renewal:${id7}:${input.eventId}`, guards });
|
|
15052
15429
|
} catch (error) {
|
|
15053
15430
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15054
15431
|
continue;
|
|
15055
15432
|
}
|
|
15056
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15433
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15057
15434
|
requireMembershipScope(saved, input);
|
|
15058
15435
|
return {
|
|
15059
|
-
membershipId:
|
|
15436
|
+
membershipId: id7,
|
|
15060
15437
|
status: String(saved.status),
|
|
15061
15438
|
termEndsAt: Number(saved.termEndsAt),
|
|
15062
15439
|
graceEndsAt: Number(saved.graceEndsAt ?? 0),
|
|
@@ -15082,12 +15459,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15082
15459
|
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
15083
15460
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
15084
15461
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
15085
|
-
const
|
|
15462
|
+
const id7 = await networkMembershipId(input);
|
|
15086
15463
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
15087
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15464
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
15088
15465
|
requireMembershipScope(member, input);
|
|
15089
15466
|
const receipt = (value = member) => ({
|
|
15090
|
-
membershipId:
|
|
15467
|
+
membershipId: id7,
|
|
15091
15468
|
status: String(value.status),
|
|
15092
15469
|
termEndsAt: Number(value.termEndsAt),
|
|
15093
15470
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
@@ -15108,13 +15485,13 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15108
15485
|
});
|
|
15109
15486
|
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15110
15487
|
await refundNetworkMembership(db, input, sk, now);
|
|
15111
|
-
const saved2 = await membershipLoad(db, "networkMembership",
|
|
15488
|
+
const saved2 = await membershipLoad(db, "networkMembership", id7);
|
|
15112
15489
|
requireMembershipScope(saved2, input);
|
|
15113
15490
|
return receipt(saved2);
|
|
15114
15491
|
}
|
|
15115
15492
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
15116
15493
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
15117
|
-
const result = await (0,
|
|
15494
|
+
const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
15118
15495
|
const sub = result.body;
|
|
15119
15496
|
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
15497
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -15123,18 +15500,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15123
15500
|
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
15501
|
const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
|
|
15125
15502
|
if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
|
|
15126
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
15503
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
15127
15504
|
requireMembershipScope(entitlement, input);
|
|
15128
15505
|
try {
|
|
15129
15506
|
await db.transact([
|
|
15130
|
-
membershipUpdate("networkMembership",
|
|
15507
|
+
membershipUpdate("networkMembership", id7, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
|
|
15131
15508
|
membershipUpdate("networkEntitlement", String(entitlement.id), {
|
|
15132
15509
|
status: status2 === "lapsed" ? "closed" : entitlement.status,
|
|
15133
15510
|
revision: Number(entitlement.revision) + 1,
|
|
15134
15511
|
updatedAt: now
|
|
15135
15512
|
})
|
|
15136
15513
|
], { guards: [
|
|
15137
|
-
{ ns: "networkMembership", id:
|
|
15514
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15138
15515
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
15139
15516
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
15140
15517
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
@@ -15144,7 +15521,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15144
15521
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15145
15522
|
continue;
|
|
15146
15523
|
}
|
|
15147
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15524
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15148
15525
|
requireMembershipScope(saved, input);
|
|
15149
15526
|
return receipt(saved);
|
|
15150
15527
|
}
|
|
@@ -15152,7 +15529,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15152
15529
|
}
|
|
15153
15530
|
|
|
15154
15531
|
// src/membership-seat-stripe.ts
|
|
15155
|
-
var
|
|
15532
|
+
var import_stripe16 = require("@odla-ai/stripe");
|
|
15156
15533
|
var reject7 = () => {
|
|
15157
15534
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
15158
15535
|
};
|
|
@@ -15164,7 +15541,7 @@ var metadataFor = (seat) => ({
|
|
|
15164
15541
|
});
|
|
15165
15542
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
15166
15543
|
async function read(sk, path, params) {
|
|
15167
|
-
const response = await (0,
|
|
15544
|
+
const response = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
15168
15545
|
if (!response.ok) return reject7();
|
|
15169
15546
|
return response.body;
|
|
15170
15547
|
}
|
|
@@ -15175,7 +15552,7 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
15175
15552
|
currency: seat.currency,
|
|
15176
15553
|
interval: seat.interval,
|
|
15177
15554
|
environment: seat.environment
|
|
15178
|
-
}, Boolean(seat.providerPaymentIntentId));
|
|
15555
|
+
}, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
|
|
15179
15556
|
if (!productId) reject7();
|
|
15180
15557
|
const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
|
|
15181
15558
|
const items = sub.items?.data;
|
|
@@ -15201,11 +15578,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15201
15578
|
}
|
|
15202
15579
|
await subscriptionForSeat(sk, seat);
|
|
15203
15580
|
const metadata = metadataFor(seat);
|
|
15204
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
15581
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe16.stripeRequest)(
|
|
15205
15582
|
{ secretKey: sk },
|
|
15206
15583
|
"GET",
|
|
15207
15584
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
15208
|
-
) : await (0,
|
|
15585
|
+
) : await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
15209
15586
|
amount: seat.amountCents,
|
|
15210
15587
|
currency: seat.currency,
|
|
15211
15588
|
customer: seat.providerCustomerId,
|
|
@@ -15219,11 +15596,15 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15219
15596
|
return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
|
|
15220
15597
|
}
|
|
15221
15598
|
async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
15222
|
-
if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15599
|
+
if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15223
15600
|
const sub = await subscriptionForSeat(sk, seat);
|
|
15224
15601
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
15225
|
-
if (existing)
|
|
15226
|
-
|
|
15602
|
+
if (existing) {
|
|
15603
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject7();
|
|
15604
|
+
return String(existing.id);
|
|
15605
|
+
}
|
|
15606
|
+
if (seat.combinedInvoice) reject7();
|
|
15607
|
+
const result = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
15227
15608
|
subscription: seat.providerSubscriptionId,
|
|
15228
15609
|
price: seat.priceId,
|
|
15229
15610
|
quantity: 1,
|
|
@@ -15440,7 +15821,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
15440
15821
|
const state = await loadMembershipAuthorityState(db, input);
|
|
15441
15822
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
15442
15823
|
requireMembershipScope(seat, input);
|
|
15443
|
-
if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15824
|
+
if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15444
15825
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
15445
15826
|
requireMembershipScope(member, input);
|
|
15446
15827
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
@@ -15760,9 +16141,14 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15760
16141
|
try {
|
|
15761
16142
|
input = JSON.parse(verified.body);
|
|
15762
16143
|
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])))) {
|
|
16144
|
+
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
16145
|
throw new TypeError("invalid request");
|
|
15765
16146
|
}
|
|
16147
|
+
if (action === "checkouts" && input.namedSeat !== void 0) {
|
|
16148
|
+
const seat = input.namedSeat;
|
|
16149
|
+
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");
|
|
16150
|
+
checkoutNamedSeatSelection(seat, seat.purchaserEmail);
|
|
16151
|
+
}
|
|
15766
16152
|
} catch {
|
|
15767
16153
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
15768
16154
|
}
|
|
@@ -15835,8 +16221,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15835
16221
|
offerId: String(input.offerId),
|
|
15836
16222
|
offerDigest: String(input.offerDigest),
|
|
15837
16223
|
quoteDigest: String(input.quoteDigest),
|
|
15838
|
-
priceId: String(input.priceId)
|
|
15839
|
-
|
|
16224
|
+
priceId: String(input.priceId),
|
|
16225
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {}
|
|
16226
|
+
}, options.now?.() ?? Date.now(), peer.seatPriceId);
|
|
15840
16227
|
return respond({ ok: true, binding });
|
|
15841
16228
|
}
|
|
15842
16229
|
const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
|
|
@@ -15864,13 +16251,13 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15864
16251
|
|
|
15865
16252
|
// src/schema-membership-authority.ts
|
|
15866
16253
|
var attr = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
15867
|
-
var
|
|
16254
|
+
var id5 = () => attr("string", false, true, true);
|
|
15868
16255
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
15869
16256
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
15870
16257
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
15871
16258
|
var membershipAuthorityEntities = {
|
|
15872
16259
|
networkMembership: { attrs: {
|
|
15873
|
-
id:
|
|
16260
|
+
id: id5(),
|
|
15874
16261
|
membershipId: key(),
|
|
15875
16262
|
...scope(),
|
|
15876
16263
|
personId: key(true),
|
|
@@ -15910,7 +16297,7 @@ var membershipAuthorityEntities = {
|
|
|
15910
16297
|
restartId: key(true)
|
|
15911
16298
|
} },
|
|
15912
16299
|
membershipRestart: { attrs: {
|
|
15913
|
-
id:
|
|
16300
|
+
id: id5(),
|
|
15914
16301
|
...scope(),
|
|
15915
16302
|
membershipId: key(),
|
|
15916
16303
|
restartId: key(),
|
|
@@ -15920,7 +16307,7 @@ var membershipAuthorityEntities = {
|
|
|
15920
16307
|
createdAt: attr("number")
|
|
15921
16308
|
} },
|
|
15922
16309
|
membershipPurchaseHistory: { attrs: {
|
|
15923
|
-
id:
|
|
16310
|
+
id: id5(),
|
|
15924
16311
|
...scope(),
|
|
15925
16312
|
membershipId: key(),
|
|
15926
16313
|
providerSubscriptionId: key(),
|
|
@@ -15929,7 +16316,7 @@ var membershipAuthorityEntities = {
|
|
|
15929
16316
|
archivedAt: attr("number")
|
|
15930
16317
|
} },
|
|
15931
16318
|
networkEntitlement: { attrs: {
|
|
15932
|
-
id:
|
|
16319
|
+
id: id5(),
|
|
15933
16320
|
...scope(),
|
|
15934
16321
|
membershipId: key(),
|
|
15935
16322
|
seatId: key(true),
|
|
@@ -15946,10 +16333,12 @@ var membershipAuthorityEntities = {
|
|
|
15946
16333
|
journeyApprovedAt: attr("number", true)
|
|
15947
16334
|
} },
|
|
15948
16335
|
networkSeat: { attrs: {
|
|
15949
|
-
id:
|
|
16336
|
+
id: id5(),
|
|
15950
16337
|
...scope(),
|
|
15951
16338
|
primaryMembershipId: key(),
|
|
15952
|
-
purchaserUserId: key(),
|
|
16339
|
+
purchaserUserId: key(true),
|
|
16340
|
+
purchaserEmailNormalized: key(true),
|
|
16341
|
+
combinedInvoice: attr("json", true),
|
|
15953
16342
|
recipientName: attr("string"),
|
|
15954
16343
|
recipientEmailNormalized: key(),
|
|
15955
16344
|
recipientClaimId: key(),
|
|
@@ -15981,9 +16370,9 @@ var membershipAuthorityEntities = {
|
|
|
15981
16370
|
refundedCents: attr("number", true),
|
|
15982
16371
|
providerRefundIds: attr("json", true)
|
|
15983
16372
|
} },
|
|
15984
|
-
networkSeatRecipient: { attrs: { id:
|
|
16373
|
+
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
15985
16374
|
membershipEffect: { attrs: {
|
|
15986
|
-
id:
|
|
16375
|
+
id: id5(),
|
|
15987
16376
|
chapterId: key(),
|
|
15988
16377
|
environment: key(),
|
|
15989
16378
|
runtime: key(),
|
|
@@ -16002,11 +16391,11 @@ var membershipAuthorityRules = Object.fromEntries(Object.keys(membershipAuthorit
|
|
|
16002
16391
|
|
|
16003
16392
|
// src/schema-membership-offers.ts
|
|
16004
16393
|
var attr2 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
16005
|
-
var
|
|
16394
|
+
var id6 = () => attr2("string", false, true, true);
|
|
16006
16395
|
var key2 = () => attr2("string", false, false, true);
|
|
16007
16396
|
var membershipOfferEntities = {
|
|
16008
16397
|
membershipAuthorityState: { attrs: {
|
|
16009
|
-
id:
|
|
16398
|
+
id: id6(),
|
|
16010
16399
|
chapterId: key2(),
|
|
16011
16400
|
environment: key2(),
|
|
16012
16401
|
mode: key2(),
|
|
@@ -16016,7 +16405,7 @@ var membershipOfferEntities = {
|
|
|
16016
16405
|
updatedAt: attr2("number")
|
|
16017
16406
|
} },
|
|
16018
16407
|
membershipCheckoutOffer: { attrs: {
|
|
16019
|
-
id:
|
|
16408
|
+
id: id6(),
|
|
16020
16409
|
applicationId: key2(),
|
|
16021
16410
|
appId: key2(),
|
|
16022
16411
|
chapterId: key2(),
|
|
@@ -16031,7 +16420,7 @@ var membershipOfferEntities = {
|
|
|
16031
16420
|
authorityRevision: attr2("number")
|
|
16032
16421
|
} },
|
|
16033
16422
|
membershipOfferCheckout: { attrs: {
|
|
16034
|
-
id:
|
|
16423
|
+
id: id6(),
|
|
16035
16424
|
appId: key2(),
|
|
16036
16425
|
chapterId: key2(),
|
|
16037
16426
|
environment: key2(),
|
|
@@ -16042,7 +16431,8 @@ var membershipOfferEntities = {
|
|
|
16042
16431
|
offerDigest: attr2("string"),
|
|
16043
16432
|
quoteDigest: attr2("string"),
|
|
16044
16433
|
priceId: attr2("string"),
|
|
16045
|
-
boundAt: attr2("number")
|
|
16434
|
+
boundAt: attr2("number"),
|
|
16435
|
+
namedSeat: attr2("json", true)
|
|
16046
16436
|
} }
|
|
16047
16437
|
};
|
|
16048
16438
|
var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
|
|
@@ -16097,7 +16487,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
16097
16487
|
}
|
|
16098
16488
|
|
|
16099
16489
|
// src/membership-seat-cancel.ts
|
|
16100
|
-
var
|
|
16490
|
+
var import_stripe17 = require("@odla-ai/stripe");
|
|
16101
16491
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
16102
16492
|
membershipClock(now);
|
|
16103
16493
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -16125,7 +16515,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16125
16515
|
}
|
|
16126
16516
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
16127
16517
|
const request = async (method, path, params) => {
|
|
16128
|
-
const result = await (0,
|
|
16518
|
+
const result = await (0, import_stripe17.stripeRequest)(
|
|
16129
16519
|
{ secretKey },
|
|
16130
16520
|
method,
|
|
16131
16521
|
path,
|
|
@@ -16138,7 +16528,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16138
16528
|
let payment;
|
|
16139
16529
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
16140
16530
|
else {
|
|
16141
|
-
const snapshot = await listStripeObjects((0,
|
|
16531
|
+
const snapshot = await listStripeObjects((0, import_stripe17.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
16142
16532
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
16143
16533
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
16144
16534
|
payment = matches2[0];
|
|
@@ -16151,9 +16541,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16151
16541
|
if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
|
|
16152
16542
|
if (payment.status !== "canceled") {
|
|
16153
16543
|
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 !==
|
|
16544
|
+
const id7 = payment.id;
|
|
16545
|
+
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id7)}/cancel`, { cancellation_reason: "abandoned" });
|
|
16546
|
+
if (payment.id !== id7) throw new Error("seat cancellation receipt mismatch");
|
|
16157
16547
|
}
|
|
16158
16548
|
if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
|
|
16159
16549
|
await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
|
|
@@ -16485,7 +16875,8 @@ async function requestMembershipCheckoutBinding(options) {
|
|
|
16485
16875
|
offerId: options.offerId,
|
|
16486
16876
|
offerDigest: options.offerDigest,
|
|
16487
16877
|
quoteDigest: options.quoteDigest,
|
|
16488
|
-
priceId: options.priceId
|
|
16878
|
+
priceId: options.priceId,
|
|
16879
|
+
...options.namedSeat ? { namedSeat: options.namedSeat } : {}
|
|
16489
16880
|
});
|
|
16490
16881
|
return verifyMembershipCheckoutBinding(result.binding, options);
|
|
16491
16882
|
}
|