@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.js
CHANGED
|
@@ -79,10 +79,10 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch, source =
|
|
|
79
79
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
80
80
|
return user ? toRecord(user, source) : null;
|
|
81
81
|
}
|
|
82
|
-
async function clerkGetUser(secretKey,
|
|
83
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
82
|
+
async function clerkGetUser(secretKey, id7, fetchImpl = fetch, source = "public") {
|
|
83
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id7)}`, secretKey, fetchImpl).catch(() => null);
|
|
84
84
|
const record4 = data ? toRecord(data, source) : null;
|
|
85
|
-
return record4?.id ===
|
|
85
|
+
return record4?.id === id7 ? record4 : null;
|
|
86
86
|
}
|
|
87
87
|
async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
88
88
|
const out = [];
|
|
@@ -98,8 +98,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
|
98
98
|
}
|
|
99
99
|
return out;
|
|
100
100
|
}
|
|
101
|
-
async function clerkSetRole(secretKey,
|
|
102
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
101
|
+
async function clerkSetRole(secretKey, id7, role, fetchImpl = fetch, source = "public") {
|
|
102
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id7)}/metadata`, {
|
|
103
103
|
method: "PATCH",
|
|
104
104
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
105
105
|
redirect: "manual",
|
|
@@ -158,10 +158,10 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
158
158
|
const existing = await find();
|
|
159
159
|
if (existing) return { record: existing, created: false };
|
|
160
160
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
161
|
-
const
|
|
161
|
+
const id7 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
162
162
|
try {
|
|
163
163
|
const created = await createRecord({ ...deps, db: deps.db }, {
|
|
164
|
-
id:
|
|
164
|
+
id: id7,
|
|
165
165
|
type: "person",
|
|
166
166
|
input: { ...input, email },
|
|
167
167
|
...options
|
|
@@ -614,7 +614,10 @@ function verifyInitialMembershipInvoice(intent, evidence) {
|
|
|
614
614
|
if (!intent.providerSubscriptionId || !intent.providerCustomerId) return { status: "retry" };
|
|
615
615
|
try {
|
|
616
616
|
const terms2 = intent.billingTerms ? subscriptionBillingTerms(intent.billingTerms) : void 0;
|
|
617
|
-
const
|
|
617
|
+
const quote = intent.paymentQuote;
|
|
618
|
+
const seatCents = quote?.namedSeat ? quote.lineItems.namedSeatCents : 0;
|
|
619
|
+
if (!Number.isSafeInteger(seatCents) || Number(seatCents) < 0 || quote?.namedSeat && quote.seatOffer?.amountCents !== seatCents) return { status: "mismatch" };
|
|
620
|
+
const due = subscriptionAmountDue({ priceCents: Number(intent.priceCents), billingTerms: terms2 }) + Number(seatCents);
|
|
618
621
|
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" };
|
|
619
622
|
return { status: "verified", ...terms2?.initialTermEndsAt ? { renewalAt: terms2.initialTermEndsAt } : {} };
|
|
620
623
|
} catch {
|
|
@@ -965,8 +968,8 @@ async function rebuildNetworkCommercialProjection(db, sources, mutationId, now =
|
|
|
965
968
|
return projection;
|
|
966
969
|
}
|
|
967
970
|
async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
968
|
-
const
|
|
969
|
-
const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id:
|
|
971
|
+
const id7 = exceptionId(fact.eventId, "collision");
|
|
972
|
+
const exception = (await db.query({ networkCommercialExceptions: { $: { where: { id: id7 }, limit: 1 } } })).networkCommercialExceptions?.[0];
|
|
970
973
|
const exact = stored.digest === fact.digest || stored.digest === legacyDigest;
|
|
971
974
|
const canonical3 = !exact && sameNetworkCommercialFact(stored.fact, fact);
|
|
972
975
|
if (exact || canonical3) {
|
|
@@ -987,7 +990,7 @@ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
|
987
990
|
action: "canonical_replay",
|
|
988
991
|
reason: "a resend matched the retained canonical fact"
|
|
989
992
|
} },
|
|
990
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
993
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
991
994
|
status: "resolved",
|
|
992
995
|
resolvedDigest: stored.digest,
|
|
993
996
|
reviewedDigest: fact.digest,
|
|
@@ -996,7 +999,7 @@ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
|
996
999
|
updatedAt: now
|
|
997
1000
|
} }
|
|
998
1001
|
], { mutationId: `network-commercial-resolve:${fact.eventId}:${fact.digest}`, guards: [
|
|
999
|
-
{ ns: "networkCommercialExceptions", id:
|
|
1002
|
+
{ ns: "networkCommercialExceptions", id: id7, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
1000
1003
|
] });
|
|
1001
1004
|
const projection2 = await rebuildNetworkCommercialProjection(db, sources, `network-commercial-resolve-rebuild:${fact.eventId}:${fact.digest}`, now, alerts);
|
|
1002
1005
|
return { status: "replayed", resolvedCollision: true, projection: projection2 };
|
|
@@ -1006,9 +1009,9 @@ async function collide(db, sources, stored, fact, now, legacyDigest, alerts) {
|
|
|
1006
1009
|
}
|
|
1007
1010
|
const stale = RESOLUTION_ATTRS.filter((attr3) => exception !== void 0 && Object.hasOwn(exception, attr3));
|
|
1008
1011
|
await db.transact([
|
|
1009
|
-
...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id:
|
|
1010
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
1011
|
-
id:
|
|
1012
|
+
...stale.length ? [{ t: "retract", ns: "networkCommercialExceptions", id: id7, attrs: stale }] : [],
|
|
1013
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
1014
|
+
id: id7,
|
|
1012
1015
|
eventId: fact.eventId,
|
|
1013
1016
|
kind: "collision",
|
|
1014
1017
|
status: "active",
|
|
@@ -1028,12 +1031,12 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
|
|
|
1028
1031
|
const found = await db.query({ networkCommercialEvents: { $: { where: { eventId: fact.eventId }, limit: 1 } } });
|
|
1029
1032
|
const existing = found.networkCommercialEvents?.[0];
|
|
1030
1033
|
if (existing) return collide(db, sources, existing, fact, now, legacyDigest, alerts);
|
|
1031
|
-
const
|
|
1034
|
+
const id7 = eventRowId(fact.eventId);
|
|
1032
1035
|
let lostRace = false;
|
|
1033
1036
|
let duplicate = false;
|
|
1034
1037
|
try {
|
|
1035
|
-
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id:
|
|
1036
|
-
id:
|
|
1038
|
+
const write = await db.transact([{ t: "update", ns: "networkCommercialEvents", id: id7, attrs: {
|
|
1039
|
+
id: id7,
|
|
1037
1040
|
eventId: fact.eventId,
|
|
1038
1041
|
digest: fact.digest,
|
|
1039
1042
|
eventType: fact.eventType,
|
|
@@ -1043,7 +1046,7 @@ async function ingestNetworkCommercialFact(db, sources, fact, now = Date.now(),
|
|
|
1043
1046
|
receivedAt: now
|
|
1044
1047
|
} }], {
|
|
1045
1048
|
mutationId: `network-commercial-ingest:${fact.eventId}:${fact.digest}`,
|
|
1046
|
-
guards: [{ ns: "networkCommercialEvents", id:
|
|
1049
|
+
guards: [{ ns: "networkCommercialEvents", id: id7, exists: false }]
|
|
1047
1050
|
});
|
|
1048
1051
|
duplicate = write.duplicate;
|
|
1049
1052
|
} catch (error) {
|
|
@@ -1087,10 +1090,10 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1087
1090
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
1088
1091
|
const fact = normalizeNetworkCommercialStripeEvent(provider.event, provider.digest);
|
|
1089
1092
|
if (!sameNetworkCommercialFact(stored.fact, fact)) throw new Error("Stripe facts differ; collision requires investigation");
|
|
1090
|
-
const
|
|
1093
|
+
const id7 = crypto.randomUUID();
|
|
1091
1094
|
await db.transact([
|
|
1092
|
-
{ t: "update", ns: "paymentRecoveryReviews", id:
|
|
1093
|
-
id:
|
|
1095
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: id7, attrs: {
|
|
1096
|
+
id: id7,
|
|
1094
1097
|
eventId,
|
|
1095
1098
|
actorUserId,
|
|
1096
1099
|
reviewedAt: now,
|
|
@@ -1105,9 +1108,9 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1105
1108
|
t: "update",
|
|
1106
1109
|
ns: "networkCommercialExceptions",
|
|
1107
1110
|
id: exceptionId2,
|
|
1108
|
-
attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId:
|
|
1111
|
+
attrs: { status: "resolved", resolvedDigest: provider.digest, resolvedBy: actorUserId, reviewId: id7, updatedAt: now }
|
|
1109
1112
|
}] : []
|
|
1110
|
-
], { mutationId: `payment-recovery:${
|
|
1113
|
+
], { mutationId: `payment-recovery:${id7}`, guards: [
|
|
1111
1114
|
{ ns: "networkCommercialEvents", id: eventRowId2, exists: true, equals: { digest: stored.digest } },
|
|
1112
1115
|
{
|
|
1113
1116
|
ns: "networkCommercialExceptions",
|
|
@@ -1116,8 +1119,8 @@ async function recoverNetworkCommercialEvent(db, sources, eventId, actorUserId,
|
|
|
1116
1119
|
...exception ? { equals: { status: exception.status, updatedAt: exception.updatedAt } } : {}
|
|
1117
1120
|
}
|
|
1118
1121
|
] });
|
|
1119
|
-
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${
|
|
1120
|
-
return { recovered: true, eventId, reviewId:
|
|
1122
|
+
await rebuildNetworkCommercialProjection(db, sources, `payment-recovery-rebuild:${id7}`, now, alerts);
|
|
1123
|
+
return { recovered: true, eventId, reviewId: id7 };
|
|
1121
1124
|
}
|
|
1122
1125
|
|
|
1123
1126
|
// src/payments.ts
|
|
@@ -1901,11 +1904,11 @@ async function sendTemplated(deps, input) {
|
|
|
1901
1904
|
const previous = prior[0];
|
|
1902
1905
|
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" };
|
|
1903
1906
|
const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
|
|
1904
|
-
const
|
|
1907
|
+
const id7 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1905
1908
|
const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
|
|
1906
1909
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
|
|
1907
1910
|
const row2 = {
|
|
1908
|
-
id:
|
|
1911
|
+
id: id7,
|
|
1909
1912
|
groupId: input.group.id,
|
|
1910
1913
|
to: decision.to,
|
|
1911
1914
|
template: input.template,
|
|
@@ -1923,15 +1926,15 @@ async function sendTemplated(deps, input) {
|
|
|
1923
1926
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
1924
1927
|
if (validateEmailCopy2({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
1925
1928
|
try {
|
|
1926
|
-
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1927
|
-
mutationId: `email-claim:${
|
|
1929
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
1930
|
+
mutationId: `email-claim:${id7}:${attempt}`,
|
|
1928
1931
|
guards: [previous ? {
|
|
1929
1932
|
ns: "emailLog",
|
|
1930
|
-
id:
|
|
1933
|
+
id: id7,
|
|
1931
1934
|
exists: true,
|
|
1932
1935
|
equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
|
|
1933
1936
|
...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
|
|
1934
|
-
} : { ns: "emailLog", id:
|
|
1937
|
+
} : { ns: "emailLog", id: id7, exists: false }]
|
|
1935
1938
|
});
|
|
1936
1939
|
if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
|
|
1937
1940
|
} catch (error) {
|
|
@@ -1951,16 +1954,16 @@ async function sendTemplated(deps, input) {
|
|
|
1951
1954
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
1952
1955
|
messageId = response.messageId;
|
|
1953
1956
|
} catch {
|
|
1954
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1955
|
-
guards: [{ ns: "emailLog", id:
|
|
1957
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
1958
|
+
guards: [{ ns: "emailLog", id: id7, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
1956
1959
|
});
|
|
1957
1960
|
return { sent: false, reason: "delivery-uncertain" };
|
|
1958
1961
|
}
|
|
1959
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1960
|
-
mutationId: `email-confirmed:${
|
|
1962
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
1963
|
+
mutationId: `email-confirmed:${id7}:${attempt}`,
|
|
1961
1964
|
guards: [{
|
|
1962
1965
|
ns: "emailLog",
|
|
1963
|
-
id:
|
|
1966
|
+
id: id7,
|
|
1964
1967
|
exists: true,
|
|
1965
1968
|
equals: { deliveryState: "sending", deliveryAttempt: attempt }
|
|
1966
1969
|
}]
|
|
@@ -2145,8 +2148,8 @@ async function hash(value) {
|
|
|
2145
2148
|
async function membershipDiscountCouponId(input, policyId) {
|
|
2146
2149
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
2147
2150
|
}
|
|
2148
|
-
async function load(db, ns,
|
|
2149
|
-
return (await db.query({ [ns]: { $: { where: { id:
|
|
2151
|
+
async function load(db, ns, id7) {
|
|
2152
|
+
return (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2150
2153
|
}
|
|
2151
2154
|
async function membershipOfferIdForRequest(input) {
|
|
2152
2155
|
return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
|
|
@@ -2280,20 +2283,20 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2280
2283
|
|
|
2281
2284
|
// src/commercial-evidence-guard.ts
|
|
2282
2285
|
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
2283
|
-
const ns = "networkCommercialExceptions",
|
|
2284
|
-
const row2 = (await db.query({ [ns]: { $: { where: { id:
|
|
2285
|
-
if (!row2) return { ns, id:
|
|
2286
|
+
const ns = "networkCommercialExceptions", id7 = `network-commercial-exception:${eventId}:collision`;
|
|
2287
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2288
|
+
if (!row2) return { ns, id: id7, exists: false };
|
|
2286
2289
|
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
2287
2290
|
throw Object.assign(new Error(code), { code });
|
|
2288
2291
|
}
|
|
2289
|
-
return { ns, id:
|
|
2292
|
+
return { ns, id: id7, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
2290
2293
|
}
|
|
2291
2294
|
|
|
2292
2295
|
// src/membership-authority-state.ts
|
|
2293
2296
|
var membershipFail = (code) => {
|
|
2294
2297
|
throw Object.assign(new Error(code), { code });
|
|
2295
2298
|
};
|
|
2296
|
-
var membershipLoad = async (db, ns,
|
|
2299
|
+
var membershipLoad = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2297
2300
|
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
2298
2301
|
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
2299
2302
|
function requireMembershipScope(row2, input) {
|
|
@@ -2304,18 +2307,18 @@ async function loadMembershipAuthorityState(db, input) {
|
|
|
2304
2307
|
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");
|
|
2305
2308
|
return row2;
|
|
2306
2309
|
}
|
|
2307
|
-
var membershipUpdate = (ns,
|
|
2310
|
+
var membershipUpdate = (ns, id7, attrs) => ({ t: "update", ns, id: id7, attrs });
|
|
2308
2311
|
var membershipClock = (now) => {
|
|
2309
2312
|
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
2310
2313
|
};
|
|
2311
2314
|
|
|
2312
2315
|
// src/membership-restart-payment.ts
|
|
2313
2316
|
async function membershipRestartPayment(db, target, member, now) {
|
|
2314
|
-
const
|
|
2317
|
+
const id7 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id7);
|
|
2315
2318
|
requireMembershipScope(restart, target);
|
|
2316
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2319
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2317
2320
|
requireMembershipScope(entitlement, target);
|
|
2318
|
-
if (entitlement.membershipId !==
|
|
2321
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
2319
2322
|
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
2320
2323
|
return {
|
|
2321
2324
|
restartId: String(restart.restartId),
|
|
@@ -2332,49 +2335,117 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
2332
2335
|
graceEndsAt: 0
|
|
2333
2336
|
},
|
|
2334
2337
|
ops: [
|
|
2335
|
-
membershipUpdate("membershipRestart",
|
|
2336
|
-
{ t: "retract", ns: "networkMembership", id:
|
|
2338
|
+
membershipUpdate("membershipRestart", id7, { status: "complete" }),
|
|
2339
|
+
{ t: "retract", ns: "networkMembership", id: id7, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
2337
2340
|
],
|
|
2338
2341
|
guards: [
|
|
2339
|
-
{ ns: "networkMembership", id:
|
|
2340
|
-
{ ns: "membershipRestart", id:
|
|
2342
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
2343
|
+
{ ns: "membershipRestart", id: id7, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
2341
2344
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
2342
2345
|
]
|
|
2343
2346
|
};
|
|
2344
2347
|
}
|
|
2345
2348
|
|
|
2349
|
+
// src/checkout-named-seat.ts
|
|
2350
|
+
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
2351
|
+
const row2 = value;
|
|
2352
|
+
const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
2353
|
+
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
2354
|
+
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
2355
|
+
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) {
|
|
2356
|
+
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
2357
|
+
}
|
|
2358
|
+
return { recipientName: name, recipientEmail: email };
|
|
2359
|
+
}
|
|
2360
|
+
async function checkoutNamedSeatIdentity(target, email) {
|
|
2361
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
2362
|
+
target.appId,
|
|
2363
|
+
target.chapterId,
|
|
2364
|
+
target.environment,
|
|
2365
|
+
target.runtime,
|
|
2366
|
+
email
|
|
2367
|
+
])));
|
|
2368
|
+
return {
|
|
2369
|
+
seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
|
|
2370
|
+
membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
|
|
2371
|
+
recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2346
2375
|
// src/membership-checkout-binding.ts
|
|
2347
2376
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
2348
2377
|
var reject2 = (code) => {
|
|
2349
2378
|
throw Object.assign(new Error(code), { code });
|
|
2350
2379
|
};
|
|
2351
|
-
var load2 = async (db, ns,
|
|
2380
|
+
var load2 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2352
2381
|
async function membershipCheckoutBindingId(input) {
|
|
2353
2382
|
return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
|
|
2354
2383
|
}
|
|
2355
2384
|
async function verifyMembershipCheckoutBinding(value, input) {
|
|
2356
2385
|
const row2 = value;
|
|
2357
2386
|
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");
|
|
2358
|
-
|
|
2387
|
+
if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
|
|
2388
|
+
if (input.namedSeat) {
|
|
2389
|
+
const expected = {
|
|
2390
|
+
...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
|
|
2391
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
|
|
2392
|
+
};
|
|
2393
|
+
const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
|
|
2394
|
+
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");
|
|
2395
|
+
}
|
|
2396
|
+
return {
|
|
2397
|
+
id: row2.id,
|
|
2398
|
+
...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
|
|
2399
|
+
boundAt: row2.boundAt,
|
|
2400
|
+
...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
|
|
2401
|
+
};
|
|
2359
2402
|
}
|
|
2360
|
-
async function bindMembershipCheckout(db, input, now) {
|
|
2403
|
+
async function bindMembershipCheckout(db, input, now, seatPriceId) {
|
|
2361
2404
|
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");
|
|
2362
2405
|
const stateId = `${input.chapterId}:${input.environment}`;
|
|
2363
2406
|
const state = await load2(db, "membershipAuthorityState", stateId);
|
|
2364
2407
|
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");
|
|
2365
|
-
const
|
|
2366
|
-
const existing = await load2(db, "membershipOfferCheckout",
|
|
2408
|
+
const id7 = await membershipCheckoutBindingId(input);
|
|
2409
|
+
const existing = await load2(db, "membershipOfferCheckout", id7);
|
|
2367
2410
|
if (existing) return verifyMembershipCheckoutBinding(existing, input);
|
|
2368
2411
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
2369
2412
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
2370
2413
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
2371
|
-
const restart = await load2(db, "membershipRestart",
|
|
2372
|
-
|
|
2414
|
+
const restart = await load2(db, "membershipRestart", id7.replace(/^checkout_/, "membership_"));
|
|
2415
|
+
let namedSeat;
|
|
2416
|
+
if (input.namedSeat) {
|
|
2417
|
+
if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
|
|
2418
|
+
const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
|
|
2419
|
+
namedSeat = {
|
|
2420
|
+
...selection,
|
|
2421
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
|
|
2422
|
+
...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
|
|
2423
|
+
priceId: seatPriceId,
|
|
2424
|
+
amountCents: offer.contract.offering.seatPriceCents
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
const receipt = { id: id7, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
|
|
2373
2428
|
try {
|
|
2374
|
-
await db.transact([
|
|
2375
|
-
|
|
2429
|
+
await db.transact([
|
|
2430
|
+
{ t: "update", ns: "membershipOfferCheckout", id: id7, attrs: receipt },
|
|
2431
|
+
...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
|
|
2432
|
+
id: namedSeat.recipientClaimId,
|
|
2433
|
+
appId: input.appId,
|
|
2434
|
+
chapterId: input.chapterId,
|
|
2435
|
+
environment: input.environment,
|
|
2436
|
+
runtime: input.runtime,
|
|
2437
|
+
seatId: namedSeat.seatId,
|
|
2438
|
+
recipientEmailNormalized: namedSeat.recipientEmail,
|
|
2439
|
+
createdAt: now
|
|
2440
|
+
} }] : []
|
|
2441
|
+
], {
|
|
2442
|
+
mutationId: `membership-checkout:${id7}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
2376
2443
|
guards: [
|
|
2377
|
-
{ ns: "membershipOfferCheckout", id:
|
|
2444
|
+
{ ns: "membershipOfferCheckout", id: id7, exists: false },
|
|
2445
|
+
...namedSeat ? [
|
|
2446
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2447
|
+
{ ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
|
|
2448
|
+
] : [],
|
|
2378
2449
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
2379
2450
|
{ ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2380
2451
|
]
|
|
@@ -2382,11 +2453,78 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
2382
2453
|
} catch (error) {
|
|
2383
2454
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2384
2455
|
}
|
|
2385
|
-
const saved = await load2(db, "membershipOfferCheckout",
|
|
2456
|
+
const saved = await load2(db, "membershipOfferCheckout", id7);
|
|
2386
2457
|
if (!saved) return reject2("membership_binding_unconfirmed");
|
|
2387
2458
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
2388
2459
|
}
|
|
2389
2460
|
|
|
2461
|
+
// src/membership-checkout-seat-payment.ts
|
|
2462
|
+
function checkoutSeatPaymentOperations(target, seat, input) {
|
|
2463
|
+
const { now, paidAt, endsAt } = input;
|
|
2464
|
+
const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
|
|
2465
|
+
const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
|
|
2466
|
+
return [
|
|
2467
|
+
membershipUpdate("networkSeat", seat.seatId, {
|
|
2468
|
+
id: seat.seatId,
|
|
2469
|
+
...membershipScope(target),
|
|
2470
|
+
primaryMembershipId: seat.membershipId,
|
|
2471
|
+
purchaserEmailNormalized: seat.purchaserEmail,
|
|
2472
|
+
recipientName: seat.recipientName,
|
|
2473
|
+
recipientEmailNormalized: seat.recipientEmail,
|
|
2474
|
+
recipientClaimId: seat.recipientClaimId,
|
|
2475
|
+
priceId: seat.priceId,
|
|
2476
|
+
primaryPriceId: input.primaryPriceId,
|
|
2477
|
+
amountCents: seat.amountCents,
|
|
2478
|
+
currency: input.offering.currency,
|
|
2479
|
+
interval: input.offering.interval,
|
|
2480
|
+
providerCustomerId: input.customerId,
|
|
2481
|
+
providerSubscriptionId: input.subscriptionId,
|
|
2482
|
+
combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
|
|
2483
|
+
status: included ? "pending_acceptance" : "renewal_pending",
|
|
2484
|
+
preparedAt: input.boundAt,
|
|
2485
|
+
purchasedAt: paidAt,
|
|
2486
|
+
termStartedAt: paidAt,
|
|
2487
|
+
termEndsAt: endsAt,
|
|
2488
|
+
acceptanceEndsAt: endsAt,
|
|
2489
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2490
|
+
offeringSnapshot: input.offering,
|
|
2491
|
+
paymentEventId: input.eventId,
|
|
2492
|
+
...included ? { entitlementId } : {},
|
|
2493
|
+
revision: 1,
|
|
2494
|
+
createdAt: now,
|
|
2495
|
+
updatedAt: now
|
|
2496
|
+
}),
|
|
2497
|
+
...included ? [membershipUpdate("networkEntitlement", entitlementId, {
|
|
2498
|
+
id: entitlementId,
|
|
2499
|
+
...membershipScope(target),
|
|
2500
|
+
membershipId: seat.membershipId,
|
|
2501
|
+
seatId: seat.seatId,
|
|
2502
|
+
kind: "named_seat",
|
|
2503
|
+
status: "pending_acceptance",
|
|
2504
|
+
termStartedAt: paidAt,
|
|
2505
|
+
termEndsAt: endsAt,
|
|
2506
|
+
offeringSnapshot: input.offering,
|
|
2507
|
+
sourceEventId: input.eventId,
|
|
2508
|
+
revision: 1,
|
|
2509
|
+
createdAt: now,
|
|
2510
|
+
updatedAt: now
|
|
2511
|
+
})] : [],
|
|
2512
|
+
membershipUpdate("membershipEffect", effectId, {
|
|
2513
|
+
id: effectId,
|
|
2514
|
+
chapterId: target.chapterId,
|
|
2515
|
+
environment: target.environment,
|
|
2516
|
+
runtime: target.runtime,
|
|
2517
|
+
membershipId: seat.membershipId,
|
|
2518
|
+
seatId: seat.seatId,
|
|
2519
|
+
kind: included ? "invite_named_recipient" : "attach_seat_renewal",
|
|
2520
|
+
status: "pending",
|
|
2521
|
+
attempts: 0,
|
|
2522
|
+
createdAt: now,
|
|
2523
|
+
updatedAt: now
|
|
2524
|
+
})
|
|
2525
|
+
];
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2390
2528
|
// src/membership-payment-authority.ts
|
|
2391
2529
|
async function networkMembershipId(input) {
|
|
2392
2530
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -2402,11 +2540,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2402
2540
|
{ ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
|
|
2403
2541
|
);
|
|
2404
2542
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
2543
|
+
const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
|
|
2544
|
+
if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
|
|
2545
|
+
const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
|
|
2405
2546
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
2406
2547
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
2407
2548
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
2408
2549
|
const fact = eventRow.fact;
|
|
2409
|
-
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 !==
|
|
2550
|
+
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({
|
|
2410
2551
|
appId: input.appId,
|
|
2411
2552
|
chapterId: input.chapterId,
|
|
2412
2553
|
environment: input.environment,
|
|
@@ -2418,14 +2559,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2418
2559
|
const paidAt = fact.invoicePaidAt;
|
|
2419
2560
|
const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
|
|
2420
2561
|
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");
|
|
2421
|
-
const
|
|
2422
|
-
let member = await membershipLoad(db, "networkMembership",
|
|
2562
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
2563
|
+
let member = await membershipLoad(db, "networkMembership", id7);
|
|
2423
2564
|
if (!member || member.status === "restarting") {
|
|
2424
2565
|
const offering = offer.contract.offering;
|
|
2425
2566
|
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
2426
2567
|
const attrs = {
|
|
2427
|
-
id:
|
|
2428
|
-
membershipId:
|
|
2568
|
+
id: id7,
|
|
2569
|
+
membershipId: id7,
|
|
2429
2570
|
...membershipScope(input),
|
|
2430
2571
|
baseSkuId: input.skuId,
|
|
2431
2572
|
status: "paid_pending_approval",
|
|
@@ -2441,8 +2582,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2441
2582
|
founding: offer.contract.founding,
|
|
2442
2583
|
cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
|
|
2443
2584
|
baseAmountCents: offering.basePriceCents,
|
|
2444
|
-
amountCents:
|
|
2585
|
+
amountCents: totalCents,
|
|
2445
2586
|
renewalAmountCents: offer.contract.renewalCents,
|
|
2587
|
+
...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
|
|
2446
2588
|
currency: offering.currency,
|
|
2447
2589
|
interval: offering.interval,
|
|
2448
2590
|
initialTermStartedAt: paidAt,
|
|
@@ -2459,11 +2601,11 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2459
2601
|
};
|
|
2460
2602
|
try {
|
|
2461
2603
|
await db.transact([
|
|
2462
|
-
membershipUpdate("networkMembership",
|
|
2604
|
+
membershipUpdate("networkMembership", id7, attrs),
|
|
2463
2605
|
membershipUpdate("networkEntitlement", entitlementId, {
|
|
2464
2606
|
id: entitlementId,
|
|
2465
2607
|
...membershipScope(input),
|
|
2466
|
-
membershipId:
|
|
2608
|
+
membershipId: id7,
|
|
2467
2609
|
kind: "primary",
|
|
2468
2610
|
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
2469
2611
|
termStartedAt: paidAt,
|
|
@@ -2475,23 +2617,45 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2475
2617
|
updatedAt: now,
|
|
2476
2618
|
...restart?.entitlementAttrs
|
|
2477
2619
|
}),
|
|
2478
|
-
...restart?.ops ?? []
|
|
2479
|
-
|
|
2480
|
-
|
|
2620
|
+
...restart?.ops ?? [],
|
|
2621
|
+
...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
|
|
2622
|
+
offering,
|
|
2623
|
+
primaryPriceId: String(binding.priceId),
|
|
2624
|
+
invoiceId: fact.invoiceId,
|
|
2625
|
+
invoiceTotalCents: totalCents,
|
|
2626
|
+
subscriptionId: input.providerSubscriptionId,
|
|
2627
|
+
customerId: input.providerCustomerId,
|
|
2628
|
+
eventId: input.eventId,
|
|
2629
|
+
paidAt: Number(paidAt),
|
|
2630
|
+
endsAt: Number(endsAt),
|
|
2631
|
+
boundAt: Number(binding.boundAt),
|
|
2632
|
+
now
|
|
2633
|
+
}) : []
|
|
2634
|
+
], { mutationId: `membership-paid:${id7}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
2635
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id7, exists: false }],
|
|
2636
|
+
...namedSeat ? [
|
|
2637
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2638
|
+
{
|
|
2639
|
+
ns: "networkSeatRecipient",
|
|
2640
|
+
id: namedSeat.recipientClaimId,
|
|
2641
|
+
exists: true,
|
|
2642
|
+
equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
|
|
2643
|
+
}
|
|
2644
|
+
] : [],
|
|
2481
2645
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
2482
|
-
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
2646
|
+
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
|
|
2483
2647
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
2484
2648
|
evidenceGuard
|
|
2485
2649
|
] });
|
|
2486
2650
|
} catch (error) {
|
|
2487
2651
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2488
2652
|
}
|
|
2489
|
-
member = await membershipLoad(db, "networkMembership",
|
|
2653
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
2490
2654
|
}
|
|
2491
2655
|
requireMembershipScope(member, input);
|
|
2492
2656
|
if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
|
|
2493
2657
|
return {
|
|
2494
|
-
membershipId:
|
|
2658
|
+
membershipId: id7,
|
|
2495
2659
|
status: String(member.status),
|
|
2496
2660
|
cohort: String(member.cohort),
|
|
2497
2661
|
initialTermStartedAt: Number(member.initialTermStartedAt),
|
|
@@ -2562,19 +2726,19 @@ async function stopPreviousSubscription(member, target, secret) {
|
|
|
2562
2726
|
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
2563
2727
|
membershipClock(now);
|
|
2564
2728
|
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
2565
|
-
const
|
|
2566
|
-
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([
|
|
2729
|
+
const id7 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
2730
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id7, input.previousSubscriptionId]))}`;
|
|
2567
2731
|
const state = await loadMembershipAuthorityState(db, input);
|
|
2568
|
-
const previous = await membershipLoad(db, "networkMembership",
|
|
2732
|
+
const previous = await membershipLoad(db, "networkMembership", id7);
|
|
2569
2733
|
requireMembershipScope(previous, input);
|
|
2570
2734
|
const receipt = {
|
|
2571
2735
|
applicationId: input.applicationId,
|
|
2572
|
-
membershipId:
|
|
2736
|
+
membershipId: id7,
|
|
2573
2737
|
restartId,
|
|
2574
2738
|
memberNumber: Number(previous.memberNumber),
|
|
2575
2739
|
status: "ready"
|
|
2576
2740
|
};
|
|
2577
|
-
const existing = await membershipLoad(db, "membershipRestart",
|
|
2741
|
+
const existing = await membershipLoad(db, "membershipRestart", id7);
|
|
2578
2742
|
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
2579
2743
|
requireMembershipScope(existing, input);
|
|
2580
2744
|
return receipt;
|
|
@@ -2590,33 +2754,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2590
2754
|
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
2591
2755
|
id: restartId,
|
|
2592
2756
|
...membershipScope(input),
|
|
2593
|
-
membershipId:
|
|
2757
|
+
membershipId: id7,
|
|
2594
2758
|
providerSubscriptionId: input.previousSubscriptionId,
|
|
2595
2759
|
memberSnapshot: previous,
|
|
2596
2760
|
bindingSnapshot: binding2,
|
|
2597
2761
|
archivedAt: now
|
|
2598
2762
|
}),
|
|
2599
|
-
membershipUpdate("networkMembership",
|
|
2763
|
+
membershipUpdate("networkMembership", id7, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
2600
2764
|
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
2601
2765
|
...seats.guards,
|
|
2602
|
-
{ ns: "networkMembership", id:
|
|
2766
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
2603
2767
|
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
2604
2768
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
2605
2769
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2606
2770
|
] });
|
|
2607
2771
|
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2608
|
-
const reserved = await membershipLoad(db, "networkMembership",
|
|
2772
|
+
const reserved = await membershipLoad(db, "networkMembership", id7);
|
|
2609
2773
|
requireMembershipScope(reserved, input);
|
|
2610
2774
|
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2611
2775
|
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
2612
2776
|
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
2613
2777
|
requireMembershipScope(binding, input);
|
|
2614
2778
|
await db.transact([
|
|
2615
|
-
membershipUpdate("membershipRestart",
|
|
2779
|
+
membershipUpdate("membershipRestart", id7, { id: id7, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
2616
2780
|
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
2617
2781
|
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
2618
|
-
{ ns: "membershipRestart", id:
|
|
2619
|
-
{ ns: "networkMembership", id:
|
|
2782
|
+
{ ns: "membershipRestart", id: id7, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
2783
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
2620
2784
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
2621
2785
|
] });
|
|
2622
2786
|
return receipt;
|
|
@@ -2626,15 +2790,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2626
2790
|
async function readMembershipAccess(db, input, now) {
|
|
2627
2791
|
membershipClock(now);
|
|
2628
2792
|
await loadMembershipAuthorityState(db, input);
|
|
2629
|
-
const
|
|
2630
|
-
if (!member) return { membershipId:
|
|
2793
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
2794
|
+
if (!member) return { membershipId: id7, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
|
|
2631
2795
|
requireMembershipScope(member, input);
|
|
2632
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2796
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2633
2797
|
requireMembershipScope(entitlement, input);
|
|
2634
2798
|
const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
2635
|
-
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 ===
|
|
2799
|
+
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;
|
|
2636
2800
|
return {
|
|
2637
|
-
membershipId:
|
|
2801
|
+
membershipId: id7,
|
|
2638
2802
|
allowed: allowed2,
|
|
2639
2803
|
status: String(member.status),
|
|
2640
2804
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
@@ -2752,7 +2916,7 @@ async function refundChapterNetworkMembership(db, env, ctx, app) {
|
|
|
2752
2916
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2753
2917
|
const receipt = await ctx.membershipAuthority.refundMembership(target, env);
|
|
2754
2918
|
const offer = intent.membershipOffer;
|
|
2755
|
-
if (receipt.membershipId !== await networkMembershipId(target) || receipt.status !== "refunded" || receipt.subscriptionCanceled !== true || receipt.refundedCents !== offer.contract?.dueTodayCents) throw new Error("membership refund unconfirmed");
|
|
2919
|
+
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");
|
|
2756
2920
|
return receipt;
|
|
2757
2921
|
}
|
|
2758
2922
|
async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
@@ -2803,14 +2967,14 @@ function facts(snapshots) {
|
|
|
2803
2967
|
["payment_intent.updated", snapshots.payments?.objects ?? []]
|
|
2804
2968
|
];
|
|
2805
2969
|
return groups.flatMap(([eventType, objects]) => objects.map((object2) => {
|
|
2806
|
-
const
|
|
2970
|
+
const id7 = typeof object2.id === "string" ? object2.id : "invalid";
|
|
2807
2971
|
return normalizeNetworkCommercialStripeEvent({
|
|
2808
|
-
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${
|
|
2972
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id7}`,
|
|
2809
2973
|
type: eventType,
|
|
2810
2974
|
created: typeof object2.created === "number" ? object2.created : 0,
|
|
2811
2975
|
livemode: object2.livemode === true,
|
|
2812
2976
|
data: { object: object2 }
|
|
2813
|
-
}, `stripe-snapshot:${eventType}:${
|
|
2977
|
+
}, `stripe-snapshot:${eventType}:${id7}`);
|
|
2814
2978
|
}));
|
|
2815
2979
|
}
|
|
2816
2980
|
var selected = (row2) => ({
|
|
@@ -2885,9 +3049,9 @@ async function chapterBillingSubscriptions(secretKey, chapterId, env) {
|
|
|
2885
3049
|
// src/approve-application.ts
|
|
2886
3050
|
async function approveApplication(input) {
|
|
2887
3051
|
const { db, env, ctx } = input;
|
|
2888
|
-
const
|
|
2889
|
-
if (!
|
|
2890
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
3052
|
+
const id7 = String(input.app.id ?? "");
|
|
3053
|
+
if (!id7) return { ok: false, status: 404, error: "not found" };
|
|
3054
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2891
3055
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) {
|
|
2892
3056
|
return { ok: false, status: 404, error: "not found" };
|
|
2893
3057
|
}
|
|
@@ -2921,10 +3085,10 @@ async function approveApplication(input) {
|
|
|
2921
3085
|
try {
|
|
2922
3086
|
if (!duplicate) {
|
|
2923
3087
|
const write = await db.transact(
|
|
2924
|
-
[{ t: "update", ns: "applications", id:
|
|
2925
|
-
{ mutationId: `approve:${
|
|
3088
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3089
|
+
{ mutationId: `approve:${id7}`, guards: [{
|
|
2926
3090
|
ns: "applications",
|
|
2927
|
-
id:
|
|
3091
|
+
id: id7,
|
|
2928
3092
|
exists: true,
|
|
2929
3093
|
equals: Object.fromEntries(guardFields.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
2930
3094
|
absent: guardFields.filter((key3) => app[key3] === void 0)
|
|
@@ -2936,7 +3100,7 @@ async function approveApplication(input) {
|
|
|
2936
3100
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2937
3101
|
return { ok: false, status: 409, error: "application changed; reload before approving" };
|
|
2938
3102
|
}
|
|
2939
|
-
const current = (await db.query({ applications: { $: { where: { id:
|
|
3103
|
+
const current = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2940
3104
|
if (!current || current.status !== "approved" || current.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => current[key3] !== app[key3])) {
|
|
2941
3105
|
return { ok: false, status: 409, error: "application changed; approval effects were not applied" };
|
|
2942
3106
|
}
|
|
@@ -2946,7 +3110,7 @@ async function approveApplication(input) {
|
|
|
2946
3110
|
} catch {
|
|
2947
3111
|
return { ok: false, status: 503, error: "network membership approval reconciliation pending" };
|
|
2948
3112
|
}
|
|
2949
|
-
const afterAuthority = (await db.query({ applications: { $: { where: { id:
|
|
3113
|
+
const afterAuthority = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2950
3114
|
if (!afterAuthority || afterAuthority.status !== "approved" || afterAuthority.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => afterAuthority[key3] !== app[key3])) {
|
|
2951
3115
|
return { ok: false, status: 409, error: "application changed during network approval" };
|
|
2952
3116
|
}
|
|
@@ -2959,7 +3123,7 @@ async function approveApplication(input) {
|
|
|
2959
3123
|
if (!userId && typeof app.email === "string") {
|
|
2960
3124
|
const found = await clerkGetUserByEmail(secret, app.email, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
2961
3125
|
userId = found?.id ?? null;
|
|
2962
|
-
if (userId) await db.transact([{ t: "update", ns: "applications", id:
|
|
3126
|
+
if (userId) await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { clerkUserId: userId } }]);
|
|
2963
3127
|
}
|
|
2964
3128
|
if (userId) {
|
|
2965
3129
|
const user = await clerkGetUser(secret, userId, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
@@ -2981,8 +3145,8 @@ async function approveApplication(input) {
|
|
|
2981
3145
|
template: send,
|
|
2982
3146
|
to: app.email,
|
|
2983
3147
|
vars: { firstName: String(app.firstName ?? ""), membersUrl: `${input.origin}/members/` },
|
|
2984
|
-
applicationId:
|
|
2985
|
-
dedupeKey: `approve:${
|
|
3148
|
+
applicationId: id7,
|
|
3149
|
+
dedupeKey: `approve:${id7}`
|
|
2986
3150
|
}
|
|
2987
3151
|
);
|
|
2988
3152
|
emailLogged = result.sent;
|
|
@@ -2990,10 +3154,10 @@ async function approveApplication(input) {
|
|
|
2990
3154
|
return { ok: false, status: 503, error: "approval notice reconciliation pending", rolePromoted, emailLogged };
|
|
2991
3155
|
}
|
|
2992
3156
|
}
|
|
2993
|
-
await db.transact([{ t: "update", ns: "applications", id:
|
|
3157
|
+
await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { approvalEffectsPending: false } }], {
|
|
2994
3158
|
guards: [{
|
|
2995
3159
|
ns: "applications",
|
|
2996
|
-
id:
|
|
3160
|
+
id: id7,
|
|
2997
3161
|
exists: true,
|
|
2998
3162
|
equals: { status: "approved", ...app.canceled !== void 0 ? { canceled: app.canceled } : {} },
|
|
2999
3163
|
...app.canceled === void 0 ? { absent: ["canceled"] } : {}
|
|
@@ -3215,10 +3379,10 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3215
3379
|
tierId: target.tierId,
|
|
3216
3380
|
...typeof existing?.clerkUserId === "string" ? { clerkUserId: existing.clerkUserId } : {}
|
|
3217
3381
|
};
|
|
3218
|
-
const
|
|
3382
|
+
const id7 = existing ? String(existing.id) : `gift-application:${String(claim.id)}`;
|
|
3219
3383
|
const status2 = existing && existing.status !== "submitted" ? String(existing.status) : "paid_pending_vetting";
|
|
3220
|
-
ops.push({ t: "update", ns: "applications", id:
|
|
3221
|
-
id:
|
|
3384
|
+
ops.push({ t: "update", ns: "applications", id: id7, attrs: {
|
|
3385
|
+
id: id7,
|
|
3222
3386
|
groupId: chapterId,
|
|
3223
3387
|
email: beneficiary.email,
|
|
3224
3388
|
emailNormalized,
|
|
@@ -3229,12 +3393,12 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3229
3393
|
clerkUserId: beneficiary.userId,
|
|
3230
3394
|
...existing ? {} : { createdAt: now }
|
|
3231
3395
|
} });
|
|
3232
|
-
result = { kind: "application", id:
|
|
3396
|
+
result = { kind: "application", id: id7 };
|
|
3233
3397
|
} else {
|
|
3234
3398
|
if (beneficiary.applicationId) return null;
|
|
3235
|
-
const
|
|
3236
|
-
ops.push({ t: "update", ns: "giftEntitlements", id:
|
|
3237
|
-
id:
|
|
3399
|
+
const id7 = `gift-entitlement:${String(claim.id)}`;
|
|
3400
|
+
ops.push({ t: "update", ns: "giftEntitlements", id: id7, attrs: {
|
|
3401
|
+
id: id7,
|
|
3238
3402
|
claimId: String(claim.id),
|
|
3239
3403
|
orderId: String(claim.orderId),
|
|
3240
3404
|
chapterId,
|
|
@@ -3244,7 +3408,7 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3244
3408
|
active: true,
|
|
3245
3409
|
createdAt: now
|
|
3246
3410
|
} });
|
|
3247
|
-
result = { kind: "entitlement", id:
|
|
3411
|
+
result = { kind: "entitlement", id: id7 };
|
|
3248
3412
|
}
|
|
3249
3413
|
ops.unshift({ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
|
|
3250
3414
|
status: "redeemed",
|
|
@@ -3418,9 +3582,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
3418
3582
|
return "stored";
|
|
3419
3583
|
}
|
|
3420
3584
|
if (existing) return existing.digest === input.digest || existing.digest === input.legacyDigest ? "replayed" : "collision";
|
|
3421
|
-
const
|
|
3422
|
-
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id:
|
|
3423
|
-
id:
|
|
3585
|
+
const id7 = `stripe-event:${input.eventId}`;
|
|
3586
|
+
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id: id7, attrs: {
|
|
3587
|
+
id: id7,
|
|
3424
3588
|
eventId: input.eventId,
|
|
3425
3589
|
digest: input.digest,
|
|
3426
3590
|
disposition: input.disposition,
|
|
@@ -3652,9 +3816,9 @@ var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
|
|
|
3652
3816
|
var fail3 = (message, code) => Object.assign(new Error(message), { code });
|
|
3653
3817
|
async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
|
|
3654
3818
|
if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3655
|
-
const
|
|
3819
|
+
const id7 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
|
|
3656
3820
|
const found = await db.query({
|
|
3657
|
-
networkCommercialExceptions: { $: { where: { id:
|
|
3821
|
+
networkCommercialExceptions: { $: { where: { id: id7 }, limit: 1 } },
|
|
3658
3822
|
networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
|
|
3659
3823
|
});
|
|
3660
3824
|
const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
|
|
@@ -3677,7 +3841,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3677
3841
|
action: "resolve",
|
|
3678
3842
|
reason: input.reason
|
|
3679
3843
|
} },
|
|
3680
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
3844
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
3681
3845
|
status: "resolved",
|
|
3682
3846
|
resolvedDigest: stored.digest,
|
|
3683
3847
|
...dismissed ? { reviewedDigest: dismissed } : {},
|
|
@@ -3686,7 +3850,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3686
3850
|
updatedAt: now
|
|
3687
3851
|
} }
|
|
3688
3852
|
], { mutationId: input.mutationId, guards: [
|
|
3689
|
-
{ ns: "networkCommercialExceptions", id:
|
|
3853
|
+
{ ns: "networkCommercialExceptions", id: id7, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
3690
3854
|
] });
|
|
3691
3855
|
if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
|
|
3692
3856
|
const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
|
|
@@ -3801,10 +3965,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3801
3965
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3802
3966
|
const event2 = normalizeWebhookEvent(provider.event, { requireProviderPayment: Boolean(ctx.membershipAuthority) });
|
|
3803
3967
|
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);
|
|
3804
|
-
const
|
|
3968
|
+
const id7 = crypto.randomUUID();
|
|
3805
3969
|
await db.transact([
|
|
3806
|
-
{ t: "update", ns: "paymentRecoveryReviews", id:
|
|
3807
|
-
id:
|
|
3970
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: id7, attrs: {
|
|
3971
|
+
id: id7,
|
|
3808
3972
|
eventId,
|
|
3809
3973
|
actorUserId: user.userId,
|
|
3810
3974
|
reviewedAt: Date.now(),
|
|
@@ -3813,8 +3977,8 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3813
3977
|
scope: `${target.appId}:${target.environment}:${target.runtime}`,
|
|
3814
3978
|
previousState: row2
|
|
3815
3979
|
} },
|
|
3816
|
-
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId:
|
|
3817
|
-
], { mutationId: `payment-recovery:${
|
|
3980
|
+
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId: id7 } }
|
|
3981
|
+
], { mutationId: `payment-recovery:${id7}`, guards: [{
|
|
3818
3982
|
ns: "stripeEventReceipts",
|
|
3819
3983
|
id: String(row2.id),
|
|
3820
3984
|
exists: true,
|
|
@@ -3883,43 +4047,121 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3883
4047
|
}
|
|
3884
4048
|
|
|
3885
4049
|
// src/membership-seat-refund-stripe.ts
|
|
4050
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4051
|
+
|
|
4052
|
+
// src/membership-checkout-seat-refund.ts
|
|
3886
4053
|
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3887
4054
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3888
4055
|
throw Object.assign(new Error(code), { code });
|
|
3889
4056
|
};
|
|
4057
|
+
var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
4058
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
4059
|
+
const invoiceRef = seat.combinedInvoice;
|
|
4060
|
+
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();
|
|
4061
|
+
const call = async (method, path, params, key3) => {
|
|
4062
|
+
const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4063
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4064
|
+
return result.body;
|
|
4065
|
+
};
|
|
4066
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
4067
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4068
|
+
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();
|
|
4069
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
4070
|
+
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();
|
|
4071
|
+
let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
|
|
4072
|
+
if (!chargeId && !paymentId) {
|
|
4073
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
4074
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
4075
|
+
const payment = payments.data[0];
|
|
4076
|
+
if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
4077
|
+
paymentId = id2(payment.payment.payment_intent);
|
|
4078
|
+
}
|
|
4079
|
+
if (!chargeId) {
|
|
4080
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
4081
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
4082
|
+
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();
|
|
4083
|
+
chargeId = id2(payment.latest_charge);
|
|
4084
|
+
}
|
|
4085
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
4086
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
4087
|
+
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();
|
|
4088
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4089
|
+
if (items.length > 1) fail4();
|
|
4090
|
+
if (items.length && sub.status !== "canceled") {
|
|
4091
|
+
const item = items[0];
|
|
4092
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
4093
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4094
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
4095
|
+
}
|
|
4096
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
4097
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
4098
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
4099
|
+
let refundedCents = 0;
|
|
4100
|
+
const refundIds = [];
|
|
4101
|
+
for (const refund of list.data) {
|
|
4102
|
+
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();
|
|
4103
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
4104
|
+
if (refund.status === "succeeded") {
|
|
4105
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
4106
|
+
refundedCents += refund.amount;
|
|
4107
|
+
refundIds.push(refund.id);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
4111
|
+
if (refundedCents < seat.amountCents) {
|
|
4112
|
+
const remaining = seat.amountCents - refundedCents;
|
|
4113
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
4114
|
+
charge: chargeId,
|
|
4115
|
+
amount: remaining,
|
|
4116
|
+
reason: "requested_by_customer",
|
|
4117
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
4118
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
4119
|
+
if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
4120
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
4121
|
+
refundedCents += remaining;
|
|
4122
|
+
refundIds.push(refund.id);
|
|
4123
|
+
}
|
|
4124
|
+
return { refundedCents, refundIds };
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
// src/membership-seat-refund-stripe.ts
|
|
4128
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
4129
|
+
throw Object.assign(new Error(code), { code });
|
|
4130
|
+
};
|
|
3890
4131
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3891
|
-
if (
|
|
4132
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4133
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
3892
4134
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3893
|
-
const result = await
|
|
3894
|
-
if (!result.ok || !result.body) return
|
|
4135
|
+
const result = await stripeRequest5({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4136
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
3895
4137
|
return result.body;
|
|
3896
4138
|
};
|
|
3897
4139
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3898
4140
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
3899
|
-
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))
|
|
4141
|
+
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();
|
|
3900
4142
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
3901
|
-
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))
|
|
4143
|
+
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();
|
|
3902
4144
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3903
|
-
if (items.length > 1)
|
|
4145
|
+
if (items.length > 1) fail5();
|
|
3904
4146
|
if (items.length && sub.status !== "canceled") {
|
|
3905
4147
|
const item = items[0];
|
|
3906
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
4148
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
3907
4149
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3908
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
4150
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
3909
4151
|
}
|
|
3910
4152
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
3911
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100)
|
|
4153
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
3912
4154
|
let refundedCents = 0, pendingCents = 0;
|
|
3913
4155
|
const refundIds = [];
|
|
3914
4156
|
for (const refund of list.data) {
|
|
3915
|
-
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))
|
|
4157
|
+
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();
|
|
3916
4158
|
if (refund.status === "succeeded") {
|
|
3917
4159
|
refundedCents += refund.amount;
|
|
3918
4160
|
refundIds.push(refund.id);
|
|
3919
4161
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3920
4162
|
}
|
|
3921
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3922
|
-
if (pendingCents)
|
|
4163
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
4164
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
3923
4165
|
if (refundedCents < seat.amountCents) {
|
|
3924
4166
|
const remaining = seat.amountCents - refundedCents;
|
|
3925
4167
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3928,8 +4170,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3928
4170
|
reason: "requested_by_customer",
|
|
3929
4171
|
metadata
|
|
3930
4172
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
3931
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
3932
|
-
if (refund.status !== "succeeded")
|
|
4173
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
4174
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
3933
4175
|
refundedCents += remaining;
|
|
3934
4176
|
refundIds.push(refund.id);
|
|
3935
4177
|
}
|
|
@@ -3951,8 +4193,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
3951
4193
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
3952
4194
|
requireMembershipScope(member, input);
|
|
3953
4195
|
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
3954
|
-
const
|
|
3955
|
-
let seat = await membershipLoad(db, "networkSeat",
|
|
4196
|
+
const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4197
|
+
let seat = await membershipLoad(db, "networkSeat", id7);
|
|
3956
4198
|
if (!seat) {
|
|
3957
4199
|
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
3958
4200
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
@@ -3960,7 +4202,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
3960
4202
|
membershipFail("named_seat_purchase_unavailable");
|
|
3961
4203
|
}
|
|
3962
4204
|
const attrs = {
|
|
3963
|
-
id:
|
|
4205
|
+
id: id7,
|
|
3964
4206
|
...membershipScope(input),
|
|
3965
4207
|
primaryMembershipId: memberId,
|
|
3966
4208
|
recipientName: name,
|
|
@@ -3985,20 +4227,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
3985
4227
|
};
|
|
3986
4228
|
try {
|
|
3987
4229
|
await db.transact([
|
|
3988
|
-
membershipUpdate("networkSeat",
|
|
4230
|
+
membershipUpdate("networkSeat", id7, attrs),
|
|
3989
4231
|
membershipUpdate("networkSeatRecipient", claimId, {
|
|
3990
4232
|
id: claimId,
|
|
3991
4233
|
appId: input.appId,
|
|
3992
4234
|
chapterId: input.chapterId,
|
|
3993
4235
|
environment: input.environment,
|
|
3994
4236
|
runtime: input.runtime,
|
|
3995
|
-
seatId:
|
|
4237
|
+
seatId: id7,
|
|
3996
4238
|
recipientEmailNormalized: email,
|
|
3997
4239
|
createdAt: now
|
|
3998
4240
|
}),
|
|
3999
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId:
|
|
4000
|
-
], { mutationId: `named-seat-reserve:${
|
|
4001
|
-
{ ns: "networkSeat", id:
|
|
4241
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4242
|
+
], { mutationId: `named-seat-reserve:${id7}`, guards: [
|
|
4243
|
+
{ ns: "networkSeat", id: id7, exists: false },
|
|
4002
4244
|
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4003
4245
|
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4004
4246
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
@@ -4006,7 +4248,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4006
4248
|
} catch (error) {
|
|
4007
4249
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4008
4250
|
}
|
|
4009
|
-
seat = await membershipLoad(db, "networkSeat",
|
|
4251
|
+
seat = await membershipLoad(db, "networkSeat", id7);
|
|
4010
4252
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4011
4253
|
}
|
|
4012
4254
|
requireMembershipScope(seat, input);
|
|
@@ -4016,7 +4258,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4016
4258
|
|
|
4017
4259
|
// src/membership-seat-included.ts
|
|
4018
4260
|
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4019
|
-
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4261
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4020
4262
|
function namedSeatRecipientState(seat) {
|
|
4021
4263
|
switch (String(seat.status)) {
|
|
4022
4264
|
case "renewal_pending":
|
|
@@ -4120,7 +4362,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4120
4362
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4121
4363
|
requireMembershipScope(member, input);
|
|
4122
4364
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4123
|
-
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");
|
|
4365
|
+
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");
|
|
4124
4366
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4125
4367
|
await db.transact([
|
|
4126
4368
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4233,8 +4475,8 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4233
4475
|
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4234
4476
|
for (const member of rows5.slice(0, limit)) {
|
|
4235
4477
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4236
|
-
const
|
|
4237
|
-
receipt.skipped.push({ membershipId:
|
|
4478
|
+
const id7 = String(member.id), skip = (reason) => {
|
|
4479
|
+
receipt.skipped.push({ membershipId: id7, reason });
|
|
4238
4480
|
};
|
|
4239
4481
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4240
4482
|
if (offering.seatPriceCents <= target) {
|
|
@@ -4250,16 +4492,16 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4250
4492
|
skip(`seat is ${String(seat.status)}`);
|
|
4251
4493
|
continue;
|
|
4252
4494
|
}
|
|
4253
|
-
const ops = [membershipUpdate("networkMembership",
|
|
4495
|
+
const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4254
4496
|
const guards = [
|
|
4255
|
-
{ ns: "networkMembership", id:
|
|
4497
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
4256
4498
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4257
4499
|
];
|
|
4258
4500
|
if (seat) {
|
|
4259
4501
|
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4260
4502
|
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4261
4503
|
}
|
|
4262
|
-
await db.transact(ops, { mutationId: `seat-reprice:${
|
|
4504
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
|
|
4263
4505
|
receipt.repriced++;
|
|
4264
4506
|
}
|
|
4265
4507
|
return receipt;
|
|
@@ -4460,25 +4702,28 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4460
4702
|
};
|
|
4461
4703
|
|
|
4462
4704
|
// src/membership-discount-reterm.ts
|
|
4463
|
-
import { stripeRequest as
|
|
4705
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
4464
4706
|
|
|
4465
4707
|
// src/subscription-provider-contract.ts
|
|
4466
|
-
import { stripeRequest as
|
|
4708
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4467
4709
|
function subscriptionContractMismatch() {
|
|
4468
4710
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4469
4711
|
}
|
|
4470
4712
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4471
4713
|
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();
|
|
4472
|
-
const result = await
|
|
4714
|
+
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4473
4715
|
const price = result.body;
|
|
4474
4716
|
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();
|
|
4475
4717
|
return typeof price.product === "string" ? price.product : void 0;
|
|
4476
4718
|
}
|
|
4477
4719
|
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4478
4720
|
const items = sub?.items;
|
|
4479
|
-
|
|
4721
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
4722
|
+
const primary = items?.data?.find((item) => item.price?.id === input.priceId);
|
|
4723
|
+
const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
|
|
4724
|
+
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();
|
|
4480
4725
|
const invoice = sub.latest_invoice;
|
|
4481
|
-
if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
|
|
4726
|
+
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();
|
|
4482
4727
|
verifySubscriptionBillingResult(sub, input);
|
|
4483
4728
|
}
|
|
4484
4729
|
|
|
@@ -4490,8 +4735,8 @@ var couponOf = (discount) => {
|
|
|
4490
4735
|
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4491
4736
|
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4492
4737
|
};
|
|
4493
|
-
async function retermPolicy(db, scope2,
|
|
4494
|
-
const row2 = await membershipLoad(db, "discountPolicyVersion",
|
|
4738
|
+
async function retermPolicy(db, scope2, id7) {
|
|
4739
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
|
|
4495
4740
|
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4496
4741
|
let policy2;
|
|
4497
4742
|
try {
|
|
@@ -4499,12 +4744,12 @@ async function retermPolicy(db, scope2, id6) {
|
|
|
4499
4744
|
} catch {
|
|
4500
4745
|
return membershipFail("membership_reterm_policy_ineligible");
|
|
4501
4746
|
}
|
|
4502
|
-
if (policy2.id !==
|
|
4747
|
+
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");
|
|
4503
4748
|
return policy2;
|
|
4504
4749
|
}
|
|
4505
4750
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
4506
4751
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4507
|
-
const read2 = await
|
|
4752
|
+
const read2 = await stripeRequest7({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4508
4753
|
const sub = read2.body, items = sub?.items?.data;
|
|
4509
4754
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4510
4755
|
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) {
|
|
@@ -4515,7 +4760,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
4515
4760
|
const present = discounts.map(couponOf);
|
|
4516
4761
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
4517
4762
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4518
|
-
const written = await
|
|
4763
|
+
const written = await stripeRequest7({ secretKey: sk }, "POST", path, {
|
|
4519
4764
|
"discounts[0][coupon]": couponId,
|
|
4520
4765
|
"expand[0]": "discounts",
|
|
4521
4766
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -4531,7 +4776,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4531
4776
|
const limit = input.limit ?? 100;
|
|
4532
4777
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4533
4778
|
if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
|
|
4534
|
-
if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((
|
|
4779
|
+
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");
|
|
4535
4780
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4536
4781
|
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4537
4782
|
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
@@ -4541,10 +4786,10 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4541
4786
|
let rows5;
|
|
4542
4787
|
if (input.membershipIds) {
|
|
4543
4788
|
rows5 = [];
|
|
4544
|
-
for (const
|
|
4545
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
4546
|
-
if (!member) skip(
|
|
4547
|
-
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(
|
|
4789
|
+
for (const id7 of input.membershipIds) {
|
|
4790
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
4791
|
+
if (!member) skip(id7, "not_found");
|
|
4792
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
4548
4793
|
else rows5.push(member);
|
|
4549
4794
|
}
|
|
4550
4795
|
} else {
|
|
@@ -4554,32 +4799,32 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4554
4799
|
rows5 = found.slice(0, limit);
|
|
4555
4800
|
}
|
|
4556
4801
|
for (const member of rows5) {
|
|
4557
|
-
const
|
|
4802
|
+
const id7 = String(member.id);
|
|
4558
4803
|
if (member.discountPolicyVersionId === policy2.id) {
|
|
4559
|
-
receipt.unchanged.push(
|
|
4804
|
+
receipt.unchanged.push(id7);
|
|
4560
4805
|
continue;
|
|
4561
4806
|
}
|
|
4562
4807
|
if (!RETERMABLE.has(String(member.status))) {
|
|
4563
|
-
skip(
|
|
4808
|
+
skip(id7, "status");
|
|
4564
4809
|
continue;
|
|
4565
4810
|
}
|
|
4566
4811
|
if (member.founding !== true) {
|
|
4567
|
-
skip(
|
|
4812
|
+
skip(id7, "not_founding");
|
|
4568
4813
|
continue;
|
|
4569
4814
|
}
|
|
4570
4815
|
if (member.cohort !== policy2.cohortLabel) {
|
|
4571
|
-
skip(
|
|
4816
|
+
skip(id7, "cohort_mismatch");
|
|
4572
4817
|
continue;
|
|
4573
4818
|
}
|
|
4574
4819
|
const skuId = String(member.baseSkuId);
|
|
4575
4820
|
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
4576
|
-
skip(
|
|
4821
|
+
skip(id7, "sku_not_discounted");
|
|
4577
4822
|
continue;
|
|
4578
4823
|
}
|
|
4579
4824
|
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4580
4825
|
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
4581
4826
|
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4582
|
-
skip(
|
|
4827
|
+
skip(id7, "would_not_widen");
|
|
4583
4828
|
continue;
|
|
4584
4829
|
}
|
|
4585
4830
|
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
@@ -4607,22 +4852,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4607
4852
|
} catch (error) {
|
|
4608
4853
|
const code = error.code;
|
|
4609
4854
|
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4610
|
-
skip(
|
|
4855
|
+
skip(id7, "provider_mismatch");
|
|
4611
4856
|
continue;
|
|
4612
4857
|
}
|
|
4613
4858
|
throw error;
|
|
4614
4859
|
}
|
|
4615
4860
|
try {
|
|
4616
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
4861
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
4617
4862
|
discountPolicyVersionId: policy2.id,
|
|
4618
4863
|
discountSnapshot: policy2,
|
|
4619
4864
|
renewalAmountCents: renewal,
|
|
4620
4865
|
revision: Number(member.revision) + 1,
|
|
4621
4866
|
updatedAt: now
|
|
4622
4867
|
})], {
|
|
4623
|
-
mutationId: `membership-reterm:${
|
|
4868
|
+
mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
|
|
4624
4869
|
guards: [
|
|
4625
|
-
{ ns: "networkMembership", id:
|
|
4870
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4626
4871
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4627
4872
|
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4628
4873
|
]
|
|
@@ -4632,7 +4877,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4632
4877
|
return membershipFail("membership_reterm_retry_required");
|
|
4633
4878
|
}
|
|
4634
4879
|
receipt.retermed.push({
|
|
4635
|
-
membershipId:
|
|
4880
|
+
membershipId: id7,
|
|
4636
4881
|
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4637
4882
|
previousRenewalAmountCents: current,
|
|
4638
4883
|
renewalAmountCents: renewal,
|
|
@@ -4668,7 +4913,7 @@ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
|
4668
4913
|
return json({ error: BAD }, 400);
|
|
4669
4914
|
}
|
|
4670
4915
|
const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
|
|
4671
|
-
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((
|
|
4916
|
+
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)) {
|
|
4672
4917
|
return json({ error: BAD }, 400);
|
|
4673
4918
|
}
|
|
4674
4919
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
@@ -4943,12 +5188,12 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4943
5188
|
if (!app || app.clerkUserId !== user.userId || app.namedSeatId || app.giftClaimId) return json({ error: "membership is not eligible to restart" }, 409);
|
|
4944
5189
|
if (app.membershipRestartId && app.status === "submitted") return json({ applicationId: app.id });
|
|
4945
5190
|
try {
|
|
4946
|
-
const
|
|
5191
|
+
const id7 = String(app.id), intent = await membershipLoad(db, "subscriptionCheckoutIntents", id7);
|
|
4947
5192
|
if (!intent || !app.stripeSubscriptionId || intent.providerSubscriptionId !== app.stripeSubscriptionId) return json({ error: "membership history needs reconciliation" }, 409);
|
|
4948
5193
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
4949
5194
|
const restart = await ctx.membershipAuthority.restart({ ...target, previousSubscriptionId: String(app.stripeSubscriptionId) }, env);
|
|
4950
|
-
if (restart.status !== "ready" || restart.applicationId !==
|
|
4951
|
-
const now = Date.now(), quote = await membershipLoad(db, "membershipQuoteProjections",
|
|
5195
|
+
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");
|
|
5196
|
+
const now = Date.now(), quote = await membershipLoad(db, "membershipQuoteProjections", id7);
|
|
4952
5197
|
const attrs = {
|
|
4953
5198
|
status: "submitted",
|
|
4954
5199
|
membershipRestartId: restart.restartId,
|
|
@@ -4968,8 +5213,8 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4968
5213
|
quoteSnapshot: quote ?? {},
|
|
4969
5214
|
archivedAt: now
|
|
4970
5215
|
} },
|
|
4971
|
-
{ t: "delete", ns: "subscriptionCheckoutIntents", id:
|
|
4972
|
-
{ t: "retract", ns: "applications", id:
|
|
5216
|
+
{ t: "delete", ns: "subscriptionCheckoutIntents", id: id7 },
|
|
5217
|
+
{ t: "retract", ns: "applications", id: id7, attrs: [
|
|
4973
5218
|
"stripeSubscriptionId",
|
|
4974
5219
|
"canceled",
|
|
4975
5220
|
"renewalAt",
|
|
@@ -4980,28 +5225,28 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4980
5225
|
"approvalEffectsOrigin",
|
|
4981
5226
|
"approvalEffectsLastAttemptAt"
|
|
4982
5227
|
] },
|
|
4983
|
-
{ t: "update", ns: "applications", id:
|
|
4984
|
-
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id:
|
|
5228
|
+
{ t: "update", ns: "applications", id: id7, attrs },
|
|
5229
|
+
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id: id7, attrs: { status: "restart", updatedAt: now } }] : []
|
|
4985
5230
|
];
|
|
4986
5231
|
try {
|
|
4987
5232
|
await db.transact(ops, { mutationId: `membership-restart-application:${restart.restartId}`, guards: [
|
|
4988
|
-
{ ns: "applications", id:
|
|
5233
|
+
{ ns: "applications", id: id7, exists: true, equals: {
|
|
4989
5234
|
clerkUserId: user.userId,
|
|
4990
5235
|
groupId: ctx.chapter.id,
|
|
4991
5236
|
status: app.status,
|
|
4992
5237
|
stripeSubscriptionId: app.stripeSubscriptionId
|
|
4993
5238
|
} },
|
|
4994
|
-
{ ns: "subscriptionCheckoutIntents", id:
|
|
5239
|
+
{ ns: "subscriptionCheckoutIntents", id: id7, exists: true, equals: { providerSubscriptionId: app.stripeSubscriptionId } },
|
|
4995
5240
|
{ ns: "membershipCheckoutHistory", id: restart.restartId, exists: false },
|
|
4996
|
-
{ ns: "membershipQuoteProjections", id:
|
|
5241
|
+
{ ns: "membershipQuoteProjections", id: id7, exists: Boolean(quote), ...quote ? { equals: { revision: quote.revision, status: quote.status } } : {} }
|
|
4997
5242
|
] });
|
|
4998
5243
|
} catch (error) {
|
|
4999
5244
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5000
5245
|
}
|
|
5001
|
-
const saved = await membershipLoad(db, "applications",
|
|
5246
|
+
const saved = await membershipLoad(db, "applications", id7);
|
|
5002
5247
|
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");
|
|
5003
5248
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: saved, stage: String(saved.status) }).catch(() => void 0);
|
|
5004
|
-
return json({ applicationId:
|
|
5249
|
+
return json({ applicationId: id7 });
|
|
5005
5250
|
} catch {
|
|
5006
5251
|
return json({ error: "We could not prepare your new subscription. Your account is saved. Please retry." }, 503);
|
|
5007
5252
|
}
|
|
@@ -5132,8 +5377,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5132
5377
|
if (app.requireDisclaimerAck && !acked) {
|
|
5133
5378
|
return { ok: false, error: "disclaimerAck is required" };
|
|
5134
5379
|
}
|
|
5135
|
-
const
|
|
5136
|
-
const row2 = { id:
|
|
5380
|
+
const id7 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
5381
|
+
const row2 = { id: id7, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
5137
5382
|
for (const f of [...app.required, ...app.optional]) {
|
|
5138
5383
|
if (fieldStates[f]?.visible === false) continue;
|
|
5139
5384
|
if (typeof fields[f] === "string") row2[f] = fields[f].trim();
|
|
@@ -5167,12 +5412,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5167
5412
|
}
|
|
5168
5413
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
5169
5414
|
const ops = [
|
|
5170
|
-
{ t: "update", ns: "applications", id:
|
|
5415
|
+
{ t: "update", ns: "applications", id: id7, attrs: row2 }
|
|
5171
5416
|
];
|
|
5172
5417
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
5173
5418
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
5174
5419
|
consumedAt: opts.now,
|
|
5175
|
-
consumedApplicationId:
|
|
5420
|
+
consumedApplicationId: id7
|
|
5176
5421
|
} });
|
|
5177
5422
|
}
|
|
5178
5423
|
try {
|
|
@@ -5180,7 +5425,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5180
5425
|
ops,
|
|
5181
5426
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
5182
5427
|
);
|
|
5183
|
-
return { ok: true, id:
|
|
5428
|
+
return { ok: true, id: id7, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
5184
5429
|
} catch (error) {
|
|
5185
5430
|
if (error.code === "unique_violation") return {
|
|
5186
5431
|
ok: false,
|
|
@@ -5386,14 +5631,14 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5386
5631
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
5387
5632
|
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
5388
5633
|
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
5389
|
-
const
|
|
5634
|
+
const id7 = typeof grant.id === "string" ? grant.id : "";
|
|
5390
5635
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
5391
5636
|
const normalized = normalizeAdmissionEmail(email);
|
|
5392
5637
|
const decision = grant.decision;
|
|
5393
5638
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
5394
5639
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
5395
|
-
if (!ID2.test(
|
|
5396
|
-
if (seenIds.has(
|
|
5640
|
+
if (!ID2.test(id7)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
5641
|
+
if (seenIds.has(id7)) errors.push(`document.grants repeats id "${id7}"`);
|
|
5397
5642
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
5398
5643
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
5399
5644
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -5403,10 +5648,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5403
5648
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
5404
5649
|
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
5405
5650
|
}
|
|
5406
|
-
seenIds.add(
|
|
5651
|
+
seenIds.add(id7);
|
|
5407
5652
|
seenEmails.add(normalized);
|
|
5408
5653
|
grants.push({
|
|
5409
|
-
id:
|
|
5654
|
+
id: id7,
|
|
5410
5655
|
email,
|
|
5411
5656
|
decision,
|
|
5412
5657
|
...note ? { note } : {},
|
|
@@ -5478,8 +5723,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5478
5723
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
5479
5724
|
const ops = [];
|
|
5480
5725
|
for (const row2 of current) {
|
|
5481
|
-
const
|
|
5482
|
-
if (
|
|
5726
|
+
const id7 = String(row2.id ?? "");
|
|
5727
|
+
if (id7 && !incomingIds.has(id7)) ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5483
5728
|
active: false,
|
|
5484
5729
|
revokedAt: receivedAt,
|
|
5485
5730
|
revokedBy: document.source.actor,
|
|
@@ -5488,10 +5733,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5488
5733
|
} });
|
|
5489
5734
|
}
|
|
5490
5735
|
for (const grant of document.grants) {
|
|
5491
|
-
const
|
|
5492
|
-
const existing = current.find((row2) => row2.id ===
|
|
5493
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
5494
|
-
id:
|
|
5736
|
+
const id7 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
5737
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
5738
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5739
|
+
id: id7,
|
|
5495
5740
|
sourceGrantId: grant.id,
|
|
5496
5741
|
sourceScope: "parent",
|
|
5497
5742
|
sourceId: document.source.appId,
|
|
@@ -5512,7 +5757,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5512
5757
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
5513
5758
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
5514
5759
|
];
|
|
5515
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
5760
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id7, attrs: retract });
|
|
5516
5761
|
}
|
|
5517
5762
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
5518
5763
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -5543,9 +5788,9 @@ var APPLICATION_LABELS = {
|
|
|
5543
5788
|
phone: "Phone",
|
|
5544
5789
|
state: "State"
|
|
5545
5790
|
};
|
|
5546
|
-
function humanize(
|
|
5547
|
-
const text8 =
|
|
5548
|
-
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` :
|
|
5791
|
+
function humanize(id7) {
|
|
5792
|
+
const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5793
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
|
|
5549
5794
|
}
|
|
5550
5795
|
function applicationDefs(chapter) {
|
|
5551
5796
|
try {
|
|
@@ -5554,23 +5799,23 @@ function applicationDefs(chapter) {
|
|
|
5554
5799
|
return {};
|
|
5555
5800
|
}
|
|
5556
5801
|
}
|
|
5557
|
-
function applicationType(
|
|
5802
|
+
function applicationType(id7, def) {
|
|
5558
5803
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
5559
|
-
return
|
|
5804
|
+
return id7 === "email" ? "email" : id7 === "linkedin" ? "url" : "string";
|
|
5560
5805
|
}
|
|
5561
5806
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
5562
5807
|
const defs = applicationDefs(chapter);
|
|
5563
|
-
return [...application.required, ...application.optional].map((
|
|
5564
|
-
const def = defs[
|
|
5565
|
-
const conditions = application.conditions[
|
|
5566
|
-
const type = applicationType(
|
|
5808
|
+
return [...application.required, ...application.optional].map((id7, order2) => {
|
|
5809
|
+
const def = defs[id7];
|
|
5810
|
+
const conditions = application.conditions[id7];
|
|
5811
|
+
const type = applicationType(id7, def);
|
|
5567
5812
|
return {
|
|
5568
|
-
id:
|
|
5569
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
5813
|
+
id: id7,
|
|
5814
|
+
label: def?.label ?? APPLICATION_LABELS[id7] ?? humanize(id7),
|
|
5570
5815
|
type,
|
|
5571
|
-
required: application.required.includes(
|
|
5816
|
+
required: application.required.includes(id7),
|
|
5572
5817
|
order: order2,
|
|
5573
|
-
maxLength: application.maxLen[
|
|
5818
|
+
maxLength: application.maxLen[id7] ?? application.defaultMaxLen,
|
|
5574
5819
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
5575
5820
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
5576
5821
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -5580,17 +5825,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
5580
5825
|
function resolveFormationFlowFields(crm, formation) {
|
|
5581
5826
|
if (!formation.enabled) return [];
|
|
5582
5827
|
const def = crm.type(formation.type);
|
|
5583
|
-
return [...formation.required, ...formation.optional].map((
|
|
5584
|
-
const field = def.fields[
|
|
5828
|
+
return [...formation.required, ...formation.optional].map((id7, order2) => {
|
|
5829
|
+
const field = def.fields[id7];
|
|
5585
5830
|
return {
|
|
5586
|
-
id:
|
|
5587
|
-
label: field.label ?? humanize(
|
|
5831
|
+
id: id7,
|
|
5832
|
+
label: field.label ?? humanize(id7),
|
|
5588
5833
|
type: field.type,
|
|
5589
|
-
required: formation.required.includes(
|
|
5834
|
+
required: formation.required.includes(id7),
|
|
5590
5835
|
order: order2,
|
|
5591
|
-
maxLength: formation.maxLen[
|
|
5836
|
+
maxLength: formation.maxLen[id7] ?? formation.defaultMaxLen,
|
|
5592
5837
|
...field.options ? { options: [...field.options] } : {},
|
|
5593
|
-
...
|
|
5838
|
+
...id7 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
5594
5839
|
};
|
|
5595
5840
|
});
|
|
5596
5841
|
}
|
|
@@ -5640,10 +5885,10 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5640
5885
|
}
|
|
5641
5886
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5642
5887
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
5643
|
-
const
|
|
5644
|
-
if (
|
|
5645
|
-
if (ids2.has(
|
|
5646
|
-
ids2.add(
|
|
5888
|
+
const id7 = text3(item.id, `${path}.id`, errors, 80);
|
|
5889
|
+
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5890
|
+
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
5891
|
+
ids2.add(id7);
|
|
5647
5892
|
const targetRaw = record(item.target);
|
|
5648
5893
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
5649
5894
|
if (targetRaw?.type === "tier") {
|
|
@@ -5669,7 +5914,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5669
5914
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
5670
5915
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
5671
5916
|
offers.push({
|
|
5672
|
-
id:
|
|
5917
|
+
id: id7,
|
|
5673
5918
|
target,
|
|
5674
5919
|
billingMode: item.billingMode,
|
|
5675
5920
|
stripePriceId,
|
|
@@ -5769,9 +6014,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5769
6014
|
const ops = [];
|
|
5770
6015
|
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
5771
6016
|
for (const row2 of currentRows) {
|
|
5772
|
-
const
|
|
5773
|
-
if (
|
|
5774
|
-
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers",
|
|
6017
|
+
const id8 = String(row2.id ?? "");
|
|
6018
|
+
if (id8 && !incoming.has(id8)) {
|
|
6019
|
+
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers", id8), attrs: { active: false } });
|
|
5775
6020
|
}
|
|
5776
6021
|
}
|
|
5777
6022
|
for (const tier of document.tiers) {
|
|
@@ -5806,9 +6051,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5806
6051
|
if (!("stripePriceId" in attrs)) {
|
|
5807
6052
|
ops.push({ t: "retract", ns: "groups", id: groupRef, attrs: ["stripePriceId"] });
|
|
5808
6053
|
}
|
|
5809
|
-
const
|
|
5810
|
-
ops.push({ t: "update", ns: "signupControls", id:
|
|
5811
|
-
id:
|
|
6054
|
+
const id7 = `signup-control:${document.target.environment}:${document.revision}`;
|
|
6055
|
+
ops.push({ t: "update", ns: "signupControls", id: id7, attrs: {
|
|
6056
|
+
id: id7,
|
|
5812
6057
|
sourceId: document.source.appId,
|
|
5813
6058
|
targetId: document.target.chapterId,
|
|
5814
6059
|
environment: document.target.environment,
|
|
@@ -5847,6 +6092,8 @@ function foundingOf(offer) {
|
|
|
5847
6092
|
async function createPaymentQuote(input) {
|
|
5848
6093
|
const { group, tier, policy: policy2 } = input;
|
|
5849
6094
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
6095
|
+
if (input.seatOffer && (!terms2 || !Number.isSafeInteger(input.seatOffer.amountCents) || input.seatOffer.amountCents < 0) || input.namedSeat && !input.seatOffer) throw new Error("invalid checkout seat offer");
|
|
6096
|
+
const seatCents = input.namedSeat ? input.seatOffer.amountCents : 0;
|
|
5850
6097
|
const compareAt = input.presentation?.compareAtPriceCents;
|
|
5851
6098
|
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
5852
6099
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
@@ -5855,6 +6102,7 @@ async function createPaymentQuote(input) {
|
|
|
5855
6102
|
discountCents: tier.priceCents - dueToday,
|
|
5856
6103
|
dueTodayCents: dueToday
|
|
5857
6104
|
};
|
|
6105
|
+
if (!Number.isSafeInteger(lines.dueTodayCents + seatCents) || !Number.isSafeInteger(tier.priceCents + seatCents)) throw new Error("invalid checkout total");
|
|
5858
6106
|
const quote = {
|
|
5859
6107
|
applicationId: input.applicationId,
|
|
5860
6108
|
chapterId: input.chapterId,
|
|
@@ -5862,11 +6110,17 @@ async function createPaymentQuote(input) {
|
|
|
5862
6110
|
tierName: tier.name,
|
|
5863
6111
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
5864
6112
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
5865
|
-
lineItems:
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
6113
|
+
lineItems: {
|
|
6114
|
+
...presentation ? {
|
|
6115
|
+
standardCents: presentation.compareAtPriceCents,
|
|
6116
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
6117
|
+
dueTodayCents: lines.dueTodayCents
|
|
6118
|
+
} : lines,
|
|
6119
|
+
dueTodayCents: lines.dueTodayCents + seatCents,
|
|
6120
|
+
...input.namedSeat ? { namedSeatCents: seatCents } : {}
|
|
6121
|
+
},
|
|
6122
|
+
...input.seatOffer ? { seatOffer: input.seatOffer } : {},
|
|
6123
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {},
|
|
5870
6124
|
...presentation ? { presentation } : {},
|
|
5871
6125
|
refundPolicyText: policy2?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
5872
6126
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
@@ -5878,7 +6132,7 @@ async function createPaymentQuote(input) {
|
|
|
5878
6132
|
discountDuration: terms2.discount.duration === "once" ? "first_term" : "while_active"
|
|
5879
6133
|
} : {},
|
|
5880
6134
|
...terms2.initialTermEndsAt !== void 0 ? { initialTermEndsAt: terms2.initialTermEndsAt } : {},
|
|
5881
|
-
renewalCents: terms2.discount?.duration === "forever" ? dueToday : tier.priceCents,
|
|
6135
|
+
renewalCents: (terms2.discount?.duration === "forever" ? dueToday : tier.priceCents) + seatCents,
|
|
5882
6136
|
autoRenew: true
|
|
5883
6137
|
} } : {}
|
|
5884
6138
|
};
|
|
@@ -5898,22 +6152,23 @@ async function createPaymentQuote(input) {
|
|
|
5898
6152
|
// src/membership-quote-projection.ts
|
|
5899
6153
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
5900
6154
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
5901
|
-
var
|
|
6155
|
+
var fail6 = (code) => {
|
|
5902
6156
|
throw Object.assign(new Error(code), { code });
|
|
5903
6157
|
};
|
|
5904
|
-
var load3 = async (db, ns,
|
|
6158
|
+
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
5905
6159
|
async function restore2(row2, target, now, allowExpired = false) {
|
|
5906
|
-
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return
|
|
5907
|
-
if (row2.status !== "ready") return
|
|
6160
|
+
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");
|
|
6161
|
+
if (row2.status !== "ready") return fail6("membership_quote_pending");
|
|
5908
6162
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
5909
6163
|
...target,
|
|
5910
6164
|
requestId: row2.requestId
|
|
5911
6165
|
}), now, lifetimeMs: 6e4 });
|
|
5912
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6166
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
|
|
5913
6167
|
const tier = row2.tier, quote = row2.quote;
|
|
5914
6168
|
const offering = offer.contract.offering;
|
|
6169
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
|
|
5915
6170
|
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) {
|
|
5916
|
-
return
|
|
6171
|
+
return fail6("membership_catalog_mismatch");
|
|
5917
6172
|
}
|
|
5918
6173
|
const expected = await createPaymentQuote({
|
|
5919
6174
|
...target,
|
|
@@ -5922,19 +6177,21 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
5922
6177
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
5923
6178
|
billingTerms: offer.contract.billingTerms,
|
|
5924
6179
|
presentation: quote.presentation,
|
|
5925
|
-
founding: foundingOf(offer)
|
|
6180
|
+
founding: foundingOf(offer),
|
|
6181
|
+
seatOffer: quote.seatOffer,
|
|
6182
|
+
namedSeat: quote.namedSeat
|
|
5926
6183
|
});
|
|
5927
|
-
if (quote.digest !== expected.digest) return
|
|
6184
|
+
if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
|
|
5928
6185
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
5929
6186
|
}
|
|
5930
6187
|
async function readMembershipQuote(db, target, now) {
|
|
5931
6188
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, now);
|
|
5932
6189
|
}
|
|
5933
6190
|
async function prepareMembershipQuote(db, input, issue) {
|
|
5934
|
-
const { applicationId:
|
|
5935
|
-
if (app.id !==
|
|
5936
|
-
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
5937
|
-
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return
|
|
6191
|
+
const { applicationId: id7, now, app } = input;
|
|
6192
|
+
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");
|
|
6193
|
+
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6194
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
|
|
5938
6195
|
if (row2?.status === "ready") {
|
|
5939
6196
|
try {
|
|
5940
6197
|
return await restore2(row2, input, now);
|
|
@@ -5945,15 +6202,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5945
6202
|
const identity2 = ["groupId", "tierId", "stripeRuntime", "email", "emailNormalized", "status", "canceled", "membershipRestartId", "namedSeatId", "namedSeatClaimPending", "giftClaimId", "giftOfferId"];
|
|
5946
6203
|
const guards = [{
|
|
5947
6204
|
ns: "applications",
|
|
5948
|
-
id:
|
|
6205
|
+
id: id7,
|
|
5949
6206
|
exists: true,
|
|
5950
6207
|
equals: Object.fromEntries(identity2.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
5951
6208
|
absent: identity2.filter((key3) => app[key3] === void 0)
|
|
5952
|
-
}, { ns: "subscriptionCheckoutIntents", id:
|
|
6209
|
+
}, { ns: "subscriptionCheckoutIntents", id: id7, exists: false }];
|
|
5953
6210
|
if (!row2 || row2.status === "ready" || row2.status === "restart") {
|
|
5954
6211
|
const revision = Number(row2?.revision ?? 0) + 1;
|
|
5955
6212
|
const pending = {
|
|
5956
|
-
id:
|
|
6213
|
+
id: id7,
|
|
5957
6214
|
...Object.fromEntries(keys2.map((key3) => [key3, input[key3]])),
|
|
5958
6215
|
revision,
|
|
5959
6216
|
status: "pending",
|
|
@@ -5961,25 +6218,25 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5961
6218
|
updatedAt: now
|
|
5962
6219
|
};
|
|
5963
6220
|
try {
|
|
5964
|
-
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id:
|
|
5965
|
-
mutationId: `membership-quote:${
|
|
6221
|
+
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id: id7, attrs: pending }], {
|
|
6222
|
+
mutationId: `membership-quote:${id7}:${revision}`,
|
|
5966
6223
|
guards: [...guards, {
|
|
5967
6224
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
5968
|
-
id:
|
|
6225
|
+
id: id7,
|
|
5969
6226
|
exists: Boolean(row2),
|
|
5970
6227
|
...row2 ? { equals: { revision: row2.revision, status: row2.status } } : {}
|
|
5971
6228
|
}]
|
|
5972
6229
|
});
|
|
5973
6230
|
} catch (error) {
|
|
5974
6231
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5975
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6232
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
5976
6233
|
if (!winner || winner.revision !== revision) throw error;
|
|
5977
6234
|
}
|
|
5978
|
-
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6235
|
+
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
5979
6236
|
}
|
|
5980
6237
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
5981
6238
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
5982
|
-
return
|
|
6239
|
+
return fail6("membership_quote_pending");
|
|
5983
6240
|
}
|
|
5984
6241
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
5985
6242
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6000,7 +6257,8 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6000
6257
|
policy: input.policy,
|
|
6001
6258
|
billingTerms: offer.contract.billingTerms,
|
|
6002
6259
|
presentation: input.presentation,
|
|
6003
|
-
founding: foundingOf(offer)
|
|
6260
|
+
founding: foundingOf(offer),
|
|
6261
|
+
...input.namedSeatsEnabled ? { seatOffer: { amountCents: offer.contract.offering.seatPriceCents } } : {}
|
|
6004
6262
|
});
|
|
6005
6263
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
6006
6264
|
await restore2(ready, target, now, true);
|
|
@@ -6008,23 +6266,100 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6008
6266
|
await db.transact([{
|
|
6009
6267
|
t: "update",
|
|
6010
6268
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6011
|
-
id:
|
|
6269
|
+
id: id7,
|
|
6012
6270
|
attrs: { offer, offerDigest: offer.digest, tier, quote, status: "ready", updatedAt: now }
|
|
6013
6271
|
}], {
|
|
6014
|
-
mutationId: `membership-quote-ready:${
|
|
6272
|
+
mutationId: `membership-quote-ready:${id7}:${row2.revision}`,
|
|
6015
6273
|
guards: [...guards, {
|
|
6016
6274
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6017
|
-
id:
|
|
6275
|
+
id: id7,
|
|
6018
6276
|
exists: true,
|
|
6019
6277
|
equals: { status: "pending", revision: row2.revision, requestId: row2.requestId }
|
|
6020
6278
|
}]
|
|
6021
6279
|
});
|
|
6022
6280
|
} catch (error) {
|
|
6023
6281
|
if (error.code !== "transact_guard_failed") throw error;
|
|
6024
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6282
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6025
6283
|
if (winner?.status !== "ready" || winner.requestId !== row2.requestId || winner.offerDigest !== offer.digest) throw error;
|
|
6026
6284
|
}
|
|
6027
|
-
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6285
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6286
|
+
}
|
|
6287
|
+
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6288
|
+
if (!input.enabled) return fail6("named_seat_purchase_unavailable");
|
|
6289
|
+
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6290
|
+
const current = await restore2(row2, target, input.now);
|
|
6291
|
+
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
6292
|
+
const quote = await createPaymentQuote({
|
|
6293
|
+
...target,
|
|
6294
|
+
tier: current.tier,
|
|
6295
|
+
group: { merchantDisclosureText: current.quote.merchantDisclosureText },
|
|
6296
|
+
policy: { refundPolicyText: current.quote.refundPolicyText, trustCopy: current.quote.trustCopy },
|
|
6297
|
+
billingTerms: current.billingTerms,
|
|
6298
|
+
presentation: current.quote.presentation,
|
|
6299
|
+
founding: current.quote.founding,
|
|
6300
|
+
seatOffer: { amountCents: current.membershipOffer.contract.offering.seatPriceCents },
|
|
6301
|
+
namedSeat
|
|
6302
|
+
});
|
|
6303
|
+
if (quote.digest === current.quote.digest) return current;
|
|
6304
|
+
await db.transact([{
|
|
6305
|
+
t: "update",
|
|
6306
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6307
|
+
id: target.applicationId,
|
|
6308
|
+
attrs: { quote, revision: current.projectionRevision + 1, updatedAt: input.now }
|
|
6309
|
+
}], {
|
|
6310
|
+
mutationId: `membership-quote-selection:${target.applicationId}:${current.projectionRevision}:${quote.digest}`,
|
|
6311
|
+
guards: [
|
|
6312
|
+
{
|
|
6313
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6314
|
+
id: target.applicationId,
|
|
6315
|
+
exists: true,
|
|
6316
|
+
equals: { revision: current.projectionRevision, status: "ready" }
|
|
6317
|
+
},
|
|
6318
|
+
{ ns: "subscriptionCheckoutIntents", id: target.applicationId, exists: false }
|
|
6319
|
+
]
|
|
6320
|
+
});
|
|
6321
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, input.now);
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
// src/bounded-request-text.ts
|
|
6325
|
+
async function readBoundedRequestText(req, maxBytes) {
|
|
6326
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
6327
|
+
const reader = req.body?.getReader();
|
|
6328
|
+
const cancel = () => {
|
|
6329
|
+
void reader?.cancel().catch(() => {
|
|
6330
|
+
});
|
|
6331
|
+
};
|
|
6332
|
+
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
6333
|
+
let complete = false;
|
|
6334
|
+
req.signal.addEventListener("abort", cancel, { once: true });
|
|
6335
|
+
try {
|
|
6336
|
+
if (req.signal.aborted) return invalid2();
|
|
6337
|
+
const declared = req.headers.get("content-length");
|
|
6338
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
6339
|
+
return json({ error: "request body too large" }, 413);
|
|
6340
|
+
}
|
|
6341
|
+
if (!reader) return "";
|
|
6342
|
+
const decoder = new TextDecoder();
|
|
6343
|
+
let bytes = 0;
|
|
6344
|
+
let text8 = "";
|
|
6345
|
+
for (; ; ) {
|
|
6346
|
+
const { done, value } = await reader.read();
|
|
6347
|
+
if (req.signal.aborted) return invalid2();
|
|
6348
|
+
if (done) {
|
|
6349
|
+
complete = true;
|
|
6350
|
+
return text8 + decoder.decode();
|
|
6351
|
+
}
|
|
6352
|
+
bytes += value.byteLength;
|
|
6353
|
+
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6354
|
+
text8 += decoder.decode(value, { stream: true });
|
|
6355
|
+
}
|
|
6356
|
+
} catch {
|
|
6357
|
+
return invalid2();
|
|
6358
|
+
} finally {
|
|
6359
|
+
req.signal.removeEventListener("abort", cancel);
|
|
6360
|
+
if (!complete) cancel();
|
|
6361
|
+
reader?.releaseLock();
|
|
6362
|
+
}
|
|
6028
6363
|
}
|
|
6029
6364
|
|
|
6030
6365
|
// src/payment-quote-route.ts
|
|
@@ -6058,8 +6393,11 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6058
6393
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
6059
6394
|
billingTerms,
|
|
6060
6395
|
presentation: quote2.presentation,
|
|
6061
|
-
founding: foundingOf(intent.membershipOffer)
|
|
6396
|
+
founding: foundingOf(intent.membershipOffer),
|
|
6397
|
+
seatOffer: quote2.seatOffer,
|
|
6398
|
+
namedSeat: quote2.namedSeat
|
|
6062
6399
|
});
|
|
6400
|
+
if (quote2.seatOffer && quote2.seatOffer.amountCents !== intent.membershipOffer?.contract.offering.seatPriceCents) throw new Error("invalid seat snapshot");
|
|
6063
6401
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
6064
6402
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
6065
6403
|
}
|
|
@@ -6088,7 +6426,16 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6088
6426
|
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
6089
6427
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
6090
6428
|
db,
|
|
6091
|
-
{
|
|
6429
|
+
{
|
|
6430
|
+
...target,
|
|
6431
|
+
app,
|
|
6432
|
+
group,
|
|
6433
|
+
tier,
|
|
6434
|
+
policy: policy2,
|
|
6435
|
+
presentation,
|
|
6436
|
+
namedSeatsEnabled: Boolean(ctx.membershipAuthority.namedSeats) && !app.membershipRestartId,
|
|
6437
|
+
now: Date.now()
|
|
6438
|
+
},
|
|
6092
6439
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
6093
6440
|
);
|
|
6094
6441
|
const quote = await createPaymentQuote({
|
|
@@ -6104,20 +6451,54 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6104
6451
|
});
|
|
6105
6452
|
return { tier, quote, billingTerms: void 0 };
|
|
6106
6453
|
}
|
|
6107
|
-
async function paymentQuoteRoute(url, env, ctx, prepare = false) {
|
|
6108
|
-
|
|
6109
|
-
if (
|
|
6454
|
+
async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
6455
|
+
let selection;
|
|
6456
|
+
if (request?.body) {
|
|
6457
|
+
try {
|
|
6458
|
+
const text8 = await readBoundedRequestText(request, 2048);
|
|
6459
|
+
if (text8 instanceof Response) return text8;
|
|
6460
|
+
const body = JSON.parse(text8);
|
|
6461
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6462
|
+
selection = body.namedSeat;
|
|
6463
|
+
} catch {
|
|
6464
|
+
return json({ error: "invalid seat selection" }, 400);
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
const id7 = url.searchParams.get("application");
|
|
6468
|
+
if (!id7 || id7.length > 160) return json({ error: "application required" }, 400);
|
|
6110
6469
|
const db = ctx.makeDb(env);
|
|
6111
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
6470
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
6112
6471
|
if (!app || String(app.groupId ?? ctx.chapter.id) !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
6113
6472
|
if (typeof app.stripeRuntime === "string" && app.stripeRuntime !== stripeRuntimeIdentity(env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
6114
6473
|
if (app.status !== "submitted" || app.giftOfferId || app.namedSeatId || app.namedSeatClaimPending || app.giftClaimId || app.canceled === true) return json({ error: "membership checkout unavailable" }, 409);
|
|
6115
6474
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
6116
6475
|
if (!group) return json({ error: "payments not configured" }, 503);
|
|
6117
6476
|
try {
|
|
6118
|
-
|
|
6119
|
-
|
|
6477
|
+
let { quote } = await checkoutQuoteForApplication(db, app, group, env, ctx, prepare);
|
|
6478
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
6479
|
+
if (prepare && !intent && !quote.seatOffer && ctx.membershipAuthority?.namedSeats && !app.membershipRestartId && selection === void 0) selection = null;
|
|
6480
|
+
if (selection !== void 0) {
|
|
6481
|
+
if (intent) return json({ error: "checkout has already started", code: "checkout_intent_mismatch" }, 409);
|
|
6482
|
+
({ quote } = await selectCheckoutNamedSeat(
|
|
6483
|
+
db,
|
|
6484
|
+
{
|
|
6485
|
+
appId: env.ODLA_APP_ID,
|
|
6486
|
+
chapterId: ctx.chapter.id,
|
|
6487
|
+
environment: env.ODLA_ENV,
|
|
6488
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
6489
|
+
applicationId: id7,
|
|
6490
|
+
skuId: String(app.tierId)
|
|
6491
|
+
},
|
|
6492
|
+
{ selection, purchaserEmail: String(app.email ?? ""), now: Date.now(), enabled: Boolean(ctx.membershipAuthority?.namedSeats) && !app.membershipRestartId }
|
|
6493
|
+
));
|
|
6494
|
+
}
|
|
6495
|
+
return new Response(JSON.stringify(quote), { headers: {
|
|
6496
|
+
"content-type": "application/json",
|
|
6497
|
+
"cache-control": "no-store",
|
|
6498
|
+
"x-chapter-checkout-started": intent ? "true" : "false"
|
|
6499
|
+
} });
|
|
6120
6500
|
} catch (error) {
|
|
6501
|
+
if (error.code === "invalid_named_seat_recipient") return json({ error: "Enter a different recipient's name and email." }, 400);
|
|
6121
6502
|
const expired = error.code === "membership_quote_expired";
|
|
6122
6503
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_expired" } : {} }, expired ? 409 : 503);
|
|
6123
6504
|
}
|
|
@@ -6223,47 +6604,6 @@ async function resumeJoin(url, env, ctx) {
|
|
|
6223
6604
|
return json({ error: "Application continuation is unavailable. Retry when its payment or membership configuration is restored." }, 409);
|
|
6224
6605
|
}
|
|
6225
6606
|
|
|
6226
|
-
// src/bounded-request-text.ts
|
|
6227
|
-
async function readBoundedRequestText(req, maxBytes) {
|
|
6228
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
6229
|
-
const reader = req.body?.getReader();
|
|
6230
|
-
const cancel = () => {
|
|
6231
|
-
void reader?.cancel().catch(() => {
|
|
6232
|
-
});
|
|
6233
|
-
};
|
|
6234
|
-
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
6235
|
-
let complete = false;
|
|
6236
|
-
req.signal.addEventListener("abort", cancel, { once: true });
|
|
6237
|
-
try {
|
|
6238
|
-
if (req.signal.aborted) return invalid2();
|
|
6239
|
-
const declared = req.headers.get("content-length");
|
|
6240
|
-
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
6241
|
-
return json({ error: "request body too large" }, 413);
|
|
6242
|
-
}
|
|
6243
|
-
if (!reader) return "";
|
|
6244
|
-
const decoder = new TextDecoder();
|
|
6245
|
-
let bytes = 0;
|
|
6246
|
-
let text8 = "";
|
|
6247
|
-
for (; ; ) {
|
|
6248
|
-
const { done, value } = await reader.read();
|
|
6249
|
-
if (req.signal.aborted) return invalid2();
|
|
6250
|
-
if (done) {
|
|
6251
|
-
complete = true;
|
|
6252
|
-
return text8 + decoder.decode();
|
|
6253
|
-
}
|
|
6254
|
-
bytes += value.byteLength;
|
|
6255
|
-
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6256
|
-
text8 += decoder.decode(value, { stream: true });
|
|
6257
|
-
}
|
|
6258
|
-
} catch {
|
|
6259
|
-
return invalid2();
|
|
6260
|
-
} finally {
|
|
6261
|
-
req.signal.removeEventListener("abort", cancel);
|
|
6262
|
-
if (!complete) cancel();
|
|
6263
|
-
reader?.releaseLock();
|
|
6264
|
-
}
|
|
6265
|
-
}
|
|
6266
|
-
|
|
6267
6607
|
// src/named-seat-application.ts
|
|
6268
6608
|
async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
|
|
6269
6609
|
const app = await membershipLoad(db, "applications", applicationId);
|
|
@@ -7158,10 +7498,10 @@ function tiersOf(value, errors) {
|
|
|
7158
7498
|
return [];
|
|
7159
7499
|
}
|
|
7160
7500
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7161
|
-
const
|
|
7162
|
-
if (
|
|
7163
|
-
if (seen.has(
|
|
7164
|
-
seen.add(
|
|
7501
|
+
const id7 = text4(item.id, `${path}.id`, errors, 80);
|
|
7502
|
+
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7503
|
+
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7504
|
+
seen.add(id7);
|
|
7165
7505
|
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
7166
7506
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7167
7507
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
@@ -7182,7 +7522,7 @@ function tiersOf(value, errors) {
|
|
|
7182
7522
|
}
|
|
7183
7523
|
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7184
7524
|
return [{
|
|
7185
|
-
id:
|
|
7525
|
+
id: id7,
|
|
7186
7526
|
name,
|
|
7187
7527
|
available: item.available === true,
|
|
7188
7528
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -7244,15 +7584,15 @@ function policyOf(value, errors, tiers) {
|
|
|
7244
7584
|
if (input.tierPolicies !== void 0) {
|
|
7245
7585
|
const overrides = recordOf(input.tierPolicies);
|
|
7246
7586
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
7247
|
-
else for (const [
|
|
7248
|
-
const path = `document.policy.tierPolicies.${
|
|
7587
|
+
else for (const [id7, raw2] of Object.entries(overrides)) {
|
|
7588
|
+
const path = `document.policy.tierPolicies.${id7}`;
|
|
7249
7589
|
const policy2 = recordOf(raw2);
|
|
7250
|
-
if (!tiers.some((tier) => tier.id ===
|
|
7590
|
+
if (!tiers.some((tier) => tier.id === id7) || !policy2) {
|
|
7251
7591
|
errors.push(`${path} must name a declared tier and policy`);
|
|
7252
7592
|
continue;
|
|
7253
7593
|
}
|
|
7254
7594
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7255
|
-
tierPolicies[
|
|
7595
|
+
tierPolicies[id7] = {
|
|
7256
7596
|
refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
7257
7597
|
...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7258
7598
|
};
|
|
@@ -7545,14 +7885,14 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7545
7885
|
continue;
|
|
7546
7886
|
}
|
|
7547
7887
|
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
7548
|
-
const
|
|
7888
|
+
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7549
7889
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7550
7890
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7551
|
-
if (
|
|
7891
|
+
if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7552
7892
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7553
|
-
if (ids2.has(
|
|
7893
|
+
if (ids2.has(id7) || knownParentIds.has(id7)) errors.push(`${path}.id duplicates page "${id7}"`);
|
|
7554
7894
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
7555
|
-
ids2.add(
|
|
7895
|
+
ids2.add(id7);
|
|
7556
7896
|
slugs.add(slug);
|
|
7557
7897
|
const navRaw = record2(raw2.navigation);
|
|
7558
7898
|
let navigation = { section: "General", order: index2 };
|
|
@@ -7608,7 +7948,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7608
7948
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
7609
7949
|
}
|
|
7610
7950
|
}
|
|
7611
|
-
pages.push({ id:
|
|
7951
|
+
pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
7612
7952
|
}
|
|
7613
7953
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
7614
7954
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -7751,20 +8091,20 @@ async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
|
7751
8091
|
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7752
8092
|
}
|
|
7753
8093
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
7754
|
-
const
|
|
7755
|
-
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id:
|
|
8094
|
+
const id7 = `member-content-local:${environment}`;
|
|
8095
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
|
|
7756
8096
|
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
7757
8097
|
}
|
|
7758
8098
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
7759
8099
|
const { document, digest } = envelope;
|
|
7760
8100
|
const target = document.target;
|
|
7761
|
-
const
|
|
8101
|
+
const id7 = `member-content-control:${target.environment}:${document.revision}`;
|
|
7762
8102
|
const ops = [];
|
|
7763
8103
|
if (previous?.active) {
|
|
7764
8104
|
ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
|
|
7765
8105
|
}
|
|
7766
|
-
ops.push({ t: "update", ns: "memberContentControls", id:
|
|
7767
|
-
id:
|
|
8106
|
+
ops.push({ t: "update", ns: "memberContentControls", id: id7, attrs: {
|
|
8107
|
+
id: id7,
|
|
7768
8108
|
sourceId: document.source.appId,
|
|
7769
8109
|
targetId: target.chapterId,
|
|
7770
8110
|
environment: target.environment,
|
|
@@ -7777,11 +8117,11 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
7777
8117
|
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
|
|
7778
8118
|
}
|
|
7779
8119
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7780
|
-
const
|
|
8120
|
+
const id7 = `member-content-local:${environment}`;
|
|
7781
8121
|
const current = await loadLocalMemberContent(db, document.source.appId, environment);
|
|
7782
8122
|
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
|
|
7783
8123
|
const row2 = {
|
|
7784
|
-
id:
|
|
8124
|
+
id: id7,
|
|
7785
8125
|
chapterId: document.source.appId,
|
|
7786
8126
|
environment,
|
|
7787
8127
|
revision: document.revision,
|
|
@@ -7789,9 +8129,9 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
|
|
|
7789
8129
|
updatedAt,
|
|
7790
8130
|
...updatedBy ? { updatedBy } : {}
|
|
7791
8131
|
};
|
|
7792
|
-
await db.transact([{ t: "update", ns: "memberContentLocal", id:
|
|
8132
|
+
await db.transact([{ t: "update", ns: "memberContentLocal", id: id7, attrs: row2 }], {
|
|
7793
8133
|
mutationId: `member-content-local:${environment}:${document.revision}`,
|
|
7794
|
-
guards: current ? [{ ns: "memberContentLocal", id:
|
|
8134
|
+
guards: current ? [{ ns: "memberContentLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id: id7, exists: false }]
|
|
7795
8135
|
});
|
|
7796
8136
|
return row2;
|
|
7797
8137
|
}
|
|
@@ -7928,7 +8268,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
7928
8268
|
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
7929
8269
|
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
7930
8270
|
effective,
|
|
7931
|
-
tiers: tiers.map(({ id:
|
|
8271
|
+
tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
|
|
7932
8272
|
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
7933
8273
|
});
|
|
7934
8274
|
}
|
|
@@ -8086,11 +8426,11 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8086
8426
|
return json({ error: "expiresAt must be a future epoch timestamp" }, 400);
|
|
8087
8427
|
}
|
|
8088
8428
|
const sourceGrantId = crypto.randomUUID();
|
|
8089
|
-
const
|
|
8429
|
+
const id7 = `admission:local:${ctx.chapter.id}:${sourceGrantId}`;
|
|
8090
8430
|
const createdAt = Date.now();
|
|
8091
8431
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8092
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8093
|
-
id:
|
|
8432
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8433
|
+
id: id7,
|
|
8094
8434
|
sourceGrantId,
|
|
8095
8435
|
sourceScope: "local",
|
|
8096
8436
|
sourceId: ctx.chapter.id,
|
|
@@ -8106,7 +8446,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8106
8446
|
updatedAt: createdAt,
|
|
8107
8447
|
updatedBy: actor
|
|
8108
8448
|
} }], { mutationId: `admission-local-create:${sourceGrantId}` });
|
|
8109
|
-
return json({ grant: { id:
|
|
8449
|
+
return json({ grant: { id: id7, sourceGrantId, sourceScope: "local", email, decision, active: true, createdAt, ...note ? { note } : {}, ...expiresAt ? { expiresAt } : {} } }, 201);
|
|
8110
8450
|
}
|
|
8111
8451
|
const prefix = "/api/admin/admission-grants/";
|
|
8112
8452
|
let revokeId = "";
|
|
@@ -8120,20 +8460,20 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8120
8460
|
if (/^admission:local:[a-z0-9-]+:[0-9a-f-]+$/.test(revokeId) && req.method === "DELETE") {
|
|
8121
8461
|
const auth = await gate(req, env, ctx);
|
|
8122
8462
|
if (auth instanceof Response) return auth;
|
|
8123
|
-
const
|
|
8124
|
-
const found = await auth.db.query({ admissionGrants: { $: { where: { id:
|
|
8463
|
+
const id7 = revokeId;
|
|
8464
|
+
const found = await auth.db.query({ admissionGrants: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8125
8465
|
const row2 = Array.isArray(found.admissionGrants) ? found.admissionGrants[0] : void 0;
|
|
8126
8466
|
if (!row2 || row2.targetId !== ctx.chapter.id || row2.sourceScope !== "local") return json({ error: "not found" }, 404);
|
|
8127
8467
|
if (row2.active !== true) return json({ ok: true, replayed: true });
|
|
8128
8468
|
const revokedAt = Date.now();
|
|
8129
8469
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8130
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8470
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8131
8471
|
active: false,
|
|
8132
8472
|
revokedAt,
|
|
8133
8473
|
revokedBy: actor,
|
|
8134
8474
|
updatedAt: revokedAt,
|
|
8135
8475
|
updatedBy: actor
|
|
8136
|
-
} }], { mutationId: `admission-local-revoke:${
|
|
8476
|
+
} }], { mutationId: `admission-local-revoke:${id7}` });
|
|
8137
8477
|
return json({ ok: true, replayed: false });
|
|
8138
8478
|
}
|
|
8139
8479
|
return null;
|
|
@@ -8195,8 +8535,8 @@ async function loadLeaderAdmissionAssignments(db, targetId) {
|
|
|
8195
8535
|
return await readAdmissionRows(db, "leaderAdmissionAssignments", { targetId }, MAX_ASSIGNMENTS);
|
|
8196
8536
|
}
|
|
8197
8537
|
async function loadLeaderAdmissionDelivery(db, targetId, environment) {
|
|
8198
|
-
const
|
|
8199
|
-
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id:
|
|
8538
|
+
const id7 = deliveryId(targetId, environment);
|
|
8539
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8200
8540
|
return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
|
|
8201
8541
|
}
|
|
8202
8542
|
function personAdmissionEmail(ctx, record4) {
|
|
@@ -8224,8 +8564,8 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8224
8564
|
const now = Date.now();
|
|
8225
8565
|
const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
8226
8566
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
8227
|
-
const
|
|
8228
|
-
const existing = current.find((row2) => row2.id ===
|
|
8567
|
+
const id7 = assignmentId(opts.target.id, opts.personId);
|
|
8568
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
8229
8569
|
const digest = await signupControlDigest({
|
|
8230
8570
|
targetId: opts.target.id,
|
|
8231
8571
|
personId: opts.personId,
|
|
@@ -8255,7 +8595,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8255
8595
|
return { delivery: saved, duplicate: true };
|
|
8256
8596
|
}
|
|
8257
8597
|
const assignment = {
|
|
8258
|
-
id:
|
|
8598
|
+
id: id7,
|
|
8259
8599
|
targetId: opts.target.id,
|
|
8260
8600
|
personId: opts.personId,
|
|
8261
8601
|
grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
@@ -8270,7 +8610,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8270
8610
|
updatedAt: now,
|
|
8271
8611
|
updatedBy: opts.actor
|
|
8272
8612
|
};
|
|
8273
|
-
const desired = current.filter((row2) => row2.id !==
|
|
8613
|
+
const desired = current.filter((row2) => row2.id !== id7 && row2.active).concat(assignment.active ? [assignment] : []);
|
|
8274
8614
|
const grants = await Promise.all(desired.map((row2) => grantInput(opts.db, opts.ctx, row2, opts.resolveEmail)));
|
|
8275
8615
|
const revision = (previous?.revision ?? 0) + 1;
|
|
8276
8616
|
const envelope = await createAdmissionGrantEnvelope({
|
|
@@ -8293,7 +8633,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8293
8633
|
updatedAt: now
|
|
8294
8634
|
};
|
|
8295
8635
|
const ops = [
|
|
8296
|
-
{ t: "update", ns: "leaderAdmissionAssignments", id:
|
|
8636
|
+
{ t: "update", ns: "leaderAdmissionAssignments", id: id7, attrs: assignment },
|
|
8297
8637
|
{ t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
|
|
8298
8638
|
{ t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
|
|
8299
8639
|
id: mutationReceiptId,
|
|
@@ -8308,7 +8648,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8308
8648
|
...!opts.note && existing?.note !== void 0 ? ["note"] : [],
|
|
8309
8649
|
...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
8310
8650
|
];
|
|
8311
|
-
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id:
|
|
8651
|
+
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id: id7, attrs: retract });
|
|
8312
8652
|
const tx = await opts.db.transact(ops, {
|
|
8313
8653
|
mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
|
|
8314
8654
|
guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
|
|
@@ -8368,11 +8708,11 @@ function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
|
|
|
8368
8708
|
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
8369
8709
|
);
|
|
8370
8710
|
const def = crm.type(record4.type);
|
|
8371
|
-
const editFields = editable.map((
|
|
8372
|
-
const field = def.fields[
|
|
8711
|
+
const editFields = editable.map((id7) => {
|
|
8712
|
+
const field = def.fields[id7];
|
|
8373
8713
|
return {
|
|
8374
|
-
id:
|
|
8375
|
-
label: field.label ??
|
|
8714
|
+
id: id7,
|
|
8715
|
+
label: field.label ?? id7,
|
|
8376
8716
|
type: field.type,
|
|
8377
8717
|
required: field.required ?? false,
|
|
8378
8718
|
...field.options ? { options: field.options } : {}
|
|
@@ -8489,8 +8829,8 @@ async function gate2(req, env, ctx) {
|
|
|
8489
8829
|
if (!await networkWriteAllowed(req, db, user, ctx, true)) return { response: reject6(403, "super_admin_required", "super-admin required") };
|
|
8490
8830
|
return { db, user };
|
|
8491
8831
|
}
|
|
8492
|
-
function targetFor(ctx,
|
|
8493
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
8832
|
+
function targetFor(ctx, id7) {
|
|
8833
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7 && target.admissionGrants);
|
|
8494
8834
|
}
|
|
8495
8835
|
async function deliverSaved(db, env, ctx, target, delivery) {
|
|
8496
8836
|
if (delivery.status === "acknowledged") return delivery;
|
|
@@ -8594,7 +8934,7 @@ var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
|
8594
8934
|
if (!personId || !MUTATION_ID2.test(mutationId)) {
|
|
8595
8935
|
return reject6(422, "invalid_request", "personId and a stable mutationId are required");
|
|
8596
8936
|
}
|
|
8597
|
-
const resolveEmail = (
|
|
8937
|
+
const resolveEmail = (id7) => resolveAdmissionPersonEmail(db, env, ctx, target, id7);
|
|
8598
8938
|
let email;
|
|
8599
8939
|
try {
|
|
8600
8940
|
email = await resolveEmail(personId);
|
|
@@ -8833,28 +9173,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8833
9173
|
};
|
|
8834
9174
|
|
|
8835
9175
|
// src/payments-stripe.ts
|
|
8836
|
-
import { stripeRequest as
|
|
9176
|
+
import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
8837
9177
|
|
|
8838
9178
|
// src/payments-refund.ts
|
|
8839
|
-
import { stripeRequest as
|
|
9179
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
8840
9180
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8841
|
-
var
|
|
9181
|
+
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8842
9182
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
8843
|
-
function
|
|
9183
|
+
function fail7(code = "refund_target_mismatch") {
|
|
8844
9184
|
throw Object.assign(new Error(code), { code });
|
|
8845
9185
|
}
|
|
8846
9186
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8847
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9187
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
|
|
8848
9188
|
const call = async (method, path, params, key3) => {
|
|
8849
|
-
const result = await
|
|
8850
|
-
if (!result.ok)
|
|
8851
|
-
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body))
|
|
9189
|
+
const result = await stripeRequest8({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9190
|
+
if (!result.ok) fail7("stripe_error");
|
|
9191
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
|
|
8852
9192
|
return result.body;
|
|
8853
9193
|
};
|
|
8854
9194
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8855
|
-
if (
|
|
9195
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
|
|
8856
9196
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
8857
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9197
|
+
if (row(sub.metadata)[key3] !== value) fail7();
|
|
8858
9198
|
}
|
|
8859
9199
|
let cursor;
|
|
8860
9200
|
let earliest;
|
|
@@ -8867,79 +9207,79 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8867
9207
|
limit: 100,
|
|
8868
9208
|
...cursor ? { starting_after: cursor } : {}
|
|
8869
9209
|
});
|
|
8870
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9210
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
|
|
8871
9211
|
for (const value of list.data) {
|
|
8872
9212
|
const invoice2 = row(value);
|
|
8873
9213
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
8874
|
-
if (!providerId2(
|
|
8875
|
-
seen.add(
|
|
9214
|
+
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();
|
|
9215
|
+
seen.add(id3(invoice2));
|
|
8876
9216
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
8877
9217
|
}
|
|
8878
9218
|
if (!list.has_more) break;
|
|
8879
|
-
if (!list.data.length || page === 9)
|
|
8880
|
-
cursor =
|
|
9219
|
+
if (!list.data.length || page === 9) fail7("refund_history_incomplete");
|
|
9220
|
+
cursor = id3(list.data[list.data.length - 1]);
|
|
8881
9221
|
}
|
|
8882
|
-
if (!earliest)
|
|
9222
|
+
if (!earliest) fail7("no_charge");
|
|
8883
9223
|
const invoice = earliest;
|
|
8884
|
-
if (input.expectedInvoiceId !== void 0 &&
|
|
8885
|
-
let chargeId =
|
|
8886
|
-
let paymentIntentId =
|
|
9224
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
|
|
9225
|
+
let chargeId = id3(invoice.charge);
|
|
9226
|
+
let paymentIntentId = id3(invoice.payment_intent);
|
|
8887
9227
|
if (!chargeId && !paymentIntentId) {
|
|
8888
|
-
const payments = await call("GET", "/v1/invoice_payments", { invoice:
|
|
8889
|
-
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1)
|
|
9228
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
|
|
9229
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
|
|
8890
9230
|
const payment = row(payments.data[0]);
|
|
8891
|
-
if (
|
|
8892
|
-
paymentIntentId =
|
|
9231
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
|
|
9232
|
+
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
8893
9233
|
}
|
|
8894
9234
|
if (!chargeId) {
|
|
8895
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9235
|
+
if (!providerId2(paymentIntentId, "pi")) fail7();
|
|
8896
9236
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
8897
|
-
if (
|
|
8898
|
-
chargeId =
|
|
9237
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
|
|
9238
|
+
chargeId = id3(payment.latest_charge);
|
|
8899
9239
|
}
|
|
8900
|
-
if (!providerId2(chargeId, "ch"))
|
|
9240
|
+
if (!providerId2(chargeId, "ch")) fail7();
|
|
8901
9241
|
const charge = await call("GET", `/v1/charges/${chargeId}`);
|
|
8902
|
-
if (
|
|
8903
|
-
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true))
|
|
9242
|
+
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();
|
|
9243
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
|
|
8904
9244
|
if (charge.amount_refunded !== charge.amount) {
|
|
8905
9245
|
const refund = await call("POST", "/v1/refunds", {
|
|
8906
9246
|
charge: chargeId,
|
|
8907
9247
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
8908
9248
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
8909
|
-
if (
|
|
9249
|
+
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");
|
|
8910
9250
|
}
|
|
8911
9251
|
let subscriptionCanceled = sub.status === "canceled";
|
|
8912
9252
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
8913
9253
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8914
|
-
if (
|
|
9254
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
|
|
8915
9255
|
subscriptionCanceled = true;
|
|
8916
9256
|
}
|
|
8917
9257
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
8918
|
-
if (
|
|
9258
|
+
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");
|
|
8919
9259
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
8920
9260
|
}
|
|
8921
9261
|
|
|
8922
9262
|
// src/subscription-payment-identity.ts
|
|
8923
|
-
import { stripeRequest as
|
|
8924
|
-
var
|
|
9263
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
9264
|
+
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8925
9265
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8926
|
-
let paymentId =
|
|
9266
|
+
let paymentId = id4(invoice.payment_intent);
|
|
8927
9267
|
if (!paymentId) {
|
|
8928
|
-
if (!/^in_[A-Za-z0-9]+$/.test(
|
|
8929
|
-
const result2 = await
|
|
9268
|
+
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9269
|
+
const result2 = await stripeRequest9({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
8930
9270
|
const list = result2.body;
|
|
8931
9271
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8932
9272
|
const link = list.data[0];
|
|
8933
|
-
if (
|
|
8934
|
-
paymentId =
|
|
9273
|
+
if (id4(link.invoice) !== id4(invoice) || !["open", "paid"].includes(link.status) || link.payment?.type !== "payment_intent") subscriptionContractMismatch();
|
|
9274
|
+
paymentId = id4(link.payment.payment_intent);
|
|
8935
9275
|
}
|
|
8936
9276
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8937
9277
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8938
|
-
const result = await
|
|
9278
|
+
const result = await stripeRequest9({ secretKey }, "GET", path);
|
|
8939
9279
|
const payment = result.body;
|
|
8940
|
-
if (!result.ok || payment?.id !== paymentId ||
|
|
9280
|
+
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();
|
|
8941
9281
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8942
|
-
const tagged = await
|
|
9282
|
+
const tagged = await stripeRequest9(
|
|
8943
9283
|
{ secretKey },
|
|
8944
9284
|
"POST",
|
|
8945
9285
|
path,
|
|
@@ -8951,7 +9291,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8951
9291
|
|
|
8952
9292
|
// src/payments-stripe.ts
|
|
8953
9293
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8954
|
-
return
|
|
9294
|
+
return stripeRequest10(
|
|
8955
9295
|
{ secretKey: sk },
|
|
8956
9296
|
method,
|
|
8957
9297
|
path,
|
|
@@ -8959,7 +9299,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
8959
9299
|
idempotencyKey ? { idempotencyKey } : {}
|
|
8960
9300
|
);
|
|
8961
9301
|
}
|
|
8962
|
-
function
|
|
9302
|
+
function fail8(op, r) {
|
|
8963
9303
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
8964
9304
|
err.code = "stripe_error";
|
|
8965
9305
|
throw err;
|
|
@@ -8998,6 +9338,15 @@ function createStripeProvider(config) {
|
|
|
8998
9338
|
let customerId = input.existingCustomerId;
|
|
8999
9339
|
if (customerId && !/^cus_[A-Za-z0-9]+$/.test(customerId) || input.existingSubscriptionId && (!customerId || !/^sub_[A-Za-z0-9]+$/.test(input.existingSubscriptionId))) subscriptionContractMismatch();
|
|
9000
9340
|
const productId = await verifySubscriptionPrice(sk, input, Boolean(input.existingSubscriptionId));
|
|
9341
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
9342
|
+
const seatProductId = seat ? await verifySubscriptionPrice(sk, {
|
|
9343
|
+
...input,
|
|
9344
|
+
priceId: seat.priceId,
|
|
9345
|
+
priceCents: seat.amountCents,
|
|
9346
|
+
billingTerms: void 0,
|
|
9347
|
+
namedSeat: void 0
|
|
9348
|
+
}, Boolean(input.existingSubscriptionId)) : void 0;
|
|
9349
|
+
if (seat && (!seatProductId || !productId || seatProductId === productId)) subscriptionContractMismatch();
|
|
9001
9350
|
if (input.billingTerms?.discount && !productId) subscriptionContractMismatch();
|
|
9002
9351
|
await prepareSubscriptionBillingTerms(sk, { ...input, discountProductId: productId }, Boolean(input.existingSubscriptionId));
|
|
9003
9352
|
const billingParams = subscriptionBillingParams(input, productId);
|
|
@@ -9009,7 +9358,7 @@ function createStripeProvider(config) {
|
|
|
9009
9358
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9010
9359
|
`cus:${input.applicationId}`
|
|
9011
9360
|
);
|
|
9012
|
-
if (!cust.ok)
|
|
9361
|
+
if (!cust.ok) fail8("customer create", cust);
|
|
9013
9362
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9014
9363
|
customerId = cust.body.id;
|
|
9015
9364
|
}
|
|
@@ -9029,19 +9378,32 @@ function createStripeProvider(config) {
|
|
|
9029
9378
|
{
|
|
9030
9379
|
customer: customerId,
|
|
9031
9380
|
"items[0][price]": input.priceId,
|
|
9381
|
+
...seat ? {
|
|
9382
|
+
"items[1][price]": seat.priceId,
|
|
9383
|
+
"items[1][quantity]": 1,
|
|
9384
|
+
"items[1][metadata][seatId]": seat.seatId,
|
|
9385
|
+
"items[1][metadata][membershipId]": seat.membershipId
|
|
9386
|
+
} : {},
|
|
9032
9387
|
payment_behavior: "default_incomplete",
|
|
9033
9388
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
9034
9389
|
"payment_settings[payment_method_types][0]": "card",
|
|
9035
9390
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
9036
9391
|
"expand[1]": "latest_invoice.payment_intent",
|
|
9037
9392
|
metadata: meta,
|
|
9038
|
-
...billingParams
|
|
9393
|
+
...billingParams,
|
|
9394
|
+
...seat && input.billingTerms?.initialTermEndsAt ? {
|
|
9395
|
+
"add_invoice_items[1][price_data][product]": seatProductId,
|
|
9396
|
+
"add_invoice_items[1][price_data][currency]": input.currency,
|
|
9397
|
+
"add_invoice_items[1][price_data][unit_amount]": seat.amountCents,
|
|
9398
|
+
"add_invoice_items[1][quantity]": 1,
|
|
9399
|
+
"add_invoice_items[1][discountable]": false
|
|
9400
|
+
} : {}
|
|
9039
9401
|
},
|
|
9040
9402
|
// Hardening: one subscription per application, so a client
|
|
9041
9403
|
// retry between create and the db write can't orphan a second one.
|
|
9042
9404
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9043
9405
|
);
|
|
9044
|
-
if (!sub.ok)
|
|
9406
|
+
if (!sub.ok) fail8("subscription create", sub);
|
|
9045
9407
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9046
9408
|
const subscriptionId = sub.body.id;
|
|
9047
9409
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9051,14 +9413,14 @@ function createStripeProvider(config) {
|
|
|
9051
9413
|
"expand[2]": "discounts",
|
|
9052
9414
|
"expand[3]": "latest_invoice.discounts"
|
|
9053
9415
|
});
|
|
9054
|
-
if (!expanded.ok)
|
|
9416
|
+
if (!expanded.ok) fail8("subscription invoice evidence", expanded);
|
|
9055
9417
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9056
9418
|
sub = expanded;
|
|
9057
9419
|
}
|
|
9058
9420
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9059
9421
|
const invoice = sub.body.latest_invoice;
|
|
9060
9422
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9061
|
-
if (!clientSecret2)
|
|
9423
|
+
if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
|
|
9062
9424
|
await tagSubscriptionPayment(
|
|
9063
9425
|
sk,
|
|
9064
9426
|
invoice,
|
|
@@ -9270,9 +9632,9 @@ function matchesIntent(stored, expected) {
|
|
|
9270
9632
|
"purchaseAttemptId"
|
|
9271
9633
|
].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;
|
|
9272
9634
|
}
|
|
9273
|
-
async function loadIntent(db,
|
|
9635
|
+
async function loadIntent(db, id7) {
|
|
9274
9636
|
return first3((await db.query({
|
|
9275
|
-
subscriptionCheckoutIntents: { $: { where: { id:
|
|
9637
|
+
subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } }
|
|
9276
9638
|
})).subscriptionCheckoutIntents);
|
|
9277
9639
|
}
|
|
9278
9640
|
async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCheckout) {
|
|
@@ -9293,16 +9655,22 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9293
9655
|
if (input.purchaseAttemptId !== application?.membershipRestartId) mismatch();
|
|
9294
9656
|
if (application?.namedSeatId || application?.namedSeatClaimPending || application?.giftClaimId) mismatch();
|
|
9295
9657
|
if (input.membershipOffer && (!application || application.canceled === true || application.status !== "submitted")) mismatch();
|
|
9658
|
+
if (input.paymentQuote?.namedSeat && application?.email !== input.email) mismatch();
|
|
9296
9659
|
const applicationGuard = {
|
|
9297
9660
|
ns: "applications",
|
|
9298
9661
|
id: input.applicationId,
|
|
9299
9662
|
exists: true,
|
|
9300
|
-
equals: {
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
...
|
|
9305
|
-
|
|
9663
|
+
equals: {
|
|
9664
|
+
status: "submitted",
|
|
9665
|
+
...input.paymentQuote?.namedSeat ? { email: input.email } : {},
|
|
9666
|
+
...input.purchaseAttemptId ? { membershipRestartId: input.purchaseAttemptId } : {},
|
|
9667
|
+
...input.membershipOffer ? {
|
|
9668
|
+
groupId: input.groupId,
|
|
9669
|
+
tierId: input.tierId,
|
|
9670
|
+
stripeRuntime: stripeRuntimeIdentity(input.runtime),
|
|
9671
|
+
...application?.canceled !== void 0 ? { canceled: application.canceled } : {}
|
|
9672
|
+
} : {}
|
|
9673
|
+
},
|
|
9306
9674
|
absent: ["namedSeatId", "namedSeatClaimPending", "giftClaimId", ...!input.purchaseAttemptId ? ["membershipRestartId"] : [], ...input.membershipOffer && application?.canceled === void 0 ? ["canceled"] : []]
|
|
9307
9675
|
};
|
|
9308
9676
|
let stored = await loadIntent(db, input.applicationId);
|
|
@@ -9326,7 +9694,11 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9326
9694
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
9327
9695
|
id: input.applicationId,
|
|
9328
9696
|
exists: true,
|
|
9329
|
-
equals: {
|
|
9697
|
+
equals: {
|
|
9698
|
+
status: "ready",
|
|
9699
|
+
offerDigest: input.membershipOffer.digest,
|
|
9700
|
+
...input.projectionRevision !== void 0 ? { revision: input.projectionRevision } : {}
|
|
9701
|
+
}
|
|
9330
9702
|
}
|
|
9331
9703
|
] : []
|
|
9332
9704
|
]
|
|
@@ -9341,7 +9713,8 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9341
9713
|
if (!stored.providerSubscriptionId && input.now - Number(stored.createdAt) >= 23 * 60 * 6e4) {
|
|
9342
9714
|
throw Object.assign(new Error("checkout requires provider reconciliation"), { code: "checkout_recovery_required" });
|
|
9343
9715
|
}
|
|
9344
|
-
|
|
9716
|
+
const binding = input.membershipOffer ? await authorizeNetworkCheckout() : void 0;
|
|
9717
|
+
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();
|
|
9345
9718
|
const result = await provider.createSubscription({
|
|
9346
9719
|
appId: input.appId,
|
|
9347
9720
|
chapterId: input.chapterId,
|
|
@@ -9356,6 +9729,7 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9356
9729
|
priceCents: input.priceCents,
|
|
9357
9730
|
currency: input.currency,
|
|
9358
9731
|
interval: input.interval,
|
|
9732
|
+
...binding?.namedSeat ? { namedSeat: binding.namedSeat } : {},
|
|
9359
9733
|
...stored.billingTerms ? { billingTerms: stored.billingTerms } : {},
|
|
9360
9734
|
...typeof stored.providerSubscriptionId === "string" ? { existingSubscriptionId: stored.providerSubscriptionId } : {},
|
|
9361
9735
|
existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
|
|
@@ -9472,6 +9846,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9472
9846
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_changed" } : {} }, expired ? 409 : 503);
|
|
9473
9847
|
}
|
|
9474
9848
|
const { tier, quote, billingTerms } = checkout2;
|
|
9849
|
+
if (quote.namedSeat && body.seatTermsAck !== true) return json({ error: "seatTermsAck required" }, 400);
|
|
9475
9850
|
const membershipOffer = "membershipOffer" in checkout2 ? checkout2.membershipOffer : void 0;
|
|
9476
9851
|
if (ctx.membershipAuthority && !membershipOffer) return json({ error: "checkout requires reconciliation", code: "checkout_recovery_required" }, 409);
|
|
9477
9852
|
if (!tier) return json({ error: "tier not found", code: "tier_unresolved" }, 400);
|
|
@@ -9496,6 +9871,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9496
9871
|
priceCents: billingTerms ? tier.priceCents : quote.lineItems.dueTodayCents,
|
|
9497
9872
|
...billingTerms ? { billingTerms } : {},
|
|
9498
9873
|
...membershipOffer ? { membershipOffer } : {},
|
|
9874
|
+
..."projectionRevision" in checkout2 ? { projectionRevision: checkout2.projectionRevision } : {},
|
|
9499
9875
|
...ctx.requirePaymentQuote || ctx.membershipAuthority || body.quoteDigest ? { paymentQuote: quote } : {},
|
|
9500
9876
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
9501
9877
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
@@ -9517,9 +9893,10 @@ async function startSubscription(req, env, ctx) {
|
|
|
9517
9893
|
offerId: membershipOffer.id,
|
|
9518
9894
|
offerDigest: membershipOffer.digest,
|
|
9519
9895
|
quoteDigest: quote.digest,
|
|
9520
|
-
priceId: String(priceId)
|
|
9896
|
+
priceId: String(priceId),
|
|
9897
|
+
...quote.namedSeat ? { namedSeat: { ...quote.namedSeat, purchaserEmail: normalizeChapterEmail(String(app.email ?? "")) } } : {}
|
|
9521
9898
|
};
|
|
9522
|
-
|
|
9899
|
+
return verifyMembershipCheckoutBinding(await ctx.membershipAuthority.bindCheckout(binding, env), binding);
|
|
9523
9900
|
} : void 0);
|
|
9524
9901
|
} catch (err) {
|
|
9525
9902
|
const code = paymentErrorCode(err);
|
|
@@ -9541,7 +9918,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
9541
9918
|
var handlePayments = async (req, url, env, ctx) => {
|
|
9542
9919
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
9543
9920
|
if (req.method === "GET" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx);
|
|
9544
|
-
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true);
|
|
9921
|
+
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true, req);
|
|
9545
9922
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
9546
9923
|
if (req.method === "POST" && url.pathname === "/api/webhooks/stripe") return ingestWebhook(req, env, ctx);
|
|
9547
9924
|
return null;
|
|
@@ -9555,16 +9932,16 @@ var userFor = async (req, env, ctx) => {
|
|
|
9555
9932
|
const user = await ctx.verifyUser(req, env);
|
|
9556
9933
|
return user?.email ? user : null;
|
|
9557
9934
|
};
|
|
9558
|
-
var loadClaim = async (db,
|
|
9935
|
+
var loadClaim = async (db, id7) => first4((await db.query({ giftClaims: { $: { where: { id: id7 }, limit: 1 } } })).giftClaims);
|
|
9559
9936
|
async function consumeRateLimit(db, chapterId, requester, now) {
|
|
9560
9937
|
const bucket = await giftRequesterBucket(chapterId, requester);
|
|
9561
|
-
const
|
|
9562
|
-
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id:
|
|
9938
|
+
const id7 = `gift-rate:${chapterId}:${bucket}`;
|
|
9939
|
+
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id: id7 }, limit: 1 } } })).giftRedemptionAttempts);
|
|
9563
9940
|
const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
|
|
9564
9941
|
const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
|
|
9565
9942
|
if (attemptCount > 10) return false;
|
|
9566
|
-
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id:
|
|
9567
|
-
id:
|
|
9943
|
+
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id: id7, attrs: {
|
|
9944
|
+
id: id7,
|
|
9568
9945
|
chapterId,
|
|
9569
9946
|
bucket,
|
|
9570
9947
|
attemptCount,
|
|
@@ -9738,14 +10115,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
|
9738
10115
|
async function readNamedSeatOffer(db, input, now) {
|
|
9739
10116
|
membershipClock(now);
|
|
9740
10117
|
await loadMembershipAuthorityState(db, input);
|
|
9741
|
-
const
|
|
10118
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
9742
10119
|
requireMembershipScope(member, input);
|
|
9743
10120
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
9744
10121
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
9745
10122
|
if (seat) requireMembershipScope(seat, input);
|
|
9746
10123
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
9747
10124
|
const offer = {
|
|
9748
|
-
membershipId:
|
|
10125
|
+
membershipId: id7,
|
|
9749
10126
|
amountCents: offering.seatPriceCents,
|
|
9750
10127
|
currency: offering.currency,
|
|
9751
10128
|
interval: offering.interval,
|
|
@@ -10114,18 +10491,18 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
10114
10491
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10115
10492
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10116
10493
|
try {
|
|
10117
|
-
const
|
|
10494
|
+
const id7 = decodeURIComponent(match[1]);
|
|
10118
10495
|
const body = await namedSeatRequest(req);
|
|
10119
10496
|
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))) {
|
|
10120
10497
|
return json({ error: "invalid delivery evidence" }, 400);
|
|
10121
10498
|
}
|
|
10122
|
-
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog",
|
|
10499
|
+
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog", id7);
|
|
10123
10500
|
if (!row2 || row2.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
10124
10501
|
if (row2.applicationId) {
|
|
10125
10502
|
const app = await membershipLoad(chapterDb, "applications", String(row2.applicationId));
|
|
10126
10503
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10127
10504
|
}
|
|
10128
|
-
await reconcileLifecycleDelivery(chapterDb, { ...body, id:
|
|
10505
|
+
await reconcileLifecycleDelivery(chapterDb, { ...body, id: id7, groupId: ctx.chapter.id, actorId: user.userId }, Date.now());
|
|
10129
10506
|
return json({ ok: true, sent: false });
|
|
10130
10507
|
} catch {
|
|
10131
10508
|
return json({ error: "delivery evidence is invalid, stale, or still in progress; reload the exact attempt" }, 409);
|
|
@@ -10228,13 +10605,13 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10228
10605
|
const user = await ctx.verifyUser(req, env);
|
|
10229
10606
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10230
10607
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10231
|
-
let
|
|
10608
|
+
let id7;
|
|
10232
10609
|
try {
|
|
10233
|
-
|
|
10610
|
+
id7 = dedicated ? url.searchParams.get("recordId") ?? "" : decodeURIComponent(match[1]);
|
|
10234
10611
|
} catch {
|
|
10235
10612
|
return json({ error: "invalid record id" }, 422);
|
|
10236
10613
|
}
|
|
10237
|
-
const record4 = await getRecord3({ crm: ctx.chapter.crm, db },
|
|
10614
|
+
const record4 = await getRecord3({ crm: ctx.chapter.crm, db }, id7);
|
|
10238
10615
|
if (!record4) return json({ error: "not found" }, 404);
|
|
10239
10616
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
10240
10617
|
if (match?.[2]) {
|
|
@@ -10249,9 +10626,9 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10249
10626
|
}
|
|
10250
10627
|
if (req.method === "GET" && dedicated) {
|
|
10251
10628
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
10252
|
-
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([
|
|
10253
|
-
id:
|
|
10254
|
-
label: field.label ??
|
|
10629
|
+
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id8]) => !excluded.includes(id8)).map(([id8, field]) => ({
|
|
10630
|
+
id: id8,
|
|
10631
|
+
label: field.label ?? id8,
|
|
10255
10632
|
type: field.type,
|
|
10256
10633
|
required: field.required ?? false,
|
|
10257
10634
|
...field.options ? { options: field.options } : {}
|
|
@@ -11022,12 +11399,12 @@ var readJson = async (req) => {
|
|
|
11022
11399
|
return null;
|
|
11023
11400
|
}
|
|
11024
11401
|
};
|
|
11025
|
-
async function loadApp(db,
|
|
11026
|
-
const { applications } = await db.query({ applications: { $: { where: { id:
|
|
11402
|
+
async function loadApp(db, id7) {
|
|
11403
|
+
const { applications } = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11027
11404
|
return applications?.[0] ?? null;
|
|
11028
11405
|
}
|
|
11029
|
-
async function loadGroup(db,
|
|
11030
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11406
|
+
async function loadGroup(db, id7) {
|
|
11407
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11031
11408
|
return groups?.[0] ?? null;
|
|
11032
11409
|
}
|
|
11033
11410
|
var handleAdminApplicationActions = async (req, url, env, ctx) => {
|
|
@@ -11113,8 +11490,8 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
11113
11490
|
const got = await gate4(req, env, ctx);
|
|
11114
11491
|
if (got instanceof Response) return got;
|
|
11115
11492
|
const db = got;
|
|
11116
|
-
const
|
|
11117
|
-
const app = await loadApp(db,
|
|
11493
|
+
const id7 = m[1];
|
|
11494
|
+
const app = await loadApp(db, id7);
|
|
11118
11495
|
if (!app) return json({ error: "not found" }, 404);
|
|
11119
11496
|
const result = await approveApplication({ db, env, ctx, app, origin: url.origin, reason: "manual" });
|
|
11120
11497
|
if (!result.ok) return json({ error: result.error }, result.status);
|
|
@@ -11143,10 +11520,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
11143
11520
|
const got = await gate4(req, env, ctx);
|
|
11144
11521
|
if (got instanceof Response) return got;
|
|
11145
11522
|
const db = got;
|
|
11146
|
-
const
|
|
11523
|
+
const id7 = m[1];
|
|
11147
11524
|
const body = await readJson(req);
|
|
11148
11525
|
if (!body) return json({ error: "invalid JSON body" }, 400);
|
|
11149
|
-
const app = await loadApp(db,
|
|
11526
|
+
const app = await loadApp(db, id7);
|
|
11150
11527
|
if (!app) return json({ error: "not found" }, 404);
|
|
11151
11528
|
return patchApplication(db, env, ctx, app, body);
|
|
11152
11529
|
};
|
|
@@ -11256,10 +11633,10 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
11256
11633
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
11257
11634
|
await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
11258
11635
|
} else {
|
|
11259
|
-
const
|
|
11636
|
+
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11260
11637
|
await db.transact([
|
|
11261
|
-
{ t: "merge", ns: "groups", id:
|
|
11262
|
-
{ t: "update", ns: "groups", id:
|
|
11638
|
+
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
|
|
11639
|
+
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11263
11640
|
]);
|
|
11264
11641
|
}
|
|
11265
11642
|
return json({ ok: true });
|
|
@@ -11274,8 +11651,8 @@ async function gate5(req, env, ctx) {
|
|
|
11274
11651
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
11275
11652
|
return rawDb;
|
|
11276
11653
|
}
|
|
11277
|
-
async function loadGroup2(db,
|
|
11278
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11654
|
+
async function loadGroup2(db, id7) {
|
|
11655
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11279
11656
|
return groups?.[0] ?? null;
|
|
11280
11657
|
}
|
|
11281
11658
|
var str3 = (v, d = "") => typeof v === "string" ? v : d;
|
|
@@ -11529,7 +11906,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11529
11906
|
if ("response" in got) return got.response;
|
|
11530
11907
|
return json({
|
|
11531
11908
|
targets: ctx.chapter.network.targets.map(({
|
|
11532
|
-
id:
|
|
11909
|
+
id: id7,
|
|
11533
11910
|
name,
|
|
11534
11911
|
url: targetUrl,
|
|
11535
11912
|
fields,
|
|
@@ -11538,7 +11915,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11538
11915
|
stageTransitions,
|
|
11539
11916
|
commercialParity
|
|
11540
11917
|
}) => ({
|
|
11541
|
-
id:
|
|
11918
|
+
id: id7,
|
|
11542
11919
|
name,
|
|
11543
11920
|
url: targetUrl,
|
|
11544
11921
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
@@ -11700,7 +12077,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
11700
12077
|
if (typeof body.recordId !== "string" || !Array.isArray(body.targetIds) || body.targetIds.length === 0) {
|
|
11701
12078
|
return json({ error: "recordId and a non-empty targetIds array are required" }, 400);
|
|
11702
12079
|
}
|
|
11703
|
-
const requested = new Set(body.targetIds.filter((
|
|
12080
|
+
const requested = new Set(body.targetIds.filter((id7) => typeof id7 === "string"));
|
|
11704
12081
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
11705
12082
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
11706
12083
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
@@ -11833,7 +12210,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
11833
12210
|
const appId = record4.fields.applicationId;
|
|
11834
12211
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
11835
12212
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
11836
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((
|
|
12213
|
+
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id7) => ({ id: id7, label: id7.replaceAll("_", " ") })) : result.edit.allowedStages.filter((stage) => !isApplicationStage(ctx, stage.id));
|
|
11837
12214
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
11838
12215
|
const meta = lock?.meta;
|
|
11839
12216
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -11938,9 +12315,9 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
11938
12315
|
// src/network-mutation-receipt.ts
|
|
11939
12316
|
import { CRM_NS as CRM_NS2 } from "@odla-ai/crm";
|
|
11940
12317
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
11941
|
-
async function priorMutation(db,
|
|
12318
|
+
async function priorMutation(db, id7) {
|
|
11942
12319
|
const result = await db.query({
|
|
11943
|
-
[CRM_NS2.activity]: { $: { where: { id:
|
|
12320
|
+
[CRM_NS2.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
11944
12321
|
});
|
|
11945
12322
|
const row2 = (result[CRM_NS2.activity] ?? [])[0];
|
|
11946
12323
|
const meta = row2?.meta;
|
|
@@ -12200,8 +12577,8 @@ async function gate7(req, env, ctx) {
|
|
|
12200
12577
|
if (!await networkWriteAllowed(req, db, user, ctx)) return { response: json({ error: "super-admin required", code: "super_admin_required" }, 403) };
|
|
12201
12578
|
return { db, user };
|
|
12202
12579
|
}
|
|
12203
|
-
function targetFor2(ctx,
|
|
12204
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
12580
|
+
function targetFor2(ctx, id7) {
|
|
12581
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12205
12582
|
}
|
|
12206
12583
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
12207
12584
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12336,7 +12713,7 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
12336
12713
|
if (ctx.chapter.network.requireSuperAdminWrites && !await ctx.isSuperAdminEmail(got.db, got.user.email, got.user.userId)) {
|
|
12337
12714
|
return json({ ...result.page, records: result.page.records.map(({ edit: _edit, ...record4 }) => ({
|
|
12338
12715
|
...record4,
|
|
12339
|
-
fields: Object.fromEntries(Object.entries(record4.fields).filter(([
|
|
12716
|
+
fields: Object.fromEntries(Object.entries(record4.fields).filter(([id7]) => id7 === "name" || id7 === "email"))
|
|
12340
12717
|
})) });
|
|
12341
12718
|
}
|
|
12342
12719
|
return json(result.page);
|
|
@@ -12406,9 +12783,9 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12406
12783
|
}
|
|
12407
12784
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
12408
12785
|
if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
|
|
12409
|
-
const
|
|
12786
|
+
const id7 = crypto.randomUUID();
|
|
12410
12787
|
const note = {
|
|
12411
|
-
id:
|
|
12788
|
+
id: id7,
|
|
12412
12789
|
targetId: identity2.targetId,
|
|
12413
12790
|
recordType: identity2.recordType,
|
|
12414
12791
|
recordId: identity2.recordId,
|
|
@@ -12420,7 +12797,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12420
12797
|
await got.db.transact([{
|
|
12421
12798
|
t: "update",
|
|
12422
12799
|
ns: "networkNotes",
|
|
12423
|
-
id:
|
|
12800
|
+
id: id7,
|
|
12424
12801
|
attrs: { ...note, recordKey: identity2.recordKey }
|
|
12425
12802
|
}]);
|
|
12426
12803
|
return json({ note }, 201);
|
|
@@ -12524,13 +12901,13 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12524
12901
|
return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
|
|
12525
12902
|
}
|
|
12526
12903
|
const createdAt = Date.now();
|
|
12527
|
-
const
|
|
12904
|
+
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
12528
12905
|
const result = await addActivity(
|
|
12529
12906
|
{
|
|
12530
12907
|
crm: ctx.chapter.crm,
|
|
12531
12908
|
db,
|
|
12532
12909
|
now: () => createdAt,
|
|
12533
|
-
newId: () =>
|
|
12910
|
+
newId: () => id7
|
|
12534
12911
|
},
|
|
12535
12912
|
{
|
|
12536
12913
|
recordId,
|
|
@@ -12545,11 +12922,11 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12545
12922
|
body
|
|
12546
12923
|
}
|
|
12547
12924
|
},
|
|
12548
|
-
mutationId:
|
|
12925
|
+
mutationId: id7
|
|
12549
12926
|
}
|
|
12550
12927
|
);
|
|
12551
12928
|
const note = {
|
|
12552
|
-
id:
|
|
12929
|
+
id: id7,
|
|
12553
12930
|
sourceId: verified.sender,
|
|
12554
12931
|
recordType,
|
|
12555
12932
|
recordId,
|
|
@@ -12558,8 +12935,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12558
12935
|
};
|
|
12559
12936
|
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
12560
12937
|
};
|
|
12561
|
-
function targetFor3(ctx,
|
|
12562
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
12938
|
+
function targetFor3(ctx, id7) {
|
|
12939
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12563
12940
|
}
|
|
12564
12941
|
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
12565
12942
|
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
@@ -12828,10 +13205,10 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12828
13205
|
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
12829
13206
|
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
12830
13207
|
}
|
|
12831
|
-
const
|
|
12832
|
-
const previous = state.links.find((link2) => link2.id ===
|
|
13208
|
+
const id7 = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
13209
|
+
const previous = state.links.find((link2) => link2.id === id7);
|
|
12833
13210
|
const link = {
|
|
12834
|
-
id:
|
|
13211
|
+
id: id7,
|
|
12835
13212
|
personId: person.id,
|
|
12836
13213
|
chapterId: input.chapterId,
|
|
12837
13214
|
chapterPersonId: input.chapterPersonId,
|
|
@@ -12842,11 +13219,11 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12842
13219
|
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
12843
13220
|
await db.transact([
|
|
12844
13221
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
12845
|
-
{ t: "update", ns: "networkChapterPersonLinks", id:
|
|
13222
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { ...link } },
|
|
12846
13223
|
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
12847
13224
|
], { mutationId, guards: [
|
|
12848
13225
|
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
12849
|
-
{ ns: "networkChapterPersonLinks", id:
|
|
13226
|
+
{ ns: "networkChapterPersonLinks", id: id7, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
12850
13227
|
] });
|
|
12851
13228
|
return { person: nextPerson, link };
|
|
12852
13229
|
}
|
|
@@ -12883,9 +13260,9 @@ var defaultPolicy = (now, consent) => ({
|
|
|
12883
13260
|
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
12884
13261
|
async function linkOps(personId, links, now) {
|
|
12885
13262
|
return Promise.all(links.map(async (link) => {
|
|
12886
|
-
const
|
|
13263
|
+
const id7 = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
12887
13264
|
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
12888
|
-
return { t: "update", ns: "networkChapterPersonLinks", id:
|
|
13265
|
+
return { t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { id: id7, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
12889
13266
|
}));
|
|
12890
13267
|
}
|
|
12891
13268
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -13104,10 +13481,10 @@ async function networkPersonBulkBatchDigest(rows5) {
|
|
|
13104
13481
|
}
|
|
13105
13482
|
async function networkPersonRegistryStateDigest(state) {
|
|
13106
13483
|
const snapshot = {
|
|
13107
|
-
persons: state.persons.map(({ id:
|
|
13108
|
-
claims: state.claims.map(({ id:
|
|
13109
|
-
aliases: state.aliases.map(({ id:
|
|
13110
|
-
links: state.links.map(({ id:
|
|
13484
|
+
persons: state.persons.map(({ id: id7, revision, state: personState, mergedIntoId }) => ({ id: id7, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13485
|
+
claims: state.claims.map(({ id: id7, personId }) => ({ id: id7, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13486
|
+
aliases: state.aliases.map(({ id: id7, revision, status: status2, personId }) => ({ id: id7, revision, status: status2, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13487
|
+
links: state.links.map(({ id: id7, personId, state: linkState }) => ({ id: id7, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
13111
13488
|
};
|
|
13112
13489
|
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
13113
13490
|
}
|
|
@@ -13332,7 +13709,7 @@ var safeNetworkPersonAlias = async (alias) => ({
|
|
|
13332
13709
|
recordedAt: alias.consent.recordedAt,
|
|
13333
13710
|
hasBasis: Boolean(alias.consent.basis)
|
|
13334
13711
|
},
|
|
13335
|
-
identityClaims: alias.identityClaims.map(({ id:
|
|
13712
|
+
identityClaims: alias.identityClaims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13336
13713
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
13337
13714
|
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
13338
13715
|
chapterId: link.chapterId,
|
|
@@ -13361,7 +13738,7 @@ async function projectNetworkPersonRegistryState(state) {
|
|
|
13361
13738
|
return {
|
|
13362
13739
|
...safeNetworkPerson(person, claims),
|
|
13363
13740
|
canonicalPersonId: canonicalId,
|
|
13364
|
-
identityClaims: claims.map(({ id:
|
|
13741
|
+
identityClaims: claims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13365
13742
|
chapterLinks: safeLinks,
|
|
13366
13743
|
chapterProjections: projectNetworkContactPolicy(canonical3.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
13367
13744
|
};
|
|
@@ -14154,14 +14531,14 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14154
14531
|
}
|
|
14155
14532
|
}
|
|
14156
14533
|
const db = ctx.makeDb(env);
|
|
14157
|
-
const
|
|
14534
|
+
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
14158
14535
|
try {
|
|
14159
14536
|
const created = await createRecord3(
|
|
14160
14537
|
{
|
|
14161
14538
|
crm: ctx.chapter.crm,
|
|
14162
14539
|
db,
|
|
14163
14540
|
now: () => Date.now(),
|
|
14164
|
-
newId: () =>
|
|
14541
|
+
newId: () => id7
|
|
14165
14542
|
},
|
|
14166
14543
|
{
|
|
14167
14544
|
type: formation.type,
|
|
@@ -14170,7 +14547,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14170
14547
|
}
|
|
14171
14548
|
);
|
|
14172
14549
|
return json({
|
|
14173
|
-
id:
|
|
14550
|
+
id: id7,
|
|
14174
14551
|
duplicate: created.duplicate,
|
|
14175
14552
|
status: created.record.stage ?? null
|
|
14176
14553
|
}, created.duplicate ? 200 : 201);
|
|
@@ -14247,8 +14624,8 @@ function resourceValue(value, path, errors) {
|
|
|
14247
14624
|
return null;
|
|
14248
14625
|
}
|
|
14249
14626
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
14250
|
-
const
|
|
14251
|
-
if (
|
|
14627
|
+
const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
14628
|
+
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
14252
14629
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
14253
14630
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
14254
14631
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14305,7 +14682,7 @@ function resourceValue(value, path, errors) {
|
|
|
14305
14682
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
14306
14683
|
...promotedFrom ? { promotedFrom } : {}
|
|
14307
14684
|
};
|
|
14308
|
-
return { id:
|
|
14685
|
+
return { id: id7, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
|
|
14309
14686
|
}
|
|
14310
14687
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
14311
14688
|
const errors = [];
|
|
@@ -14336,7 +14713,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
14336
14713
|
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
14337
14714
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
14338
14715
|
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
14339
|
-
const ids2 = resources.map(({ id:
|
|
14716
|
+
const ids2 = resources.map(({ id: id7 }) => id7);
|
|
14340
14717
|
if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
|
|
14341
14718
|
for (const resource of resources) {
|
|
14342
14719
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -14425,18 +14802,18 @@ async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environme
|
|
|
14425
14802
|
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
14426
14803
|
}
|
|
14427
14804
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
14428
|
-
const
|
|
14429
|
-
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id:
|
|
14805
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14806
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
|
|
14430
14807
|
return local3(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
14431
14808
|
}
|
|
14432
14809
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
14433
14810
|
const { document, digest } = envelope;
|
|
14434
14811
|
const target = document.target;
|
|
14435
|
-
const
|
|
14812
|
+
const id7 = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
14436
14813
|
const ops = [];
|
|
14437
14814
|
if (previous?.active) ops.push({ t: "update", ns: "builderLibraryControls", id: previous.id, attrs: { active: false } });
|
|
14438
|
-
ops.push({ t: "update", ns: "builderLibraryControls", id:
|
|
14439
|
-
id:
|
|
14815
|
+
ops.push({ t: "update", ns: "builderLibraryControls", id: id7, attrs: {
|
|
14816
|
+
id: id7,
|
|
14440
14817
|
sourceId: document.source.appId,
|
|
14441
14818
|
targetId: target.appId,
|
|
14442
14819
|
environment: target.environment,
|
|
@@ -14449,13 +14826,13 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
14449
14826
|
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest}` });
|
|
14450
14827
|
}
|
|
14451
14828
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
14452
|
-
const
|
|
14829
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14453
14830
|
const current = await loadLocalBuilderLibrary(db, document.source.appId, environment);
|
|
14454
14831
|
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("builder_library_revision_conflict");
|
|
14455
|
-
const row2 = { id:
|
|
14456
|
-
await db.transact([{ t: "update", ns: "builderLibraryLocal", id:
|
|
14832
|
+
const row2 = { id: id7, appId: document.source.appId, environment, revision: document.revision, document, updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
14833
|
+
await db.transact([{ t: "update", ns: "builderLibraryLocal", id: id7, attrs: row2 }], {
|
|
14457
14834
|
mutationId: `builder-library-local:${environment}:${document.revision}`,
|
|
14458
|
-
guards: current ? [{ ns: "builderLibraryLocal", id:
|
|
14835
|
+
guards: current ? [{ ns: "builderLibraryLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "builderLibraryLocal", id: id7, exists: false }]
|
|
14459
14836
|
});
|
|
14460
14837
|
return row2;
|
|
14461
14838
|
}
|
|
@@ -14670,30 +15047,30 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14670
15047
|
};
|
|
14671
15048
|
|
|
14672
15049
|
// src/membership-cancel-authority.ts
|
|
14673
|
-
import { stripeRequest as
|
|
15050
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
14674
15051
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14675
15052
|
membershipClock(now);
|
|
14676
|
-
const
|
|
15053
|
+
const id7 = await networkMembershipId(target);
|
|
14677
15054
|
const state = await loadMembershipAuthorityState(db, target);
|
|
14678
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15055
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14679
15056
|
requireMembershipScope(member, target);
|
|
14680
15057
|
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
14681
15058
|
membershipFail("membership_cancellation_unavailable");
|
|
14682
15059
|
}
|
|
14683
15060
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14684
|
-
const read2 = await
|
|
15061
|
+
const read2 = await stripeRequest11({ secretKey: stripeSecret }, "GET", path);
|
|
14685
15062
|
const sub = read2.body;
|
|
14686
15063
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14687
15064
|
...stripePartitionMetadata(target),
|
|
14688
15065
|
applicationId: target.applicationId
|
|
14689
15066
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14690
15067
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14691
|
-
const write = await
|
|
15068
|
+
const write = await stripeRequest11(
|
|
14692
15069
|
{ secretKey: stripeSecret },
|
|
14693
15070
|
"POST",
|
|
14694
15071
|
path,
|
|
14695
15072
|
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
14696
|
-
{ idempotencyKey: `membership-cancel-at-term:${
|
|
15073
|
+
{ idempotencyKey: `membership-cancel-at-term:${id7}:${member.providerSubscriptionId}` }
|
|
14697
15074
|
);
|
|
14698
15075
|
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
14699
15076
|
membershipFail("membership_cancellation_pending");
|
|
@@ -14701,7 +15078,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14701
15078
|
}
|
|
14702
15079
|
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
14703
15080
|
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
14704
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
15081
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
14705
15082
|
status: status2,
|
|
14706
15083
|
autoRenew: false,
|
|
14707
15084
|
graceEndsAt: 0,
|
|
@@ -14710,12 +15087,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14710
15087
|
updatedAt: now
|
|
14711
15088
|
})], {
|
|
14712
15089
|
guards: [
|
|
14713
|
-
{ ns: "networkMembership", id:
|
|
15090
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14714
15091
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14715
15092
|
]
|
|
14716
15093
|
});
|
|
14717
15094
|
}
|
|
14718
|
-
return { membershipId:
|
|
15095
|
+
return { membershipId: id7, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
14719
15096
|
}
|
|
14720
15097
|
|
|
14721
15098
|
// src/membership-authority-handler.ts
|
|
@@ -14754,17 +15131,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
|
|
|
14754
15131
|
// src/membership-approval-authority.ts
|
|
14755
15132
|
async function approveNetworkMembership(db, input, now) {
|
|
14756
15133
|
membershipClock(now);
|
|
14757
|
-
const
|
|
15134
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
14758
15135
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14759
15136
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14760
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15137
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14761
15138
|
requireMembershipScope(member, input);
|
|
14762
15139
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14763
15140
|
requireMembershipScope(entitlement, input);
|
|
14764
|
-
if (entitlement.membershipId !==
|
|
15141
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
|
|
14765
15142
|
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) {
|
|
14766
15143
|
return {
|
|
14767
|
-
membershipId:
|
|
15144
|
+
membershipId: id7,
|
|
14768
15145
|
status: String(member.status),
|
|
14769
15146
|
cohort: String(member.cohort),
|
|
14770
15147
|
memberNumber: Number(member.memberNumber),
|
|
@@ -14784,7 +15161,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14784
15161
|
const seatActivation = await namedSeatPrimaryActivation(db, member, now);
|
|
14785
15162
|
try {
|
|
14786
15163
|
await db.transact([
|
|
14787
|
-
membershipUpdate("networkMembership",
|
|
15164
|
+
membershipUpdate("networkMembership", id7, {
|
|
14788
15165
|
status: member.autoRenew ? "active" : "cancel_at_term_end",
|
|
14789
15166
|
memberNumber,
|
|
14790
15167
|
journeyApprovedAt: now,
|
|
@@ -14799,8 +15176,8 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14799
15176
|
updatedAt: now
|
|
14800
15177
|
}),
|
|
14801
15178
|
...seatActivation.ops
|
|
14802
|
-
], { mutationId: `membership-approve:${
|
|
14803
|
-
{ ns: "networkMembership", id:
|
|
15179
|
+
], { mutationId: `membership-approve:${id7}`, guards: [
|
|
15180
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
|
|
14804
15181
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
|
|
14805
15182
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
14806
15183
|
...seatActivation.guards
|
|
@@ -14815,41 +15192,41 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14815
15192
|
// src/membership-refund-authority.ts
|
|
14816
15193
|
async function refundNetworkMembership(db, input, secretKey, now) {
|
|
14817
15194
|
membershipClock(now);
|
|
14818
|
-
const
|
|
14819
|
-
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15195
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
15196
|
+
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14820
15197
|
requireMembershipScope(member, input);
|
|
14821
|
-
if (member.status === "refunded") return { membershipId:
|
|
15198
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14822
15199
|
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
|
|
14823
15200
|
if (member.status !== "refund_pending") {
|
|
14824
15201
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14825
15202
|
requireMembershipScope(entitlement, input);
|
|
14826
15203
|
await db.transact([
|
|
14827
|
-
membershipUpdate("networkMembership",
|
|
15204
|
+
membershipUpdate("networkMembership", id7, {
|
|
14828
15205
|
status: "refund_pending",
|
|
14829
15206
|
refundRequestedAt: now,
|
|
14830
15207
|
revision: Number(member.revision) + 1,
|
|
14831
15208
|
updatedAt: now
|
|
14832
15209
|
}),
|
|
14833
15210
|
membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
14834
|
-
membershipUpdate("membershipEffect", `${
|
|
14835
|
-
id: `${
|
|
15211
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, {
|
|
15212
|
+
id: `${id7}:refund`,
|
|
14836
15213
|
chapterId: input.chapterId,
|
|
14837
15214
|
environment: input.environment,
|
|
14838
15215
|
runtime: input.runtime,
|
|
14839
|
-
membershipId:
|
|
15216
|
+
membershipId: id7,
|
|
14840
15217
|
kind: "refund_membership",
|
|
14841
15218
|
status: "pending",
|
|
14842
15219
|
attempts: 0,
|
|
14843
15220
|
createdAt: now,
|
|
14844
15221
|
updatedAt: now
|
|
14845
15222
|
})
|
|
14846
|
-
], { mutationId: `membership-refund-request:${
|
|
14847
|
-
{ ns: "networkMembership", id:
|
|
15223
|
+
], { mutationId: `membership-refund-request:${id7}`, guards: [
|
|
15224
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14848
15225
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
|
|
14849
15226
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14850
15227
|
] });
|
|
14851
15228
|
}
|
|
14852
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15229
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14853
15230
|
requireMembershipScope(member, input);
|
|
14854
15231
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14855
15232
|
const result = await refundSubscriptionPayment(secretKey, {
|
|
@@ -14863,23 +15240,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14863
15240
|
if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
|
|
14864
15241
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14865
15242
|
state = await loadMembershipAuthorityState(db, input);
|
|
14866
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15243
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14867
15244
|
requireMembershipScope(member, input);
|
|
14868
|
-
if (member.status === "refunded") return { membershipId:
|
|
15245
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14869
15246
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14870
15247
|
const counted = member.founding === true && member.memberNumber !== void 0;
|
|
14871
15248
|
const counts = state.cohortCounts;
|
|
14872
15249
|
if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
|
|
14873
15250
|
try {
|
|
14874
15251
|
await db.transact([
|
|
14875
|
-
membershipUpdate("networkMembership",
|
|
15252
|
+
membershipUpdate("networkMembership", id7, {
|
|
14876
15253
|
status: "refunded",
|
|
14877
15254
|
refundedCents: result.refundedCents,
|
|
14878
15255
|
autoRenew: false,
|
|
14879
15256
|
revision: Number(member.revision) + 1,
|
|
14880
15257
|
updatedAt: now
|
|
14881
15258
|
}),
|
|
14882
|
-
membershipUpdate("membershipEffect", `${
|
|
15259
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, { status: "complete", updatedAt: now }),
|
|
14883
15260
|
...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
|
|
14884
15261
|
cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
|
|
14885
15262
|
revision: Number(state.revision) + 1,
|
|
@@ -14890,7 +15267,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14890
15267
|
chapterId: input.chapterId,
|
|
14891
15268
|
environment: input.environment,
|
|
14892
15269
|
runtime: input.runtime,
|
|
14893
|
-
membershipId:
|
|
15270
|
+
membershipId: id7,
|
|
14894
15271
|
seatId: member.namedSeatId,
|
|
14895
15272
|
kind: "refund_seat_with_primary",
|
|
14896
15273
|
status: "pending",
|
|
@@ -14898,8 +15275,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14898
15275
|
createdAt: now,
|
|
14899
15276
|
updatedAt: now
|
|
14900
15277
|
})] : []
|
|
14901
|
-
], { mutationId: `membership-refunded:${
|
|
14902
|
-
{ ns: "networkMembership", id:
|
|
15278
|
+
], { mutationId: `membership-refunded:${id7}`, guards: [
|
|
15279
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "refund_pending", revision: member.revision } },
|
|
14903
15280
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14904
15281
|
] });
|
|
14905
15282
|
} catch (error) {
|
|
@@ -14910,12 +15287,12 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14910
15287
|
}
|
|
14911
15288
|
|
|
14912
15289
|
// src/membership-provider-lifecycle.ts
|
|
14913
|
-
import { stripeRequest as
|
|
15290
|
+
import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
|
|
14914
15291
|
|
|
14915
15292
|
// src/membership-renewal-authority.ts
|
|
14916
15293
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
14917
15294
|
membershipClock(now);
|
|
14918
|
-
const
|
|
15295
|
+
const id7 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
14919
15296
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
14920
15297
|
const fact = eventRow?.fact;
|
|
14921
15298
|
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({
|
|
@@ -14928,10 +15305,10 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14928
15305
|
return membershipFail("membership_renewal_evidence_pending");
|
|
14929
15306
|
}
|
|
14930
15307
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14931
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15308
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14932
15309
|
requireMembershipScope(member, input);
|
|
14933
15310
|
const receipt = () => ({
|
|
14934
|
-
membershipId:
|
|
15311
|
+
membershipId: id7,
|
|
14935
15312
|
status: String(member.status),
|
|
14936
15313
|
termEndsAt: Number(member.termEndsAt),
|
|
14937
15314
|
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
@@ -14956,13 +15333,13 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14956
15333
|
const paid = fact.eventType === "invoice.payment_succeeded";
|
|
14957
15334
|
if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
|
|
14958
15335
|
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");
|
|
14959
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
15336
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
14960
15337
|
requireMembershipScope(entitlement, input);
|
|
14961
15338
|
const graceEndsAt = paid ? 0 : start + 14 * 864e5;
|
|
14962
15339
|
const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
|
|
14963
15340
|
const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
|
|
14964
15341
|
const ops = [
|
|
14965
|
-
membershipUpdate("networkMembership",
|
|
15342
|
+
membershipUpdate("networkMembership", id7, {
|
|
14966
15343
|
status: status2,
|
|
14967
15344
|
graceEndsAt,
|
|
14968
15345
|
...paid ? {
|
|
@@ -14973,7 +15350,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14973
15350
|
revision: Number(member.revision) + 1,
|
|
14974
15351
|
updatedAt: now
|
|
14975
15352
|
}),
|
|
14976
|
-
membershipUpdate("networkEntitlement", `${
|
|
15353
|
+
membershipUpdate("networkEntitlement", `${id7}:primary`, {
|
|
14977
15354
|
status: entitlementStatus,
|
|
14978
15355
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14979
15356
|
sourceEventId: input.eventId,
|
|
@@ -14982,8 +15359,8 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14982
15359
|
})
|
|
14983
15360
|
];
|
|
14984
15361
|
const guards = [
|
|
14985
|
-
{ ns: "networkMembership", id:
|
|
14986
|
-
{ ns: "networkEntitlement", id: `${
|
|
15362
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15363
|
+
{ ns: "networkEntitlement", id: `${id7}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
14987
15364
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
14988
15365
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
14989
15366
|
evidenceGuard
|
|
@@ -15008,15 +15385,15 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15008
15385
|
);
|
|
15009
15386
|
}
|
|
15010
15387
|
try {
|
|
15011
|
-
await db.transact(ops, { mutationId: `membership-renewal:${
|
|
15388
|
+
await db.transact(ops, { mutationId: `membership-renewal:${id7}:${input.eventId}`, guards });
|
|
15012
15389
|
} catch (error) {
|
|
15013
15390
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15014
15391
|
continue;
|
|
15015
15392
|
}
|
|
15016
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15393
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15017
15394
|
requireMembershipScope(saved, input);
|
|
15018
15395
|
return {
|
|
15019
|
-
membershipId:
|
|
15396
|
+
membershipId: id7,
|
|
15020
15397
|
status: String(saved.status),
|
|
15021
15398
|
termEndsAt: Number(saved.termEndsAt),
|
|
15022
15399
|
graceEndsAt: Number(saved.graceEndsAt ?? 0),
|
|
@@ -15042,12 +15419,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15042
15419
|
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
15043
15420
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
15044
15421
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
15045
|
-
const
|
|
15422
|
+
const id7 = await networkMembershipId(input);
|
|
15046
15423
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
15047
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15424
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
15048
15425
|
requireMembershipScope(member, input);
|
|
15049
15426
|
const receipt = (value = member) => ({
|
|
15050
|
-
membershipId:
|
|
15427
|
+
membershipId: id7,
|
|
15051
15428
|
status: String(value.status),
|
|
15052
15429
|
termEndsAt: Number(value.termEndsAt),
|
|
15053
15430
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
@@ -15068,13 +15445,13 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15068
15445
|
});
|
|
15069
15446
|
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15070
15447
|
await refundNetworkMembership(db, input, sk, now);
|
|
15071
|
-
const saved2 = await membershipLoad(db, "networkMembership",
|
|
15448
|
+
const saved2 = await membershipLoad(db, "networkMembership", id7);
|
|
15072
15449
|
requireMembershipScope(saved2, input);
|
|
15073
15450
|
return receipt(saved2);
|
|
15074
15451
|
}
|
|
15075
15452
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
15076
15453
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
15077
|
-
const result = await
|
|
15454
|
+
const result = await stripeRequest12({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
15078
15455
|
const sub = result.body;
|
|
15079
15456
|
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)) {
|
|
15080
15457
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -15083,18 +15460,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15083
15460
|
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";
|
|
15084
15461
|
const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
|
|
15085
15462
|
if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
|
|
15086
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
15463
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
15087
15464
|
requireMembershipScope(entitlement, input);
|
|
15088
15465
|
try {
|
|
15089
15466
|
await db.transact([
|
|
15090
|
-
membershipUpdate("networkMembership",
|
|
15467
|
+
membershipUpdate("networkMembership", id7, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
|
|
15091
15468
|
membershipUpdate("networkEntitlement", String(entitlement.id), {
|
|
15092
15469
|
status: status2 === "lapsed" ? "closed" : entitlement.status,
|
|
15093
15470
|
revision: Number(entitlement.revision) + 1,
|
|
15094
15471
|
updatedAt: now
|
|
15095
15472
|
})
|
|
15096
15473
|
], { guards: [
|
|
15097
|
-
{ ns: "networkMembership", id:
|
|
15474
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15098
15475
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
15099
15476
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
15100
15477
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
@@ -15104,7 +15481,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15104
15481
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15105
15482
|
continue;
|
|
15106
15483
|
}
|
|
15107
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15484
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15108
15485
|
requireMembershipScope(saved, input);
|
|
15109
15486
|
return receipt(saved);
|
|
15110
15487
|
}
|
|
@@ -15112,7 +15489,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15112
15489
|
}
|
|
15113
15490
|
|
|
15114
15491
|
// src/membership-seat-stripe.ts
|
|
15115
|
-
import { stripeRequest as
|
|
15492
|
+
import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
|
|
15116
15493
|
var reject7 = () => {
|
|
15117
15494
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
15118
15495
|
};
|
|
@@ -15124,7 +15501,7 @@ var metadataFor = (seat) => ({
|
|
|
15124
15501
|
});
|
|
15125
15502
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
15126
15503
|
async function read(sk, path, params) {
|
|
15127
|
-
const response = await
|
|
15504
|
+
const response = await stripeRequest13({ secretKey: sk }, "GET", path, params);
|
|
15128
15505
|
if (!response.ok) return reject7();
|
|
15129
15506
|
return response.body;
|
|
15130
15507
|
}
|
|
@@ -15135,7 +15512,7 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
15135
15512
|
currency: seat.currency,
|
|
15136
15513
|
interval: seat.interval,
|
|
15137
15514
|
environment: seat.environment
|
|
15138
|
-
}, Boolean(seat.providerPaymentIntentId));
|
|
15515
|
+
}, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
|
|
15139
15516
|
if (!productId) reject7();
|
|
15140
15517
|
const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
|
|
15141
15518
|
const items = sub.items?.data;
|
|
@@ -15161,11 +15538,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15161
15538
|
}
|
|
15162
15539
|
await subscriptionForSeat(sk, seat);
|
|
15163
15540
|
const metadata = metadataFor(seat);
|
|
15164
|
-
const result = seat.providerPaymentIntentId ? await
|
|
15541
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest13(
|
|
15165
15542
|
{ secretKey: sk },
|
|
15166
15543
|
"GET",
|
|
15167
15544
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
15168
|
-
) : await
|
|
15545
|
+
) : await stripeRequest13({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
15169
15546
|
amount: seat.amountCents,
|
|
15170
15547
|
currency: seat.currency,
|
|
15171
15548
|
customer: seat.providerCustomerId,
|
|
@@ -15179,11 +15556,15 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15179
15556
|
return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
|
|
15180
15557
|
}
|
|
15181
15558
|
async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
15182
|
-
if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15559
|
+
if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15183
15560
|
const sub = await subscriptionForSeat(sk, seat);
|
|
15184
15561
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
15185
|
-
if (existing)
|
|
15186
|
-
|
|
15562
|
+
if (existing) {
|
|
15563
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject7();
|
|
15564
|
+
return String(existing.id);
|
|
15565
|
+
}
|
|
15566
|
+
if (seat.combinedInvoice) reject7();
|
|
15567
|
+
const result = await stripeRequest13({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
15187
15568
|
subscription: seat.providerSubscriptionId,
|
|
15188
15569
|
price: seat.priceId,
|
|
15189
15570
|
quantity: 1,
|
|
@@ -15400,7 +15781,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
15400
15781
|
const state = await loadMembershipAuthorityState(db, input);
|
|
15401
15782
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
15402
15783
|
requireMembershipScope(seat, input);
|
|
15403
|
-
if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15784
|
+
if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15404
15785
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
15405
15786
|
requireMembershipScope(member, input);
|
|
15406
15787
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
@@ -15720,9 +16101,14 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15720
16101
|
try {
|
|
15721
16102
|
input = JSON.parse(verified.body);
|
|
15722
16103
|
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] ?? []];
|
|
15723
|
-
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])))) {
|
|
16104
|
+
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])))) {
|
|
15724
16105
|
throw new TypeError("invalid request");
|
|
15725
16106
|
}
|
|
16107
|
+
if (action === "checkouts" && input.namedSeat !== void 0) {
|
|
16108
|
+
const seat = input.namedSeat;
|
|
16109
|
+
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");
|
|
16110
|
+
checkoutNamedSeatSelection(seat, seat.purchaserEmail);
|
|
16111
|
+
}
|
|
15726
16112
|
} catch {
|
|
15727
16113
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
15728
16114
|
}
|
|
@@ -15795,8 +16181,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15795
16181
|
offerId: String(input.offerId),
|
|
15796
16182
|
offerDigest: String(input.offerDigest),
|
|
15797
16183
|
quoteDigest: String(input.quoteDigest),
|
|
15798
|
-
priceId: String(input.priceId)
|
|
15799
|
-
|
|
16184
|
+
priceId: String(input.priceId),
|
|
16185
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {}
|
|
16186
|
+
}, options.now?.() ?? Date.now(), peer.seatPriceId);
|
|
15800
16187
|
return respond({ ok: true, binding });
|
|
15801
16188
|
}
|
|
15802
16189
|
const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
|
|
@@ -15824,13 +16211,13 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15824
16211
|
|
|
15825
16212
|
// src/schema-membership-authority.ts
|
|
15826
16213
|
var attr = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
15827
|
-
var
|
|
16214
|
+
var id5 = () => attr("string", false, true, true);
|
|
15828
16215
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
15829
16216
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
15830
16217
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
15831
16218
|
var membershipAuthorityEntities = {
|
|
15832
16219
|
networkMembership: { attrs: {
|
|
15833
|
-
id:
|
|
16220
|
+
id: id5(),
|
|
15834
16221
|
membershipId: key(),
|
|
15835
16222
|
...scope(),
|
|
15836
16223
|
personId: key(true),
|
|
@@ -15870,7 +16257,7 @@ var membershipAuthorityEntities = {
|
|
|
15870
16257
|
restartId: key(true)
|
|
15871
16258
|
} },
|
|
15872
16259
|
membershipRestart: { attrs: {
|
|
15873
|
-
id:
|
|
16260
|
+
id: id5(),
|
|
15874
16261
|
...scope(),
|
|
15875
16262
|
membershipId: key(),
|
|
15876
16263
|
restartId: key(),
|
|
@@ -15880,7 +16267,7 @@ var membershipAuthorityEntities = {
|
|
|
15880
16267
|
createdAt: attr("number")
|
|
15881
16268
|
} },
|
|
15882
16269
|
membershipPurchaseHistory: { attrs: {
|
|
15883
|
-
id:
|
|
16270
|
+
id: id5(),
|
|
15884
16271
|
...scope(),
|
|
15885
16272
|
membershipId: key(),
|
|
15886
16273
|
providerSubscriptionId: key(),
|
|
@@ -15889,7 +16276,7 @@ var membershipAuthorityEntities = {
|
|
|
15889
16276
|
archivedAt: attr("number")
|
|
15890
16277
|
} },
|
|
15891
16278
|
networkEntitlement: { attrs: {
|
|
15892
|
-
id:
|
|
16279
|
+
id: id5(),
|
|
15893
16280
|
...scope(),
|
|
15894
16281
|
membershipId: key(),
|
|
15895
16282
|
seatId: key(true),
|
|
@@ -15906,10 +16293,12 @@ var membershipAuthorityEntities = {
|
|
|
15906
16293
|
journeyApprovedAt: attr("number", true)
|
|
15907
16294
|
} },
|
|
15908
16295
|
networkSeat: { attrs: {
|
|
15909
|
-
id:
|
|
16296
|
+
id: id5(),
|
|
15910
16297
|
...scope(),
|
|
15911
16298
|
primaryMembershipId: key(),
|
|
15912
|
-
purchaserUserId: key(),
|
|
16299
|
+
purchaserUserId: key(true),
|
|
16300
|
+
purchaserEmailNormalized: key(true),
|
|
16301
|
+
combinedInvoice: attr("json", true),
|
|
15913
16302
|
recipientName: attr("string"),
|
|
15914
16303
|
recipientEmailNormalized: key(),
|
|
15915
16304
|
recipientClaimId: key(),
|
|
@@ -15941,9 +16330,9 @@ var membershipAuthorityEntities = {
|
|
|
15941
16330
|
refundedCents: attr("number", true),
|
|
15942
16331
|
providerRefundIds: attr("json", true)
|
|
15943
16332
|
} },
|
|
15944
|
-
networkSeatRecipient: { attrs: { id:
|
|
16333
|
+
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
15945
16334
|
membershipEffect: { attrs: {
|
|
15946
|
-
id:
|
|
16335
|
+
id: id5(),
|
|
15947
16336
|
chapterId: key(),
|
|
15948
16337
|
environment: key(),
|
|
15949
16338
|
runtime: key(),
|
|
@@ -15962,11 +16351,11 @@ var membershipAuthorityRules = Object.fromEntries(Object.keys(membershipAuthorit
|
|
|
15962
16351
|
|
|
15963
16352
|
// src/schema-membership-offers.ts
|
|
15964
16353
|
var attr2 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
15965
|
-
var
|
|
16354
|
+
var id6 = () => attr2("string", false, true, true);
|
|
15966
16355
|
var key2 = () => attr2("string", false, false, true);
|
|
15967
16356
|
var membershipOfferEntities = {
|
|
15968
16357
|
membershipAuthorityState: { attrs: {
|
|
15969
|
-
id:
|
|
16358
|
+
id: id6(),
|
|
15970
16359
|
chapterId: key2(),
|
|
15971
16360
|
environment: key2(),
|
|
15972
16361
|
mode: key2(),
|
|
@@ -15976,7 +16365,7 @@ var membershipOfferEntities = {
|
|
|
15976
16365
|
updatedAt: attr2("number")
|
|
15977
16366
|
} },
|
|
15978
16367
|
membershipCheckoutOffer: { attrs: {
|
|
15979
|
-
id:
|
|
16368
|
+
id: id6(),
|
|
15980
16369
|
applicationId: key2(),
|
|
15981
16370
|
appId: key2(),
|
|
15982
16371
|
chapterId: key2(),
|
|
@@ -15991,7 +16380,7 @@ var membershipOfferEntities = {
|
|
|
15991
16380
|
authorityRevision: attr2("number")
|
|
15992
16381
|
} },
|
|
15993
16382
|
membershipOfferCheckout: { attrs: {
|
|
15994
|
-
id:
|
|
16383
|
+
id: id6(),
|
|
15995
16384
|
appId: key2(),
|
|
15996
16385
|
chapterId: key2(),
|
|
15997
16386
|
environment: key2(),
|
|
@@ -16002,7 +16391,8 @@ var membershipOfferEntities = {
|
|
|
16002
16391
|
offerDigest: attr2("string"),
|
|
16003
16392
|
quoteDigest: attr2("string"),
|
|
16004
16393
|
priceId: attr2("string"),
|
|
16005
|
-
boundAt: attr2("number")
|
|
16394
|
+
boundAt: attr2("number"),
|
|
16395
|
+
namedSeat: attr2("json", true)
|
|
16006
16396
|
} }
|
|
16007
16397
|
};
|
|
16008
16398
|
var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
|
|
@@ -16057,7 +16447,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
16057
16447
|
}
|
|
16058
16448
|
|
|
16059
16449
|
// src/membership-seat-cancel.ts
|
|
16060
|
-
import { initStripe as initStripe3, stripeRequest as
|
|
16450
|
+
import { initStripe as initStripe3, stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
|
|
16061
16451
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
16062
16452
|
membershipClock(now);
|
|
16063
16453
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -16085,7 +16475,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16085
16475
|
}
|
|
16086
16476
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
16087
16477
|
const request = async (method, path, params) => {
|
|
16088
|
-
const result = await
|
|
16478
|
+
const result = await stripeRequest14(
|
|
16089
16479
|
{ secretKey },
|
|
16090
16480
|
method,
|
|
16091
16481
|
path,
|
|
@@ -16111,9 +16501,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16111
16501
|
if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
|
|
16112
16502
|
if (payment.status !== "canceled") {
|
|
16113
16503
|
if (!["requires_payment_method", "requires_confirmation", "requires_action", "processing"].includes(payment.status)) throw new Error("seat payment outcome unresolved");
|
|
16114
|
-
const
|
|
16115
|
-
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(
|
|
16116
|
-
if (payment.id !==
|
|
16504
|
+
const id7 = payment.id;
|
|
16505
|
+
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id7)}/cancel`, { cancellation_reason: "abandoned" });
|
|
16506
|
+
if (payment.id !== id7) throw new Error("seat cancellation receipt mismatch");
|
|
16117
16507
|
}
|
|
16118
16508
|
if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
|
|
16119
16509
|
await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
|
|
@@ -16445,7 +16835,8 @@ async function requestMembershipCheckoutBinding(options) {
|
|
|
16445
16835
|
offerId: options.offerId,
|
|
16446
16836
|
offerDigest: options.offerDigest,
|
|
16447
16837
|
quoteDigest: options.quoteDigest,
|
|
16448
|
-
priceId: options.priceId
|
|
16838
|
+
priceId: options.priceId,
|
|
16839
|
+
...options.namedSeat ? { namedSeat: options.namedSeat } : {}
|
|
16449
16840
|
});
|
|
16450
16841
|
return verifyMembershipCheckoutBinding(result.binding, options);
|
|
16451
16842
|
}
|