@odla-ai/chapter 0.54.2 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/{chunk-SPEL4OGE.js → chunk-IQZWCG42.js} +581 -362
- package/dist/chunk-IQZWCG42.js.map +1 -0
- package/dist/index.cjs +522 -266
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +518 -262
- package/dist/index.js.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/member/index.d.ts +12 -1
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +1110 -616
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +22 -1
- package/dist/worker/index.d.ts +22 -1
- package/dist/worker/index.js +1103 -609
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-SPEL4OGE.js.map +0 -1
package/dist/worker/index.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
|
|
@@ -1710,7 +1713,7 @@ function findTier(tiers, tierId) {
|
|
|
1710
1713
|
if (!tierId) return tiers.length === 1 ? tiers[0] : null;
|
|
1711
1714
|
return tiers.find((tier) => tier.id === tierId) ?? null;
|
|
1712
1715
|
}
|
|
1713
|
-
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
1716
|
+
function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map(), seats = /* @__PURE__ */ new Map()) {
|
|
1714
1717
|
return tiers.map((tier) => ({
|
|
1715
1718
|
id: tier.id,
|
|
1716
1719
|
name: tier.name,
|
|
@@ -1719,7 +1722,8 @@ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Ma
|
|
|
1719
1722
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
1720
1723
|
blurb: tier.blurb ?? "",
|
|
1721
1724
|
free: tierIsFree(tier),
|
|
1722
|
-
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
1725
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {},
|
|
1726
|
+
...seats.has(tier.id) ? { seatOffer: seats.get(tier.id) } : {}
|
|
1723
1727
|
}));
|
|
1724
1728
|
}
|
|
1725
1729
|
|
|
@@ -1777,7 +1781,7 @@ async function frozenCheckoutVariables(db, app) {
|
|
|
1777
1781
|
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
1778
1782
|
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
1779
1783
|
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
1780
|
-
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
1784
|
+
const amount = quote?.lineItems?.dueTodayCents ?? contract?.dueTodayCents ?? intent?.priceCents;
|
|
1781
1785
|
const currency = offering?.currency ?? intent?.currency;
|
|
1782
1786
|
let formatted = "";
|
|
1783
1787
|
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
@@ -1901,11 +1905,11 @@ async function sendTemplated(deps, input) {
|
|
|
1901
1905
|
const previous = prior[0];
|
|
1902
1906
|
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
1907
|
const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
|
|
1904
|
-
const
|
|
1908
|
+
const id7 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1905
1909
|
const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
|
|
1906
1910
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
|
|
1907
1911
|
const row2 = {
|
|
1908
|
-
id:
|
|
1912
|
+
id: id7,
|
|
1909
1913
|
groupId: input.group.id,
|
|
1910
1914
|
to: decision.to,
|
|
1911
1915
|
template: input.template,
|
|
@@ -1923,15 +1927,15 @@ async function sendTemplated(deps, input) {
|
|
|
1923
1927
|
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
1924
1928
|
if (validateEmailCopy2({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
1925
1929
|
try {
|
|
1926
|
-
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1927
|
-
mutationId: `email-claim:${
|
|
1930
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: row2 }], {
|
|
1931
|
+
mutationId: `email-claim:${id7}:${attempt}`,
|
|
1928
1932
|
guards: [previous ? {
|
|
1929
1933
|
ns: "emailLog",
|
|
1930
|
-
id:
|
|
1934
|
+
id: id7,
|
|
1931
1935
|
exists: true,
|
|
1932
1936
|
equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
|
|
1933
1937
|
...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
|
|
1934
|
-
} : { ns: "emailLog", id:
|
|
1938
|
+
} : { ns: "emailLog", id: id7, exists: false }]
|
|
1935
1939
|
});
|
|
1936
1940
|
if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
|
|
1937
1941
|
} catch (error) {
|
|
@@ -1951,16 +1955,16 @@ async function sendTemplated(deps, input) {
|
|
|
1951
1955
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
1952
1956
|
messageId = response.messageId;
|
|
1953
1957
|
} catch {
|
|
1954
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1955
|
-
guards: [{ ns: "emailLog", id:
|
|
1958
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
1959
|
+
guards: [{ ns: "emailLog", id: id7, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
1956
1960
|
});
|
|
1957
1961
|
return { sent: false, reason: "delivery-uncertain" };
|
|
1958
1962
|
}
|
|
1959
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
1960
|
-
mutationId: `email-confirmed:${
|
|
1963
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id7, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
1964
|
+
mutationId: `email-confirmed:${id7}:${attempt}`,
|
|
1961
1965
|
guards: [{
|
|
1962
1966
|
ns: "emailLog",
|
|
1963
|
-
id:
|
|
1967
|
+
id: id7,
|
|
1964
1968
|
exists: true,
|
|
1965
1969
|
equals: { deliveryState: "sending", deliveryAttempt: attempt }
|
|
1966
1970
|
}]
|
|
@@ -2145,8 +2149,8 @@ async function hash(value) {
|
|
|
2145
2149
|
async function membershipDiscountCouponId(input, policyId) {
|
|
2146
2150
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
2147
2151
|
}
|
|
2148
|
-
async function load(db, ns,
|
|
2149
|
-
return (await db.query({ [ns]: { $: { where: { id:
|
|
2152
|
+
async function load(db, ns, id7) {
|
|
2153
|
+
return (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2150
2154
|
}
|
|
2151
2155
|
async function membershipOfferIdForRequest(input) {
|
|
2152
2156
|
return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
|
|
@@ -2212,6 +2216,7 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
2212
2216
|
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
2213
2217
|
dueTodayCents: contract.dueTodayCents,
|
|
2214
2218
|
renewalCents: contract.renewalCents,
|
|
2219
|
+
...offering.seatPriceCents !== void 0 ? { seatPriceCents: offering.seatPriceCents } : {},
|
|
2215
2220
|
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
2216
2221
|
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
2217
2222
|
};
|
|
@@ -2280,20 +2285,20 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
2280
2285
|
|
|
2281
2286
|
// src/commercial-evidence-guard.ts
|
|
2282
2287
|
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:
|
|
2288
|
+
const ns = "networkCommercialExceptions", id7 = `network-commercial-exception:${eventId}:collision`;
|
|
2289
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2290
|
+
if (!row2) return { ns, id: id7, exists: false };
|
|
2286
2291
|
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
2287
2292
|
throw Object.assign(new Error(code), { code });
|
|
2288
2293
|
}
|
|
2289
|
-
return { ns, id:
|
|
2294
|
+
return { ns, id: id7, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
2290
2295
|
}
|
|
2291
2296
|
|
|
2292
2297
|
// src/membership-authority-state.ts
|
|
2293
2298
|
var membershipFail = (code) => {
|
|
2294
2299
|
throw Object.assign(new Error(code), { code });
|
|
2295
2300
|
};
|
|
2296
|
-
var membershipLoad = async (db, ns,
|
|
2301
|
+
var membershipLoad = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2297
2302
|
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
2298
2303
|
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
2299
2304
|
function requireMembershipScope(row2, input) {
|
|
@@ -2304,18 +2309,18 @@ async function loadMembershipAuthorityState(db, input) {
|
|
|
2304
2309
|
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
2310
|
return row2;
|
|
2306
2311
|
}
|
|
2307
|
-
var membershipUpdate = (ns,
|
|
2312
|
+
var membershipUpdate = (ns, id7, attrs) => ({ t: "update", ns, id: id7, attrs });
|
|
2308
2313
|
var membershipClock = (now) => {
|
|
2309
2314
|
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
2310
2315
|
};
|
|
2311
2316
|
|
|
2312
2317
|
// src/membership-restart-payment.ts
|
|
2313
2318
|
async function membershipRestartPayment(db, target, member, now) {
|
|
2314
|
-
const
|
|
2319
|
+
const id7 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id7);
|
|
2315
2320
|
requireMembershipScope(restart, target);
|
|
2316
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2321
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2317
2322
|
requireMembershipScope(entitlement, target);
|
|
2318
|
-
if (entitlement.membershipId !==
|
|
2323
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
2319
2324
|
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
2320
2325
|
return {
|
|
2321
2326
|
restartId: String(restart.restartId),
|
|
@@ -2332,49 +2337,117 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
2332
2337
|
graceEndsAt: 0
|
|
2333
2338
|
},
|
|
2334
2339
|
ops: [
|
|
2335
|
-
membershipUpdate("membershipRestart",
|
|
2336
|
-
{ t: "retract", ns: "networkMembership", id:
|
|
2340
|
+
membershipUpdate("membershipRestart", id7, { status: "complete" }),
|
|
2341
|
+
{ t: "retract", ns: "networkMembership", id: id7, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
2337
2342
|
],
|
|
2338
2343
|
guards: [
|
|
2339
|
-
{ ns: "networkMembership", id:
|
|
2340
|
-
{ ns: "membershipRestart", id:
|
|
2344
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
2345
|
+
{ ns: "membershipRestart", id: id7, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
2341
2346
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
2342
2347
|
]
|
|
2343
2348
|
};
|
|
2344
2349
|
}
|
|
2345
2350
|
|
|
2351
|
+
// src/checkout-named-seat.ts
|
|
2352
|
+
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
2353
|
+
const row2 = value;
|
|
2354
|
+
const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
2355
|
+
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
2356
|
+
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
2357
|
+
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) {
|
|
2358
|
+
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
2359
|
+
}
|
|
2360
|
+
return { recipientName: name, recipientEmail: email };
|
|
2361
|
+
}
|
|
2362
|
+
async function checkoutNamedSeatIdentity(target, email) {
|
|
2363
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
2364
|
+
target.appId,
|
|
2365
|
+
target.chapterId,
|
|
2366
|
+
target.environment,
|
|
2367
|
+
target.runtime,
|
|
2368
|
+
email
|
|
2369
|
+
])));
|
|
2370
|
+
return {
|
|
2371
|
+
seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
|
|
2372
|
+
membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
|
|
2373
|
+
recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2346
2377
|
// src/membership-checkout-binding.ts
|
|
2347
2378
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
2348
2379
|
var reject2 = (code) => {
|
|
2349
2380
|
throw Object.assign(new Error(code), { code });
|
|
2350
2381
|
};
|
|
2351
|
-
var load2 = async (db, ns,
|
|
2382
|
+
var load2 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
2352
2383
|
async function membershipCheckoutBindingId(input) {
|
|
2353
2384
|
return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
|
|
2354
2385
|
}
|
|
2355
2386
|
async function verifyMembershipCheckoutBinding(value, input) {
|
|
2356
2387
|
const row2 = value;
|
|
2357
2388
|
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
|
-
|
|
2389
|
+
if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
|
|
2390
|
+
if (input.namedSeat) {
|
|
2391
|
+
const expected = {
|
|
2392
|
+
...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
|
|
2393
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
|
|
2394
|
+
};
|
|
2395
|
+
const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
|
|
2396
|
+
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");
|
|
2397
|
+
}
|
|
2398
|
+
return {
|
|
2399
|
+
id: row2.id,
|
|
2400
|
+
...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
|
|
2401
|
+
boundAt: row2.boundAt,
|
|
2402
|
+
...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
|
|
2403
|
+
};
|
|
2359
2404
|
}
|
|
2360
|
-
async function bindMembershipCheckout(db, input, now) {
|
|
2405
|
+
async function bindMembershipCheckout(db, input, now, seatPriceId) {
|
|
2361
2406
|
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
2407
|
const stateId = `${input.chapterId}:${input.environment}`;
|
|
2363
2408
|
const state = await load2(db, "membershipAuthorityState", stateId);
|
|
2364
2409
|
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",
|
|
2410
|
+
const id7 = await membershipCheckoutBindingId(input);
|
|
2411
|
+
const existing = await load2(db, "membershipOfferCheckout", id7);
|
|
2367
2412
|
if (existing) return verifyMembershipCheckoutBinding(existing, input);
|
|
2368
2413
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
2369
2414
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
2370
2415
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
2371
|
-
const restart = await load2(db, "membershipRestart",
|
|
2372
|
-
|
|
2416
|
+
const restart = await load2(db, "membershipRestart", id7.replace(/^checkout_/, "membership_"));
|
|
2417
|
+
let namedSeat;
|
|
2418
|
+
if (input.namedSeat) {
|
|
2419
|
+
if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
|
|
2420
|
+
const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
|
|
2421
|
+
namedSeat = {
|
|
2422
|
+
...selection,
|
|
2423
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
|
|
2424
|
+
...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
|
|
2425
|
+
priceId: seatPriceId,
|
|
2426
|
+
amountCents: offer.contract.offering.seatPriceCents
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
const receipt = { id: id7, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
|
|
2373
2430
|
try {
|
|
2374
|
-
await db.transact([
|
|
2375
|
-
|
|
2431
|
+
await db.transact([
|
|
2432
|
+
{ t: "update", ns: "membershipOfferCheckout", id: id7, attrs: receipt },
|
|
2433
|
+
...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
|
|
2434
|
+
id: namedSeat.recipientClaimId,
|
|
2435
|
+
appId: input.appId,
|
|
2436
|
+
chapterId: input.chapterId,
|
|
2437
|
+
environment: input.environment,
|
|
2438
|
+
runtime: input.runtime,
|
|
2439
|
+
seatId: namedSeat.seatId,
|
|
2440
|
+
recipientEmailNormalized: namedSeat.recipientEmail,
|
|
2441
|
+
createdAt: now
|
|
2442
|
+
} }] : []
|
|
2443
|
+
], {
|
|
2444
|
+
mutationId: `membership-checkout:${id7}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
2376
2445
|
guards: [
|
|
2377
|
-
{ ns: "membershipOfferCheckout", id:
|
|
2446
|
+
{ ns: "membershipOfferCheckout", id: id7, exists: false },
|
|
2447
|
+
...namedSeat ? [
|
|
2448
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2449
|
+
{ ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
|
|
2450
|
+
] : [],
|
|
2378
2451
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
2379
2452
|
{ ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2380
2453
|
]
|
|
@@ -2382,11 +2455,78 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
2382
2455
|
} catch (error) {
|
|
2383
2456
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2384
2457
|
}
|
|
2385
|
-
const saved = await load2(db, "membershipOfferCheckout",
|
|
2458
|
+
const saved = await load2(db, "membershipOfferCheckout", id7);
|
|
2386
2459
|
if (!saved) return reject2("membership_binding_unconfirmed");
|
|
2387
2460
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
2388
2461
|
}
|
|
2389
2462
|
|
|
2463
|
+
// src/membership-checkout-seat-payment.ts
|
|
2464
|
+
function checkoutSeatPaymentOperations(target, seat, input) {
|
|
2465
|
+
const { now, paidAt, endsAt } = input;
|
|
2466
|
+
const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
|
|
2467
|
+
const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
|
|
2468
|
+
return [
|
|
2469
|
+
membershipUpdate("networkSeat", seat.seatId, {
|
|
2470
|
+
id: seat.seatId,
|
|
2471
|
+
...membershipScope(target),
|
|
2472
|
+
primaryMembershipId: seat.membershipId,
|
|
2473
|
+
purchaserEmailNormalized: seat.purchaserEmail,
|
|
2474
|
+
recipientName: seat.recipientName,
|
|
2475
|
+
recipientEmailNormalized: seat.recipientEmail,
|
|
2476
|
+
recipientClaimId: seat.recipientClaimId,
|
|
2477
|
+
priceId: seat.priceId,
|
|
2478
|
+
primaryPriceId: input.primaryPriceId,
|
|
2479
|
+
amountCents: seat.amountCents,
|
|
2480
|
+
currency: input.offering.currency,
|
|
2481
|
+
interval: input.offering.interval,
|
|
2482
|
+
providerCustomerId: input.customerId,
|
|
2483
|
+
providerSubscriptionId: input.subscriptionId,
|
|
2484
|
+
combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
|
|
2485
|
+
status: included ? "pending_acceptance" : "renewal_pending",
|
|
2486
|
+
preparedAt: input.boundAt,
|
|
2487
|
+
purchasedAt: paidAt,
|
|
2488
|
+
termStartedAt: paidAt,
|
|
2489
|
+
termEndsAt: endsAt,
|
|
2490
|
+
acceptanceEndsAt: endsAt,
|
|
2491
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
2492
|
+
offeringSnapshot: input.offering,
|
|
2493
|
+
paymentEventId: input.eventId,
|
|
2494
|
+
...included ? { entitlementId } : {},
|
|
2495
|
+
revision: 1,
|
|
2496
|
+
createdAt: now,
|
|
2497
|
+
updatedAt: now
|
|
2498
|
+
}),
|
|
2499
|
+
...included ? [membershipUpdate("networkEntitlement", entitlementId, {
|
|
2500
|
+
id: entitlementId,
|
|
2501
|
+
...membershipScope(target),
|
|
2502
|
+
membershipId: seat.membershipId,
|
|
2503
|
+
seatId: seat.seatId,
|
|
2504
|
+
kind: "named_seat",
|
|
2505
|
+
status: "pending_acceptance",
|
|
2506
|
+
termStartedAt: paidAt,
|
|
2507
|
+
termEndsAt: endsAt,
|
|
2508
|
+
offeringSnapshot: input.offering,
|
|
2509
|
+
sourceEventId: input.eventId,
|
|
2510
|
+
revision: 1,
|
|
2511
|
+
createdAt: now,
|
|
2512
|
+
updatedAt: now
|
|
2513
|
+
})] : [],
|
|
2514
|
+
membershipUpdate("membershipEffect", effectId, {
|
|
2515
|
+
id: effectId,
|
|
2516
|
+
chapterId: target.chapterId,
|
|
2517
|
+
environment: target.environment,
|
|
2518
|
+
runtime: target.runtime,
|
|
2519
|
+
membershipId: seat.membershipId,
|
|
2520
|
+
seatId: seat.seatId,
|
|
2521
|
+
kind: included ? "invite_named_recipient" : "attach_seat_renewal",
|
|
2522
|
+
status: "pending",
|
|
2523
|
+
attempts: 0,
|
|
2524
|
+
createdAt: now,
|
|
2525
|
+
updatedAt: now
|
|
2526
|
+
})
|
|
2527
|
+
];
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2390
2530
|
// src/membership-payment-authority.ts
|
|
2391
2531
|
async function networkMembershipId(input) {
|
|
2392
2532
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -2402,11 +2542,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2402
2542
|
{ ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
|
|
2403
2543
|
);
|
|
2404
2544
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
2545
|
+
const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
|
|
2546
|
+
if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
|
|
2547
|
+
const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
|
|
2405
2548
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
2406
2549
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
2407
2550
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
2408
2551
|
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 !==
|
|
2552
|
+
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
2553
|
appId: input.appId,
|
|
2411
2554
|
chapterId: input.chapterId,
|
|
2412
2555
|
environment: input.environment,
|
|
@@ -2418,14 +2561,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2418
2561
|
const paidAt = fact.invoicePaidAt;
|
|
2419
2562
|
const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
|
|
2420
2563
|
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",
|
|
2564
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
2565
|
+
let member = await membershipLoad(db, "networkMembership", id7);
|
|
2423
2566
|
if (!member || member.status === "restarting") {
|
|
2424
2567
|
const offering = offer.contract.offering;
|
|
2425
2568
|
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
2426
2569
|
const attrs = {
|
|
2427
|
-
id:
|
|
2428
|
-
membershipId:
|
|
2570
|
+
id: id7,
|
|
2571
|
+
membershipId: id7,
|
|
2429
2572
|
...membershipScope(input),
|
|
2430
2573
|
baseSkuId: input.skuId,
|
|
2431
2574
|
status: "paid_pending_approval",
|
|
@@ -2441,8 +2584,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2441
2584
|
founding: offer.contract.founding,
|
|
2442
2585
|
cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
|
|
2443
2586
|
baseAmountCents: offering.basePriceCents,
|
|
2444
|
-
amountCents:
|
|
2587
|
+
amountCents: totalCents,
|
|
2445
2588
|
renewalAmountCents: offer.contract.renewalCents,
|
|
2589
|
+
...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
|
|
2446
2590
|
currency: offering.currency,
|
|
2447
2591
|
interval: offering.interval,
|
|
2448
2592
|
initialTermStartedAt: paidAt,
|
|
@@ -2459,11 +2603,11 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2459
2603
|
};
|
|
2460
2604
|
try {
|
|
2461
2605
|
await db.transact([
|
|
2462
|
-
membershipUpdate("networkMembership",
|
|
2606
|
+
membershipUpdate("networkMembership", id7, attrs),
|
|
2463
2607
|
membershipUpdate("networkEntitlement", entitlementId, {
|
|
2464
2608
|
id: entitlementId,
|
|
2465
2609
|
...membershipScope(input),
|
|
2466
|
-
membershipId:
|
|
2610
|
+
membershipId: id7,
|
|
2467
2611
|
kind: "primary",
|
|
2468
2612
|
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
2469
2613
|
termStartedAt: paidAt,
|
|
@@ -2475,23 +2619,45 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
2475
2619
|
updatedAt: now,
|
|
2476
2620
|
...restart?.entitlementAttrs
|
|
2477
2621
|
}),
|
|
2478
|
-
...restart?.ops ?? []
|
|
2479
|
-
|
|
2480
|
-
|
|
2622
|
+
...restart?.ops ?? [],
|
|
2623
|
+
...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
|
|
2624
|
+
offering,
|
|
2625
|
+
primaryPriceId: String(binding.priceId),
|
|
2626
|
+
invoiceId: fact.invoiceId,
|
|
2627
|
+
invoiceTotalCents: totalCents,
|
|
2628
|
+
subscriptionId: input.providerSubscriptionId,
|
|
2629
|
+
customerId: input.providerCustomerId,
|
|
2630
|
+
eventId: input.eventId,
|
|
2631
|
+
paidAt: Number(paidAt),
|
|
2632
|
+
endsAt: Number(endsAt),
|
|
2633
|
+
boundAt: Number(binding.boundAt),
|
|
2634
|
+
now
|
|
2635
|
+
}) : []
|
|
2636
|
+
], { mutationId: `membership-paid:${id7}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
2637
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id7, exists: false }],
|
|
2638
|
+
...namedSeat ? [
|
|
2639
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
2640
|
+
{
|
|
2641
|
+
ns: "networkSeatRecipient",
|
|
2642
|
+
id: namedSeat.recipientClaimId,
|
|
2643
|
+
exists: true,
|
|
2644
|
+
equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
|
|
2645
|
+
}
|
|
2646
|
+
] : [],
|
|
2481
2647
|
{ 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 } },
|
|
2648
|
+
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
|
|
2483
2649
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
2484
2650
|
evidenceGuard
|
|
2485
2651
|
] });
|
|
2486
2652
|
} catch (error) {
|
|
2487
2653
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2488
2654
|
}
|
|
2489
|
-
member = await membershipLoad(db, "networkMembership",
|
|
2655
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
2490
2656
|
}
|
|
2491
2657
|
requireMembershipScope(member, input);
|
|
2492
2658
|
if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
|
|
2493
2659
|
return {
|
|
2494
|
-
membershipId:
|
|
2660
|
+
membershipId: id7,
|
|
2495
2661
|
status: String(member.status),
|
|
2496
2662
|
cohort: String(member.cohort),
|
|
2497
2663
|
initialTermStartedAt: Number(member.initialTermStartedAt),
|
|
@@ -2562,19 +2728,19 @@ async function stopPreviousSubscription(member, target, secret) {
|
|
|
2562
2728
|
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
2563
2729
|
membershipClock(now);
|
|
2564
2730
|
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
2565
|
-
const
|
|
2566
|
-
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([
|
|
2731
|
+
const id7 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
2732
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id7, input.previousSubscriptionId]))}`;
|
|
2567
2733
|
const state = await loadMembershipAuthorityState(db, input);
|
|
2568
|
-
const previous = await membershipLoad(db, "networkMembership",
|
|
2734
|
+
const previous = await membershipLoad(db, "networkMembership", id7);
|
|
2569
2735
|
requireMembershipScope(previous, input);
|
|
2570
2736
|
const receipt = {
|
|
2571
2737
|
applicationId: input.applicationId,
|
|
2572
|
-
membershipId:
|
|
2738
|
+
membershipId: id7,
|
|
2573
2739
|
restartId,
|
|
2574
2740
|
memberNumber: Number(previous.memberNumber),
|
|
2575
2741
|
status: "ready"
|
|
2576
2742
|
};
|
|
2577
|
-
const existing = await membershipLoad(db, "membershipRestart",
|
|
2743
|
+
const existing = await membershipLoad(db, "membershipRestart", id7);
|
|
2578
2744
|
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
2579
2745
|
requireMembershipScope(existing, input);
|
|
2580
2746
|
return receipt;
|
|
@@ -2590,33 +2756,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2590
2756
|
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
2591
2757
|
id: restartId,
|
|
2592
2758
|
...membershipScope(input),
|
|
2593
|
-
membershipId:
|
|
2759
|
+
membershipId: id7,
|
|
2594
2760
|
providerSubscriptionId: input.previousSubscriptionId,
|
|
2595
2761
|
memberSnapshot: previous,
|
|
2596
2762
|
bindingSnapshot: binding2,
|
|
2597
2763
|
archivedAt: now
|
|
2598
2764
|
}),
|
|
2599
|
-
membershipUpdate("networkMembership",
|
|
2765
|
+
membershipUpdate("networkMembership", id7, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
2600
2766
|
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
2601
2767
|
...seats.guards,
|
|
2602
|
-
{ ns: "networkMembership", id:
|
|
2768
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
2603
2769
|
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
2604
2770
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
2605
2771
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
2606
2772
|
] });
|
|
2607
2773
|
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2608
|
-
const reserved = await membershipLoad(db, "networkMembership",
|
|
2774
|
+
const reserved = await membershipLoad(db, "networkMembership", id7);
|
|
2609
2775
|
requireMembershipScope(reserved, input);
|
|
2610
2776
|
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
2611
2777
|
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
2612
2778
|
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
2613
2779
|
requireMembershipScope(binding, input);
|
|
2614
2780
|
await db.transact([
|
|
2615
|
-
membershipUpdate("membershipRestart",
|
|
2781
|
+
membershipUpdate("membershipRestart", id7, { id: id7, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
2616
2782
|
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
2617
2783
|
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
2618
|
-
{ ns: "membershipRestart", id:
|
|
2619
|
-
{ ns: "networkMembership", id:
|
|
2784
|
+
{ ns: "membershipRestart", id: id7, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
2785
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
2620
2786
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
2621
2787
|
] });
|
|
2622
2788
|
return receipt;
|
|
@@ -2626,15 +2792,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
2626
2792
|
async function readMembershipAccess(db, input, now) {
|
|
2627
2793
|
membershipClock(now);
|
|
2628
2794
|
await loadMembershipAuthorityState(db, input);
|
|
2629
|
-
const
|
|
2630
|
-
if (!member) return { membershipId:
|
|
2795
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
2796
|
+
if (!member) return { membershipId: id7, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
|
|
2631
2797
|
requireMembershipScope(member, input);
|
|
2632
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
2798
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
2633
2799
|
requireMembershipScope(entitlement, input);
|
|
2634
2800
|
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 ===
|
|
2801
|
+
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
2802
|
return {
|
|
2637
|
-
membershipId:
|
|
2803
|
+
membershipId: id7,
|
|
2638
2804
|
allowed: allowed2,
|
|
2639
2805
|
status: String(member.status),
|
|
2640
2806
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
@@ -2752,7 +2918,7 @@ async function refundChapterNetworkMembership(db, env, ctx, app) {
|
|
|
2752
2918
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
2753
2919
|
const receipt = await ctx.membershipAuthority.refundMembership(target, env);
|
|
2754
2920
|
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");
|
|
2921
|
+
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
2922
|
return receipt;
|
|
2757
2923
|
}
|
|
2758
2924
|
async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
|
|
@@ -2803,14 +2969,14 @@ function facts(snapshots) {
|
|
|
2803
2969
|
["payment_intent.updated", snapshots.payments?.objects ?? []]
|
|
2804
2970
|
];
|
|
2805
2971
|
return groups.flatMap(([eventType, objects]) => objects.map((object2) => {
|
|
2806
|
-
const
|
|
2972
|
+
const id7 = typeof object2.id === "string" ? object2.id : "invalid";
|
|
2807
2973
|
return normalizeNetworkCommercialStripeEvent({
|
|
2808
|
-
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${
|
|
2974
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id7}`,
|
|
2809
2975
|
type: eventType,
|
|
2810
2976
|
created: typeof object2.created === "number" ? object2.created : 0,
|
|
2811
2977
|
livemode: object2.livemode === true,
|
|
2812
2978
|
data: { object: object2 }
|
|
2813
|
-
}, `stripe-snapshot:${eventType}:${
|
|
2979
|
+
}, `stripe-snapshot:${eventType}:${id7}`);
|
|
2814
2980
|
}));
|
|
2815
2981
|
}
|
|
2816
2982
|
var selected = (row2) => ({
|
|
@@ -2885,9 +3051,9 @@ async function chapterBillingSubscriptions(secretKey, chapterId, env) {
|
|
|
2885
3051
|
// src/approve-application.ts
|
|
2886
3052
|
async function approveApplication(input) {
|
|
2887
3053
|
const { db, env, ctx } = input;
|
|
2888
|
-
const
|
|
2889
|
-
if (!
|
|
2890
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
3054
|
+
const id7 = String(input.app.id ?? "");
|
|
3055
|
+
if (!id7) return { ok: false, status: 404, error: "not found" };
|
|
3056
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2891
3057
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) {
|
|
2892
3058
|
return { ok: false, status: 404, error: "not found" };
|
|
2893
3059
|
}
|
|
@@ -2921,10 +3087,10 @@ async function approveApplication(input) {
|
|
|
2921
3087
|
try {
|
|
2922
3088
|
if (!duplicate) {
|
|
2923
3089
|
const write = await db.transact(
|
|
2924
|
-
[{ t: "update", ns: "applications", id:
|
|
2925
|
-
{ mutationId: `approve:${
|
|
3090
|
+
[{ t: "update", ns: "applications", id: id7, attrs: { status: "approved", approvalEffectsPending: true, approvalEffectsOrigin: input.origin } }],
|
|
3091
|
+
{ mutationId: `approve:${id7}`, guards: [{
|
|
2926
3092
|
ns: "applications",
|
|
2927
|
-
id:
|
|
3093
|
+
id: id7,
|
|
2928
3094
|
exists: true,
|
|
2929
3095
|
equals: Object.fromEntries(guardFields.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
2930
3096
|
absent: guardFields.filter((key3) => app[key3] === void 0)
|
|
@@ -2936,7 +3102,7 @@ async function approveApplication(input) {
|
|
|
2936
3102
|
if (error.code !== "transact_guard_failed") throw error;
|
|
2937
3103
|
return { ok: false, status: 409, error: "application changed; reload before approving" };
|
|
2938
3104
|
}
|
|
2939
|
-
const current = (await db.query({ applications: { $: { where: { id:
|
|
3105
|
+
const current = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2940
3106
|
if (!current || current.status !== "approved" || current.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => current[key3] !== app[key3])) {
|
|
2941
3107
|
return { ok: false, status: 409, error: "application changed; approval effects were not applied" };
|
|
2942
3108
|
}
|
|
@@ -2946,7 +3112,7 @@ async function approveApplication(input) {
|
|
|
2946
3112
|
} catch {
|
|
2947
3113
|
return { ok: false, status: 503, error: "network membership approval reconciliation pending" };
|
|
2948
3114
|
}
|
|
2949
|
-
const afterAuthority = (await db.query({ applications: { $: { where: { id:
|
|
3115
|
+
const afterAuthority = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
2950
3116
|
if (!afterAuthority || afterAuthority.status !== "approved" || afterAuthority.canceled === true || guardFields.filter((key3) => key3 !== "status").some((key3) => afterAuthority[key3] !== app[key3])) {
|
|
2951
3117
|
return { ok: false, status: 409, error: "application changed during network approval" };
|
|
2952
3118
|
}
|
|
@@ -2959,7 +3125,7 @@ async function approveApplication(input) {
|
|
|
2959
3125
|
if (!userId && typeof app.email === "string") {
|
|
2960
3126
|
const found = await clerkGetUserByEmail(secret, app.email, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
2961
3127
|
userId = found?.id ?? null;
|
|
2962
|
-
if (userId) await db.transact([{ t: "update", ns: "applications", id:
|
|
3128
|
+
if (userId) await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { clerkUserId: userId } }]);
|
|
2963
3129
|
}
|
|
2964
3130
|
if (userId) {
|
|
2965
3131
|
const user = await clerkGetUser(secret, userId, fetch, clerkRoleMetadata(ctx.chapter.auth));
|
|
@@ -2981,8 +3147,8 @@ async function approveApplication(input) {
|
|
|
2981
3147
|
template: send,
|
|
2982
3148
|
to: app.email,
|
|
2983
3149
|
vars: { firstName: String(app.firstName ?? ""), membersUrl: `${input.origin}/members/` },
|
|
2984
|
-
applicationId:
|
|
2985
|
-
dedupeKey: `approve:${
|
|
3150
|
+
applicationId: id7,
|
|
3151
|
+
dedupeKey: `approve:${id7}`
|
|
2986
3152
|
}
|
|
2987
3153
|
);
|
|
2988
3154
|
emailLogged = result.sent;
|
|
@@ -2990,10 +3156,10 @@ async function approveApplication(input) {
|
|
|
2990
3156
|
return { ok: false, status: 503, error: "approval notice reconciliation pending", rolePromoted, emailLogged };
|
|
2991
3157
|
}
|
|
2992
3158
|
}
|
|
2993
|
-
await db.transact([{ t: "update", ns: "applications", id:
|
|
3159
|
+
await db.transact([{ t: "update", ns: "applications", id: id7, attrs: { approvalEffectsPending: false } }], {
|
|
2994
3160
|
guards: [{
|
|
2995
3161
|
ns: "applications",
|
|
2996
|
-
id:
|
|
3162
|
+
id: id7,
|
|
2997
3163
|
exists: true,
|
|
2998
3164
|
equals: { status: "approved", ...app.canceled !== void 0 ? { canceled: app.canceled } : {} },
|
|
2999
3165
|
...app.canceled === void 0 ? { absent: ["canceled"] } : {}
|
|
@@ -3022,6 +3188,48 @@ async function revokeRefundedApplicationRole(db, env, ctx, applicationId) {
|
|
|
3022
3188
|
}
|
|
3023
3189
|
}
|
|
3024
3190
|
|
|
3191
|
+
// src/payment-receipt.ts
|
|
3192
|
+
async function retainPaymentReceipt(db, intent, rawBody) {
|
|
3193
|
+
const quote = intent.paymentQuote;
|
|
3194
|
+
if (!quote || intent.paymentReceipt) return;
|
|
3195
|
+
const event2 = JSON.parse(rawBody), invoice = event2.data?.object;
|
|
3196
|
+
const paidAt = invoice?.status_transitions?.paid_at ?? event2.created;
|
|
3197
|
+
if (!/^in_[A-Za-z0-9_]+$/.test(invoice?.id ?? "") || !Number.isSafeInteger(paidAt) || paidAt <= 0) return;
|
|
3198
|
+
const receipt = {
|
|
3199
|
+
invoiceId: invoice.id,
|
|
3200
|
+
paidAt: paidAt * 1e3,
|
|
3201
|
+
currency: quote.currency,
|
|
3202
|
+
tierName: quote.tierName,
|
|
3203
|
+
merchant: quote.merchantDisclosureText,
|
|
3204
|
+
membershipCents: quote.lineItems.dueTodayCents - (quote.lineItems.namedSeatCents ?? 0),
|
|
3205
|
+
totalCents: quote.lineItems.dueTodayCents,
|
|
3206
|
+
...quote.namedSeat ? { namedSeat: { recipientName: quote.namedSeat.recipientName, amountCents: quote.lineItems.namedSeatCents } } : {}
|
|
3207
|
+
};
|
|
3208
|
+
await db.transact([{ t: "update", ns: "subscriptionCheckoutIntents", id: String(intent.id), attrs: { paymentReceipt: receipt } }], {
|
|
3209
|
+
mutationId: `payment-receipt:${invoice.id}`,
|
|
3210
|
+
guards: [{
|
|
3211
|
+
ns: "subscriptionCheckoutIntents",
|
|
3212
|
+
id: String(intent.id),
|
|
3213
|
+
exists: true,
|
|
3214
|
+
equals: { providerSubscriptionId: intent.providerSubscriptionId, providerCustomerId: intent.providerCustomerId },
|
|
3215
|
+
absent: ["paymentReceipt"]
|
|
3216
|
+
}]
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
function paymentReceiptText(receipt, chapterName) {
|
|
3220
|
+
const money = (cents) => new Intl.NumberFormat("en-US", { style: "currency", currency: receipt.currency }).format(cents / 100);
|
|
3221
|
+
return [
|
|
3222
|
+
chapterName,
|
|
3223
|
+
"Payment receipt",
|
|
3224
|
+
`Invoice: ${receipt.invoiceId}`,
|
|
3225
|
+
`Paid: ${new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeZone: "UTC" }).format(receipt.paidAt)}`,
|
|
3226
|
+
receipt.merchant,
|
|
3227
|
+
`${receipt.tierName}: ${money(receipt.membershipCents)}`,
|
|
3228
|
+
...receipt.namedSeat ? [`Additional member \u2014 ${receipt.namedSeat.recipientName}: ${receipt.namedSeat.amountCents === 0 ? "Included \u2014 " : ""}${money(receipt.namedSeat.amountCents)}`] : [],
|
|
3229
|
+
`Total paid: ${money(receipt.totalCents)}`
|
|
3230
|
+
].filter(Boolean).join("\n");
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3025
3233
|
// src/payment-notifications.ts
|
|
3026
3234
|
var groupFor = async (db, app) => {
|
|
3027
3235
|
const result = await db.query({ groups: { $: { where: { id: String(app.groupId ?? "") }, limit: 1 } } });
|
|
@@ -3215,10 +3423,10 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3215
3423
|
tierId: target.tierId,
|
|
3216
3424
|
...typeof existing?.clerkUserId === "string" ? { clerkUserId: existing.clerkUserId } : {}
|
|
3217
3425
|
};
|
|
3218
|
-
const
|
|
3426
|
+
const id7 = existing ? String(existing.id) : `gift-application:${String(claim.id)}`;
|
|
3219
3427
|
const status2 = existing && existing.status !== "submitted" ? String(existing.status) : "paid_pending_vetting";
|
|
3220
|
-
ops.push({ t: "update", ns: "applications", id:
|
|
3221
|
-
id:
|
|
3428
|
+
ops.push({ t: "update", ns: "applications", id: id7, attrs: {
|
|
3429
|
+
id: id7,
|
|
3222
3430
|
groupId: chapterId,
|
|
3223
3431
|
email: beneficiary.email,
|
|
3224
3432
|
emailNormalized,
|
|
@@ -3229,12 +3437,12 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3229
3437
|
clerkUserId: beneficiary.userId,
|
|
3230
3438
|
...existing ? {} : { createdAt: now }
|
|
3231
3439
|
} });
|
|
3232
|
-
result = { kind: "application", id:
|
|
3440
|
+
result = { kind: "application", id: id7 };
|
|
3233
3441
|
} else {
|
|
3234
3442
|
if (beneficiary.applicationId) return null;
|
|
3235
|
-
const
|
|
3236
|
-
ops.push({ t: "update", ns: "giftEntitlements", id:
|
|
3237
|
-
id:
|
|
3443
|
+
const id7 = `gift-entitlement:${String(claim.id)}`;
|
|
3444
|
+
ops.push({ t: "update", ns: "giftEntitlements", id: id7, attrs: {
|
|
3445
|
+
id: id7,
|
|
3238
3446
|
claimId: String(claim.id),
|
|
3239
3447
|
orderId: String(claim.orderId),
|
|
3240
3448
|
chapterId,
|
|
@@ -3244,7 +3452,7 @@ async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
|
3244
3452
|
active: true,
|
|
3245
3453
|
createdAt: now
|
|
3246
3454
|
} });
|
|
3247
|
-
result = { kind: "entitlement", id:
|
|
3455
|
+
result = { kind: "entitlement", id: id7 };
|
|
3248
3456
|
}
|
|
3249
3457
|
ops.unshift({ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
|
|
3250
3458
|
status: "redeemed",
|
|
@@ -3418,9 +3626,9 @@ async function storeStripeReceipt(db, input) {
|
|
|
3418
3626
|
return "stored";
|
|
3419
3627
|
}
|
|
3420
3628
|
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:
|
|
3629
|
+
const id7 = `stripe-event:${input.eventId}`;
|
|
3630
|
+
const write = await db.transact([{ t: "update", ns: "stripeEventReceipts", id: id7, attrs: {
|
|
3631
|
+
id: id7,
|
|
3424
3632
|
eventId: input.eventId,
|
|
3425
3633
|
digest: input.digest,
|
|
3426
3634
|
disposition: input.disposition,
|
|
@@ -3584,6 +3792,7 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
|
|
|
3584
3792
|
} catch {
|
|
3585
3793
|
return json({ error: "network membership payment reconciliation pending" }, 503);
|
|
3586
3794
|
}
|
|
3795
|
+
if (intent) await retainPaymentReceipt(db, intent, rawBody);
|
|
3587
3796
|
}
|
|
3588
3797
|
}
|
|
3589
3798
|
const invoiceEvent = event2.kind === "first_payment" || event2.kind === "renewal" || event2.kind === "renewal_failed";
|
|
@@ -3652,9 +3861,9 @@ var EVENT_ID = /^evt_[A-Za-z0-9_]{1,150}$/;
|
|
|
3652
3861
|
var fail3 = (message, code) => Object.assign(new Error(message), { code });
|
|
3653
3862
|
async function resolveNetworkCommercialCollision(db, sources, input, now = Date.now(), alerts) {
|
|
3654
3863
|
if (!EVENT_ID.test(input.eventId)) throw fail3("invalid event reference", "invalid_event");
|
|
3655
|
-
const
|
|
3864
|
+
const id7 = exceptionId(input.eventId, "collision"), reviewId = `resolve:${input.mutationId}`;
|
|
3656
3865
|
const found = await db.query({
|
|
3657
|
-
networkCommercialExceptions: { $: { where: { id:
|
|
3866
|
+
networkCommercialExceptions: { $: { where: { id: id7 }, limit: 1 } },
|
|
3658
3867
|
networkCommercialEvents: { $: { where: { eventId: input.eventId }, limit: 1 } }
|
|
3659
3868
|
});
|
|
3660
3869
|
const exception = found.networkCommercialExceptions?.[0], stored = found.networkCommercialEvents?.[0];
|
|
@@ -3677,7 +3886,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3677
3886
|
action: "resolve",
|
|
3678
3887
|
reason: input.reason
|
|
3679
3888
|
} },
|
|
3680
|
-
{ t: "update", ns: "networkCommercialExceptions", id:
|
|
3889
|
+
{ t: "update", ns: "networkCommercialExceptions", id: id7, attrs: {
|
|
3681
3890
|
status: "resolved",
|
|
3682
3891
|
resolvedDigest: stored.digest,
|
|
3683
3892
|
...dismissed ? { reviewedDigest: dismissed } : {},
|
|
@@ -3686,7 +3895,7 @@ async function resolveNetworkCommercialCollision(db, sources, input, now = Date.
|
|
|
3686
3895
|
updatedAt: now
|
|
3687
3896
|
} }
|
|
3688
3897
|
], { mutationId: input.mutationId, guards: [
|
|
3689
|
-
{ ns: "networkCommercialExceptions", id:
|
|
3898
|
+
{ ns: "networkCommercialExceptions", id: id7, exists: true, equals: { status: "active", updatedAt: exception.updatedAt } }
|
|
3690
3899
|
] });
|
|
3691
3900
|
if (write.duplicate) throw fail3("mutationId was already used for another change", "mutation_reused");
|
|
3692
3901
|
const projection = await rebuildNetworkCommercialProjection(db, sources, `${input.mutationId}:rebuild`, now, alerts);
|
|
@@ -3801,10 +4010,10 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3801
4010
|
const provider = await readStripeRecoveryEvent(db, eventId);
|
|
3802
4011
|
const event2 = normalizeWebhookEvent(provider.event, { requireProviderPayment: Boolean(ctx.membershipAuthority) });
|
|
3803
4012
|
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
|
|
4013
|
+
const id7 = crypto.randomUUID();
|
|
3805
4014
|
await db.transact([
|
|
3806
|
-
{ t: "update", ns: "paymentRecoveryReviews", id:
|
|
3807
|
-
id:
|
|
4015
|
+
{ t: "update", ns: "paymentRecoveryReviews", id: id7, attrs: {
|
|
4016
|
+
id: id7,
|
|
3808
4017
|
eventId,
|
|
3809
4018
|
actorUserId: user.userId,
|
|
3810
4019
|
reviewedAt: Date.now(),
|
|
@@ -3813,8 +4022,8 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
|
|
|
3813
4022
|
scope: `${target.appId}:${target.environment}:${target.runtime}`,
|
|
3814
4023
|
previousState: row2
|
|
3815
4024
|
} },
|
|
3816
|
-
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId:
|
|
3817
|
-
], { mutationId: `payment-recovery:${
|
|
4025
|
+
{ t: "update", ns: "stripeEventReceipts", id: String(row2.id), attrs: { retryReady: true, reviewedDigest: provider.digest, reviewId: id7 } }
|
|
4026
|
+
], { mutationId: `payment-recovery:${id7}`, guards: [{
|
|
3818
4027
|
ns: "stripeEventReceipts",
|
|
3819
4028
|
id: String(row2.id),
|
|
3820
4029
|
exists: true,
|
|
@@ -3883,43 +4092,121 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
|
|
|
3883
4092
|
}
|
|
3884
4093
|
|
|
3885
4094
|
// src/membership-seat-refund-stripe.ts
|
|
4095
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4096
|
+
|
|
4097
|
+
// src/membership-checkout-seat-refund.ts
|
|
3886
4098
|
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3887
4099
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
3888
4100
|
throw Object.assign(new Error(code), { code });
|
|
3889
4101
|
};
|
|
4102
|
+
var id2 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
4103
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
4104
|
+
const invoiceRef = seat.combinedInvoice;
|
|
4105
|
+
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();
|
|
4106
|
+
const call = async (method, path, params, key3) => {
|
|
4107
|
+
const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
4108
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4109
|
+
return result.body;
|
|
4110
|
+
};
|
|
4111
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
4112
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4113
|
+
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();
|
|
4114
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
4115
|
+
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();
|
|
4116
|
+
let chargeId = id2(invoice.charge), paymentId = id2(invoice.payment_intent);
|
|
4117
|
+
if (!chargeId && !paymentId) {
|
|
4118
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
4119
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
4120
|
+
const payment = payments.data[0];
|
|
4121
|
+
if (id2(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
4122
|
+
paymentId = id2(payment.payment.payment_intent);
|
|
4123
|
+
}
|
|
4124
|
+
if (!chargeId) {
|
|
4125
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
4126
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
4127
|
+
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();
|
|
4128
|
+
chargeId = id2(payment.latest_charge);
|
|
4129
|
+
}
|
|
4130
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
4131
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
4132
|
+
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();
|
|
4133
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
4134
|
+
if (items.length > 1) fail4();
|
|
4135
|
+
if (items.length && sub.status !== "canceled") {
|
|
4136
|
+
const item = items[0];
|
|
4137
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
4138
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
4139
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
4140
|
+
}
|
|
4141
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
4142
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
4143
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
4144
|
+
let refundedCents = 0;
|
|
4145
|
+
const refundIds = [];
|
|
4146
|
+
for (const refund of list.data) {
|
|
4147
|
+
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();
|
|
4148
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
4149
|
+
if (refund.status === "succeeded") {
|
|
4150
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
4151
|
+
refundedCents += refund.amount;
|
|
4152
|
+
refundIds.push(refund.id);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
4156
|
+
if (refundedCents < seat.amountCents) {
|
|
4157
|
+
const remaining = seat.amountCents - refundedCents;
|
|
4158
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
4159
|
+
charge: chargeId,
|
|
4160
|
+
amount: remaining,
|
|
4161
|
+
reason: "requested_by_customer",
|
|
4162
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
4163
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
4164
|
+
if (id2(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
4165
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
4166
|
+
refundedCents += remaining;
|
|
4167
|
+
refundIds.push(refund.id);
|
|
4168
|
+
}
|
|
4169
|
+
return { refundedCents, refundIds };
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// src/membership-seat-refund-stripe.ts
|
|
4173
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
4174
|
+
throw Object.assign(new Error(code), { code });
|
|
4175
|
+
};
|
|
3890
4176
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
3891
|
-
if (
|
|
4177
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
4178
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
3892
4179
|
const call = async (method, path, params, idempotencyKey) => {
|
|
3893
|
-
const result = await
|
|
3894
|
-
if (!result.ok || !result.body) return
|
|
4180
|
+
const result = await stripeRequest5({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4181
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
3895
4182
|
return result.body;
|
|
3896
4183
|
};
|
|
3897
4184
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
3898
4185
|
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))
|
|
4186
|
+
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
4187
|
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))
|
|
4188
|
+
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
4189
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
3903
|
-
if (items.length > 1)
|
|
4190
|
+
if (items.length > 1) fail5();
|
|
3904
4191
|
if (items.length && sub.status !== "canceled") {
|
|
3905
4192
|
const item = items[0];
|
|
3906
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
4193
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
3907
4194
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
3908
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
4195
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
3909
4196
|
}
|
|
3910
4197
|
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)
|
|
4198
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
3912
4199
|
let refundedCents = 0, pendingCents = 0;
|
|
3913
4200
|
const refundIds = [];
|
|
3914
4201
|
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))
|
|
4202
|
+
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
4203
|
if (refund.status === "succeeded") {
|
|
3917
4204
|
refundedCents += refund.amount;
|
|
3918
4205
|
refundIds.push(refund.id);
|
|
3919
4206
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
3920
4207
|
}
|
|
3921
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
3922
|
-
if (pendingCents)
|
|
4208
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
4209
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
3923
4210
|
if (refundedCents < seat.amountCents) {
|
|
3924
4211
|
const remaining = seat.amountCents - refundedCents;
|
|
3925
4212
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -3928,8 +4215,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
3928
4215
|
reason: "requested_by_customer",
|
|
3929
4216
|
metadata
|
|
3930
4217
|
}, `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")
|
|
4218
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
4219
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
3933
4220
|
refundedCents += remaining;
|
|
3934
4221
|
refundIds.push(refund.id);
|
|
3935
4222
|
}
|
|
@@ -3951,8 +4238,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
3951
4238
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
3952
4239
|
requireMembershipScope(member, input);
|
|
3953
4240
|
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",
|
|
4241
|
+
const id7 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4242
|
+
let seat = await membershipLoad(db, "networkSeat", id7);
|
|
3956
4243
|
if (!seat) {
|
|
3957
4244
|
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
3958
4245
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
@@ -3960,7 +4247,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
3960
4247
|
membershipFail("named_seat_purchase_unavailable");
|
|
3961
4248
|
}
|
|
3962
4249
|
const attrs = {
|
|
3963
|
-
id:
|
|
4250
|
+
id: id7,
|
|
3964
4251
|
...membershipScope(input),
|
|
3965
4252
|
primaryMembershipId: memberId,
|
|
3966
4253
|
recipientName: name,
|
|
@@ -3985,20 +4272,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
3985
4272
|
};
|
|
3986
4273
|
try {
|
|
3987
4274
|
await db.transact([
|
|
3988
|
-
membershipUpdate("networkSeat",
|
|
4275
|
+
membershipUpdate("networkSeat", id7, attrs),
|
|
3989
4276
|
membershipUpdate("networkSeatRecipient", claimId, {
|
|
3990
4277
|
id: claimId,
|
|
3991
4278
|
appId: input.appId,
|
|
3992
4279
|
chapterId: input.chapterId,
|
|
3993
4280
|
environment: input.environment,
|
|
3994
4281
|
runtime: input.runtime,
|
|
3995
|
-
seatId:
|
|
4282
|
+
seatId: id7,
|
|
3996
4283
|
recipientEmailNormalized: email,
|
|
3997
4284
|
createdAt: now
|
|
3998
4285
|
}),
|
|
3999
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId:
|
|
4000
|
-
], { mutationId: `named-seat-reserve:${
|
|
4001
|
-
{ ns: "networkSeat", id:
|
|
4286
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id7, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4287
|
+
], { mutationId: `named-seat-reserve:${id7}`, guards: [
|
|
4288
|
+
{ ns: "networkSeat", id: id7, exists: false },
|
|
4002
4289
|
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4003
4290
|
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4004
4291
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
@@ -4006,7 +4293,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4006
4293
|
} catch (error) {
|
|
4007
4294
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4008
4295
|
}
|
|
4009
|
-
seat = await membershipLoad(db, "networkSeat",
|
|
4296
|
+
seat = await membershipLoad(db, "networkSeat", id7);
|
|
4010
4297
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4011
4298
|
}
|
|
4012
4299
|
requireMembershipScope(seat, input);
|
|
@@ -4016,7 +4303,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4016
4303
|
|
|
4017
4304
|
// src/membership-seat-included.ts
|
|
4018
4305
|
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4019
|
-
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4306
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4020
4307
|
function namedSeatRecipientState(seat) {
|
|
4021
4308
|
switch (String(seat.status)) {
|
|
4022
4309
|
case "renewal_pending":
|
|
@@ -4120,7 +4407,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4120
4407
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4121
4408
|
requireMembershipScope(member, input);
|
|
4122
4409
|
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");
|
|
4410
|
+
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
4411
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4125
4412
|
await db.transact([
|
|
4126
4413
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4233,8 +4520,8 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4233
4520
|
const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
|
|
4234
4521
|
for (const member of rows5.slice(0, limit)) {
|
|
4235
4522
|
if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
|
|
4236
|
-
const
|
|
4237
|
-
receipt.skipped.push({ membershipId:
|
|
4523
|
+
const id7 = String(member.id), skip = (reason) => {
|
|
4524
|
+
receipt.skipped.push({ membershipId: id7, reason });
|
|
4238
4525
|
};
|
|
4239
4526
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
4240
4527
|
if (offering.seatPriceCents <= target) {
|
|
@@ -4250,16 +4537,16 @@ async function lowerSeatPrice(db, scope2, now, limit = 100) {
|
|
|
4250
4537
|
skip(`seat is ${String(seat.status)}`);
|
|
4251
4538
|
continue;
|
|
4252
4539
|
}
|
|
4253
|
-
const ops = [membershipUpdate("networkMembership",
|
|
4540
|
+
const ops = [membershipUpdate("networkMembership", id7, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
|
|
4254
4541
|
const guards = [
|
|
4255
|
-
{ ns: "networkMembership", id:
|
|
4542
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
4256
4543
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
|
|
4257
4544
|
];
|
|
4258
4545
|
if (seat) {
|
|
4259
4546
|
ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4260
4547
|
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
|
|
4261
4548
|
}
|
|
4262
|
-
await db.transact(ops, { mutationId: `seat-reprice:${
|
|
4549
|
+
await db.transact(ops, { mutationId: `seat-reprice:${id7}:${member.revision}`, guards });
|
|
4263
4550
|
receipt.repriced++;
|
|
4264
4551
|
}
|
|
4265
4552
|
return receipt;
|
|
@@ -4460,25 +4747,28 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
|
|
|
4460
4747
|
};
|
|
4461
4748
|
|
|
4462
4749
|
// src/membership-discount-reterm.ts
|
|
4463
|
-
import { stripeRequest as
|
|
4750
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
4464
4751
|
|
|
4465
4752
|
// src/subscription-provider-contract.ts
|
|
4466
|
-
import { stripeRequest as
|
|
4753
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4467
4754
|
function subscriptionContractMismatch() {
|
|
4468
4755
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4469
4756
|
}
|
|
4470
4757
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4471
4758
|
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
|
|
4759
|
+
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4473
4760
|
const price = result.body;
|
|
4474
4761
|
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
4762
|
return typeof price.product === "string" ? price.product : void 0;
|
|
4476
4763
|
}
|
|
4477
4764
|
function verifySubscriptionResult(sub, input, customerId, metadata) {
|
|
4478
4765
|
const items = sub?.items;
|
|
4479
|
-
|
|
4766
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
4767
|
+
const primary = items?.data?.find((item) => item.price?.id === input.priceId);
|
|
4768
|
+
const seatItem = seat && items?.data?.find((item) => item.price?.id === seat.priceId);
|
|
4769
|
+
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
4770
|
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();
|
|
4771
|
+
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
4772
|
verifySubscriptionBillingResult(sub, input);
|
|
4483
4773
|
}
|
|
4484
4774
|
|
|
@@ -4490,8 +4780,8 @@ var couponOf = (discount) => {
|
|
|
4490
4780
|
const ref = discount?.source?.coupon ?? discount?.coupon;
|
|
4491
4781
|
return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
|
|
4492
4782
|
};
|
|
4493
|
-
async function retermPolicy(db, scope2,
|
|
4494
|
-
const row2 = await membershipLoad(db, "discountPolicyVersion",
|
|
4783
|
+
async function retermPolicy(db, scope2, id7) {
|
|
4784
|
+
const row2 = await membershipLoad(db, "discountPolicyVersion", id7);
|
|
4495
4785
|
if (!row2) membershipFail("membership_reterm_policy_unknown");
|
|
4496
4786
|
let policy2;
|
|
4497
4787
|
try {
|
|
@@ -4499,12 +4789,12 @@ async function retermPolicy(db, scope2, id6) {
|
|
|
4499
4789
|
} catch {
|
|
4500
4790
|
return membershipFail("membership_reterm_policy_ineligible");
|
|
4501
4791
|
}
|
|
4502
|
-
if (policy2.id !==
|
|
4792
|
+
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
4793
|
return policy2;
|
|
4504
4794
|
}
|
|
4505
4795
|
async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
|
|
4506
4796
|
const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
|
|
4507
|
-
const read2 = await
|
|
4797
|
+
const read2 = await stripeRequest7({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
|
|
4508
4798
|
const sub = read2.body, items = sub?.items?.data;
|
|
4509
4799
|
const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
|
|
4510
4800
|
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 +4805,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
|
|
|
4515
4805
|
const present = discounts.map(couponOf);
|
|
4516
4806
|
if (present.length === 1 && present[0] === couponId) return false;
|
|
4517
4807
|
if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
|
|
4518
|
-
const written = await
|
|
4808
|
+
const written = await stripeRequest7({ secretKey: sk }, "POST", path, {
|
|
4519
4809
|
"discounts[0][coupon]": couponId,
|
|
4520
4810
|
"expand[0]": "discounts",
|
|
4521
4811
|
"metadata[discountPolicyVersionId]": policy2.id
|
|
@@ -4531,7 +4821,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4531
4821
|
const limit = input.limit ?? 100;
|
|
4532
4822
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
|
|
4533
4823
|
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((
|
|
4824
|
+
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
4825
|
const state = await loadMembershipAuthorityState(db, scope2);
|
|
4536
4826
|
const policy2 = await retermPolicy(db, scope2, input.policyVersionId);
|
|
4537
4827
|
const receipt = { policyVersionId: policy2.id, retermed: [], unchanged: [], skipped: [], more: false };
|
|
@@ -4541,10 +4831,10 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4541
4831
|
let rows5;
|
|
4542
4832
|
if (input.membershipIds) {
|
|
4543
4833
|
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(
|
|
4834
|
+
for (const id7 of input.membershipIds) {
|
|
4835
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
4836
|
+
if (!member) skip(id7, "not_found");
|
|
4837
|
+
else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id7, "scope_mismatch");
|
|
4548
4838
|
else rows5.push(member);
|
|
4549
4839
|
}
|
|
4550
4840
|
} else {
|
|
@@ -4554,32 +4844,32 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4554
4844
|
rows5 = found.slice(0, limit);
|
|
4555
4845
|
}
|
|
4556
4846
|
for (const member of rows5) {
|
|
4557
|
-
const
|
|
4847
|
+
const id7 = String(member.id);
|
|
4558
4848
|
if (member.discountPolicyVersionId === policy2.id) {
|
|
4559
|
-
receipt.unchanged.push(
|
|
4849
|
+
receipt.unchanged.push(id7);
|
|
4560
4850
|
continue;
|
|
4561
4851
|
}
|
|
4562
4852
|
if (!RETERMABLE.has(String(member.status))) {
|
|
4563
|
-
skip(
|
|
4853
|
+
skip(id7, "status");
|
|
4564
4854
|
continue;
|
|
4565
4855
|
}
|
|
4566
4856
|
if (member.founding !== true) {
|
|
4567
|
-
skip(
|
|
4857
|
+
skip(id7, "not_founding");
|
|
4568
4858
|
continue;
|
|
4569
4859
|
}
|
|
4570
4860
|
if (member.cohort !== policy2.cohortLabel) {
|
|
4571
|
-
skip(
|
|
4861
|
+
skip(id7, "cohort_mismatch");
|
|
4572
4862
|
continue;
|
|
4573
4863
|
}
|
|
4574
4864
|
const skuId = String(member.baseSkuId);
|
|
4575
4865
|
if (!policy2.discountEligibleSkuIds.includes(skuId)) {
|
|
4576
|
-
skip(
|
|
4866
|
+
skip(id7, "sku_not_discounted");
|
|
4577
4867
|
continue;
|
|
4578
4868
|
}
|
|
4579
4869
|
const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
|
|
4580
4870
|
const renewal = Math.round(base * (1e4 - policy2.percentOffBps) / 1e4);
|
|
4581
4871
|
if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
|
|
4582
|
-
skip(
|
|
4872
|
+
skip(id7, "would_not_widen");
|
|
4583
4873
|
continue;
|
|
4584
4874
|
}
|
|
4585
4875
|
const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy2.id);
|
|
@@ -4607,22 +4897,22 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4607
4897
|
} catch (error) {
|
|
4608
4898
|
const code = error.code;
|
|
4609
4899
|
if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
|
|
4610
|
-
skip(
|
|
4900
|
+
skip(id7, "provider_mismatch");
|
|
4611
4901
|
continue;
|
|
4612
4902
|
}
|
|
4613
4903
|
throw error;
|
|
4614
4904
|
}
|
|
4615
4905
|
try {
|
|
4616
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
4906
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
4617
4907
|
discountPolicyVersionId: policy2.id,
|
|
4618
4908
|
discountSnapshot: policy2,
|
|
4619
4909
|
renewalAmountCents: renewal,
|
|
4620
4910
|
revision: Number(member.revision) + 1,
|
|
4621
4911
|
updatedAt: now
|
|
4622
4912
|
})], {
|
|
4623
|
-
mutationId: `membership-reterm:${
|
|
4913
|
+
mutationId: `membership-reterm:${id7}:${policy2.id}:${member.revision}`,
|
|
4624
4914
|
guards: [
|
|
4625
|
-
{ ns: "networkMembership", id:
|
|
4915
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: member.status } },
|
|
4626
4916
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
|
|
4627
4917
|
{ ns: "discountPolicyVersion", id: policy2.id, exists: true }
|
|
4628
4918
|
]
|
|
@@ -4632,7 +4922,7 @@ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now)
|
|
|
4632
4922
|
return membershipFail("membership_reterm_retry_required");
|
|
4633
4923
|
}
|
|
4634
4924
|
receipt.retermed.push({
|
|
4635
|
-
membershipId:
|
|
4925
|
+
membershipId: id7,
|
|
4636
4926
|
...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
|
|
4637
4927
|
previousRenewalAmountCents: current,
|
|
4638
4928
|
renewalAmountCents: renewal,
|
|
@@ -4668,7 +4958,7 @@ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
|
|
|
4668
4958
|
return json({ error: BAD }, 400);
|
|
4669
4959
|
}
|
|
4670
4960
|
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((
|
|
4961
|
+
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
4962
|
return json({ error: BAD }, 400);
|
|
4673
4963
|
}
|
|
4674
4964
|
const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
|
|
@@ -4943,12 +5233,12 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4943
5233
|
if (!app || app.clerkUserId !== user.userId || app.namedSeatId || app.giftClaimId) return json({ error: "membership is not eligible to restart" }, 409);
|
|
4944
5234
|
if (app.membershipRestartId && app.status === "submitted") return json({ applicationId: app.id });
|
|
4945
5235
|
try {
|
|
4946
|
-
const
|
|
5236
|
+
const id7 = String(app.id), intent = await membershipLoad(db, "subscriptionCheckoutIntents", id7);
|
|
4947
5237
|
if (!intent || !app.stripeSubscriptionId || intent.providerSubscriptionId !== app.stripeSubscriptionId) return json({ error: "membership history needs reconciliation" }, 409);
|
|
4948
5238
|
const target = await membershipTargetForIntent(intent, app, env, ctx);
|
|
4949
5239
|
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",
|
|
5240
|
+
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");
|
|
5241
|
+
const now = Date.now(), quote = await membershipLoad(db, "membershipQuoteProjections", id7);
|
|
4952
5242
|
const attrs = {
|
|
4953
5243
|
status: "submitted",
|
|
4954
5244
|
membershipRestartId: restart.restartId,
|
|
@@ -4968,8 +5258,8 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4968
5258
|
quoteSnapshot: quote ?? {},
|
|
4969
5259
|
archivedAt: now
|
|
4970
5260
|
} },
|
|
4971
|
-
{ t: "delete", ns: "subscriptionCheckoutIntents", id:
|
|
4972
|
-
{ t: "retract", ns: "applications", id:
|
|
5261
|
+
{ t: "delete", ns: "subscriptionCheckoutIntents", id: id7 },
|
|
5262
|
+
{ t: "retract", ns: "applications", id: id7, attrs: [
|
|
4973
5263
|
"stripeSubscriptionId",
|
|
4974
5264
|
"canceled",
|
|
4975
5265
|
"renewalAt",
|
|
@@ -4980,28 +5270,28 @@ var handleMembershipRestart = async (req, url, env, ctx) => {
|
|
|
4980
5270
|
"approvalEffectsOrigin",
|
|
4981
5271
|
"approvalEffectsLastAttemptAt"
|
|
4982
5272
|
] },
|
|
4983
|
-
{ t: "update", ns: "applications", id:
|
|
4984
|
-
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id:
|
|
5273
|
+
{ t: "update", ns: "applications", id: id7, attrs },
|
|
5274
|
+
...quote ? [{ t: "update", ns: "membershipQuoteProjections", id: id7, attrs: { status: "restart", updatedAt: now } }] : []
|
|
4985
5275
|
];
|
|
4986
5276
|
try {
|
|
4987
5277
|
await db.transact(ops, { mutationId: `membership-restart-application:${restart.restartId}`, guards: [
|
|
4988
|
-
{ ns: "applications", id:
|
|
5278
|
+
{ ns: "applications", id: id7, exists: true, equals: {
|
|
4989
5279
|
clerkUserId: user.userId,
|
|
4990
5280
|
groupId: ctx.chapter.id,
|
|
4991
5281
|
status: app.status,
|
|
4992
5282
|
stripeSubscriptionId: app.stripeSubscriptionId
|
|
4993
5283
|
} },
|
|
4994
|
-
{ ns: "subscriptionCheckoutIntents", id:
|
|
5284
|
+
{ ns: "subscriptionCheckoutIntents", id: id7, exists: true, equals: { providerSubscriptionId: app.stripeSubscriptionId } },
|
|
4995
5285
|
{ ns: "membershipCheckoutHistory", id: restart.restartId, exists: false },
|
|
4996
|
-
{ ns: "membershipQuoteProjections", id:
|
|
5286
|
+
{ ns: "membershipQuoteProjections", id: id7, exists: Boolean(quote), ...quote ? { equals: { revision: quote.revision, status: quote.status } } : {} }
|
|
4997
5287
|
] });
|
|
4998
5288
|
} catch (error) {
|
|
4999
5289
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5000
5290
|
}
|
|
5001
|
-
const saved = await membershipLoad(db, "applications",
|
|
5291
|
+
const saved = await membershipLoad(db, "applications", id7);
|
|
5002
5292
|
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
5293
|
await syncApplicationToCrm(crmDeps(db, ctx), { app: saved, stage: String(saved.status) }).catch(() => void 0);
|
|
5004
|
-
return json({ applicationId:
|
|
5294
|
+
return json({ applicationId: id7 });
|
|
5005
5295
|
} catch {
|
|
5006
5296
|
return json({ error: "We could not prepare your new subscription. Your account is saved. Please retry." }, 503);
|
|
5007
5297
|
}
|
|
@@ -5132,8 +5422,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5132
5422
|
if (app.requireDisclaimerAck && !acked) {
|
|
5133
5423
|
return { ok: false, error: "disclaimerAck is required" };
|
|
5134
5424
|
}
|
|
5135
|
-
const
|
|
5136
|
-
const row2 = { id:
|
|
5425
|
+
const id7 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
5426
|
+
const row2 = { id: id7, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
5137
5427
|
for (const f of [...app.required, ...app.optional]) {
|
|
5138
5428
|
if (fieldStates[f]?.visible === false) continue;
|
|
5139
5429
|
if (typeof fields[f] === "string") row2[f] = fields[f].trim();
|
|
@@ -5167,12 +5457,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5167
5457
|
}
|
|
5168
5458
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
5169
5459
|
const ops = [
|
|
5170
|
-
{ t: "update", ns: "applications", id:
|
|
5460
|
+
{ t: "update", ns: "applications", id: id7, attrs: row2 }
|
|
5171
5461
|
];
|
|
5172
5462
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
5173
5463
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
5174
5464
|
consumedAt: opts.now,
|
|
5175
|
-
consumedApplicationId:
|
|
5465
|
+
consumedApplicationId: id7
|
|
5176
5466
|
} });
|
|
5177
5467
|
}
|
|
5178
5468
|
try {
|
|
@@ -5180,7 +5470,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
5180
5470
|
ops,
|
|
5181
5471
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
5182
5472
|
);
|
|
5183
|
-
return { ok: true, id:
|
|
5473
|
+
return { ok: true, id: id7, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
5184
5474
|
} catch (error) {
|
|
5185
5475
|
if (error.code === "unique_violation") return {
|
|
5186
5476
|
ok: false,
|
|
@@ -5289,7 +5579,7 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
|
5289
5579
|
}
|
|
5290
5580
|
|
|
5291
5581
|
// src/join-founding-preview.ts
|
|
5292
|
-
async function foundingPreviews(tiers, env, ctx) {
|
|
5582
|
+
async function foundingPreviews(tiers, env, ctx, seats = /* @__PURE__ */ new Map()) {
|
|
5293
5583
|
const result = /* @__PURE__ */ new Map();
|
|
5294
5584
|
const preview = ctx.membershipAuthority?.previewOffer;
|
|
5295
5585
|
if (!preview) return result;
|
|
@@ -5297,6 +5587,10 @@ async function foundingPreviews(tiers, env, ctx) {
|
|
|
5297
5587
|
await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
|
|
5298
5588
|
try {
|
|
5299
5589
|
const offer = await preview({ ...scope2, skuId: tier.id }, env);
|
|
5590
|
+
if (offer.skuId !== tier.id || offer.basePriceCents !== tier.priceCents) return;
|
|
5591
|
+
if (ctx.membershipAuthority?.namedSeats && Number.isSafeInteger(offer.seatPriceCents) && Number(offer.seatPriceCents) >= 0) {
|
|
5592
|
+
seats.set(tier.id, { amountCents: Number(offer.seatPriceCents) });
|
|
5593
|
+
}
|
|
5300
5594
|
if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
|
|
5301
5595
|
result.set(tier.id, {
|
|
5302
5596
|
cohortLabel: offer.cohortLabel,
|
|
@@ -5386,14 +5680,14 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5386
5680
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
5387
5681
|
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
5388
5682
|
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
5389
|
-
const
|
|
5683
|
+
const id7 = typeof grant.id === "string" ? grant.id : "";
|
|
5390
5684
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
5391
5685
|
const normalized = normalizeAdmissionEmail(email);
|
|
5392
5686
|
const decision = grant.decision;
|
|
5393
5687
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
5394
5688
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
5395
|
-
if (!ID2.test(
|
|
5396
|
-
if (seenIds.has(
|
|
5689
|
+
if (!ID2.test(id7)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
5690
|
+
if (seenIds.has(id7)) errors.push(`document.grants repeats id "${id7}"`);
|
|
5397
5691
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
5398
5692
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
5399
5693
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -5403,10 +5697,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
5403
5697
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
5404
5698
|
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
5405
5699
|
}
|
|
5406
|
-
seenIds.add(
|
|
5700
|
+
seenIds.add(id7);
|
|
5407
5701
|
seenEmails.add(normalized);
|
|
5408
5702
|
grants.push({
|
|
5409
|
-
id:
|
|
5703
|
+
id: id7,
|
|
5410
5704
|
email,
|
|
5411
5705
|
decision,
|
|
5412
5706
|
...note ? { note } : {},
|
|
@@ -5478,8 +5772,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5478
5772
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
5479
5773
|
const ops = [];
|
|
5480
5774
|
for (const row2 of current) {
|
|
5481
|
-
const
|
|
5482
|
-
if (
|
|
5775
|
+
const id7 = String(row2.id ?? "");
|
|
5776
|
+
if (id7 && !incomingIds.has(id7)) ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5483
5777
|
active: false,
|
|
5484
5778
|
revokedAt: receivedAt,
|
|
5485
5779
|
revokedBy: document.source.actor,
|
|
@@ -5488,10 +5782,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5488
5782
|
} });
|
|
5489
5783
|
}
|
|
5490
5784
|
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:
|
|
5785
|
+
const id7 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
5786
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
5787
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
5788
|
+
id: id7,
|
|
5495
5789
|
sourceGrantId: grant.id,
|
|
5496
5790
|
sourceScope: "parent",
|
|
5497
5791
|
sourceId: document.source.appId,
|
|
@@ -5512,7 +5806,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
5512
5806
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
5513
5807
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
5514
5808
|
];
|
|
5515
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
5809
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id7, attrs: retract });
|
|
5516
5810
|
}
|
|
5517
5811
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
5518
5812
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -5543,9 +5837,9 @@ var APPLICATION_LABELS = {
|
|
|
5543
5837
|
phone: "Phone",
|
|
5544
5838
|
state: "State"
|
|
5545
5839
|
};
|
|
5546
|
-
function humanize(
|
|
5547
|
-
const text8 =
|
|
5548
|
-
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` :
|
|
5840
|
+
function humanize(id7) {
|
|
5841
|
+
const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
5842
|
+
return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
|
|
5549
5843
|
}
|
|
5550
5844
|
function applicationDefs(chapter) {
|
|
5551
5845
|
try {
|
|
@@ -5554,23 +5848,23 @@ function applicationDefs(chapter) {
|
|
|
5554
5848
|
return {};
|
|
5555
5849
|
}
|
|
5556
5850
|
}
|
|
5557
|
-
function applicationType(
|
|
5851
|
+
function applicationType(id7, def) {
|
|
5558
5852
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
5559
|
-
return
|
|
5853
|
+
return id7 === "email" ? "email" : id7 === "linkedin" ? "url" : "string";
|
|
5560
5854
|
}
|
|
5561
5855
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
5562
5856
|
const defs = applicationDefs(chapter);
|
|
5563
|
-
return [...application.required, ...application.optional].map((
|
|
5564
|
-
const def = defs[
|
|
5565
|
-
const conditions = application.conditions[
|
|
5566
|
-
const type = applicationType(
|
|
5857
|
+
return [...application.required, ...application.optional].map((id7, order2) => {
|
|
5858
|
+
const def = defs[id7];
|
|
5859
|
+
const conditions = application.conditions[id7];
|
|
5860
|
+
const type = applicationType(id7, def);
|
|
5567
5861
|
return {
|
|
5568
|
-
id:
|
|
5569
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
5862
|
+
id: id7,
|
|
5863
|
+
label: def?.label ?? APPLICATION_LABELS[id7] ?? humanize(id7),
|
|
5570
5864
|
type,
|
|
5571
|
-
required: application.required.includes(
|
|
5865
|
+
required: application.required.includes(id7),
|
|
5572
5866
|
order: order2,
|
|
5573
|
-
maxLength: application.maxLen[
|
|
5867
|
+
maxLength: application.maxLen[id7] ?? application.defaultMaxLen,
|
|
5574
5868
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
5575
5869
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
5576
5870
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -5580,17 +5874,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
5580
5874
|
function resolveFormationFlowFields(crm, formation) {
|
|
5581
5875
|
if (!formation.enabled) return [];
|
|
5582
5876
|
const def = crm.type(formation.type);
|
|
5583
|
-
return [...formation.required, ...formation.optional].map((
|
|
5584
|
-
const field = def.fields[
|
|
5877
|
+
return [...formation.required, ...formation.optional].map((id7, order2) => {
|
|
5878
|
+
const field = def.fields[id7];
|
|
5585
5879
|
return {
|
|
5586
|
-
id:
|
|
5587
|
-
label: field.label ?? humanize(
|
|
5880
|
+
id: id7,
|
|
5881
|
+
label: field.label ?? humanize(id7),
|
|
5588
5882
|
type: field.type,
|
|
5589
|
-
required: formation.required.includes(
|
|
5883
|
+
required: formation.required.includes(id7),
|
|
5590
5884
|
order: order2,
|
|
5591
|
-
maxLength: formation.maxLen[
|
|
5885
|
+
maxLength: formation.maxLen[id7] ?? formation.defaultMaxLen,
|
|
5592
5886
|
...field.options ? { options: [...field.options] } : {},
|
|
5593
|
-
...
|
|
5887
|
+
...id7 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
5594
5888
|
};
|
|
5595
5889
|
});
|
|
5596
5890
|
}
|
|
@@ -5640,10 +5934,10 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5640
5934
|
}
|
|
5641
5935
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
5642
5936
|
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(
|
|
5937
|
+
const id7 = text3(item.id, `${path}.id`, errors, 80);
|
|
5938
|
+
if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5939
|
+
if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
5940
|
+
ids2.add(id7);
|
|
5647
5941
|
const targetRaw = record(item.target);
|
|
5648
5942
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
5649
5943
|
if (targetRaw?.type === "tier") {
|
|
@@ -5669,7 +5963,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
5669
5963
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
5670
5964
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
5671
5965
|
offers.push({
|
|
5672
|
-
id:
|
|
5966
|
+
id: id7,
|
|
5673
5967
|
target,
|
|
5674
5968
|
billingMode: item.billingMode,
|
|
5675
5969
|
stripePriceId,
|
|
@@ -5769,9 +6063,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5769
6063
|
const ops = [];
|
|
5770
6064
|
if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
|
|
5771
6065
|
for (const row2 of currentRows) {
|
|
5772
|
-
const
|
|
5773
|
-
if (
|
|
5774
|
-
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers",
|
|
6066
|
+
const id8 = String(row2.id ?? "");
|
|
6067
|
+
if (id8 && !incoming.has(id8)) {
|
|
6068
|
+
ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers", id8), attrs: { active: false } });
|
|
5775
6069
|
}
|
|
5776
6070
|
}
|
|
5777
6071
|
for (const tier of document.tiers) {
|
|
@@ -5806,9 +6100,9 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
|
|
|
5806
6100
|
if (!("stripePriceId" in attrs)) {
|
|
5807
6101
|
ops.push({ t: "retract", ns: "groups", id: groupRef, attrs: ["stripePriceId"] });
|
|
5808
6102
|
}
|
|
5809
|
-
const
|
|
5810
|
-
ops.push({ t: "update", ns: "signupControls", id:
|
|
5811
|
-
id:
|
|
6103
|
+
const id7 = `signup-control:${document.target.environment}:${document.revision}`;
|
|
6104
|
+
ops.push({ t: "update", ns: "signupControls", id: id7, attrs: {
|
|
6105
|
+
id: id7,
|
|
5812
6106
|
sourceId: document.source.appId,
|
|
5813
6107
|
targetId: document.target.chapterId,
|
|
5814
6108
|
environment: document.target.environment,
|
|
@@ -5847,6 +6141,8 @@ function foundingOf(offer) {
|
|
|
5847
6141
|
async function createPaymentQuote(input) {
|
|
5848
6142
|
const { group, tier, policy: policy2 } = input;
|
|
5849
6143
|
const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
|
|
6144
|
+
if (input.seatOffer && (!terms2 || !Number.isSafeInteger(input.seatOffer.amountCents) || input.seatOffer.amountCents < 0) || input.namedSeat && !input.seatOffer) throw new Error("invalid checkout seat offer");
|
|
6145
|
+
const seatCents = input.namedSeat ? input.seatOffer.amountCents : 0;
|
|
5850
6146
|
const compareAt = input.presentation?.compareAtPriceCents;
|
|
5851
6147
|
const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
|
|
5852
6148
|
const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
|
|
@@ -5855,6 +6151,7 @@ async function createPaymentQuote(input) {
|
|
|
5855
6151
|
discountCents: tier.priceCents - dueToday,
|
|
5856
6152
|
dueTodayCents: dueToday
|
|
5857
6153
|
};
|
|
6154
|
+
if (!Number.isSafeInteger(lines.dueTodayCents + seatCents) || !Number.isSafeInteger(tier.priceCents + seatCents)) throw new Error("invalid checkout total");
|
|
5858
6155
|
const quote = {
|
|
5859
6156
|
applicationId: input.applicationId,
|
|
5860
6157
|
chapterId: input.chapterId,
|
|
@@ -5862,11 +6159,17 @@ async function createPaymentQuote(input) {
|
|
|
5862
6159
|
tierName: tier.name,
|
|
5863
6160
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
5864
6161
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
5865
|
-
lineItems:
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
6162
|
+
lineItems: {
|
|
6163
|
+
...presentation ? {
|
|
6164
|
+
standardCents: presentation.compareAtPriceCents,
|
|
6165
|
+
discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
|
|
6166
|
+
dueTodayCents: lines.dueTodayCents
|
|
6167
|
+
} : lines,
|
|
6168
|
+
dueTodayCents: lines.dueTodayCents + seatCents,
|
|
6169
|
+
...input.namedSeat ? { namedSeatCents: seatCents } : {}
|
|
6170
|
+
},
|
|
6171
|
+
...input.seatOffer ? { seatOffer: input.seatOffer } : {},
|
|
6172
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {},
|
|
5870
6173
|
...presentation ? { presentation } : {},
|
|
5871
6174
|
refundPolicyText: policy2?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
|
|
5872
6175
|
merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
|
|
@@ -5878,7 +6181,7 @@ async function createPaymentQuote(input) {
|
|
|
5878
6181
|
discountDuration: terms2.discount.duration === "once" ? "first_term" : "while_active"
|
|
5879
6182
|
} : {},
|
|
5880
6183
|
...terms2.initialTermEndsAt !== void 0 ? { initialTermEndsAt: terms2.initialTermEndsAt } : {},
|
|
5881
|
-
renewalCents: terms2.discount?.duration === "forever" ? dueToday : tier.priceCents,
|
|
6184
|
+
renewalCents: (terms2.discount?.duration === "forever" ? dueToday : tier.priceCents) + seatCents,
|
|
5882
6185
|
autoRenew: true
|
|
5883
6186
|
} } : {}
|
|
5884
6187
|
};
|
|
@@ -5898,22 +6201,23 @@ async function createPaymentQuote(input) {
|
|
|
5898
6201
|
// src/membership-quote-projection.ts
|
|
5899
6202
|
var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
|
|
5900
6203
|
var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
5901
|
-
var
|
|
6204
|
+
var fail6 = (code) => {
|
|
5902
6205
|
throw Object.assign(new Error(code), { code });
|
|
5903
6206
|
};
|
|
5904
|
-
var load3 = async (db, ns,
|
|
6207
|
+
var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
|
|
5905
6208
|
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
|
|
6209
|
+
if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail6("membership_quote_unavailable");
|
|
6210
|
+
if (row2.status !== "ready") return fail6("membership_quote_pending");
|
|
5908
6211
|
const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
|
|
5909
6212
|
...target,
|
|
5910
6213
|
requestId: row2.requestId
|
|
5911
6214
|
}), now, lifetimeMs: 6e4 });
|
|
5912
|
-
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return
|
|
6215
|
+
if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
|
|
5913
6216
|
const tier = row2.tier, quote = row2.quote;
|
|
5914
6217
|
const offering = offer.contract.offering;
|
|
6218
|
+
if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
|
|
5915
6219
|
if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
|
|
5916
|
-
return
|
|
6220
|
+
return fail6("membership_catalog_mismatch");
|
|
5917
6221
|
}
|
|
5918
6222
|
const expected = await createPaymentQuote({
|
|
5919
6223
|
...target,
|
|
@@ -5922,19 +6226,21 @@ async function restore2(row2, target, now, allowExpired = false) {
|
|
|
5922
6226
|
policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
|
|
5923
6227
|
billingTerms: offer.contract.billingTerms,
|
|
5924
6228
|
presentation: quote.presentation,
|
|
5925
|
-
founding: foundingOf(offer)
|
|
6229
|
+
founding: foundingOf(offer),
|
|
6230
|
+
seatOffer: quote.seatOffer,
|
|
6231
|
+
namedSeat: quote.namedSeat
|
|
5926
6232
|
});
|
|
5927
|
-
if (quote.digest !== expected.digest) return
|
|
6233
|
+
if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
|
|
5928
6234
|
return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
|
|
5929
6235
|
}
|
|
5930
6236
|
async function readMembershipQuote(db, target, now) {
|
|
5931
6237
|
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, now);
|
|
5932
6238
|
}
|
|
5933
6239
|
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
|
|
6240
|
+
const { applicationId: id7, now, app } = input;
|
|
6241
|
+
if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail6("membership_quote_unavailable");
|
|
6242
|
+
let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6243
|
+
if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
|
|
5938
6244
|
if (row2?.status === "ready") {
|
|
5939
6245
|
try {
|
|
5940
6246
|
return await restore2(row2, input, now);
|
|
@@ -5945,15 +6251,15 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5945
6251
|
const identity2 = ["groupId", "tierId", "stripeRuntime", "email", "emailNormalized", "status", "canceled", "membershipRestartId", "namedSeatId", "namedSeatClaimPending", "giftClaimId", "giftOfferId"];
|
|
5946
6252
|
const guards = [{
|
|
5947
6253
|
ns: "applications",
|
|
5948
|
-
id:
|
|
6254
|
+
id: id7,
|
|
5949
6255
|
exists: true,
|
|
5950
6256
|
equals: Object.fromEntries(identity2.filter((key3) => app[key3] !== void 0).map((key3) => [key3, app[key3]])),
|
|
5951
6257
|
absent: identity2.filter((key3) => app[key3] === void 0)
|
|
5952
|
-
}, { ns: "subscriptionCheckoutIntents", id:
|
|
6258
|
+
}, { ns: "subscriptionCheckoutIntents", id: id7, exists: false }];
|
|
5953
6259
|
if (!row2 || row2.status === "ready" || row2.status === "restart") {
|
|
5954
6260
|
const revision = Number(row2?.revision ?? 0) + 1;
|
|
5955
6261
|
const pending = {
|
|
5956
|
-
id:
|
|
6262
|
+
id: id7,
|
|
5957
6263
|
...Object.fromEntries(keys2.map((key3) => [key3, input[key3]])),
|
|
5958
6264
|
revision,
|
|
5959
6265
|
status: "pending",
|
|
@@ -5961,25 +6267,25 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
5961
6267
|
updatedAt: now
|
|
5962
6268
|
};
|
|
5963
6269
|
try {
|
|
5964
|
-
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id:
|
|
5965
|
-
mutationId: `membership-quote:${
|
|
6270
|
+
await db.transact([{ t: "update", ns: MEMBERSHIP_QUOTE_NAMESPACE, id: id7, attrs: pending }], {
|
|
6271
|
+
mutationId: `membership-quote:${id7}:${revision}`,
|
|
5966
6272
|
guards: [...guards, {
|
|
5967
6273
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
5968
|
-
id:
|
|
6274
|
+
id: id7,
|
|
5969
6275
|
exists: Boolean(row2),
|
|
5970
6276
|
...row2 ? { equals: { revision: row2.revision, status: row2.status } } : {}
|
|
5971
6277
|
}]
|
|
5972
6278
|
});
|
|
5973
6279
|
} catch (error) {
|
|
5974
6280
|
if (error.code !== "transact_guard_failed") throw error;
|
|
5975
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6281
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
5976
6282
|
if (!winner || winner.revision !== revision) throw error;
|
|
5977
6283
|
}
|
|
5978
|
-
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6284
|
+
row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
5979
6285
|
}
|
|
5980
6286
|
if (row2?.status === "ready") return restore2(row2, input, now);
|
|
5981
6287
|
if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
|
|
5982
|
-
return
|
|
6288
|
+
return fail6("membership_quote_pending");
|
|
5983
6289
|
}
|
|
5984
6290
|
const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
|
|
5985
6291
|
const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
|
|
@@ -6000,7 +6306,8 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6000
6306
|
policy: input.policy,
|
|
6001
6307
|
billingTerms: offer.contract.billingTerms,
|
|
6002
6308
|
presentation: input.presentation,
|
|
6003
|
-
founding: foundingOf(offer)
|
|
6309
|
+
founding: foundingOf(offer),
|
|
6310
|
+
...input.namedSeatsEnabled ? { seatOffer: { amountCents: offer.contract.offering.seatPriceCents } } : {}
|
|
6004
6311
|
});
|
|
6005
6312
|
const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
|
|
6006
6313
|
await restore2(ready, target, now, true);
|
|
@@ -6008,23 +6315,100 @@ async function prepareMembershipQuote(db, input, issue) {
|
|
|
6008
6315
|
await db.transact([{
|
|
6009
6316
|
t: "update",
|
|
6010
6317
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6011
|
-
id:
|
|
6318
|
+
id: id7,
|
|
6012
6319
|
attrs: { offer, offerDigest: offer.digest, tier, quote, status: "ready", updatedAt: now }
|
|
6013
6320
|
}], {
|
|
6014
|
-
mutationId: `membership-quote-ready:${
|
|
6321
|
+
mutationId: `membership-quote-ready:${id7}:${row2.revision}`,
|
|
6015
6322
|
guards: [...guards, {
|
|
6016
6323
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6017
|
-
id:
|
|
6324
|
+
id: id7,
|
|
6018
6325
|
exists: true,
|
|
6019
6326
|
equals: { status: "pending", revision: row2.revision, requestId: row2.requestId }
|
|
6020
6327
|
}]
|
|
6021
6328
|
});
|
|
6022
6329
|
} catch (error) {
|
|
6023
6330
|
if (error.code !== "transact_guard_failed") throw error;
|
|
6024
|
-
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6331
|
+
const winner = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
|
|
6025
6332
|
if (winner?.status !== "ready" || winner.requestId !== row2.requestId || winner.offerDigest !== offer.digest) throw error;
|
|
6026
6333
|
}
|
|
6027
|
-
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6334
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
|
|
6335
|
+
}
|
|
6336
|
+
async function selectCheckoutNamedSeat(db, target, input) {
|
|
6337
|
+
if (!input.enabled) return fail6("named_seat_purchase_unavailable");
|
|
6338
|
+
const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
|
|
6339
|
+
const current = await restore2(row2, target, input.now);
|
|
6340
|
+
const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
|
|
6341
|
+
const quote = await createPaymentQuote({
|
|
6342
|
+
...target,
|
|
6343
|
+
tier: current.tier,
|
|
6344
|
+
group: { merchantDisclosureText: current.quote.merchantDisclosureText },
|
|
6345
|
+
policy: { refundPolicyText: current.quote.refundPolicyText, trustCopy: current.quote.trustCopy },
|
|
6346
|
+
billingTerms: current.billingTerms,
|
|
6347
|
+
presentation: current.quote.presentation,
|
|
6348
|
+
founding: current.quote.founding,
|
|
6349
|
+
seatOffer: { amountCents: current.membershipOffer.contract.offering.seatPriceCents },
|
|
6350
|
+
namedSeat
|
|
6351
|
+
});
|
|
6352
|
+
if (quote.digest === current.quote.digest) return current;
|
|
6353
|
+
await db.transact([{
|
|
6354
|
+
t: "update",
|
|
6355
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6356
|
+
id: target.applicationId,
|
|
6357
|
+
attrs: { quote, revision: current.projectionRevision + 1, updatedAt: input.now }
|
|
6358
|
+
}], {
|
|
6359
|
+
mutationId: `membership-quote-selection:${target.applicationId}:${current.projectionRevision}:${quote.digest}`,
|
|
6360
|
+
guards: [
|
|
6361
|
+
{
|
|
6362
|
+
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
6363
|
+
id: target.applicationId,
|
|
6364
|
+
exists: true,
|
|
6365
|
+
equals: { revision: current.projectionRevision, status: "ready" }
|
|
6366
|
+
},
|
|
6367
|
+
{ ns: "subscriptionCheckoutIntents", id: target.applicationId, exists: false }
|
|
6368
|
+
]
|
|
6369
|
+
});
|
|
6370
|
+
return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId), target, input.now);
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
// src/bounded-request-text.ts
|
|
6374
|
+
async function readBoundedRequestText(req, maxBytes) {
|
|
6375
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
6376
|
+
const reader = req.body?.getReader();
|
|
6377
|
+
const cancel = () => {
|
|
6378
|
+
void reader?.cancel().catch(() => {
|
|
6379
|
+
});
|
|
6380
|
+
};
|
|
6381
|
+
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
6382
|
+
let complete = false;
|
|
6383
|
+
req.signal.addEventListener("abort", cancel, { once: true });
|
|
6384
|
+
try {
|
|
6385
|
+
if (req.signal.aborted) return invalid2();
|
|
6386
|
+
const declared = req.headers.get("content-length");
|
|
6387
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
6388
|
+
return json({ error: "request body too large" }, 413);
|
|
6389
|
+
}
|
|
6390
|
+
if (!reader) return "";
|
|
6391
|
+
const decoder = new TextDecoder();
|
|
6392
|
+
let bytes = 0;
|
|
6393
|
+
let text8 = "";
|
|
6394
|
+
for (; ; ) {
|
|
6395
|
+
const { done, value } = await reader.read();
|
|
6396
|
+
if (req.signal.aborted) return invalid2();
|
|
6397
|
+
if (done) {
|
|
6398
|
+
complete = true;
|
|
6399
|
+
return text8 + decoder.decode();
|
|
6400
|
+
}
|
|
6401
|
+
bytes += value.byteLength;
|
|
6402
|
+
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
6403
|
+
text8 += decoder.decode(value, { stream: true });
|
|
6404
|
+
}
|
|
6405
|
+
} catch {
|
|
6406
|
+
return invalid2();
|
|
6407
|
+
} finally {
|
|
6408
|
+
req.signal.removeEventListener("abort", cancel);
|
|
6409
|
+
if (!complete) cancel();
|
|
6410
|
+
reader?.releaseLock();
|
|
6411
|
+
}
|
|
6028
6412
|
}
|
|
6029
6413
|
|
|
6030
6414
|
// src/payment-quote-route.ts
|
|
@@ -6058,8 +6442,11 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6058
6442
|
policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
|
|
6059
6443
|
billingTerms,
|
|
6060
6444
|
presentation: quote2.presentation,
|
|
6061
|
-
founding: foundingOf(intent.membershipOffer)
|
|
6445
|
+
founding: foundingOf(intent.membershipOffer),
|
|
6446
|
+
seatOffer: quote2.seatOffer,
|
|
6447
|
+
namedSeat: quote2.namedSeat
|
|
6062
6448
|
});
|
|
6449
|
+
if (quote2.seatOffer && quote2.seatOffer.amountCents !== intent.membershipOffer?.contract.offering.seatPriceCents) throw new Error("invalid seat snapshot");
|
|
6063
6450
|
if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
|
|
6064
6451
|
return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
|
|
6065
6452
|
}
|
|
@@ -6088,7 +6475,16 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6088
6475
|
const presentation = tierPresentationFor(control3?.document, tier.id);
|
|
6089
6476
|
if (ctx.membershipAuthority) return prepareMembershipQuote(
|
|
6090
6477
|
db,
|
|
6091
|
-
{
|
|
6478
|
+
{
|
|
6479
|
+
...target,
|
|
6480
|
+
app,
|
|
6481
|
+
group,
|
|
6482
|
+
tier,
|
|
6483
|
+
policy: policy2,
|
|
6484
|
+
presentation,
|
|
6485
|
+
namedSeatsEnabled: Boolean(ctx.membershipAuthority.namedSeats) && !app.membershipRestartId,
|
|
6486
|
+
now: Date.now()
|
|
6487
|
+
},
|
|
6092
6488
|
(input) => ctx.membershipAuthority.issueOffer(input, env)
|
|
6093
6489
|
);
|
|
6094
6490
|
const quote = await createPaymentQuote({
|
|
@@ -6104,20 +6500,54 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
|
|
|
6104
6500
|
});
|
|
6105
6501
|
return { tier, quote, billingTerms: void 0 };
|
|
6106
6502
|
}
|
|
6107
|
-
async function paymentQuoteRoute(url, env, ctx, prepare = false) {
|
|
6108
|
-
|
|
6109
|
-
if (
|
|
6503
|
+
async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
|
|
6504
|
+
let selection;
|
|
6505
|
+
if (request?.body) {
|
|
6506
|
+
try {
|
|
6507
|
+
const text8 = await readBoundedRequestText(request, 2048);
|
|
6508
|
+
if (text8 instanceof Response) return text8;
|
|
6509
|
+
const body = JSON.parse(text8);
|
|
6510
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
|
|
6511
|
+
selection = body.namedSeat;
|
|
6512
|
+
} catch {
|
|
6513
|
+
return json({ error: "invalid seat selection" }, 400);
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
const id7 = url.searchParams.get("application");
|
|
6517
|
+
if (!id7 || id7.length > 160) return json({ error: "application required" }, 400);
|
|
6110
6518
|
const db = ctx.makeDb(env);
|
|
6111
|
-
const app = (await db.query({ applications: { $: { where: { id:
|
|
6519
|
+
const app = (await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } })).applications?.[0];
|
|
6112
6520
|
if (!app || String(app.groupId ?? ctx.chapter.id) !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
6113
6521
|
if (typeof app.stripeRuntime === "string" && app.stripeRuntime !== stripeRuntimeIdentity(env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
6114
6522
|
if (app.status !== "submitted" || app.giftOfferId || app.namedSeatId || app.namedSeatClaimPending || app.giftClaimId || app.canceled === true) return json({ error: "membership checkout unavailable" }, 409);
|
|
6115
6523
|
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
6116
6524
|
if (!group) return json({ error: "payments not configured" }, 503);
|
|
6117
6525
|
try {
|
|
6118
|
-
|
|
6119
|
-
|
|
6526
|
+
let { quote } = await checkoutQuoteForApplication(db, app, group, env, ctx, prepare);
|
|
6527
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
6528
|
+
if (prepare && !intent && !quote.seatOffer && ctx.membershipAuthority?.namedSeats && !app.membershipRestartId && selection === void 0) selection = null;
|
|
6529
|
+
if (selection !== void 0) {
|
|
6530
|
+
if (intent) return json({ error: "checkout has already started", code: "checkout_intent_mismatch" }, 409);
|
|
6531
|
+
({ quote } = await selectCheckoutNamedSeat(
|
|
6532
|
+
db,
|
|
6533
|
+
{
|
|
6534
|
+
appId: env.ODLA_APP_ID,
|
|
6535
|
+
chapterId: ctx.chapter.id,
|
|
6536
|
+
environment: env.ODLA_ENV,
|
|
6537
|
+
runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
|
|
6538
|
+
applicationId: id7,
|
|
6539
|
+
skuId: String(app.tierId)
|
|
6540
|
+
},
|
|
6541
|
+
{ selection, purchaserEmail: String(app.email ?? ""), now: Date.now(), enabled: Boolean(ctx.membershipAuthority?.namedSeats) && !app.membershipRestartId }
|
|
6542
|
+
));
|
|
6543
|
+
}
|
|
6544
|
+
return new Response(JSON.stringify(quote), { headers: {
|
|
6545
|
+
"content-type": "application/json",
|
|
6546
|
+
"cache-control": "no-store",
|
|
6547
|
+
"x-chapter-checkout-started": intent ? "true" : "false"
|
|
6548
|
+
} });
|
|
6120
6549
|
} catch (error) {
|
|
6550
|
+
if (error.code === "invalid_named_seat_recipient") return json({ error: "Enter a different recipient's name and email." }, 400);
|
|
6121
6551
|
const expired = error.code === "membership_quote_expired";
|
|
6122
6552
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_expired" } : {} }, expired ? 409 : 503);
|
|
6123
6553
|
}
|
|
@@ -6223,47 +6653,6 @@ async function resumeJoin(url, env, ctx) {
|
|
|
6223
6653
|
return json({ error: "Application continuation is unavailable. Retry when its payment or membership configuration is restored." }, 409);
|
|
6224
6654
|
}
|
|
6225
6655
|
|
|
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
6656
|
// src/named-seat-application.ts
|
|
6268
6657
|
async function namedSeatPurchaserName(db, applicationId, chapterId, runtime) {
|
|
6269
6658
|
const app = await membershipLoad(db, "applications", applicationId);
|
|
@@ -6778,11 +7167,12 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
6778
7167
|
group,
|
|
6779
7168
|
Boolean(stripeKey)
|
|
6780
7169
|
);
|
|
6781
|
-
const
|
|
7170
|
+
const seats = /* @__PURE__ */ new Map();
|
|
7171
|
+
const founding = await foundingPreviews(offered, env, ctx, seats);
|
|
6782
7172
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
6783
7173
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
6784
7174
|
return json({
|
|
6785
|
-
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
|
|
7175
|
+
...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
|
|
6786
7176
|
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
6787
7177
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
6788
7178
|
...activeControl ? { signupControl: {
|
|
@@ -7158,10 +7548,10 @@ function tiersOf(value, errors) {
|
|
|
7158
7548
|
return [];
|
|
7159
7549
|
}
|
|
7160
7550
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
7161
|
-
const
|
|
7162
|
-
if (
|
|
7163
|
-
if (seen.has(
|
|
7164
|
-
seen.add(
|
|
7551
|
+
const id7 = text4(item.id, `${path}.id`, errors, 80);
|
|
7552
|
+
if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7553
|
+
if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
|
|
7554
|
+
seen.add(id7);
|
|
7165
7555
|
const name = text4(item.name, `${path}.name`, errors, 160);
|
|
7166
7556
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
7167
7557
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
@@ -7182,7 +7572,7 @@ function tiersOf(value, errors) {
|
|
|
7182
7572
|
}
|
|
7183
7573
|
const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
7184
7574
|
return [{
|
|
7185
|
-
id:
|
|
7575
|
+
id: id7,
|
|
7186
7576
|
name,
|
|
7187
7577
|
available: item.available === true,
|
|
7188
7578
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -7244,15 +7634,15 @@ function policyOf(value, errors, tiers) {
|
|
|
7244
7634
|
if (input.tierPolicies !== void 0) {
|
|
7245
7635
|
const overrides = recordOf(input.tierPolicies);
|
|
7246
7636
|
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.${
|
|
7637
|
+
else for (const [id7, raw2] of Object.entries(overrides)) {
|
|
7638
|
+
const path = `document.policy.tierPolicies.${id7}`;
|
|
7249
7639
|
const policy2 = recordOf(raw2);
|
|
7250
|
-
if (!tiers.some((tier) => tier.id ===
|
|
7640
|
+
if (!tiers.some((tier) => tier.id === id7) || !policy2) {
|
|
7251
7641
|
errors.push(`${path} must name a declared tier and policy`);
|
|
7252
7642
|
continue;
|
|
7253
7643
|
}
|
|
7254
7644
|
unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
7255
|
-
tierPolicies[
|
|
7645
|
+
tierPolicies[id7] = {
|
|
7256
7646
|
refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
7257
7647
|
...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
7258
7648
|
};
|
|
@@ -7545,14 +7935,14 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7545
7935
|
continue;
|
|
7546
7936
|
}
|
|
7547
7937
|
unknownKeys2(raw2, PAGE_KEYS, path, errors);
|
|
7548
|
-
const
|
|
7938
|
+
const id7 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
7549
7939
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
7550
7940
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
7551
|
-
if (
|
|
7941
|
+
if (id7 && !ID4.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7552
7942
|
if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
7553
|
-
if (ids2.has(
|
|
7943
|
+
if (ids2.has(id7) || knownParentIds.has(id7)) errors.push(`${path}.id duplicates page "${id7}"`);
|
|
7554
7944
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
7555
|
-
ids2.add(
|
|
7945
|
+
ids2.add(id7);
|
|
7556
7946
|
slugs.add(slug);
|
|
7557
7947
|
const navRaw = record2(raw2.navigation);
|
|
7558
7948
|
let navigation = { section: "General", order: index2 };
|
|
@@ -7608,7 +7998,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
7608
7998
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
7609
7999
|
}
|
|
7610
8000
|
}
|
|
7611
|
-
pages.push({ id:
|
|
8001
|
+
pages.push({ id: id7, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
7612
8002
|
}
|
|
7613
8003
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
7614
8004
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -7751,20 +8141,20 @@ async function loadLatestMemberContentControl(db, targetId, environment) {
|
|
|
7751
8141
|
return control(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
7752
8142
|
}
|
|
7753
8143
|
async function loadLocalMemberContent(db, chapterId, environment) {
|
|
7754
|
-
const
|
|
7755
|
-
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id:
|
|
8144
|
+
const id7 = `member-content-local:${environment}`;
|
|
8145
|
+
const rows5 = (await db.query({ memberContentLocal: { $: { where: { id: id7 }, limit: 1 } } })).memberContentLocal;
|
|
7756
8146
|
return local2(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
7757
8147
|
}
|
|
7758
8148
|
async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
|
|
7759
8149
|
const { document, digest } = envelope;
|
|
7760
8150
|
const target = document.target;
|
|
7761
|
-
const
|
|
8151
|
+
const id7 = `member-content-control:${target.environment}:${document.revision}`;
|
|
7762
8152
|
const ops = [];
|
|
7763
8153
|
if (previous?.active) {
|
|
7764
8154
|
ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
|
|
7765
8155
|
}
|
|
7766
|
-
ops.push({ t: "update", ns: "memberContentControls", id:
|
|
7767
|
-
id:
|
|
8156
|
+
ops.push({ t: "update", ns: "memberContentControls", id: id7, attrs: {
|
|
8157
|
+
id: id7,
|
|
7768
8158
|
sourceId: document.source.appId,
|
|
7769
8159
|
targetId: target.chapterId,
|
|
7770
8160
|
environment: target.environment,
|
|
@@ -7777,11 +8167,11 @@ async function materializeMemberContentControl(db, envelope, previous, receivedA
|
|
|
7777
8167
|
await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
|
|
7778
8168
|
}
|
|
7779
8169
|
async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
7780
|
-
const
|
|
8170
|
+
const id7 = `member-content-local:${environment}`;
|
|
7781
8171
|
const current = await loadLocalMemberContent(db, document.source.appId, environment);
|
|
7782
8172
|
if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
|
|
7783
8173
|
const row2 = {
|
|
7784
|
-
id:
|
|
8174
|
+
id: id7,
|
|
7785
8175
|
chapterId: document.source.appId,
|
|
7786
8176
|
environment,
|
|
7787
8177
|
revision: document.revision,
|
|
@@ -7789,9 +8179,9 @@ async function saveLocalMemberContent(db, document, environment, expectedRevisio
|
|
|
7789
8179
|
updatedAt,
|
|
7790
8180
|
...updatedBy ? { updatedBy } : {}
|
|
7791
8181
|
};
|
|
7792
|
-
await db.transact([{ t: "update", ns: "memberContentLocal", id:
|
|
8182
|
+
await db.transact([{ t: "update", ns: "memberContentLocal", id: id7, attrs: row2 }], {
|
|
7793
8183
|
mutationId: `member-content-local:${environment}:${document.revision}`,
|
|
7794
|
-
guards: current ? [{ ns: "memberContentLocal", id:
|
|
8184
|
+
guards: current ? [{ ns: "memberContentLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id: id7, exists: false }]
|
|
7795
8185
|
});
|
|
7796
8186
|
return row2;
|
|
7797
8187
|
}
|
|
@@ -7928,7 +8318,7 @@ var handleAdminMemberContent = async (req, url, env, ctx) => {
|
|
|
7928
8318
|
parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
|
|
7929
8319
|
chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
|
|
7930
8320
|
effective,
|
|
7931
|
-
tiers: tiers.map(({ id:
|
|
8321
|
+
tiers: tiers.map(({ id: id7, name, priceCents, active }) => ({ id: id7, name, priceCents, active })),
|
|
7932
8322
|
localAuthoring: ctx.chapter.memberContent.localAuthoring
|
|
7933
8323
|
});
|
|
7934
8324
|
}
|
|
@@ -8086,11 +8476,11 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8086
8476
|
return json({ error: "expiresAt must be a future epoch timestamp" }, 400);
|
|
8087
8477
|
}
|
|
8088
8478
|
const sourceGrantId = crypto.randomUUID();
|
|
8089
|
-
const
|
|
8479
|
+
const id7 = `admission:local:${ctx.chapter.id}:${sourceGrantId}`;
|
|
8090
8480
|
const createdAt = Date.now();
|
|
8091
8481
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8092
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8093
|
-
id:
|
|
8482
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8483
|
+
id: id7,
|
|
8094
8484
|
sourceGrantId,
|
|
8095
8485
|
sourceScope: "local",
|
|
8096
8486
|
sourceId: ctx.chapter.id,
|
|
@@ -8106,7 +8496,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8106
8496
|
updatedAt: createdAt,
|
|
8107
8497
|
updatedBy: actor
|
|
8108
8498
|
} }], { mutationId: `admission-local-create:${sourceGrantId}` });
|
|
8109
|
-
return json({ grant: { id:
|
|
8499
|
+
return json({ grant: { id: id7, sourceGrantId, sourceScope: "local", email, decision, active: true, createdAt, ...note ? { note } : {}, ...expiresAt ? { expiresAt } : {} } }, 201);
|
|
8110
8500
|
}
|
|
8111
8501
|
const prefix = "/api/admin/admission-grants/";
|
|
8112
8502
|
let revokeId = "";
|
|
@@ -8120,20 +8510,20 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
8120
8510
|
if (/^admission:local:[a-z0-9-]+:[0-9a-f-]+$/.test(revokeId) && req.method === "DELETE") {
|
|
8121
8511
|
const auth = await gate(req, env, ctx);
|
|
8122
8512
|
if (auth instanceof Response) return auth;
|
|
8123
|
-
const
|
|
8124
|
-
const found = await auth.db.query({ admissionGrants: { $: { where: { id:
|
|
8513
|
+
const id7 = revokeId;
|
|
8514
|
+
const found = await auth.db.query({ admissionGrants: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8125
8515
|
const row2 = Array.isArray(found.admissionGrants) ? found.admissionGrants[0] : void 0;
|
|
8126
8516
|
if (!row2 || row2.targetId !== ctx.chapter.id || row2.sourceScope !== "local") return json({ error: "not found" }, 404);
|
|
8127
8517
|
if (row2.active !== true) return json({ ok: true, replayed: true });
|
|
8128
8518
|
const revokedAt = Date.now();
|
|
8129
8519
|
const actor = auth.user.email ?? auth.user.userId;
|
|
8130
|
-
await auth.db.transact([{ t: "update", ns: "admissionGrants", id:
|
|
8520
|
+
await auth.db.transact([{ t: "update", ns: "admissionGrants", id: id7, attrs: {
|
|
8131
8521
|
active: false,
|
|
8132
8522
|
revokedAt,
|
|
8133
8523
|
revokedBy: actor,
|
|
8134
8524
|
updatedAt: revokedAt,
|
|
8135
8525
|
updatedBy: actor
|
|
8136
|
-
} }], { mutationId: `admission-local-revoke:${
|
|
8526
|
+
} }], { mutationId: `admission-local-revoke:${id7}` });
|
|
8137
8527
|
return json({ ok: true, replayed: false });
|
|
8138
8528
|
}
|
|
8139
8529
|
return null;
|
|
@@ -8195,8 +8585,8 @@ async function loadLeaderAdmissionAssignments(db, targetId) {
|
|
|
8195
8585
|
return await readAdmissionRows(db, "leaderAdmissionAssignments", { targetId }, MAX_ASSIGNMENTS);
|
|
8196
8586
|
}
|
|
8197
8587
|
async function loadLeaderAdmissionDelivery(db, targetId, environment) {
|
|
8198
|
-
const
|
|
8199
|
-
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id:
|
|
8588
|
+
const id7 = deliveryId(targetId, environment);
|
|
8589
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id: id7 }, limit: 1 } } });
|
|
8200
8590
|
return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
|
|
8201
8591
|
}
|
|
8202
8592
|
function personAdmissionEmail(ctx, record4) {
|
|
@@ -8224,8 +8614,8 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8224
8614
|
const now = Date.now();
|
|
8225
8615
|
const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
8226
8616
|
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
8227
|
-
const
|
|
8228
|
-
const existing = current.find((row2) => row2.id ===
|
|
8617
|
+
const id7 = assignmentId(opts.target.id, opts.personId);
|
|
8618
|
+
const existing = current.find((row2) => row2.id === id7);
|
|
8229
8619
|
const digest = await signupControlDigest({
|
|
8230
8620
|
targetId: opts.target.id,
|
|
8231
8621
|
personId: opts.personId,
|
|
@@ -8255,7 +8645,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8255
8645
|
return { delivery: saved, duplicate: true };
|
|
8256
8646
|
}
|
|
8257
8647
|
const assignment = {
|
|
8258
|
-
id:
|
|
8648
|
+
id: id7,
|
|
8259
8649
|
targetId: opts.target.id,
|
|
8260
8650
|
personId: opts.personId,
|
|
8261
8651
|
grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
@@ -8270,7 +8660,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8270
8660
|
updatedAt: now,
|
|
8271
8661
|
updatedBy: opts.actor
|
|
8272
8662
|
};
|
|
8273
|
-
const desired = current.filter((row2) => row2.id !==
|
|
8663
|
+
const desired = current.filter((row2) => row2.id !== id7 && row2.active).concat(assignment.active ? [assignment] : []);
|
|
8274
8664
|
const grants = await Promise.all(desired.map((row2) => grantInput(opts.db, opts.ctx, row2, opts.resolveEmail)));
|
|
8275
8665
|
const revision = (previous?.revision ?? 0) + 1;
|
|
8276
8666
|
const envelope = await createAdmissionGrantEnvelope({
|
|
@@ -8293,7 +8683,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8293
8683
|
updatedAt: now
|
|
8294
8684
|
};
|
|
8295
8685
|
const ops = [
|
|
8296
|
-
{ t: "update", ns: "leaderAdmissionAssignments", id:
|
|
8686
|
+
{ t: "update", ns: "leaderAdmissionAssignments", id: id7, attrs: assignment },
|
|
8297
8687
|
{ t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
|
|
8298
8688
|
{ t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
|
|
8299
8689
|
id: mutationReceiptId,
|
|
@@ -8308,7 +8698,7 @@ async function persistLeaderAdmissionDesiredSet(opts) {
|
|
|
8308
8698
|
...!opts.note && existing?.note !== void 0 ? ["note"] : [],
|
|
8309
8699
|
...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
8310
8700
|
];
|
|
8311
|
-
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id:
|
|
8701
|
+
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id: id7, attrs: retract });
|
|
8312
8702
|
const tx = await opts.db.transact(ops, {
|
|
8313
8703
|
mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
|
|
8314
8704
|
guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
|
|
@@ -8368,11 +8758,11 @@ function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
|
|
|
8368
8758
|
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
8369
8759
|
);
|
|
8370
8760
|
const def = crm.type(record4.type);
|
|
8371
|
-
const editFields = editable.map((
|
|
8372
|
-
const field = def.fields[
|
|
8761
|
+
const editFields = editable.map((id7) => {
|
|
8762
|
+
const field = def.fields[id7];
|
|
8373
8763
|
return {
|
|
8374
|
-
id:
|
|
8375
|
-
label: field.label ??
|
|
8764
|
+
id: id7,
|
|
8765
|
+
label: field.label ?? id7,
|
|
8376
8766
|
type: field.type,
|
|
8377
8767
|
required: field.required ?? false,
|
|
8378
8768
|
...field.options ? { options: field.options } : {}
|
|
@@ -8489,8 +8879,8 @@ async function gate2(req, env, ctx) {
|
|
|
8489
8879
|
if (!await networkWriteAllowed(req, db, user, ctx, true)) return { response: reject6(403, "super_admin_required", "super-admin required") };
|
|
8490
8880
|
return { db, user };
|
|
8491
8881
|
}
|
|
8492
|
-
function targetFor(ctx,
|
|
8493
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
8882
|
+
function targetFor(ctx, id7) {
|
|
8883
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7 && target.admissionGrants);
|
|
8494
8884
|
}
|
|
8495
8885
|
async function deliverSaved(db, env, ctx, target, delivery) {
|
|
8496
8886
|
if (delivery.status === "acknowledged") return delivery;
|
|
@@ -8594,7 +8984,7 @@ var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
|
8594
8984
|
if (!personId || !MUTATION_ID2.test(mutationId)) {
|
|
8595
8985
|
return reject6(422, "invalid_request", "personId and a stable mutationId are required");
|
|
8596
8986
|
}
|
|
8597
|
-
const resolveEmail = (
|
|
8987
|
+
const resolveEmail = (id7) => resolveAdmissionPersonEmail(db, env, ctx, target, id7);
|
|
8598
8988
|
let email;
|
|
8599
8989
|
try {
|
|
8600
8990
|
email = await resolveEmail(personId);
|
|
@@ -8833,28 +9223,28 @@ var handleSchedule = async (req, url, env, ctx) => {
|
|
|
8833
9223
|
};
|
|
8834
9224
|
|
|
8835
9225
|
// src/payments-stripe.ts
|
|
8836
|
-
import { stripeRequest as
|
|
9226
|
+
import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
8837
9227
|
|
|
8838
9228
|
// src/payments-refund.ts
|
|
8839
|
-
import { stripeRequest as
|
|
9229
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
8840
9230
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8841
|
-
var
|
|
9231
|
+
var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
8842
9232
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
8843
|
-
function
|
|
9233
|
+
function fail7(code = "refund_target_mismatch") {
|
|
8844
9234
|
throw Object.assign(new Error(code), { code });
|
|
8845
9235
|
}
|
|
8846
9236
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
8847
|
-
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub"))
|
|
9237
|
+
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
|
|
8848
9238
|
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))
|
|
9239
|
+
const result = await stripeRequest8({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
9240
|
+
if (!result.ok) fail7("stripe_error");
|
|
9241
|
+
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
|
|
8852
9242
|
return result.body;
|
|
8853
9243
|
};
|
|
8854
9244
|
const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8855
|
-
if (
|
|
9245
|
+
if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
|
|
8856
9246
|
for (const [key3, value] of Object.entries(input.metadata ?? {})) {
|
|
8857
|
-
if (row(sub.metadata)[key3] !== value)
|
|
9247
|
+
if (row(sub.metadata)[key3] !== value) fail7();
|
|
8858
9248
|
}
|
|
8859
9249
|
let cursor;
|
|
8860
9250
|
let earliest;
|
|
@@ -8867,79 +9257,79 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
8867
9257
|
limit: 100,
|
|
8868
9258
|
...cursor ? { starting_after: cursor } : {}
|
|
8869
9259
|
});
|
|
8870
|
-
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean")
|
|
9260
|
+
if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
|
|
8871
9261
|
for (const value of list.data) {
|
|
8872
9262
|
const invoice2 = row(value);
|
|
8873
9263
|
const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
|
|
8874
|
-
if (!providerId2(
|
|
8875
|
-
seen.add(
|
|
9264
|
+
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();
|
|
9265
|
+
seen.add(id3(invoice2));
|
|
8876
9266
|
if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
|
|
8877
9267
|
}
|
|
8878
9268
|
if (!list.has_more) break;
|
|
8879
|
-
if (!list.data.length || page === 9)
|
|
8880
|
-
cursor =
|
|
9269
|
+
if (!list.data.length || page === 9) fail7("refund_history_incomplete");
|
|
9270
|
+
cursor = id3(list.data[list.data.length - 1]);
|
|
8881
9271
|
}
|
|
8882
|
-
if (!earliest)
|
|
9272
|
+
if (!earliest) fail7("no_charge");
|
|
8883
9273
|
const invoice = earliest;
|
|
8884
|
-
if (input.expectedInvoiceId !== void 0 &&
|
|
8885
|
-
let chargeId =
|
|
8886
|
-
let paymentIntentId =
|
|
9274
|
+
if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
|
|
9275
|
+
let chargeId = id3(invoice.charge);
|
|
9276
|
+
let paymentIntentId = id3(invoice.payment_intent);
|
|
8887
9277
|
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)
|
|
9278
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
|
|
9279
|
+
if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
|
|
8890
9280
|
const payment = row(payments.data[0]);
|
|
8891
|
-
if (
|
|
8892
|
-
paymentIntentId =
|
|
9281
|
+
if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
|
|
9282
|
+
paymentIntentId = id3(row(payment.payment).payment_intent);
|
|
8893
9283
|
}
|
|
8894
9284
|
if (!chargeId) {
|
|
8895
|
-
if (!providerId2(paymentIntentId, "pi"))
|
|
9285
|
+
if (!providerId2(paymentIntentId, "pi")) fail7();
|
|
8896
9286
|
const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
|
|
8897
|
-
if (
|
|
8898
|
-
chargeId =
|
|
9287
|
+
if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
|
|
9288
|
+
chargeId = id3(payment.latest_charge);
|
|
8899
9289
|
}
|
|
8900
|
-
if (!providerId2(chargeId, "ch"))
|
|
9290
|
+
if (!providerId2(chargeId, "ch")) fail7();
|
|
8901
9291
|
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))
|
|
9292
|
+
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();
|
|
9293
|
+
if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
|
|
8904
9294
|
if (charge.amount_refunded !== charge.amount) {
|
|
8905
9295
|
const refund = await call("POST", "/v1/refunds", {
|
|
8906
9296
|
charge: chargeId,
|
|
8907
9297
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
8908
9298
|
}, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
|
|
8909
|
-
if (
|
|
9299
|
+
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
9300
|
}
|
|
8911
9301
|
let subscriptionCanceled = sub.status === "canceled";
|
|
8912
9302
|
if (input.cancelSubscription !== false && !subscriptionCanceled) {
|
|
8913
9303
|
const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
|
|
8914
|
-
if (
|
|
9304
|
+
if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
|
|
8915
9305
|
subscriptionCanceled = true;
|
|
8916
9306
|
}
|
|
8917
9307
|
const confirmed = await call("GET", `/v1/charges/${chargeId}`);
|
|
8918
|
-
if (
|
|
9308
|
+
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
9309
|
return { refundedCents: Number(charge.amount), subscriptionCanceled };
|
|
8920
9310
|
}
|
|
8921
9311
|
|
|
8922
9312
|
// src/subscription-payment-identity.ts
|
|
8923
|
-
import { stripeRequest as
|
|
8924
|
-
var
|
|
9313
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
9314
|
+
var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
|
|
8925
9315
|
async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
|
|
8926
|
-
let paymentId =
|
|
9316
|
+
let paymentId = id4(invoice.payment_intent);
|
|
8927
9317
|
if (!paymentId) {
|
|
8928
|
-
if (!/^in_[A-Za-z0-9]+$/.test(
|
|
8929
|
-
const result2 = await
|
|
9318
|
+
if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
|
|
9319
|
+
const result2 = await stripeRequest9({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
|
|
8930
9320
|
const list = result2.body;
|
|
8931
9321
|
if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
|
|
8932
9322
|
const link = list.data[0];
|
|
8933
|
-
if (
|
|
8934
|
-
paymentId =
|
|
9323
|
+
if (id4(link.invoice) !== id4(invoice) || !["open", "paid"].includes(link.status) || link.payment?.type !== "payment_intent") subscriptionContractMismatch();
|
|
9324
|
+
paymentId = id4(link.payment.payment_intent);
|
|
8935
9325
|
}
|
|
8936
9326
|
if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
|
|
8937
9327
|
const path = `/v1/payment_intents/${paymentId}`;
|
|
8938
|
-
const result = await
|
|
9328
|
+
const result = await stripeRequest9({ secretKey }, "GET", path);
|
|
8939
9329
|
const payment = result.body;
|
|
8940
|
-
if (!result.ok || payment?.id !== paymentId ||
|
|
9330
|
+
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
9331
|
if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
|
|
8942
|
-
const tagged = await
|
|
9332
|
+
const tagged = await stripeRequest9(
|
|
8943
9333
|
{ secretKey },
|
|
8944
9334
|
"POST",
|
|
8945
9335
|
path,
|
|
@@ -8951,7 +9341,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
|
|
|
8951
9341
|
|
|
8952
9342
|
// src/payments-stripe.ts
|
|
8953
9343
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
8954
|
-
return
|
|
9344
|
+
return stripeRequest10(
|
|
8955
9345
|
{ secretKey: sk },
|
|
8956
9346
|
method,
|
|
8957
9347
|
path,
|
|
@@ -8959,7 +9349,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
|
8959
9349
|
idempotencyKey ? { idempotencyKey } : {}
|
|
8960
9350
|
);
|
|
8961
9351
|
}
|
|
8962
|
-
function
|
|
9352
|
+
function fail8(op, r) {
|
|
8963
9353
|
const err = new Error(`stripe ${op} failed: ${r.status}`);
|
|
8964
9354
|
err.code = "stripe_error";
|
|
8965
9355
|
throw err;
|
|
@@ -8998,6 +9388,15 @@ function createStripeProvider(config) {
|
|
|
8998
9388
|
let customerId = input.existingCustomerId;
|
|
8999
9389
|
if (customerId && !/^cus_[A-Za-z0-9]+$/.test(customerId) || input.existingSubscriptionId && (!customerId || !/^sub_[A-Za-z0-9]+$/.test(input.existingSubscriptionId))) subscriptionContractMismatch();
|
|
9000
9390
|
const productId = await verifySubscriptionPrice(sk, input, Boolean(input.existingSubscriptionId));
|
|
9391
|
+
const seat = input.namedSeat?.amountCents ? input.namedSeat : void 0;
|
|
9392
|
+
const seatProductId = seat ? await verifySubscriptionPrice(sk, {
|
|
9393
|
+
...input,
|
|
9394
|
+
priceId: seat.priceId,
|
|
9395
|
+
priceCents: seat.amountCents,
|
|
9396
|
+
billingTerms: void 0,
|
|
9397
|
+
namedSeat: void 0
|
|
9398
|
+
}, Boolean(input.existingSubscriptionId)) : void 0;
|
|
9399
|
+
if (seat && (!seatProductId || !productId || seatProductId === productId)) subscriptionContractMismatch();
|
|
9001
9400
|
if (input.billingTerms?.discount && !productId) subscriptionContractMismatch();
|
|
9002
9401
|
await prepareSubscriptionBillingTerms(sk, { ...input, discountProductId: productId }, Boolean(input.existingSubscriptionId));
|
|
9003
9402
|
const billingParams = subscriptionBillingParams(input, productId);
|
|
@@ -9009,7 +9408,7 @@ function createStripeProvider(config) {
|
|
|
9009
9408
|
{ email: input.email, name: input.name, metadata: meta },
|
|
9010
9409
|
`cus:${input.applicationId}`
|
|
9011
9410
|
);
|
|
9012
|
-
if (!cust.ok)
|
|
9411
|
+
if (!cust.ok) fail8("customer create", cust);
|
|
9013
9412
|
if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
|
|
9014
9413
|
customerId = cust.body.id;
|
|
9015
9414
|
}
|
|
@@ -9029,19 +9428,32 @@ function createStripeProvider(config) {
|
|
|
9029
9428
|
{
|
|
9030
9429
|
customer: customerId,
|
|
9031
9430
|
"items[0][price]": input.priceId,
|
|
9431
|
+
...seat ? {
|
|
9432
|
+
"items[1][price]": seat.priceId,
|
|
9433
|
+
"items[1][quantity]": 1,
|
|
9434
|
+
"items[1][metadata][seatId]": seat.seatId,
|
|
9435
|
+
"items[1][metadata][membershipId]": seat.membershipId
|
|
9436
|
+
} : {},
|
|
9032
9437
|
payment_behavior: "default_incomplete",
|
|
9033
9438
|
"payment_settings[save_default_payment_method]": "on_subscription",
|
|
9034
9439
|
"payment_settings[payment_method_types][0]": "card",
|
|
9035
9440
|
"expand[0]": "latest_invoice.confirmation_secret",
|
|
9036
9441
|
"expand[1]": "latest_invoice.payment_intent",
|
|
9037
9442
|
metadata: meta,
|
|
9038
|
-
...billingParams
|
|
9443
|
+
...billingParams,
|
|
9444
|
+
...seat && input.billingTerms?.initialTermEndsAt ? {
|
|
9445
|
+
"add_invoice_items[1][price_data][product]": seatProductId,
|
|
9446
|
+
"add_invoice_items[1][price_data][currency]": input.currency,
|
|
9447
|
+
"add_invoice_items[1][price_data][unit_amount]": seat.amountCents,
|
|
9448
|
+
"add_invoice_items[1][quantity]": 1,
|
|
9449
|
+
"add_invoice_items[1][discountable]": false
|
|
9450
|
+
} : {}
|
|
9039
9451
|
},
|
|
9040
9452
|
// Hardening: one subscription per application, so a client
|
|
9041
9453
|
// retry between create and the db write can't orphan a second one.
|
|
9042
9454
|
`sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
|
|
9043
9455
|
);
|
|
9044
|
-
if (!sub.ok)
|
|
9456
|
+
if (!sub.ok) fail8("subscription create", sub);
|
|
9045
9457
|
if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
|
|
9046
9458
|
const subscriptionId = sub.body.id;
|
|
9047
9459
|
if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
|
|
@@ -9051,14 +9463,14 @@ function createStripeProvider(config) {
|
|
|
9051
9463
|
"expand[2]": "discounts",
|
|
9052
9464
|
"expand[3]": "latest_invoice.discounts"
|
|
9053
9465
|
});
|
|
9054
|
-
if (!expanded.ok)
|
|
9466
|
+
if (!expanded.ok) fail8("subscription invoice evidence", expanded);
|
|
9055
9467
|
if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
|
|
9056
9468
|
sub = expanded;
|
|
9057
9469
|
}
|
|
9058
9470
|
verifySubscriptionResult(sub.body, input, customerId, meta);
|
|
9059
9471
|
const invoice = sub.body.latest_invoice;
|
|
9060
9472
|
const clientSecret2 = clientSecretOf(sub.body);
|
|
9061
|
-
if (!clientSecret2)
|
|
9473
|
+
if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
|
|
9062
9474
|
await tagSubscriptionPayment(
|
|
9063
9475
|
sk,
|
|
9064
9476
|
invoice,
|
|
@@ -9270,9 +9682,9 @@ function matchesIntent(stored, expected) {
|
|
|
9270
9682
|
"purchaseAttemptId"
|
|
9271
9683
|
].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
9684
|
}
|
|
9273
|
-
async function loadIntent(db,
|
|
9685
|
+
async function loadIntent(db, id7) {
|
|
9274
9686
|
return first3((await db.query({
|
|
9275
|
-
subscriptionCheckoutIntents: { $: { where: { id:
|
|
9687
|
+
subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } }
|
|
9276
9688
|
})).subscriptionCheckoutIntents);
|
|
9277
9689
|
}
|
|
9278
9690
|
async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCheckout) {
|
|
@@ -9293,16 +9705,22 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9293
9705
|
if (input.purchaseAttemptId !== application?.membershipRestartId) mismatch();
|
|
9294
9706
|
if (application?.namedSeatId || application?.namedSeatClaimPending || application?.giftClaimId) mismatch();
|
|
9295
9707
|
if (input.membershipOffer && (!application || application.canceled === true || application.status !== "submitted")) mismatch();
|
|
9708
|
+
if (input.paymentQuote?.namedSeat && application?.email !== input.email) mismatch();
|
|
9296
9709
|
const applicationGuard = {
|
|
9297
9710
|
ns: "applications",
|
|
9298
9711
|
id: input.applicationId,
|
|
9299
9712
|
exists: true,
|
|
9300
|
-
equals: {
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
...
|
|
9305
|
-
|
|
9713
|
+
equals: {
|
|
9714
|
+
status: "submitted",
|
|
9715
|
+
...input.paymentQuote?.namedSeat ? { email: input.email } : {},
|
|
9716
|
+
...input.purchaseAttemptId ? { membershipRestartId: input.purchaseAttemptId } : {},
|
|
9717
|
+
...input.membershipOffer ? {
|
|
9718
|
+
groupId: input.groupId,
|
|
9719
|
+
tierId: input.tierId,
|
|
9720
|
+
stripeRuntime: stripeRuntimeIdentity(input.runtime),
|
|
9721
|
+
...application?.canceled !== void 0 ? { canceled: application.canceled } : {}
|
|
9722
|
+
} : {}
|
|
9723
|
+
},
|
|
9306
9724
|
absent: ["namedSeatId", "namedSeatClaimPending", "giftClaimId", ...!input.purchaseAttemptId ? ["membershipRestartId"] : [], ...input.membershipOffer && application?.canceled === void 0 ? ["canceled"] : []]
|
|
9307
9725
|
};
|
|
9308
9726
|
let stored = await loadIntent(db, input.applicationId);
|
|
@@ -9326,7 +9744,11 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9326
9744
|
ns: MEMBERSHIP_QUOTE_NAMESPACE,
|
|
9327
9745
|
id: input.applicationId,
|
|
9328
9746
|
exists: true,
|
|
9329
|
-
equals: {
|
|
9747
|
+
equals: {
|
|
9748
|
+
status: "ready",
|
|
9749
|
+
offerDigest: input.membershipOffer.digest,
|
|
9750
|
+
...input.projectionRevision !== void 0 ? { revision: input.projectionRevision } : {}
|
|
9751
|
+
}
|
|
9330
9752
|
}
|
|
9331
9753
|
] : []
|
|
9332
9754
|
]
|
|
@@ -9341,7 +9763,8 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9341
9763
|
if (!stored.providerSubscriptionId && input.now - Number(stored.createdAt) >= 23 * 60 * 6e4) {
|
|
9342
9764
|
throw Object.assign(new Error("checkout requires provider reconciliation"), { code: "checkout_recovery_required" });
|
|
9343
9765
|
}
|
|
9344
|
-
|
|
9766
|
+
const binding = input.membershipOffer ? await authorizeNetworkCheckout() : void 0;
|
|
9767
|
+
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
9768
|
const result = await provider.createSubscription({
|
|
9346
9769
|
appId: input.appId,
|
|
9347
9770
|
chapterId: input.chapterId,
|
|
@@ -9356,6 +9779,7 @@ async function startSubscriptionCheckout(db, provider, input, authorizeNetworkCh
|
|
|
9356
9779
|
priceCents: input.priceCents,
|
|
9357
9780
|
currency: input.currency,
|
|
9358
9781
|
interval: input.interval,
|
|
9782
|
+
...binding?.namedSeat ? { namedSeat: binding.namedSeat } : {},
|
|
9359
9783
|
...stored.billingTerms ? { billingTerms: stored.billingTerms } : {},
|
|
9360
9784
|
...typeof stored.providerSubscriptionId === "string" ? { existingSubscriptionId: stored.providerSubscriptionId } : {},
|
|
9361
9785
|
existingCustomerId: typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
|
|
@@ -9472,6 +9896,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9472
9896
|
return json({ error: "payment quote unavailable", ...expired ? { code: "payment_quote_changed" } : {} }, expired ? 409 : 503);
|
|
9473
9897
|
}
|
|
9474
9898
|
const { tier, quote, billingTerms } = checkout2;
|
|
9899
|
+
if (quote.namedSeat && body.seatTermsAck !== true) return json({ error: "seatTermsAck required" }, 400);
|
|
9475
9900
|
const membershipOffer = "membershipOffer" in checkout2 ? checkout2.membershipOffer : void 0;
|
|
9476
9901
|
if (ctx.membershipAuthority && !membershipOffer) return json({ error: "checkout requires reconciliation", code: "checkout_recovery_required" }, 409);
|
|
9477
9902
|
if (!tier) return json({ error: "tier not found", code: "tier_unresolved" }, 400);
|
|
@@ -9496,6 +9921,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
9496
9921
|
priceCents: billingTerms ? tier.priceCents : quote.lineItems.dueTodayCents,
|
|
9497
9922
|
...billingTerms ? { billingTerms } : {},
|
|
9498
9923
|
...membershipOffer ? { membershipOffer } : {},
|
|
9924
|
+
..."projectionRevision" in checkout2 ? { projectionRevision: checkout2.projectionRevision } : {},
|
|
9499
9925
|
...ctx.requirePaymentQuote || ctx.membershipAuthority || body.quoteDigest ? { paymentQuote: quote } : {},
|
|
9500
9926
|
currency: tier.currency ?? String(group.currency ?? "usd"),
|
|
9501
9927
|
interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
|
|
@@ -9517,9 +9943,10 @@ async function startSubscription(req, env, ctx) {
|
|
|
9517
9943
|
offerId: membershipOffer.id,
|
|
9518
9944
|
offerDigest: membershipOffer.digest,
|
|
9519
9945
|
quoteDigest: quote.digest,
|
|
9520
|
-
priceId: String(priceId)
|
|
9946
|
+
priceId: String(priceId),
|
|
9947
|
+
...quote.namedSeat ? { namedSeat: { ...quote.namedSeat, purchaserEmail: normalizeChapterEmail(String(app.email ?? "")) } } : {}
|
|
9521
9948
|
};
|
|
9522
|
-
|
|
9949
|
+
return verifyMembershipCheckoutBinding(await ctx.membershipAuthority.bindCheckout(binding, env), binding);
|
|
9523
9950
|
} : void 0);
|
|
9524
9951
|
} catch (err) {
|
|
9525
9952
|
const code = paymentErrorCode(err);
|
|
@@ -9528,6 +9955,58 @@ async function startSubscription(req, env, ctx) {
|
|
|
9528
9955
|
return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: quote.lineItems });
|
|
9529
9956
|
}
|
|
9530
9957
|
|
|
9958
|
+
// src/worker-payment-receipt.ts
|
|
9959
|
+
var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
|
|
9960
|
+
var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
9961
|
+
async function paymentReceiptRoute(req, url, env, ctx) {
|
|
9962
|
+
const id7 = url.searchParams.get("application");
|
|
9963
|
+
if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
|
|
9964
|
+
if (req.method === "POST" && req.headers.has("origin") && req.headers.get("origin") !== url.origin) return json2({ error: "forbidden" }, 403);
|
|
9965
|
+
const db = ctx.makeDb(env);
|
|
9966
|
+
const rows5 = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } }, subscriptionCheckoutIntents: { $: { where: { id: id7 }, limit: 1 } } });
|
|
9967
|
+
const app = rows5.applications?.[0], intent = rows5.subscriptionCheckoutIntents?.[0];
|
|
9968
|
+
if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
|
|
9969
|
+
const receipt = intent.paymentReceipt;
|
|
9970
|
+
if (!receipt) return json2({ pending: true }, 202);
|
|
9971
|
+
const text8 = paymentReceiptText(receipt, ctx.chapter.name);
|
|
9972
|
+
if (req.method === "POST") {
|
|
9973
|
+
if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
|
|
9974
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
9975
|
+
if (!group) return json2({ error: "Receipt email is unavailable." }, 503);
|
|
9976
|
+
const config = emailGroupFrom(group);
|
|
9977
|
+
const debugEmail = (env.ODLA_DEBUG_EMAIL ?? config.debugEmail)?.trim();
|
|
9978
|
+
if (env.ODLA_ENV !== "prod" && (!debugEmail || !/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(debugEmail))) {
|
|
9979
|
+
return json2({ error: "A test inbox is required to email receipts in this environment." }, 503);
|
|
9980
|
+
}
|
|
9981
|
+
const result = await sendTemplated(lifecycleNotifyDeps(db, env), {
|
|
9982
|
+
group: { ...config, emailTemplates: {
|
|
9983
|
+
...config.emailTemplates,
|
|
9984
|
+
paymentReceipt: { enabled: true, subject: "Your {{chapterName}} payment receipt", text: "{{receiptText}}" }
|
|
9985
|
+
} },
|
|
9986
|
+
template: "paymentReceipt",
|
|
9987
|
+
to: String(intent.purchaserEmailNormalized),
|
|
9988
|
+
applicationId: id7,
|
|
9989
|
+
vars: { chapterName: ctx.chapter.name, receiptText: text8 },
|
|
9990
|
+
dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
|
|
9991
|
+
event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
|
|
9992
|
+
});
|
|
9993
|
+
return result.sent ? json2({ sent: true, preview: env.ODLA_ENV !== "prod" }) : json2({ error: "We could not confirm email delivery. You can print or save your receipt." }, 503);
|
|
9994
|
+
}
|
|
9995
|
+
if (url.searchParams.get("format") !== "html") return json2({ receipt, chapterName: ctx.chapter.name });
|
|
9996
|
+
const nonce = crypto.randomUUID();
|
|
9997
|
+
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
|
|
9998
|
+
<style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
|
|
9999
|
+
<h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
|
|
10000
|
+
<script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
|
|
10001
|
+
headers: {
|
|
10002
|
+
"content-type": "text/html; charset=utf-8",
|
|
10003
|
+
"cache-control": "no-store",
|
|
10004
|
+
"referrer-policy": "no-referrer",
|
|
10005
|
+
"content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; frame-ancestors 'none'; base-uri 'none'`
|
|
10006
|
+
}
|
|
10007
|
+
});
|
|
10008
|
+
}
|
|
10009
|
+
|
|
9531
10010
|
// src/worker-routes-payments.ts
|
|
9532
10011
|
async function ingestWebhook(req, env, ctx) {
|
|
9533
10012
|
const db = ctx.makeDb(env);
|
|
@@ -9540,8 +10019,9 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
9540
10019
|
}
|
|
9541
10020
|
var handlePayments = async (req, url, env, ctx) => {
|
|
9542
10021
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
10022
|
+
if (["GET", "POST"].includes(req.method) && url.pathname === "/api/payments/receipt") return paymentReceiptRoute(req, url, env, ctx);
|
|
9543
10023
|
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);
|
|
10024
|
+
if (req.method === "POST" && url.pathname === "/api/payments/quote") return paymentQuoteRoute(url, env, ctx, true, req);
|
|
9545
10025
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
9546
10026
|
if (req.method === "POST" && url.pathname === "/api/webhooks/stripe") return ingestWebhook(req, env, ctx);
|
|
9547
10027
|
return null;
|
|
@@ -9555,16 +10035,16 @@ var userFor = async (req, env, ctx) => {
|
|
|
9555
10035
|
const user = await ctx.verifyUser(req, env);
|
|
9556
10036
|
return user?.email ? user : null;
|
|
9557
10037
|
};
|
|
9558
|
-
var loadClaim = async (db,
|
|
10038
|
+
var loadClaim = async (db, id7) => first4((await db.query({ giftClaims: { $: { where: { id: id7 }, limit: 1 } } })).giftClaims);
|
|
9559
10039
|
async function consumeRateLimit(db, chapterId, requester, now) {
|
|
9560
10040
|
const bucket = await giftRequesterBucket(chapterId, requester);
|
|
9561
|
-
const
|
|
9562
|
-
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id:
|
|
10041
|
+
const id7 = `gift-rate:${chapterId}:${bucket}`;
|
|
10042
|
+
const current = first4((await db.query({ giftRedemptionAttempts: { $: { where: { id: id7 }, limit: 1 } } })).giftRedemptionAttempts);
|
|
9563
10043
|
const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
|
|
9564
10044
|
const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
|
|
9565
10045
|
if (attemptCount > 10) return false;
|
|
9566
|
-
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id:
|
|
9567
|
-
id:
|
|
10046
|
+
await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id: id7, attrs: {
|
|
10047
|
+
id: id7,
|
|
9568
10048
|
chapterId,
|
|
9569
10049
|
bucket,
|
|
9570
10050
|
attemptCount,
|
|
@@ -9738,14 +10218,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
|
9738
10218
|
async function readNamedSeatOffer(db, input, now) {
|
|
9739
10219
|
membershipClock(now);
|
|
9740
10220
|
await loadMembershipAuthorityState(db, input);
|
|
9741
|
-
const
|
|
10221
|
+
const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
|
|
9742
10222
|
requireMembershipScope(member, input);
|
|
9743
10223
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
9744
10224
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
9745
10225
|
if (seat) requireMembershipScope(seat, input);
|
|
9746
10226
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
9747
10227
|
const offer = {
|
|
9748
|
-
membershipId:
|
|
10228
|
+
membershipId: id7,
|
|
9749
10229
|
amountCents: offering.seatPriceCents,
|
|
9750
10230
|
currency: offering.currency,
|
|
9751
10231
|
interval: offering.interval,
|
|
@@ -10114,18 +10594,18 @@ var handleEmailDeliveryResolution = async (req, url, env, ctx) => {
|
|
|
10114
10594
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10115
10595
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10116
10596
|
try {
|
|
10117
|
-
const
|
|
10597
|
+
const id7 = decodeURIComponent(match[1]);
|
|
10118
10598
|
const body = await namedSeatRequest(req);
|
|
10119
10599
|
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
10600
|
return json({ error: "invalid delivery evidence" }, 400);
|
|
10121
10601
|
}
|
|
10122
|
-
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog",
|
|
10602
|
+
const chapterDb = db, row2 = await membershipLoad(chapterDb, "emailLog", id7);
|
|
10123
10603
|
if (!row2 || row2.groupId !== ctx.chapter.id) return json({ error: "not found" }, 404);
|
|
10124
10604
|
if (row2.applicationId) {
|
|
10125
10605
|
const app = await membershipLoad(chapterDb, "applications", String(row2.applicationId));
|
|
10126
10606
|
if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
|
|
10127
10607
|
}
|
|
10128
|
-
await reconcileLifecycleDelivery(chapterDb, { ...body, id:
|
|
10608
|
+
await reconcileLifecycleDelivery(chapterDb, { ...body, id: id7, groupId: ctx.chapter.id, actorId: user.userId }, Date.now());
|
|
10129
10609
|
return json({ ok: true, sent: false });
|
|
10130
10610
|
} catch {
|
|
10131
10611
|
return json({ error: "delivery evidence is invalid, stale, or still in progress; reload the exact attempt" }, 409);
|
|
@@ -10228,13 +10708,13 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10228
10708
|
const user = await ctx.verifyUser(req, env);
|
|
10229
10709
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
10230
10710
|
if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
|
|
10231
|
-
let
|
|
10711
|
+
let id7;
|
|
10232
10712
|
try {
|
|
10233
|
-
|
|
10713
|
+
id7 = dedicated ? url.searchParams.get("recordId") ?? "" : decodeURIComponent(match[1]);
|
|
10234
10714
|
} catch {
|
|
10235
10715
|
return json({ error: "invalid record id" }, 422);
|
|
10236
10716
|
}
|
|
10237
|
-
const record4 = await getRecord3({ crm: ctx.chapter.crm, db },
|
|
10717
|
+
const record4 = await getRecord3({ crm: ctx.chapter.crm, db }, id7);
|
|
10238
10718
|
if (!record4) return json({ error: "not found" }, 404);
|
|
10239
10719
|
if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
|
|
10240
10720
|
if (match?.[2]) {
|
|
@@ -10249,9 +10729,9 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
|
|
|
10249
10729
|
}
|
|
10250
10730
|
if (req.method === "GET" && dedicated) {
|
|
10251
10731
|
const excluded = profileReadOnlyFields(record4, ctx);
|
|
10252
|
-
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([
|
|
10253
|
-
id:
|
|
10254
|
-
label: field.label ??
|
|
10732
|
+
const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id8]) => !excluded.includes(id8)).map(([id8, field]) => ({
|
|
10733
|
+
id: id8,
|
|
10734
|
+
label: field.label ?? id8,
|
|
10255
10735
|
type: field.type,
|
|
10256
10736
|
required: field.required ?? false,
|
|
10257
10737
|
...field.options ? { options: field.options } : {}
|
|
@@ -11022,12 +11502,12 @@ var readJson = async (req) => {
|
|
|
11022
11502
|
return null;
|
|
11023
11503
|
}
|
|
11024
11504
|
};
|
|
11025
|
-
async function loadApp(db,
|
|
11026
|
-
const { applications } = await db.query({ applications: { $: { where: { id:
|
|
11505
|
+
async function loadApp(db, id7) {
|
|
11506
|
+
const { applications } = await db.query({ applications: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11027
11507
|
return applications?.[0] ?? null;
|
|
11028
11508
|
}
|
|
11029
|
-
async function loadGroup(db,
|
|
11030
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11509
|
+
async function loadGroup(db, id7) {
|
|
11510
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11031
11511
|
return groups?.[0] ?? null;
|
|
11032
11512
|
}
|
|
11033
11513
|
var handleAdminApplicationActions = async (req, url, env, ctx) => {
|
|
@@ -11113,8 +11593,8 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
11113
11593
|
const got = await gate4(req, env, ctx);
|
|
11114
11594
|
if (got instanceof Response) return got;
|
|
11115
11595
|
const db = got;
|
|
11116
|
-
const
|
|
11117
|
-
const app = await loadApp(db,
|
|
11596
|
+
const id7 = m[1];
|
|
11597
|
+
const app = await loadApp(db, id7);
|
|
11118
11598
|
if (!app) return json({ error: "not found" }, 404);
|
|
11119
11599
|
const result = await approveApplication({ db, env, ctx, app, origin: url.origin, reason: "manual" });
|
|
11120
11600
|
if (!result.ok) return json({ error: result.error }, result.status);
|
|
@@ -11143,10 +11623,10 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
11143
11623
|
const got = await gate4(req, env, ctx);
|
|
11144
11624
|
if (got instanceof Response) return got;
|
|
11145
11625
|
const db = got;
|
|
11146
|
-
const
|
|
11626
|
+
const id7 = m[1];
|
|
11147
11627
|
const body = await readJson(req);
|
|
11148
11628
|
if (!body) return json({ error: "invalid JSON body" }, 400);
|
|
11149
|
-
const app = await loadApp(db,
|
|
11629
|
+
const app = await loadApp(db, id7);
|
|
11150
11630
|
if (!app) return json({ error: "not found" }, 404);
|
|
11151
11631
|
return patchApplication(db, env, ctx, app, body);
|
|
11152
11632
|
};
|
|
@@ -11256,10 +11736,10 @@ async function updateEmailSettings(db, ctx, env, body) {
|
|
|
11256
11736
|
const missing = Object.fromEntries(Object.keys(events).filter((key3) => !group?.emailTemplates?.[key3]).map((key3) => [key3, { subject: "", text: "", enabled: false }]));
|
|
11257
11737
|
await saveConfig({ db, crm: ctx.chapter.crm }, { emailTemplates: { ...missing, ...clean }, notificationEmail, replyTo, debugEmail });
|
|
11258
11738
|
} else {
|
|
11259
|
-
const
|
|
11739
|
+
const id7 = { ns: "groups", attr: "id", value: ctx.chapter.id };
|
|
11260
11740
|
await db.transact([
|
|
11261
|
-
{ t: "merge", ns: "groups", id:
|
|
11262
|
-
{ t: "update", ns: "groups", id:
|
|
11741
|
+
{ t: "merge", ns: "groups", id: id7, attrs: { emailTemplates: clean } },
|
|
11742
|
+
{ t: "update", ns: "groups", id: id7, attrs: { commitmentText, normsText, notificationEmail, replyTo, debugEmail, fromName } }
|
|
11263
11743
|
]);
|
|
11264
11744
|
}
|
|
11265
11745
|
return json({ ok: true });
|
|
@@ -11274,8 +11754,8 @@ async function gate5(req, env, ctx) {
|
|
|
11274
11754
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
11275
11755
|
return rawDb;
|
|
11276
11756
|
}
|
|
11277
|
-
async function loadGroup2(db,
|
|
11278
|
-
const { groups } = await db.query({ groups: { $: { where: { id:
|
|
11757
|
+
async function loadGroup2(db, id7) {
|
|
11758
|
+
const { groups } = await db.query({ groups: { $: { where: { id: id7 }, limit: 1 } } });
|
|
11279
11759
|
return groups?.[0] ?? null;
|
|
11280
11760
|
}
|
|
11281
11761
|
var str3 = (v, d = "") => typeof v === "string" ? v : d;
|
|
@@ -11529,7 +12009,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11529
12009
|
if ("response" in got) return got.response;
|
|
11530
12010
|
return json({
|
|
11531
12011
|
targets: ctx.chapter.network.targets.map(({
|
|
11532
|
-
id:
|
|
12012
|
+
id: id7,
|
|
11533
12013
|
name,
|
|
11534
12014
|
url: targetUrl,
|
|
11535
12015
|
fields,
|
|
@@ -11538,7 +12018,7 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
|
11538
12018
|
stageTransitions,
|
|
11539
12019
|
commercialParity
|
|
11540
12020
|
}) => ({
|
|
11541
|
-
id:
|
|
12021
|
+
id: id7,
|
|
11542
12022
|
name,
|
|
11543
12023
|
url: targetUrl,
|
|
11544
12024
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
@@ -11700,7 +12180,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
11700
12180
|
if (typeof body.recordId !== "string" || !Array.isArray(body.targetIds) || body.targetIds.length === 0) {
|
|
11701
12181
|
return json({ error: "recordId and a non-empty targetIds array are required" }, 400);
|
|
11702
12182
|
}
|
|
11703
|
-
const requested = new Set(body.targetIds.filter((
|
|
12183
|
+
const requested = new Set(body.targetIds.filter((id7) => typeof id7 === "string"));
|
|
11704
12184
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
11705
12185
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
11706
12186
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
@@ -11833,7 +12313,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
|
|
|
11833
12313
|
const appId = record4.fields.applicationId;
|
|
11834
12314
|
const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
|
|
11835
12315
|
if (reader.stageTransitions.includes(record4.type)) {
|
|
11836
|
-
result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((
|
|
12316
|
+
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
12317
|
const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
|
|
11838
12318
|
const meta = lock?.meta;
|
|
11839
12319
|
if (meta?.state === "pending" && typeof meta.activityId === "string") {
|
|
@@ -11938,9 +12418,9 @@ async function listReaderRecords(db, type, allowed2, emailField, window) {
|
|
|
11938
12418
|
// src/network-mutation-receipt.ts
|
|
11939
12419
|
import { CRM_NS as CRM_NS2 } from "@odla-ai/crm";
|
|
11940
12420
|
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
11941
|
-
async function priorMutation(db,
|
|
12421
|
+
async function priorMutation(db, id7) {
|
|
11942
12422
|
const result = await db.query({
|
|
11943
|
-
[CRM_NS2.activity]: { $: { where: { id:
|
|
12423
|
+
[CRM_NS2.activity]: { $: { where: { id: id7 }, limit: 1 } }
|
|
11944
12424
|
});
|
|
11945
12425
|
const row2 = (result[CRM_NS2.activity] ?? [])[0];
|
|
11946
12426
|
const meta = row2?.meta;
|
|
@@ -12200,8 +12680,8 @@ async function gate7(req, env, ctx) {
|
|
|
12200
12680
|
if (!await networkWriteAllowed(req, db, user, ctx)) return { response: json({ error: "super-admin required", code: "super_admin_required" }, 403) };
|
|
12201
12681
|
return { db, user };
|
|
12202
12682
|
}
|
|
12203
|
-
function targetFor2(ctx,
|
|
12204
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
12683
|
+
function targetFor2(ctx, id7) {
|
|
12684
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12205
12685
|
}
|
|
12206
12686
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
12207
12687
|
const secret = await getVaultSecret(db, target.secretName);
|
|
@@ -12336,7 +12816,7 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
12336
12816
|
if (ctx.chapter.network.requireSuperAdminWrites && !await ctx.isSuperAdminEmail(got.db, got.user.email, got.user.userId)) {
|
|
12337
12817
|
return json({ ...result.page, records: result.page.records.map(({ edit: _edit, ...record4 }) => ({
|
|
12338
12818
|
...record4,
|
|
12339
|
-
fields: Object.fromEntries(Object.entries(record4.fields).filter(([
|
|
12819
|
+
fields: Object.fromEntries(Object.entries(record4.fields).filter(([id7]) => id7 === "name" || id7 === "email"))
|
|
12340
12820
|
})) });
|
|
12341
12821
|
}
|
|
12342
12822
|
return json(result.page);
|
|
@@ -12406,9 +12886,9 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12406
12886
|
}
|
|
12407
12887
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
12408
12888
|
if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
|
|
12409
|
-
const
|
|
12889
|
+
const id7 = crypto.randomUUID();
|
|
12410
12890
|
const note = {
|
|
12411
|
-
id:
|
|
12891
|
+
id: id7,
|
|
12412
12892
|
targetId: identity2.targetId,
|
|
12413
12893
|
recordType: identity2.recordType,
|
|
12414
12894
|
recordId: identity2.recordId,
|
|
@@ -12420,7 +12900,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
12420
12900
|
await got.db.transact([{
|
|
12421
12901
|
t: "update",
|
|
12422
12902
|
ns: "networkNotes",
|
|
12423
|
-
id:
|
|
12903
|
+
id: id7,
|
|
12424
12904
|
attrs: { ...note, recordKey: identity2.recordKey }
|
|
12425
12905
|
}]);
|
|
12426
12906
|
return json({ note }, 201);
|
|
@@ -12524,13 +13004,13 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12524
13004
|
return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
|
|
12525
13005
|
}
|
|
12526
13006
|
const createdAt = Date.now();
|
|
12527
|
-
const
|
|
13007
|
+
const id7 = `network:${verified.sender}:${mutationId}`;
|
|
12528
13008
|
const result = await addActivity(
|
|
12529
13009
|
{
|
|
12530
13010
|
crm: ctx.chapter.crm,
|
|
12531
13011
|
db,
|
|
12532
13012
|
now: () => createdAt,
|
|
12533
|
-
newId: () =>
|
|
13013
|
+
newId: () => id7
|
|
12534
13014
|
},
|
|
12535
13015
|
{
|
|
12536
13016
|
recordId,
|
|
@@ -12545,11 +13025,11 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12545
13025
|
body
|
|
12546
13026
|
}
|
|
12547
13027
|
},
|
|
12548
|
-
mutationId:
|
|
13028
|
+
mutationId: id7
|
|
12549
13029
|
}
|
|
12550
13030
|
);
|
|
12551
13031
|
const note = {
|
|
12552
|
-
id:
|
|
13032
|
+
id: id7,
|
|
12553
13033
|
sourceId: verified.sender,
|
|
12554
13034
|
recordType,
|
|
12555
13035
|
recordId,
|
|
@@ -12558,8 +13038,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
12558
13038
|
};
|
|
12559
13039
|
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
12560
13040
|
};
|
|
12561
|
-
function targetFor3(ctx,
|
|
12562
|
-
return ctx.chapter.network.targets.find((target) => target.id ===
|
|
13041
|
+
function targetFor3(ctx, id7) {
|
|
13042
|
+
return ctx.chapter.network.targets.find((target) => target.id === id7);
|
|
12563
13043
|
}
|
|
12564
13044
|
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
12565
13045
|
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
@@ -12828,10 +13308,10 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12828
13308
|
if (!state.claims.some((claim) => claim.id === expectedClaim.id && claim.personId === person.id && claim.kind === "email")) {
|
|
12829
13309
|
throw new NetworkPersonRegistryError("identity_mismatch", "source email is not an exact verified identity on this Person");
|
|
12830
13310
|
}
|
|
12831
|
-
const
|
|
12832
|
-
const previous = state.links.find((link2) => link2.id ===
|
|
13311
|
+
const id7 = await networkChapterLinkId(person.id, input.chapterId, input.chapterPersonId);
|
|
13312
|
+
const previous = state.links.find((link2) => link2.id === id7);
|
|
12833
13313
|
const link = {
|
|
12834
|
-
id:
|
|
13314
|
+
id: id7,
|
|
12835
13315
|
personId: person.id,
|
|
12836
13316
|
chapterId: input.chapterId,
|
|
12837
13317
|
chapterPersonId: input.chapterPersonId,
|
|
@@ -12842,11 +13322,11 @@ async function linkNetworkChapterPerson(db, input, mutationId, actor, now = Date
|
|
|
12842
13322
|
const nextPerson = { ...person, revision: person.revision + 1, updatedAt: now };
|
|
12843
13323
|
await db.transact([
|
|
12844
13324
|
{ t: "update", ns: "networkPersons", id: person.id, attrs: { ...nextPerson } },
|
|
12845
|
-
{ t: "update", ns: "networkChapterPersonLinks", id:
|
|
13325
|
+
{ t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { ...link } },
|
|
12846
13326
|
networkPersonEventOp("chapter_source_linked", mutationId, actor, now, { chapterId: input.chapterId }, { personId: person.id })
|
|
12847
13327
|
], { mutationId, guards: [
|
|
12848
13328
|
{ ns: "networkPersons", id: person.id, exists: true, equals: { revision: person.revision, state: "active" } },
|
|
12849
|
-
{ ns: "networkChapterPersonLinks", id:
|
|
13329
|
+
{ ns: "networkChapterPersonLinks", id: id7, exists: Boolean(previous), ...previous ? { equals: { state: "revoked" } } : {} }
|
|
12850
13330
|
] });
|
|
12851
13331
|
return { person: nextPerson, link };
|
|
12852
13332
|
}
|
|
@@ -12883,9 +13363,9 @@ var defaultPolicy = (now, consent) => ({
|
|
|
12883
13363
|
var isGuardConflict = (error) => error instanceof Error && error.code === "transact_guard_failed";
|
|
12884
13364
|
async function linkOps(personId, links, now) {
|
|
12885
13365
|
return Promise.all(links.map(async (link) => {
|
|
12886
|
-
const
|
|
13366
|
+
const id7 = await networkChapterLinkId(personId, link.chapterId, link.chapterPersonId);
|
|
12887
13367
|
const sourceKey = await networkChapterPersonSourceKey(link.chapterId, link.chapterPersonId);
|
|
12888
|
-
return { t: "update", ns: "networkChapterPersonLinks", id:
|
|
13368
|
+
return { t: "update", ns: "networkChapterPersonLinks", id: id7, attrs: { id: id7, personId, chapterId: link.chapterId, chapterPersonId: link.chapterPersonId, sourceKey, state: "shadow", createdAt: now } };
|
|
12889
13369
|
}));
|
|
12890
13370
|
}
|
|
12891
13371
|
async function ambiguousAlias(db, input, aliasId, claims, candidates, mutationId, actor, now) {
|
|
@@ -13104,10 +13584,10 @@ async function networkPersonBulkBatchDigest(rows5) {
|
|
|
13104
13584
|
}
|
|
13105
13585
|
async function networkPersonRegistryStateDigest(state) {
|
|
13106
13586
|
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:
|
|
13587
|
+
persons: state.persons.map(({ id: id7, revision, state: personState, mergedIntoId }) => ({ id: id7, revision, state: personState, mergedIntoId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13588
|
+
claims: state.claims.map(({ id: id7, personId }) => ({ id: id7, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13589
|
+
aliases: state.aliases.map(({ id: id7, revision, status: status2, personId }) => ({ id: id7, revision, status: status2, personId })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
13590
|
+
links: state.links.map(({ id: id7, personId, state: linkState }) => ({ id: id7, personId, state: linkState })).sort((a, b) => a.id.localeCompare(b.id))
|
|
13111
13591
|
};
|
|
13112
13592
|
return networkPersonBulkSha256(JSON.stringify(stableValue(snapshot)));
|
|
13113
13593
|
}
|
|
@@ -13332,7 +13812,7 @@ var safeNetworkPersonAlias = async (alias) => ({
|
|
|
13332
13812
|
recordedAt: alias.consent.recordedAt,
|
|
13333
13813
|
hasBasis: Boolean(alias.consent.basis)
|
|
13334
13814
|
},
|
|
13335
|
-
identityClaims: alias.identityClaims.map(({ id:
|
|
13815
|
+
identityClaims: alias.identityClaims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13336
13816
|
chapterIds: [...new Set(alias.chapterLinks.map((link) => link.chapterId))],
|
|
13337
13817
|
chapterSources: await Promise.all(alias.chapterLinks.map(async (link) => ({
|
|
13338
13818
|
chapterId: link.chapterId,
|
|
@@ -13361,7 +13841,7 @@ async function projectNetworkPersonRegistryState(state) {
|
|
|
13361
13841
|
return {
|
|
13362
13842
|
...safeNetworkPerson(person, claims),
|
|
13363
13843
|
canonicalPersonId: canonicalId,
|
|
13364
|
-
identityClaims: claims.map(({ id:
|
|
13844
|
+
identityClaims: claims.map(({ id: id7, kind, hint, verifiedAt }) => ({ id: id7, kind, hint: hint ?? null, verifiedAt })),
|
|
13365
13845
|
chapterLinks: safeLinks,
|
|
13366
13846
|
chapterProjections: projectNetworkContactPolicy(canonical3.contactPolicy, safeLinks).map(({ contactPolicy, ...projection }) => ({ ...projection, contactPolicy: safeNetworkContactPolicy(contactPolicy) }))
|
|
13367
13847
|
};
|
|
@@ -14154,14 +14634,14 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14154
14634
|
}
|
|
14155
14635
|
}
|
|
14156
14636
|
const db = ctx.makeDb(env);
|
|
14157
|
-
const
|
|
14637
|
+
const id7 = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
14158
14638
|
try {
|
|
14159
14639
|
const created = await createRecord3(
|
|
14160
14640
|
{
|
|
14161
14641
|
crm: ctx.chapter.crm,
|
|
14162
14642
|
db,
|
|
14163
14643
|
now: () => Date.now(),
|
|
14164
|
-
newId: () =>
|
|
14644
|
+
newId: () => id7
|
|
14165
14645
|
},
|
|
14166
14646
|
{
|
|
14167
14647
|
type: formation.type,
|
|
@@ -14170,7 +14650,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
14170
14650
|
}
|
|
14171
14651
|
);
|
|
14172
14652
|
return json({
|
|
14173
|
-
id:
|
|
14653
|
+
id: id7,
|
|
14174
14654
|
duplicate: created.duplicate,
|
|
14175
14655
|
status: created.record.stage ?? null
|
|
14176
14656
|
}, created.duplicate ? 200 : 201);
|
|
@@ -14247,8 +14727,8 @@ function resourceValue(value, path, errors) {
|
|
|
14247
14727
|
return null;
|
|
14248
14728
|
}
|
|
14249
14729
|
unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
|
|
14250
|
-
const
|
|
14251
|
-
if (
|
|
14730
|
+
const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
14731
|
+
if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
14252
14732
|
const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
14253
14733
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
14254
14734
|
const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -14305,7 +14785,7 @@ function resourceValue(value, path, errors) {
|
|
|
14305
14785
|
updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
14306
14786
|
...promotedFrom ? { promotedFrom } : {}
|
|
14307
14787
|
};
|
|
14308
|
-
return { id:
|
|
14788
|
+
return { id: id7, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance: provenance2 };
|
|
14309
14789
|
}
|
|
14310
14790
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
14311
14791
|
const errors = [];
|
|
@@ -14336,7 +14816,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
14336
14816
|
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
14337
14817
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
14338
14818
|
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
14339
|
-
const ids2 = resources.map(({ id:
|
|
14819
|
+
const ids2 = resources.map(({ id: id7 }) => id7);
|
|
14340
14820
|
if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
|
|
14341
14821
|
for (const resource of resources) {
|
|
14342
14822
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -14425,18 +14905,18 @@ async function loadLatestBuilderLibraryControl(db, sourceId, targetId, environme
|
|
|
14425
14905
|
return control2(Array.isArray(rows5) ? [...rows5].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
|
|
14426
14906
|
}
|
|
14427
14907
|
async function loadLocalBuilderLibrary(db, appId, environment) {
|
|
14428
|
-
const
|
|
14429
|
-
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id:
|
|
14908
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14909
|
+
const rows5 = (await db.query({ builderLibraryLocal: { $: { where: { id: id7 }, limit: 1 } } })).builderLibraryLocal;
|
|
14430
14910
|
return local3(Array.isArray(rows5) ? rows5[0] : void 0);
|
|
14431
14911
|
}
|
|
14432
14912
|
async function materializeBuilderLibraryControl(db, envelope, previous, receivedAt) {
|
|
14433
14913
|
const { document, digest } = envelope;
|
|
14434
14914
|
const target = document.target;
|
|
14435
|
-
const
|
|
14915
|
+
const id7 = `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}`;
|
|
14436
14916
|
const ops = [];
|
|
14437
14917
|
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:
|
|
14918
|
+
ops.push({ t: "update", ns: "builderLibraryControls", id: id7, attrs: {
|
|
14919
|
+
id: id7,
|
|
14440
14920
|
sourceId: document.source.appId,
|
|
14441
14921
|
targetId: target.appId,
|
|
14442
14922
|
environment: target.environment,
|
|
@@ -14449,13 +14929,13 @@ async function materializeBuilderLibraryControl(db, envelope, previous, received
|
|
|
14449
14929
|
await db.transact(ops, { mutationId: `builder-library-control:${document.source.appId}:${target.environment}:${document.revision}:${digest}` });
|
|
14450
14930
|
}
|
|
14451
14931
|
async function saveLocalBuilderLibrary(db, document, environment, expectedRevision, updatedAt, updatedBy) {
|
|
14452
|
-
const
|
|
14932
|
+
const id7 = `builder-library-local:${environment}`;
|
|
14453
14933
|
const current = await loadLocalBuilderLibrary(db, document.source.appId, environment);
|
|
14454
14934
|
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:
|
|
14935
|
+
const row2 = { id: id7, appId: document.source.appId, environment, revision: document.revision, document, updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
14936
|
+
await db.transact([{ t: "update", ns: "builderLibraryLocal", id: id7, attrs: row2 }], {
|
|
14457
14937
|
mutationId: `builder-library-local:${environment}:${document.revision}`,
|
|
14458
|
-
guards: current ? [{ ns: "builderLibraryLocal", id:
|
|
14938
|
+
guards: current ? [{ ns: "builderLibraryLocal", id: id7, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "builderLibraryLocal", id: id7, exists: false }]
|
|
14459
14939
|
});
|
|
14460
14940
|
return row2;
|
|
14461
14941
|
}
|
|
@@ -14670,30 +15150,30 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
14670
15150
|
};
|
|
14671
15151
|
|
|
14672
15152
|
// src/membership-cancel-authority.ts
|
|
14673
|
-
import { stripeRequest as
|
|
15153
|
+
import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
|
|
14674
15154
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
14675
15155
|
membershipClock(now);
|
|
14676
|
-
const
|
|
15156
|
+
const id7 = await networkMembershipId(target);
|
|
14677
15157
|
const state = await loadMembershipAuthorityState(db, target);
|
|
14678
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15158
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14679
15159
|
requireMembershipScope(member, target);
|
|
14680
15160
|
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
14681
15161
|
membershipFail("membership_cancellation_unavailable");
|
|
14682
15162
|
}
|
|
14683
15163
|
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
14684
|
-
const read2 = await
|
|
15164
|
+
const read2 = await stripeRequest11({ secretKey: stripeSecret }, "GET", path);
|
|
14685
15165
|
const sub = read2.body;
|
|
14686
15166
|
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
14687
15167
|
...stripePartitionMetadata(target),
|
|
14688
15168
|
applicationId: target.applicationId
|
|
14689
15169
|
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
14690
15170
|
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
14691
|
-
const write = await
|
|
15171
|
+
const write = await stripeRequest11(
|
|
14692
15172
|
{ secretKey: stripeSecret },
|
|
14693
15173
|
"POST",
|
|
14694
15174
|
path,
|
|
14695
15175
|
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
14696
|
-
{ idempotencyKey: `membership-cancel-at-term:${
|
|
15176
|
+
{ idempotencyKey: `membership-cancel-at-term:${id7}:${member.providerSubscriptionId}` }
|
|
14697
15177
|
);
|
|
14698
15178
|
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
14699
15179
|
membershipFail("membership_cancellation_pending");
|
|
@@ -14701,7 +15181,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14701
15181
|
}
|
|
14702
15182
|
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
14703
15183
|
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
14704
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
15184
|
+
await db.transact([membershipUpdate("networkMembership", id7, {
|
|
14705
15185
|
status: status2,
|
|
14706
15186
|
autoRenew: false,
|
|
14707
15187
|
graceEndsAt: 0,
|
|
@@ -14710,12 +15190,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
14710
15190
|
updatedAt: now
|
|
14711
15191
|
})], {
|
|
14712
15192
|
guards: [
|
|
14713
|
-
{ ns: "networkMembership", id:
|
|
15193
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14714
15194
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14715
15195
|
]
|
|
14716
15196
|
});
|
|
14717
15197
|
}
|
|
14718
|
-
return { membershipId:
|
|
15198
|
+
return { membershipId: id7, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
14719
15199
|
}
|
|
14720
15200
|
|
|
14721
15201
|
// src/membership-authority-handler.ts
|
|
@@ -14754,17 +15234,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
|
|
|
14754
15234
|
// src/membership-approval-authority.ts
|
|
14755
15235
|
async function approveNetworkMembership(db, input, now) {
|
|
14756
15236
|
membershipClock(now);
|
|
14757
|
-
const
|
|
15237
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
14758
15238
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14759
15239
|
const state = await loadMembershipAuthorityState(db, input);
|
|
14760
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
15240
|
+
const member = await membershipLoad(db, "networkMembership", id7);
|
|
14761
15241
|
requireMembershipScope(member, input);
|
|
14762
15242
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14763
15243
|
requireMembershipScope(entitlement, input);
|
|
14764
|
-
if (entitlement.membershipId !==
|
|
15244
|
+
if (entitlement.membershipId !== id7 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
|
|
14765
15245
|
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
15246
|
return {
|
|
14767
|
-
membershipId:
|
|
15247
|
+
membershipId: id7,
|
|
14768
15248
|
status: String(member.status),
|
|
14769
15249
|
cohort: String(member.cohort),
|
|
14770
15250
|
memberNumber: Number(member.memberNumber),
|
|
@@ -14784,7 +15264,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14784
15264
|
const seatActivation = await namedSeatPrimaryActivation(db, member, now);
|
|
14785
15265
|
try {
|
|
14786
15266
|
await db.transact([
|
|
14787
|
-
membershipUpdate("networkMembership",
|
|
15267
|
+
membershipUpdate("networkMembership", id7, {
|
|
14788
15268
|
status: member.autoRenew ? "active" : "cancel_at_term_end",
|
|
14789
15269
|
memberNumber,
|
|
14790
15270
|
journeyApprovedAt: now,
|
|
@@ -14799,8 +15279,8 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14799
15279
|
updatedAt: now
|
|
14800
15280
|
}),
|
|
14801
15281
|
...seatActivation.ops
|
|
14802
|
-
], { mutationId: `membership-approve:${
|
|
14803
|
-
{ ns: "networkMembership", id:
|
|
15282
|
+
], { mutationId: `membership-approve:${id7}`, guards: [
|
|
15283
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
|
|
14804
15284
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
|
|
14805
15285
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
14806
15286
|
...seatActivation.guards
|
|
@@ -14815,41 +15295,41 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
14815
15295
|
// src/membership-refund-authority.ts
|
|
14816
15296
|
async function refundNetworkMembership(db, input, secretKey, now) {
|
|
14817
15297
|
membershipClock(now);
|
|
14818
|
-
const
|
|
14819
|
-
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15298
|
+
const id7 = await networkMembershipId(input), entitlementId = `${id7}:primary`;
|
|
15299
|
+
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14820
15300
|
requireMembershipScope(member, input);
|
|
14821
|
-
if (member.status === "refunded") return { membershipId:
|
|
15301
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14822
15302
|
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
|
|
14823
15303
|
if (member.status !== "refund_pending") {
|
|
14824
15304
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
14825
15305
|
requireMembershipScope(entitlement, input);
|
|
14826
15306
|
await db.transact([
|
|
14827
|
-
membershipUpdate("networkMembership",
|
|
15307
|
+
membershipUpdate("networkMembership", id7, {
|
|
14828
15308
|
status: "refund_pending",
|
|
14829
15309
|
refundRequestedAt: now,
|
|
14830
15310
|
revision: Number(member.revision) + 1,
|
|
14831
15311
|
updatedAt: now
|
|
14832
15312
|
}),
|
|
14833
15313
|
membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
14834
|
-
membershipUpdate("membershipEffect", `${
|
|
14835
|
-
id: `${
|
|
15314
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, {
|
|
15315
|
+
id: `${id7}:refund`,
|
|
14836
15316
|
chapterId: input.chapterId,
|
|
14837
15317
|
environment: input.environment,
|
|
14838
15318
|
runtime: input.runtime,
|
|
14839
|
-
membershipId:
|
|
15319
|
+
membershipId: id7,
|
|
14840
15320
|
kind: "refund_membership",
|
|
14841
15321
|
status: "pending",
|
|
14842
15322
|
attempts: 0,
|
|
14843
15323
|
createdAt: now,
|
|
14844
15324
|
updatedAt: now
|
|
14845
15325
|
})
|
|
14846
|
-
], { mutationId: `membership-refund-request:${
|
|
14847
|
-
{ ns: "networkMembership", id:
|
|
15326
|
+
], { mutationId: `membership-refund-request:${id7}`, guards: [
|
|
15327
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
14848
15328
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
|
|
14849
15329
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
14850
15330
|
] });
|
|
14851
15331
|
}
|
|
14852
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15332
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14853
15333
|
requireMembershipScope(member, input);
|
|
14854
15334
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14855
15335
|
const result = await refundSubscriptionPayment(secretKey, {
|
|
@@ -14863,23 +15343,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14863
15343
|
if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
|
|
14864
15344
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14865
15345
|
state = await loadMembershipAuthorityState(db, input);
|
|
14866
|
-
member = await membershipLoad(db, "networkMembership",
|
|
15346
|
+
member = await membershipLoad(db, "networkMembership", id7);
|
|
14867
15347
|
requireMembershipScope(member, input);
|
|
14868
|
-
if (member.status === "refunded") return { membershipId:
|
|
15348
|
+
if (member.status === "refunded") return { membershipId: id7, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
14869
15349
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
14870
15350
|
const counted = member.founding === true && member.memberNumber !== void 0;
|
|
14871
15351
|
const counts = state.cohortCounts;
|
|
14872
15352
|
if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
|
|
14873
15353
|
try {
|
|
14874
15354
|
await db.transact([
|
|
14875
|
-
membershipUpdate("networkMembership",
|
|
15355
|
+
membershipUpdate("networkMembership", id7, {
|
|
14876
15356
|
status: "refunded",
|
|
14877
15357
|
refundedCents: result.refundedCents,
|
|
14878
15358
|
autoRenew: false,
|
|
14879
15359
|
revision: Number(member.revision) + 1,
|
|
14880
15360
|
updatedAt: now
|
|
14881
15361
|
}),
|
|
14882
|
-
membershipUpdate("membershipEffect", `${
|
|
15362
|
+
membershipUpdate("membershipEffect", `${id7}:refund`, { status: "complete", updatedAt: now }),
|
|
14883
15363
|
...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
|
|
14884
15364
|
cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
|
|
14885
15365
|
revision: Number(state.revision) + 1,
|
|
@@ -14890,7 +15370,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14890
15370
|
chapterId: input.chapterId,
|
|
14891
15371
|
environment: input.environment,
|
|
14892
15372
|
runtime: input.runtime,
|
|
14893
|
-
membershipId:
|
|
15373
|
+
membershipId: id7,
|
|
14894
15374
|
seatId: member.namedSeatId,
|
|
14895
15375
|
kind: "refund_seat_with_primary",
|
|
14896
15376
|
status: "pending",
|
|
@@ -14898,8 +15378,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14898
15378
|
createdAt: now,
|
|
14899
15379
|
updatedAt: now
|
|
14900
15380
|
})] : []
|
|
14901
|
-
], { mutationId: `membership-refunded:${
|
|
14902
|
-
{ ns: "networkMembership", id:
|
|
15381
|
+
], { mutationId: `membership-refunded:${id7}`, guards: [
|
|
15382
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { status: "refund_pending", revision: member.revision } },
|
|
14903
15383
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
14904
15384
|
] });
|
|
14905
15385
|
} catch (error) {
|
|
@@ -14910,12 +15390,12 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
14910
15390
|
}
|
|
14911
15391
|
|
|
14912
15392
|
// src/membership-provider-lifecycle.ts
|
|
14913
|
-
import { stripeRequest as
|
|
15393
|
+
import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
|
|
14914
15394
|
|
|
14915
15395
|
// src/membership-renewal-authority.ts
|
|
14916
15396
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
14917
15397
|
membershipClock(now);
|
|
14918
|
-
const
|
|
15398
|
+
const id7 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
14919
15399
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
14920
15400
|
const fact = eventRow?.fact;
|
|
14921
15401
|
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 +15408,10 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14928
15408
|
return membershipFail("membership_renewal_evidence_pending");
|
|
14929
15409
|
}
|
|
14930
15410
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
14931
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15411
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
14932
15412
|
requireMembershipScope(member, input);
|
|
14933
15413
|
const receipt = () => ({
|
|
14934
|
-
membershipId:
|
|
15414
|
+
membershipId: id7,
|
|
14935
15415
|
status: String(member.status),
|
|
14936
15416
|
termEndsAt: Number(member.termEndsAt),
|
|
14937
15417
|
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
@@ -14956,13 +15436,13 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14956
15436
|
const paid = fact.eventType === "invoice.payment_succeeded";
|
|
14957
15437
|
if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
|
|
14958
15438
|
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", `${
|
|
15439
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
14960
15440
|
requireMembershipScope(entitlement, input);
|
|
14961
15441
|
const graceEndsAt = paid ? 0 : start + 14 * 864e5;
|
|
14962
15442
|
const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
|
|
14963
15443
|
const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
|
|
14964
15444
|
const ops = [
|
|
14965
|
-
membershipUpdate("networkMembership",
|
|
15445
|
+
membershipUpdate("networkMembership", id7, {
|
|
14966
15446
|
status: status2,
|
|
14967
15447
|
graceEndsAt,
|
|
14968
15448
|
...paid ? {
|
|
@@ -14973,7 +15453,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14973
15453
|
revision: Number(member.revision) + 1,
|
|
14974
15454
|
updatedAt: now
|
|
14975
15455
|
}),
|
|
14976
|
-
membershipUpdate("networkEntitlement", `${
|
|
15456
|
+
membershipUpdate("networkEntitlement", `${id7}:primary`, {
|
|
14977
15457
|
status: entitlementStatus,
|
|
14978
15458
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
14979
15459
|
sourceEventId: input.eventId,
|
|
@@ -14982,8 +15462,8 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
14982
15462
|
})
|
|
14983
15463
|
];
|
|
14984
15464
|
const guards = [
|
|
14985
|
-
{ ns: "networkMembership", id:
|
|
14986
|
-
{ ns: "networkEntitlement", id: `${
|
|
15465
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15466
|
+
{ ns: "networkEntitlement", id: `${id7}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
14987
15467
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
14988
15468
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
14989
15469
|
evidenceGuard
|
|
@@ -15008,15 +15488,15 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
15008
15488
|
);
|
|
15009
15489
|
}
|
|
15010
15490
|
try {
|
|
15011
|
-
await db.transact(ops, { mutationId: `membership-renewal:${
|
|
15491
|
+
await db.transact(ops, { mutationId: `membership-renewal:${id7}:${input.eventId}`, guards });
|
|
15012
15492
|
} catch (error) {
|
|
15013
15493
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15014
15494
|
continue;
|
|
15015
15495
|
}
|
|
15016
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15496
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15017
15497
|
requireMembershipScope(saved, input);
|
|
15018
15498
|
return {
|
|
15019
|
-
membershipId:
|
|
15499
|
+
membershipId: id7,
|
|
15020
15500
|
status: String(saved.status),
|
|
15021
15501
|
termEndsAt: Number(saved.termEndsAt),
|
|
15022
15502
|
graceEndsAt: Number(saved.graceEndsAt ?? 0),
|
|
@@ -15042,12 +15522,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15042
15522
|
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
15043
15523
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
15044
15524
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
15045
|
-
const
|
|
15525
|
+
const id7 = await networkMembershipId(input);
|
|
15046
15526
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
15047
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
15527
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
|
|
15048
15528
|
requireMembershipScope(member, input);
|
|
15049
15529
|
const receipt = (value = member) => ({
|
|
15050
|
-
membershipId:
|
|
15530
|
+
membershipId: id7,
|
|
15051
15531
|
status: String(value.status),
|
|
15052
15532
|
termEndsAt: Number(value.termEndsAt),
|
|
15053
15533
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
@@ -15068,13 +15548,13 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15068
15548
|
});
|
|
15069
15549
|
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
15070
15550
|
await refundNetworkMembership(db, input, sk, now);
|
|
15071
|
-
const saved2 = await membershipLoad(db, "networkMembership",
|
|
15551
|
+
const saved2 = await membershipLoad(db, "networkMembership", id7);
|
|
15072
15552
|
requireMembershipScope(saved2, input);
|
|
15073
15553
|
return receipt(saved2);
|
|
15074
15554
|
}
|
|
15075
15555
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
15076
15556
|
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
15077
|
-
const result = await
|
|
15557
|
+
const result = await stripeRequest12({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
15078
15558
|
const sub = result.body;
|
|
15079
15559
|
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
15560
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -15083,18 +15563,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15083
15563
|
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
15564
|
const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
|
|
15085
15565
|
if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
|
|
15086
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
15566
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id7}:primary`);
|
|
15087
15567
|
requireMembershipScope(entitlement, input);
|
|
15088
15568
|
try {
|
|
15089
15569
|
await db.transact([
|
|
15090
|
-
membershipUpdate("networkMembership",
|
|
15570
|
+
membershipUpdate("networkMembership", id7, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
|
|
15091
15571
|
membershipUpdate("networkEntitlement", String(entitlement.id), {
|
|
15092
15572
|
status: status2 === "lapsed" ? "closed" : entitlement.status,
|
|
15093
15573
|
revision: Number(entitlement.revision) + 1,
|
|
15094
15574
|
updatedAt: now
|
|
15095
15575
|
})
|
|
15096
15576
|
], { guards: [
|
|
15097
|
-
{ ns: "networkMembership", id:
|
|
15577
|
+
{ ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
|
|
15098
15578
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
15099
15579
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
15100
15580
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
@@ -15104,7 +15584,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15104
15584
|
if (error.code !== "transact_guard_failed") throw error;
|
|
15105
15585
|
continue;
|
|
15106
15586
|
}
|
|
15107
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
15587
|
+
const saved = await membershipLoad(db, "networkMembership", id7);
|
|
15108
15588
|
requireMembershipScope(saved, input);
|
|
15109
15589
|
return receipt(saved);
|
|
15110
15590
|
}
|
|
@@ -15112,7 +15592,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
15112
15592
|
}
|
|
15113
15593
|
|
|
15114
15594
|
// src/membership-seat-stripe.ts
|
|
15115
|
-
import { stripeRequest as
|
|
15595
|
+
import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
|
|
15116
15596
|
var reject7 = () => {
|
|
15117
15597
|
throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
|
|
15118
15598
|
};
|
|
@@ -15124,7 +15604,7 @@ var metadataFor = (seat) => ({
|
|
|
15124
15604
|
});
|
|
15125
15605
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
15126
15606
|
async function read(sk, path, params) {
|
|
15127
|
-
const response = await
|
|
15607
|
+
const response = await stripeRequest13({ secretKey: sk }, "GET", path, params);
|
|
15128
15608
|
if (!response.ok) return reject7();
|
|
15129
15609
|
return response.body;
|
|
15130
15610
|
}
|
|
@@ -15135,7 +15615,7 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
15135
15615
|
currency: seat.currency,
|
|
15136
15616
|
interval: seat.interval,
|
|
15137
15617
|
environment: seat.environment
|
|
15138
|
-
}, Boolean(seat.providerPaymentIntentId));
|
|
15618
|
+
}, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
|
|
15139
15619
|
if (!productId) reject7();
|
|
15140
15620
|
const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
|
|
15141
15621
|
const items = sub.items?.data;
|
|
@@ -15161,11 +15641,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15161
15641
|
}
|
|
15162
15642
|
await subscriptionForSeat(sk, seat);
|
|
15163
15643
|
const metadata = metadataFor(seat);
|
|
15164
|
-
const result = seat.providerPaymentIntentId ? await
|
|
15644
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest13(
|
|
15165
15645
|
{ secretKey: sk },
|
|
15166
15646
|
"GET",
|
|
15167
15647
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
15168
|
-
) : await
|
|
15648
|
+
) : await stripeRequest13({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
15169
15649
|
amount: seat.amountCents,
|
|
15170
15650
|
currency: seat.currency,
|
|
15171
15651
|
customer: seat.providerCustomerId,
|
|
@@ -15179,11 +15659,15 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
15179
15659
|
return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
|
|
15180
15660
|
}
|
|
15181
15661
|
async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
15182
|
-
if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15662
|
+
if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject7();
|
|
15183
15663
|
const sub = await subscriptionForSeat(sk, seat);
|
|
15184
15664
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
15185
|
-
if (existing)
|
|
15186
|
-
|
|
15665
|
+
if (existing) {
|
|
15666
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject7();
|
|
15667
|
+
return String(existing.id);
|
|
15668
|
+
}
|
|
15669
|
+
if (seat.combinedInvoice) reject7();
|
|
15670
|
+
const result = await stripeRequest13({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
15187
15671
|
subscription: seat.providerSubscriptionId,
|
|
15188
15672
|
price: seat.priceId,
|
|
15189
15673
|
quantity: 1,
|
|
@@ -15400,7 +15884,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
15400
15884
|
const state = await loadMembershipAuthorityState(db, input);
|
|
15401
15885
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
15402
15886
|
requireMembershipScope(seat, input);
|
|
15403
|
-
if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15887
|
+
if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
15404
15888
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
15405
15889
|
requireMembershipScope(member, input);
|
|
15406
15890
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
@@ -15720,9 +16204,14 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15720
16204
|
try {
|
|
15721
16205
|
input = JSON.parse(verified.body);
|
|
15722
16206
|
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])))) {
|
|
16207
|
+
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
16208
|
throw new TypeError("invalid request");
|
|
15725
16209
|
}
|
|
16210
|
+
if (action === "checkouts" && input.namedSeat !== void 0) {
|
|
16211
|
+
const seat = input.namedSeat;
|
|
16212
|
+
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");
|
|
16213
|
+
checkoutNamedSeatSelection(seat, seat.purchaserEmail);
|
|
16214
|
+
}
|
|
15726
16215
|
} catch {
|
|
15727
16216
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
15728
16217
|
}
|
|
@@ -15795,8 +16284,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15795
16284
|
offerId: String(input.offerId),
|
|
15796
16285
|
offerDigest: String(input.offerDigest),
|
|
15797
16286
|
quoteDigest: String(input.quoteDigest),
|
|
15798
|
-
priceId: String(input.priceId)
|
|
15799
|
-
|
|
16287
|
+
priceId: String(input.priceId),
|
|
16288
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {}
|
|
16289
|
+
}, options.now?.() ?? Date.now(), peer.seatPriceId);
|
|
15800
16290
|
return respond({ ok: true, binding });
|
|
15801
16291
|
}
|
|
15802
16292
|
const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
|
|
@@ -15824,13 +16314,13 @@ function createMembershipAuthorityHandler(options) {
|
|
|
15824
16314
|
|
|
15825
16315
|
// src/schema-membership-authority.ts
|
|
15826
16316
|
var attr = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
15827
|
-
var
|
|
16317
|
+
var id5 = () => attr("string", false, true, true);
|
|
15828
16318
|
var key = (optional = false) => attr("string", optional, false, true);
|
|
15829
16319
|
var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
|
|
15830
16320
|
var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
|
|
15831
16321
|
var membershipAuthorityEntities = {
|
|
15832
16322
|
networkMembership: { attrs: {
|
|
15833
|
-
id:
|
|
16323
|
+
id: id5(),
|
|
15834
16324
|
membershipId: key(),
|
|
15835
16325
|
...scope(),
|
|
15836
16326
|
personId: key(true),
|
|
@@ -15870,7 +16360,7 @@ var membershipAuthorityEntities = {
|
|
|
15870
16360
|
restartId: key(true)
|
|
15871
16361
|
} },
|
|
15872
16362
|
membershipRestart: { attrs: {
|
|
15873
|
-
id:
|
|
16363
|
+
id: id5(),
|
|
15874
16364
|
...scope(),
|
|
15875
16365
|
membershipId: key(),
|
|
15876
16366
|
restartId: key(),
|
|
@@ -15880,7 +16370,7 @@ var membershipAuthorityEntities = {
|
|
|
15880
16370
|
createdAt: attr("number")
|
|
15881
16371
|
} },
|
|
15882
16372
|
membershipPurchaseHistory: { attrs: {
|
|
15883
|
-
id:
|
|
16373
|
+
id: id5(),
|
|
15884
16374
|
...scope(),
|
|
15885
16375
|
membershipId: key(),
|
|
15886
16376
|
providerSubscriptionId: key(),
|
|
@@ -15889,7 +16379,7 @@ var membershipAuthorityEntities = {
|
|
|
15889
16379
|
archivedAt: attr("number")
|
|
15890
16380
|
} },
|
|
15891
16381
|
networkEntitlement: { attrs: {
|
|
15892
|
-
id:
|
|
16382
|
+
id: id5(),
|
|
15893
16383
|
...scope(),
|
|
15894
16384
|
membershipId: key(),
|
|
15895
16385
|
seatId: key(true),
|
|
@@ -15906,10 +16396,12 @@ var membershipAuthorityEntities = {
|
|
|
15906
16396
|
journeyApprovedAt: attr("number", true)
|
|
15907
16397
|
} },
|
|
15908
16398
|
networkSeat: { attrs: {
|
|
15909
|
-
id:
|
|
16399
|
+
id: id5(),
|
|
15910
16400
|
...scope(),
|
|
15911
16401
|
primaryMembershipId: key(),
|
|
15912
|
-
purchaserUserId: key(),
|
|
16402
|
+
purchaserUserId: key(true),
|
|
16403
|
+
purchaserEmailNormalized: key(true),
|
|
16404
|
+
combinedInvoice: attr("json", true),
|
|
15913
16405
|
recipientName: attr("string"),
|
|
15914
16406
|
recipientEmailNormalized: key(),
|
|
15915
16407
|
recipientClaimId: key(),
|
|
@@ -15941,9 +16433,9 @@ var membershipAuthorityEntities = {
|
|
|
15941
16433
|
refundedCents: attr("number", true),
|
|
15942
16434
|
providerRefundIds: attr("json", true)
|
|
15943
16435
|
} },
|
|
15944
|
-
networkSeatRecipient: { attrs: { id:
|
|
16436
|
+
networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
|
|
15945
16437
|
membershipEffect: { attrs: {
|
|
15946
|
-
id:
|
|
16438
|
+
id: id5(),
|
|
15947
16439
|
chapterId: key(),
|
|
15948
16440
|
environment: key(),
|
|
15949
16441
|
runtime: key(),
|
|
@@ -15962,11 +16454,11 @@ var membershipAuthorityRules = Object.fromEntries(Object.keys(membershipAuthorit
|
|
|
15962
16454
|
|
|
15963
16455
|
// src/schema-membership-offers.ts
|
|
15964
16456
|
var attr2 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
15965
|
-
var
|
|
16457
|
+
var id6 = () => attr2("string", false, true, true);
|
|
15966
16458
|
var key2 = () => attr2("string", false, false, true);
|
|
15967
16459
|
var membershipOfferEntities = {
|
|
15968
16460
|
membershipAuthorityState: { attrs: {
|
|
15969
|
-
id:
|
|
16461
|
+
id: id6(),
|
|
15970
16462
|
chapterId: key2(),
|
|
15971
16463
|
environment: key2(),
|
|
15972
16464
|
mode: key2(),
|
|
@@ -15976,7 +16468,7 @@ var membershipOfferEntities = {
|
|
|
15976
16468
|
updatedAt: attr2("number")
|
|
15977
16469
|
} },
|
|
15978
16470
|
membershipCheckoutOffer: { attrs: {
|
|
15979
|
-
id:
|
|
16471
|
+
id: id6(),
|
|
15980
16472
|
applicationId: key2(),
|
|
15981
16473
|
appId: key2(),
|
|
15982
16474
|
chapterId: key2(),
|
|
@@ -15991,7 +16483,7 @@ var membershipOfferEntities = {
|
|
|
15991
16483
|
authorityRevision: attr2("number")
|
|
15992
16484
|
} },
|
|
15993
16485
|
membershipOfferCheckout: { attrs: {
|
|
15994
|
-
id:
|
|
16486
|
+
id: id6(),
|
|
15995
16487
|
appId: key2(),
|
|
15996
16488
|
chapterId: key2(),
|
|
15997
16489
|
environment: key2(),
|
|
@@ -16002,7 +16494,8 @@ var membershipOfferEntities = {
|
|
|
16002
16494
|
offerDigest: attr2("string"),
|
|
16003
16495
|
quoteDigest: attr2("string"),
|
|
16004
16496
|
priceId: attr2("string"),
|
|
16005
|
-
boundAt: attr2("number")
|
|
16497
|
+
boundAt: attr2("number"),
|
|
16498
|
+
namedSeat: attr2("json", true)
|
|
16006
16499
|
} }
|
|
16007
16500
|
};
|
|
16008
16501
|
var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
|
|
@@ -16057,7 +16550,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
|
|
|
16057
16550
|
}
|
|
16058
16551
|
|
|
16059
16552
|
// src/membership-seat-cancel.ts
|
|
16060
|
-
import { initStripe as initStripe3, stripeRequest as
|
|
16553
|
+
import { initStripe as initStripe3, stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
|
|
16061
16554
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
16062
16555
|
membershipClock(now);
|
|
16063
16556
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -16085,7 +16578,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16085
16578
|
}
|
|
16086
16579
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
16087
16580
|
const request = async (method, path, params) => {
|
|
16088
|
-
const result = await
|
|
16581
|
+
const result = await stripeRequest14(
|
|
16089
16582
|
{ secretKey },
|
|
16090
16583
|
method,
|
|
16091
16584
|
path,
|
|
@@ -16111,9 +16604,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
16111
16604
|
if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
|
|
16112
16605
|
if (payment.status !== "canceled") {
|
|
16113
16606
|
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 !==
|
|
16607
|
+
const id7 = payment.id;
|
|
16608
|
+
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id7)}/cancel`, { cancellation_reason: "abandoned" });
|
|
16609
|
+
if (payment.id !== id7) throw new Error("seat cancellation receipt mismatch");
|
|
16117
16610
|
}
|
|
16118
16611
|
if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
|
|
16119
16612
|
await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
|
|
@@ -16435,7 +16928,7 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
16435
16928
|
async function requestMembershipOfferPreview(options) {
|
|
16436
16929
|
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
16437
16930
|
const preview = result.preview;
|
|
16438
|
-
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
16931
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.seatPriceCents !== void 0 && (!Number.isSafeInteger(preview.seatPriceCents) || preview.seatPriceCents < 0) || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
16439
16932
|
return preview;
|
|
16440
16933
|
}
|
|
16441
16934
|
async function requestMembershipCheckoutBinding(options) {
|
|
@@ -16445,7 +16938,8 @@ async function requestMembershipCheckoutBinding(options) {
|
|
|
16445
16938
|
offerId: options.offerId,
|
|
16446
16939
|
offerDigest: options.offerDigest,
|
|
16447
16940
|
quoteDigest: options.quoteDigest,
|
|
16448
|
-
priceId: options.priceId
|
|
16941
|
+
priceId: options.priceId,
|
|
16942
|
+
...options.namedSeat ? { namedSeat: options.namedSeat } : {}
|
|
16449
16943
|
});
|
|
16450
16944
|
return verifyMembershipCheckoutBinding(result.binding, options);
|
|
16451
16945
|
}
|