@odla-ai/chapter 0.54.1 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/dist/{chunk-SPEL4OGE.js → chunk-VEOGTSVX.js} +507 -359
- package/dist/chunk-VEOGTSVX.js.map +1 -0
- package/dist/{chunk-NDZCR3OW.js → chunk-ZAE24YKY.js} +54 -21
- package/dist/{chunk-NDZCR3OW.js.map → chunk-ZAE24YKY.js.map} +1 -1
- package/dist/{copy-context-qvFo6eA7.d.ts → copy-context-CaUXIxUI.d.ts} +17 -2
- package/dist/index.cjs +795 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -19
- package/dist/index.d.ts +97 -19
- package/dist/index.js +776 -374
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +2 -2
- package/dist/ui/member/index.d.ts +9 -2
- package/dist/ui/member/index.js +1 -1
- package/dist/worker/index.cjs +1680 -1145
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +37 -3
- package/dist/worker/index.d.ts +37 -3
- package/dist/worker/index.js +1638 -1103
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-SPEL4OGE.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -122,8 +122,8 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
122
122
|
if (app.requireDisclaimerAck && !acked) {
|
|
123
123
|
return { ok: false, error: "disclaimerAck is required" };
|
|
124
124
|
}
|
|
125
|
-
const
|
|
126
|
-
const row2 = { id:
|
|
125
|
+
const id11 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
126
|
+
const row2 = { id: id11, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
127
127
|
for (const f of [...app.required, ...app.optional]) {
|
|
128
128
|
if (fieldStates[f]?.visible === false) continue;
|
|
129
129
|
if (typeof fields[f] === "string") row2[f] = fields[f].trim();
|
|
@@ -157,12 +157,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
157
157
|
}
|
|
158
158
|
if (acked) row2.disclaimerAckAt = opts.now;
|
|
159
159
|
const ops = [
|
|
160
|
-
{ t: "update", ns: "applications", id:
|
|
160
|
+
{ t: "update", ns: "applications", id: id11, attrs: row2 }
|
|
161
161
|
];
|
|
162
162
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
163
163
|
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
164
164
|
consumedAt: opts.now,
|
|
165
|
-
consumedApplicationId:
|
|
165
|
+
consumedApplicationId: id11
|
|
166
166
|
} });
|
|
167
167
|
}
|
|
168
168
|
try {
|
|
@@ -170,7 +170,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
170
170
|
ops,
|
|
171
171
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
172
172
|
);
|
|
173
|
-
return { ok: true, id:
|
|
173
|
+
return { ok: true, id: id11, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
174
174
|
} catch (error) {
|
|
175
175
|
if (error.code === "unique_violation") return {
|
|
176
176
|
ok: false,
|
|
@@ -1101,6 +1101,7 @@ Warmly,
|
|
|
1101
1101
|
${name}`;
|
|
1102
1102
|
return {
|
|
1103
1103
|
adminNotification: {
|
|
1104
|
+
enabled: true,
|
|
1104
1105
|
subject: `New application: {{firstName}} {{lastName}}`,
|
|
1105
1106
|
text: `A new application came in for ${name}.
|
|
1106
1107
|
|
|
@@ -1111,18 +1112,21 @@ Tier: {{tier}}
|
|
|
1111
1112
|
Review it: {{adminUrl}}`
|
|
1112
1113
|
},
|
|
1113
1114
|
submitConfirmation: {
|
|
1115
|
+
enabled: true,
|
|
1114
1116
|
subject: `We received your application to ${name}`,
|
|
1115
1117
|
text: `Hi {{firstName}},
|
|
1116
1118
|
|
|
1117
1119
|
Thanks for applying to ${name}. {{submitNextStep}}${sign}`
|
|
1118
1120
|
},
|
|
1119
1121
|
paymentConfirmation: {
|
|
1122
|
+
enabled: true,
|
|
1120
1123
|
subject: `Welcome to ${name}`,
|
|
1121
1124
|
text: `Hi {{firstName}},
|
|
1122
1125
|
|
|
1123
1126
|
Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}}${sign}`
|
|
1124
1127
|
},
|
|
1125
1128
|
prepEmail: {
|
|
1129
|
+
enabled: true,
|
|
1126
1130
|
subject: `Your ${name} intro call`,
|
|
1127
1131
|
text: `Hi {{firstName}},
|
|
1128
1132
|
|
|
@@ -1131,18 +1135,19 @@ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startA
|
|
|
1131
1135
|
Join here: {{meetingLink}}${sign}`
|
|
1132
1136
|
},
|
|
1133
1137
|
onboardingInvite: {
|
|
1138
|
+
enabled: true,
|
|
1134
1139
|
subject: `You're in, ${name}`,
|
|
1135
1140
|
text: `Hi {{firstName}},
|
|
1136
1141
|
|
|
1137
1142
|
Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
|
|
1138
1143
|
},
|
|
1139
|
-
seatAccepted: SEAT_ACCEPTED_TEMPLATE
|
|
1144
|
+
seatAccepted: { ...SEAT_ACCEPTED_TEMPLATE, enabled: true }
|
|
1140
1145
|
};
|
|
1141
1146
|
}
|
|
1142
1147
|
function buildGroupSeed(config) {
|
|
1143
1148
|
const prices = config.prices;
|
|
1144
1149
|
const emails = config.emails;
|
|
1145
|
-
const
|
|
1150
|
+
const policy2 = config.policy ?? {};
|
|
1146
1151
|
const scheduling = {
|
|
1147
1152
|
...DEFAULT_SCHEDULING,
|
|
1148
1153
|
...config.scheduling ?? {},
|
|
@@ -1157,17 +1162,17 @@ function buildGroupSeed(config) {
|
|
|
1157
1162
|
interval: prices?.interval ?? "year",
|
|
1158
1163
|
notificationEmail: emails?.notificationEmail ?? "",
|
|
1159
1164
|
replyTo: emails?.replyTo ?? emails?.notificationEmail ?? "",
|
|
1160
|
-
disclaimerText:
|
|
1161
|
-
refundPolicyText:
|
|
1162
|
-
merchantDisclosureText:
|
|
1163
|
-
trustCopy:
|
|
1165
|
+
disclaimerText: policy2.disclaimerText ?? "",
|
|
1166
|
+
refundPolicyText: policy2.refundPolicyText ?? "",
|
|
1167
|
+
merchantDisclosureText: policy2.merchantDisclosureText ?? "",
|
|
1168
|
+
trustCopy: policy2.trustCopy ?? "",
|
|
1164
1169
|
emailTemplates: emails?.templates ?? defaultEmailTemplates(config.name),
|
|
1165
1170
|
schedulingJson: scheduling
|
|
1166
1171
|
};
|
|
1167
1172
|
if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
|
|
1168
1173
|
if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
|
|
1169
|
-
if (
|
|
1170
|
-
if (
|
|
1174
|
+
if (policy2.commitmentText) row2.commitmentText = policy2.commitmentText;
|
|
1175
|
+
if (policy2.normsText) row2.normsText = policy2.normsText;
|
|
1171
1176
|
return row2;
|
|
1172
1177
|
}
|
|
1173
1178
|
|
|
@@ -1700,9 +1705,9 @@ var APPLICATION_LABELS = {
|
|
|
1700
1705
|
phone: "Phone",
|
|
1701
1706
|
state: "State"
|
|
1702
1707
|
};
|
|
1703
|
-
function humanize(
|
|
1704
|
-
const text6 =
|
|
1705
|
-
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` :
|
|
1708
|
+
function humanize(id11) {
|
|
1709
|
+
const text6 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1710
|
+
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id11;
|
|
1706
1711
|
}
|
|
1707
1712
|
function applicationDefs(chapter) {
|
|
1708
1713
|
try {
|
|
@@ -1711,23 +1716,23 @@ function applicationDefs(chapter) {
|
|
|
1711
1716
|
return {};
|
|
1712
1717
|
}
|
|
1713
1718
|
}
|
|
1714
|
-
function applicationType(
|
|
1719
|
+
function applicationType(id11, def) {
|
|
1715
1720
|
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1716
|
-
return
|
|
1721
|
+
return id11 === "email" ? "email" : id11 === "linkedin" ? "url" : "string";
|
|
1717
1722
|
}
|
|
1718
1723
|
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1719
1724
|
const defs = applicationDefs(chapter);
|
|
1720
|
-
return [...application.required, ...application.optional].map((
|
|
1721
|
-
const def = defs[
|
|
1722
|
-
const conditions = application.conditions[
|
|
1723
|
-
const type = applicationType(
|
|
1725
|
+
return [...application.required, ...application.optional].map((id11, order2) => {
|
|
1726
|
+
const def = defs[id11];
|
|
1727
|
+
const conditions = application.conditions[id11];
|
|
1728
|
+
const type = applicationType(id11, def);
|
|
1724
1729
|
return {
|
|
1725
|
-
id:
|
|
1726
|
-
label: def?.label ?? APPLICATION_LABELS[
|
|
1730
|
+
id: id11,
|
|
1731
|
+
label: def?.label ?? APPLICATION_LABELS[id11] ?? humanize(id11),
|
|
1727
1732
|
type,
|
|
1728
|
-
required: application.required.includes(
|
|
1733
|
+
required: application.required.includes(id11),
|
|
1729
1734
|
order: order2,
|
|
1730
|
-
maxLength: application.maxLen[
|
|
1735
|
+
maxLength: application.maxLen[id11] ?? application.defaultMaxLen,
|
|
1731
1736
|
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
1732
1737
|
...conditions ? { conditions: { ...conditions } } : {},
|
|
1733
1738
|
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
@@ -1737,17 +1742,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
|
|
|
1737
1742
|
function resolveFormationFlowFields(crm, formation) {
|
|
1738
1743
|
if (!formation.enabled) return [];
|
|
1739
1744
|
const def = crm.type(formation.type);
|
|
1740
|
-
return [...formation.required, ...formation.optional].map((
|
|
1741
|
-
const field2 = def.fields[
|
|
1745
|
+
return [...formation.required, ...formation.optional].map((id11, order2) => {
|
|
1746
|
+
const field2 = def.fields[id11];
|
|
1742
1747
|
return {
|
|
1743
|
-
id:
|
|
1744
|
-
label: field2.label ?? humanize(
|
|
1748
|
+
id: id11,
|
|
1749
|
+
label: field2.label ?? humanize(id11),
|
|
1745
1750
|
type: field2.type,
|
|
1746
|
-
required: formation.required.includes(
|
|
1751
|
+
required: formation.required.includes(id11),
|
|
1747
1752
|
order: order2,
|
|
1748
|
-
maxLength: formation.maxLen[
|
|
1753
|
+
maxLength: formation.maxLen[id11] ?? formation.defaultMaxLen,
|
|
1749
1754
|
...field2.options ? { options: [...field2.options] } : {},
|
|
1750
|
-
...
|
|
1755
|
+
...id11 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1751
1756
|
};
|
|
1752
1757
|
});
|
|
1753
1758
|
}
|
|
@@ -2051,12 +2056,12 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
|
|
|
2051
2056
|
if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
|
|
2052
2057
|
const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
|
|
2053
2058
|
if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
|
|
2054
|
-
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((
|
|
2059
|
+
if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id11) => !SLUG2.test(id11))) {
|
|
2055
2060
|
throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
|
|
2056
2061
|
}
|
|
2057
|
-
const unknown = chapterIds.filter((
|
|
2062
|
+
const unknown = chapterIds.filter((id11) => !targetIds.includes(id11));
|
|
2058
2063
|
if (unknown.length) {
|
|
2059
|
-
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((
|
|
2064
|
+
throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id11) => `"${id11}"`).join(", ")} are not configured network targets`);
|
|
2060
2065
|
}
|
|
2061
2066
|
return { mode: "shadow", chapterIds };
|
|
2062
2067
|
}
|
|
@@ -2179,9 +2184,9 @@ function isResolvedCrm(x) {
|
|
|
2179
2184
|
}
|
|
2180
2185
|
function defineChapter(config) {
|
|
2181
2186
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
2182
|
-
const { id:
|
|
2183
|
-
if (typeof
|
|
2184
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
2187
|
+
const { id: id11, name } = config;
|
|
2188
|
+
if (typeof id11 !== "string" || !SLUG7.test(id11)) {
|
|
2189
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id11)}`);
|
|
2185
2190
|
}
|
|
2186
2191
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
2187
2192
|
const mode = config.mode ?? "chapter";
|
|
@@ -2288,7 +2293,7 @@ function defineChapter(config) {
|
|
|
2288
2293
|
};
|
|
2289
2294
|
const chapter = {
|
|
2290
2295
|
config,
|
|
2291
|
-
id:
|
|
2296
|
+
id: id11,
|
|
2292
2297
|
name,
|
|
2293
2298
|
mode,
|
|
2294
2299
|
brand,
|
|
@@ -2476,13 +2481,13 @@ function firstPrice(object2) {
|
|
|
2476
2481
|
return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
|
|
2477
2482
|
}
|
|
2478
2483
|
function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
2479
|
-
const
|
|
2480
|
-
const eventId = providerId(
|
|
2481
|
-
const eventType = text(
|
|
2482
|
-
const object2 = raw(raw(
|
|
2484
|
+
const event2 = raw(input);
|
|
2485
|
+
const eventId = providerId(event2.id, "evt_") ?? "";
|
|
2486
|
+
const eventType = text(event2.type) ?? "";
|
|
2487
|
+
const object2 = raw(raw(event2.data).object);
|
|
2483
2488
|
const objectId = providerId(object2.id) ?? "";
|
|
2484
2489
|
const candidates = metadataCandidates(object2);
|
|
2485
|
-
const
|
|
2490
|
+
const identity2 = {};
|
|
2486
2491
|
const identityKeys2 = {
|
|
2487
2492
|
appId: "odlaAppId",
|
|
2488
2493
|
chapterId: "chapterId",
|
|
@@ -2492,7 +2497,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2492
2497
|
};
|
|
2493
2498
|
for (const [target, source] of Object.entries(identityKeys2)) {
|
|
2494
2499
|
const value = metadataValue(candidates, source);
|
|
2495
|
-
if (value)
|
|
2500
|
+
if (value) identity2[target] = value;
|
|
2496
2501
|
}
|
|
2497
2502
|
let objectKind = "ignored";
|
|
2498
2503
|
if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
|
|
@@ -2514,11 +2519,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2514
2519
|
eventId,
|
|
2515
2520
|
digest,
|
|
2516
2521
|
eventType,
|
|
2517
|
-
eventCreated: integer(
|
|
2518
|
-
livemode:
|
|
2522
|
+
eventCreated: integer(event2.created) ?? 0,
|
|
2523
|
+
livemode: event2.livemode === true,
|
|
2519
2524
|
objectKind,
|
|
2520
2525
|
objectId,
|
|
2521
|
-
identity,
|
|
2526
|
+
identity: identity2,
|
|
2522
2527
|
...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
|
|
2523
2528
|
...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
|
|
2524
2529
|
...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
|
|
@@ -2703,14 +2708,14 @@ function facts(snapshots) {
|
|
|
2703
2708
|
["payment_intent.updated", snapshots.payments?.objects ?? []]
|
|
2704
2709
|
];
|
|
2705
2710
|
return groups2.flatMap(([eventType, objects]) => objects.map((object2) => {
|
|
2706
|
-
const
|
|
2711
|
+
const id11 = typeof object2.id === "string" ? object2.id : "invalid";
|
|
2707
2712
|
return normalizeNetworkCommercialStripeEvent({
|
|
2708
|
-
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${
|
|
2713
|
+
id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id11}`,
|
|
2709
2714
|
type: eventType,
|
|
2710
2715
|
created: typeof object2.created === "number" ? object2.created : 0,
|
|
2711
2716
|
livemode: object2.livemode === true,
|
|
2712
2717
|
data: { object: object2 }
|
|
2713
|
-
}, `stripe-snapshot:${eventType}:${
|
|
2718
|
+
}, `stripe-snapshot:${eventType}:${id11}`);
|
|
2714
2719
|
}));
|
|
2715
2720
|
}
|
|
2716
2721
|
var selected = (row2) => ({
|
|
@@ -3365,15 +3370,15 @@ function membershipDiscountPolicyVersion(value) {
|
|
|
3365
3370
|
// src/membership-offer.ts
|
|
3366
3371
|
function createMembershipCommercialOffer(input) {
|
|
3367
3372
|
const offering = membershipOfferingVersion(input.offering);
|
|
3368
|
-
const
|
|
3373
|
+
const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
|
|
3369
3374
|
const now = input.now;
|
|
3370
3375
|
if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
|
|
3371
3376
|
throw new TypeError("invalid offer clock or founding count");
|
|
3372
3377
|
}
|
|
3373
3378
|
if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
|
|
3374
|
-
if (
|
|
3375
|
-
const founding = Boolean(
|
|
3376
|
-
const earned = founding ?
|
|
3379
|
+
if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
|
|
3380
|
+
const founding = Boolean(policy2 && policy2.status === "published" && offering.basePriceCents > 0 && policy2.cohortEligibleSkuIds.includes(offering.baseSkuId) && now >= policy2.qualifyingStartsAt && now < policy2.qualifyingEndsAt && input.approvedFoundingCount < policy2.softTarget);
|
|
3381
|
+
const earned = founding ? policy2 : null;
|
|
3377
3382
|
const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
|
|
3378
3383
|
if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
|
|
3379
3384
|
if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
|
|
@@ -3419,8 +3424,8 @@ async function hash(value) {
|
|
|
3419
3424
|
async function membershipDiscountCouponId(input, policyId) {
|
|
3420
3425
|
return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
|
|
3421
3426
|
}
|
|
3422
|
-
async function load(db, ns,
|
|
3423
|
-
return (await db.query({ [ns]: { $: { where: { id:
|
|
3427
|
+
async function load(db, ns, id11) {
|
|
3428
|
+
return (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3424
3429
|
}
|
|
3425
3430
|
async function membershipOfferIdForRequest(input) {
|
|
3426
3431
|
return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
|
|
@@ -3465,15 +3470,15 @@ async function previewMembershipCommercialOffer(db, input) {
|
|
|
3465
3470
|
}
|
|
3466
3471
|
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3467
3472
|
const state = await authoritativeState(db, input);
|
|
3468
|
-
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3469
|
-
const approvedFoundingCount =
|
|
3473
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
|
|
3474
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3470
3475
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3471
3476
|
const contract = createMembershipCommercialOffer({
|
|
3472
3477
|
offering,
|
|
3473
|
-
discountPolicy:
|
|
3478
|
+
discountPolicy: policy2,
|
|
3474
3479
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3475
3480
|
now: input.now,
|
|
3476
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3481
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3477
3482
|
});
|
|
3478
3483
|
const discount = contract.billingTerms.discount;
|
|
3479
3484
|
return {
|
|
@@ -3502,12 +3507,12 @@ async function currentPolicy(db, input, restartAware, state) {
|
|
|
3502
3507
|
});
|
|
3503
3508
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3504
3509
|
const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3505
|
-
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((
|
|
3510
|
+
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy3) => policy3.chapterId === input.chapterId && policy3.status === "published" && policy3.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy3.qualifyingStartsAt && input.now < policy3.qualifyingEndsAt);
|
|
3506
3511
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3507
3512
|
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3508
|
-
const
|
|
3509
|
-
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 ||
|
|
3510
|
-
return { offering, policy };
|
|
3513
|
+
const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3514
|
+
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy2 && policies.filter((item) => item.version === policy2.version).length !== 1) reject("ambiguous_membership_policy");
|
|
3515
|
+
return { offering, policy: policy2 };
|
|
3511
3516
|
}
|
|
3512
3517
|
async function issueMembershipCommercialOffer(db, input) {
|
|
3513
3518
|
validate(input);
|
|
@@ -3515,15 +3520,15 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3515
3520
|
const state = await authoritativeState(db, input);
|
|
3516
3521
|
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3517
3522
|
if (existing) return restore(existing, input);
|
|
3518
|
-
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3519
|
-
const approvedFoundingCount =
|
|
3523
|
+
const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
|
|
3524
|
+
const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
|
|
3520
3525
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3521
3526
|
const contract = createMembershipCommercialOffer({
|
|
3522
3527
|
offering,
|
|
3523
|
-
discountPolicy:
|
|
3528
|
+
discountPolicy: policy2,
|
|
3524
3529
|
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3525
3530
|
now: input.now,
|
|
3526
|
-
couponId: await membershipDiscountCouponId(input,
|
|
3531
|
+
couponId: await membershipDiscountCouponId(input, policy2?.id)
|
|
3527
3532
|
});
|
|
3528
3533
|
const value = {
|
|
3529
3534
|
id: input.offerId,
|
|
@@ -3557,20 +3562,20 @@ import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
|
|
|
3557
3562
|
|
|
3558
3563
|
// src/commercial-evidence-guard.ts
|
|
3559
3564
|
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
3560
|
-
const ns = "networkCommercialExceptions",
|
|
3561
|
-
const row2 = (await db.query({ [ns]: { $: { where: { id:
|
|
3562
|
-
if (!row2) return { ns, id:
|
|
3565
|
+
const ns = "networkCommercialExceptions", id11 = `network-commercial-exception:${eventId}:collision`;
|
|
3566
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3567
|
+
if (!row2) return { ns, id: id11, exists: false };
|
|
3563
3568
|
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
3564
3569
|
throw Object.assign(new Error(code), { code });
|
|
3565
3570
|
}
|
|
3566
|
-
return { ns, id:
|
|
3571
|
+
return { ns, id: id11, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
3567
3572
|
}
|
|
3568
3573
|
|
|
3569
3574
|
// src/membership-authority-state.ts
|
|
3570
3575
|
var membershipFail = (code) => {
|
|
3571
3576
|
throw Object.assign(new Error(code), { code });
|
|
3572
3577
|
};
|
|
3573
|
-
var membershipLoad = async (db, ns,
|
|
3578
|
+
var membershipLoad = async (db, ns, id11) => (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3574
3579
|
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3575
3580
|
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3576
3581
|
function requireMembershipScope(row2, input) {
|
|
@@ -3581,18 +3586,18 @@ async function loadMembershipAuthorityState(db, input) {
|
|
|
3581
3586
|
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");
|
|
3582
3587
|
return row2;
|
|
3583
3588
|
}
|
|
3584
|
-
var membershipUpdate = (ns,
|
|
3589
|
+
var membershipUpdate = (ns, id11, attrs) => ({ t: "update", ns, id: id11, attrs });
|
|
3585
3590
|
var membershipClock = (now) => {
|
|
3586
3591
|
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3587
3592
|
};
|
|
3588
3593
|
|
|
3589
3594
|
// src/membership-restart-payment.ts
|
|
3590
3595
|
async function membershipRestartPayment(db, target, member, now) {
|
|
3591
|
-
const
|
|
3596
|
+
const id11 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id11);
|
|
3592
3597
|
requireMembershipScope(restart, target);
|
|
3593
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
3598
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
3594
3599
|
requireMembershipScope(entitlement, target);
|
|
3595
|
-
if (entitlement.membershipId !==
|
|
3600
|
+
if (entitlement.membershipId !== id11 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
3596
3601
|
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
3597
3602
|
return {
|
|
3598
3603
|
restartId: String(restart.restartId),
|
|
@@ -3609,49 +3614,117 @@ async function membershipRestartPayment(db, target, member, now) {
|
|
|
3609
3614
|
graceEndsAt: 0
|
|
3610
3615
|
},
|
|
3611
3616
|
ops: [
|
|
3612
|
-
membershipUpdate("membershipRestart",
|
|
3613
|
-
{ t: "retract", ns: "networkMembership", id:
|
|
3617
|
+
membershipUpdate("membershipRestart", id11, { status: "complete" }),
|
|
3618
|
+
{ t: "retract", ns: "networkMembership", id: id11, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
3614
3619
|
],
|
|
3615
3620
|
guards: [
|
|
3616
|
-
{ ns: "networkMembership", id:
|
|
3617
|
-
{ ns: "membershipRestart", id:
|
|
3621
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
3622
|
+
{ ns: "membershipRestart", id: id11, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
3618
3623
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
3619
3624
|
]
|
|
3620
3625
|
};
|
|
3621
3626
|
}
|
|
3622
3627
|
|
|
3628
|
+
// src/checkout-named-seat.ts
|
|
3629
|
+
function checkoutNamedSeatSelection(value, purchaserEmail) {
|
|
3630
|
+
const row2 = value;
|
|
3631
|
+
const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
3632
|
+
const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
|
|
3633
|
+
const purchaser = normalizeChapterEmail(purchaserEmail);
|
|
3634
|
+
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) {
|
|
3635
|
+
throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
|
|
3636
|
+
}
|
|
3637
|
+
return { recipientName: name, recipientEmail: email };
|
|
3638
|
+
}
|
|
3639
|
+
async function checkoutNamedSeatIdentity(target, email) {
|
|
3640
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
3641
|
+
target.appId,
|
|
3642
|
+
target.chapterId,
|
|
3643
|
+
target.environment,
|
|
3644
|
+
target.runtime,
|
|
3645
|
+
email
|
|
3646
|
+
])));
|
|
3647
|
+
return {
|
|
3648
|
+
seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
|
|
3649
|
+
membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
|
|
3650
|
+
recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3623
3654
|
// src/membership-checkout-binding.ts
|
|
3624
3655
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
3625
3656
|
var reject2 = (code) => {
|
|
3626
3657
|
throw Object.assign(new Error(code), { code });
|
|
3627
3658
|
};
|
|
3628
|
-
var load2 = async (db, ns,
|
|
3659
|
+
var load2 = async (db, ns, id11) => (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
|
|
3629
3660
|
async function membershipCheckoutBindingId(input) {
|
|
3630
3661
|
return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
|
|
3631
3662
|
}
|
|
3632
3663
|
async function verifyMembershipCheckoutBinding(value, input) {
|
|
3633
3664
|
const row2 = value;
|
|
3634
3665
|
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");
|
|
3635
|
-
|
|
3666
|
+
if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
|
|
3667
|
+
if (input.namedSeat) {
|
|
3668
|
+
const expected = {
|
|
3669
|
+
...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
|
|
3670
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
|
|
3671
|
+
};
|
|
3672
|
+
const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
|
|
3673
|
+
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");
|
|
3674
|
+
}
|
|
3675
|
+
return {
|
|
3676
|
+
id: row2.id,
|
|
3677
|
+
...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
|
|
3678
|
+
boundAt: row2.boundAt,
|
|
3679
|
+
...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
|
|
3680
|
+
};
|
|
3636
3681
|
}
|
|
3637
|
-
async function bindMembershipCheckout(db, input, now) {
|
|
3682
|
+
async function bindMembershipCheckout(db, input, now, seatPriceId) {
|
|
3638
3683
|
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");
|
|
3639
3684
|
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3640
3685
|
const state = await load2(db, "membershipAuthorityState", stateId);
|
|
3641
3686
|
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");
|
|
3642
|
-
const
|
|
3643
|
-
const existing = await load2(db, "membershipOfferCheckout",
|
|
3687
|
+
const id11 = await membershipCheckoutBindingId(input);
|
|
3688
|
+
const existing = await load2(db, "membershipOfferCheckout", id11);
|
|
3644
3689
|
if (existing) return verifyMembershipCheckoutBinding(existing, input);
|
|
3645
3690
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
3646
3691
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
3647
3692
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
3648
|
-
const restart = await load2(db, "membershipRestart",
|
|
3649
|
-
|
|
3693
|
+
const restart = await load2(db, "membershipRestart", id11.replace(/^checkout_/, "membership_"));
|
|
3694
|
+
let namedSeat;
|
|
3695
|
+
if (input.namedSeat) {
|
|
3696
|
+
if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
|
|
3697
|
+
const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
|
|
3698
|
+
namedSeat = {
|
|
3699
|
+
...selection,
|
|
3700
|
+
purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
|
|
3701
|
+
...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
|
|
3702
|
+
priceId: seatPriceId,
|
|
3703
|
+
amountCents: offer.contract.offering.seatPriceCents
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
const receipt = { id: id11, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
|
|
3650
3707
|
try {
|
|
3651
|
-
await db.transact([
|
|
3652
|
-
|
|
3708
|
+
await db.transact([
|
|
3709
|
+
{ t: "update", ns: "membershipOfferCheckout", id: id11, attrs: receipt },
|
|
3710
|
+
...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
|
|
3711
|
+
id: namedSeat.recipientClaimId,
|
|
3712
|
+
appId: input.appId,
|
|
3713
|
+
chapterId: input.chapterId,
|
|
3714
|
+
environment: input.environment,
|
|
3715
|
+
runtime: input.runtime,
|
|
3716
|
+
seatId: namedSeat.seatId,
|
|
3717
|
+
recipientEmailNormalized: namedSeat.recipientEmail,
|
|
3718
|
+
createdAt: now
|
|
3719
|
+
} }] : []
|
|
3720
|
+
], {
|
|
3721
|
+
mutationId: `membership-checkout:${id11}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
3653
3722
|
guards: [
|
|
3654
|
-
{ ns: "membershipOfferCheckout", id:
|
|
3723
|
+
{ ns: "membershipOfferCheckout", id: id11, exists: false },
|
|
3724
|
+
...namedSeat ? [
|
|
3725
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
3726
|
+
{ ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
|
|
3727
|
+
] : [],
|
|
3655
3728
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
3656
3729
|
{ ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
3657
3730
|
]
|
|
@@ -3659,11 +3732,78 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3659
3732
|
} catch (error) {
|
|
3660
3733
|
if (error.code !== "transact_guard_failed") throw error;
|
|
3661
3734
|
}
|
|
3662
|
-
const saved = await load2(db, "membershipOfferCheckout",
|
|
3735
|
+
const saved = await load2(db, "membershipOfferCheckout", id11);
|
|
3663
3736
|
if (!saved) return reject2("membership_binding_unconfirmed");
|
|
3664
3737
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
3665
3738
|
}
|
|
3666
3739
|
|
|
3740
|
+
// src/membership-checkout-seat-payment.ts
|
|
3741
|
+
function checkoutSeatPaymentOperations(target, seat, input) {
|
|
3742
|
+
const { now, paidAt, endsAt } = input;
|
|
3743
|
+
const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
|
|
3744
|
+
const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
|
|
3745
|
+
return [
|
|
3746
|
+
membershipUpdate("networkSeat", seat.seatId, {
|
|
3747
|
+
id: seat.seatId,
|
|
3748
|
+
...membershipScope(target),
|
|
3749
|
+
primaryMembershipId: seat.membershipId,
|
|
3750
|
+
purchaserEmailNormalized: seat.purchaserEmail,
|
|
3751
|
+
recipientName: seat.recipientName,
|
|
3752
|
+
recipientEmailNormalized: seat.recipientEmail,
|
|
3753
|
+
recipientClaimId: seat.recipientClaimId,
|
|
3754
|
+
priceId: seat.priceId,
|
|
3755
|
+
primaryPriceId: input.primaryPriceId,
|
|
3756
|
+
amountCents: seat.amountCents,
|
|
3757
|
+
currency: input.offering.currency,
|
|
3758
|
+
interval: input.offering.interval,
|
|
3759
|
+
providerCustomerId: input.customerId,
|
|
3760
|
+
providerSubscriptionId: input.subscriptionId,
|
|
3761
|
+
combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
|
|
3762
|
+
status: included ? "pending_acceptance" : "renewal_pending",
|
|
3763
|
+
preparedAt: input.boundAt,
|
|
3764
|
+
purchasedAt: paidAt,
|
|
3765
|
+
termStartedAt: paidAt,
|
|
3766
|
+
termEndsAt: endsAt,
|
|
3767
|
+
acceptanceEndsAt: endsAt,
|
|
3768
|
+
seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
|
|
3769
|
+
offeringSnapshot: input.offering,
|
|
3770
|
+
paymentEventId: input.eventId,
|
|
3771
|
+
...included ? { entitlementId } : {},
|
|
3772
|
+
revision: 1,
|
|
3773
|
+
createdAt: now,
|
|
3774
|
+
updatedAt: now
|
|
3775
|
+
}),
|
|
3776
|
+
...included ? [membershipUpdate("networkEntitlement", entitlementId, {
|
|
3777
|
+
id: entitlementId,
|
|
3778
|
+
...membershipScope(target),
|
|
3779
|
+
membershipId: seat.membershipId,
|
|
3780
|
+
seatId: seat.seatId,
|
|
3781
|
+
kind: "named_seat",
|
|
3782
|
+
status: "pending_acceptance",
|
|
3783
|
+
termStartedAt: paidAt,
|
|
3784
|
+
termEndsAt: endsAt,
|
|
3785
|
+
offeringSnapshot: input.offering,
|
|
3786
|
+
sourceEventId: input.eventId,
|
|
3787
|
+
revision: 1,
|
|
3788
|
+
createdAt: now,
|
|
3789
|
+
updatedAt: now
|
|
3790
|
+
})] : [],
|
|
3791
|
+
membershipUpdate("membershipEffect", effectId, {
|
|
3792
|
+
id: effectId,
|
|
3793
|
+
chapterId: target.chapterId,
|
|
3794
|
+
environment: target.environment,
|
|
3795
|
+
runtime: target.runtime,
|
|
3796
|
+
membershipId: seat.membershipId,
|
|
3797
|
+
seatId: seat.seatId,
|
|
3798
|
+
kind: included ? "invite_named_recipient" : "attach_seat_renewal",
|
|
3799
|
+
status: "pending",
|
|
3800
|
+
attempts: 0,
|
|
3801
|
+
createdAt: now,
|
|
3802
|
+
updatedAt: now
|
|
3803
|
+
})
|
|
3804
|
+
];
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3667
3807
|
// src/membership-payment-authority.ts
|
|
3668
3808
|
async function networkMembershipId(input) {
|
|
3669
3809
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -3679,11 +3819,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3679
3819
|
{ ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
|
|
3680
3820
|
);
|
|
3681
3821
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
3822
|
+
const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
|
|
3823
|
+
if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
|
|
3824
|
+
const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
|
|
3682
3825
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3683
3826
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
3684
3827
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
3685
3828
|
const fact = eventRow.fact;
|
|
3686
|
-
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 !==
|
|
3829
|
+
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({
|
|
3687
3830
|
appId: input.appId,
|
|
3688
3831
|
chapterId: input.chapterId,
|
|
3689
3832
|
environment: input.environment,
|
|
@@ -3695,14 +3838,14 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3695
3838
|
const paidAt = fact.invoicePaidAt;
|
|
3696
3839
|
const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
|
|
3697
3840
|
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");
|
|
3698
|
-
const
|
|
3699
|
-
let member = await membershipLoad(db, "networkMembership",
|
|
3841
|
+
const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
|
|
3842
|
+
let member = await membershipLoad(db, "networkMembership", id11);
|
|
3700
3843
|
if (!member || member.status === "restarting") {
|
|
3701
3844
|
const offering = offer.contract.offering;
|
|
3702
3845
|
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
3703
3846
|
const attrs = {
|
|
3704
|
-
id:
|
|
3705
|
-
membershipId:
|
|
3847
|
+
id: id11,
|
|
3848
|
+
membershipId: id11,
|
|
3706
3849
|
...membershipScope(input),
|
|
3707
3850
|
baseSkuId: input.skuId,
|
|
3708
3851
|
status: "paid_pending_approval",
|
|
@@ -3718,8 +3861,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3718
3861
|
founding: offer.contract.founding,
|
|
3719
3862
|
cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
|
|
3720
3863
|
baseAmountCents: offering.basePriceCents,
|
|
3721
|
-
amountCents:
|
|
3864
|
+
amountCents: totalCents,
|
|
3722
3865
|
renewalAmountCents: offer.contract.renewalCents,
|
|
3866
|
+
...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
|
|
3723
3867
|
currency: offering.currency,
|
|
3724
3868
|
interval: offering.interval,
|
|
3725
3869
|
initialTermStartedAt: paidAt,
|
|
@@ -3736,11 +3880,11 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3736
3880
|
};
|
|
3737
3881
|
try {
|
|
3738
3882
|
await db.transact([
|
|
3739
|
-
membershipUpdate("networkMembership",
|
|
3883
|
+
membershipUpdate("networkMembership", id11, attrs),
|
|
3740
3884
|
membershipUpdate("networkEntitlement", entitlementId, {
|
|
3741
3885
|
id: entitlementId,
|
|
3742
3886
|
...membershipScope(input),
|
|
3743
|
-
membershipId:
|
|
3887
|
+
membershipId: id11,
|
|
3744
3888
|
kind: "primary",
|
|
3745
3889
|
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
3746
3890
|
termStartedAt: paidAt,
|
|
@@ -3752,23 +3896,45 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3752
3896
|
updatedAt: now,
|
|
3753
3897
|
...restart?.entitlementAttrs
|
|
3754
3898
|
}),
|
|
3755
|
-
...restart?.ops ?? []
|
|
3756
|
-
|
|
3757
|
-
|
|
3899
|
+
...restart?.ops ?? [],
|
|
3900
|
+
...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
|
|
3901
|
+
offering,
|
|
3902
|
+
primaryPriceId: String(binding.priceId),
|
|
3903
|
+
invoiceId: fact.invoiceId,
|
|
3904
|
+
invoiceTotalCents: totalCents,
|
|
3905
|
+
subscriptionId: input.providerSubscriptionId,
|
|
3906
|
+
customerId: input.providerCustomerId,
|
|
3907
|
+
eventId: input.eventId,
|
|
3908
|
+
paidAt: Number(paidAt),
|
|
3909
|
+
endsAt: Number(endsAt),
|
|
3910
|
+
boundAt: Number(binding.boundAt),
|
|
3911
|
+
now
|
|
3912
|
+
}) : []
|
|
3913
|
+
], { mutationId: `membership-paid:${id11}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
3914
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id11, exists: false }],
|
|
3915
|
+
...namedSeat ? [
|
|
3916
|
+
{ ns: "networkSeat", id: namedSeat.seatId, exists: false },
|
|
3917
|
+
{
|
|
3918
|
+
ns: "networkSeatRecipient",
|
|
3919
|
+
id: namedSeat.recipientClaimId,
|
|
3920
|
+
exists: true,
|
|
3921
|
+
equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
|
|
3922
|
+
}
|
|
3923
|
+
] : [],
|
|
3758
3924
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3759
|
-
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
3925
|
+
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
|
|
3760
3926
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3761
3927
|
evidenceGuard
|
|
3762
3928
|
] });
|
|
3763
3929
|
} catch (error) {
|
|
3764
3930
|
if (error.code !== "transact_guard_failed") throw error;
|
|
3765
3931
|
}
|
|
3766
|
-
member = await membershipLoad(db, "networkMembership",
|
|
3932
|
+
member = await membershipLoad(db, "networkMembership", id11);
|
|
3767
3933
|
}
|
|
3768
3934
|
requireMembershipScope(member, input);
|
|
3769
3935
|
if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
|
|
3770
3936
|
return {
|
|
3771
|
-
membershipId:
|
|
3937
|
+
membershipId: id11,
|
|
3772
3938
|
status: String(member.status),
|
|
3773
3939
|
cohort: String(member.cohort),
|
|
3774
3940
|
initialTermStartedAt: Number(member.initialTermStartedAt),
|
|
@@ -3780,9 +3946,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3780
3946
|
// src/membership-cancel-authority.ts
|
|
3781
3947
|
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
3782
3948
|
membershipClock(now);
|
|
3783
|
-
const
|
|
3949
|
+
const id11 = await networkMembershipId(target);
|
|
3784
3950
|
const state = await loadMembershipAuthorityState(db, target);
|
|
3785
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
3951
|
+
const member = await membershipLoad(db, "networkMembership", id11);
|
|
3786
3952
|
requireMembershipScope(member, target);
|
|
3787
3953
|
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
3788
3954
|
membershipFail("membership_cancellation_unavailable");
|
|
@@ -3800,7 +3966,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
3800
3966
|
"POST",
|
|
3801
3967
|
path,
|
|
3802
3968
|
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
3803
|
-
{ idempotencyKey: `membership-cancel-at-term:${
|
|
3969
|
+
{ idempotencyKey: `membership-cancel-at-term:${id11}:${member.providerSubscriptionId}` }
|
|
3804
3970
|
);
|
|
3805
3971
|
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
3806
3972
|
membershipFail("membership_cancellation_pending");
|
|
@@ -3808,7 +3974,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
3808
3974
|
}
|
|
3809
3975
|
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
3810
3976
|
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
3811
|
-
await db.transact([membershipUpdate("networkMembership",
|
|
3977
|
+
await db.transact([membershipUpdate("networkMembership", id11, {
|
|
3812
3978
|
status: status2,
|
|
3813
3979
|
autoRenew: false,
|
|
3814
3980
|
graceEndsAt: 0,
|
|
@@ -3817,12 +3983,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
|
3817
3983
|
updatedAt: now
|
|
3818
3984
|
})], {
|
|
3819
3985
|
guards: [
|
|
3820
|
-
{ ns: "networkMembership", id:
|
|
3986
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
3821
3987
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
3822
3988
|
]
|
|
3823
3989
|
});
|
|
3824
3990
|
}
|
|
3825
|
-
return { membershipId:
|
|
3991
|
+
return { membershipId: id11, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
3826
3992
|
}
|
|
3827
3993
|
|
|
3828
3994
|
// src/membership-authority-handler.ts
|
|
@@ -3861,17 +4027,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
|
|
|
3861
4027
|
// src/membership-approval-authority.ts
|
|
3862
4028
|
async function approveNetworkMembership(db, input, now) {
|
|
3863
4029
|
membershipClock(now);
|
|
3864
|
-
const
|
|
4030
|
+
const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
|
|
3865
4031
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
3866
4032
|
const state = await loadMembershipAuthorityState(db, input);
|
|
3867
|
-
const member = await membershipLoad(db, "networkMembership",
|
|
4033
|
+
const member = await membershipLoad(db, "networkMembership", id11);
|
|
3868
4034
|
requireMembershipScope(member, input);
|
|
3869
4035
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
3870
4036
|
requireMembershipScope(entitlement, input);
|
|
3871
|
-
if (entitlement.membershipId !==
|
|
4037
|
+
if (entitlement.membershipId !== id11 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
|
|
3872
4038
|
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) {
|
|
3873
4039
|
return {
|
|
3874
|
-
membershipId:
|
|
4040
|
+
membershipId: id11,
|
|
3875
4041
|
status: String(member.status),
|
|
3876
4042
|
cohort: String(member.cohort),
|
|
3877
4043
|
memberNumber: Number(member.memberNumber),
|
|
@@ -3891,7 +4057,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
3891
4057
|
const seatActivation = await namedSeatPrimaryActivation(db, member, now);
|
|
3892
4058
|
try {
|
|
3893
4059
|
await db.transact([
|
|
3894
|
-
membershipUpdate("networkMembership",
|
|
4060
|
+
membershipUpdate("networkMembership", id11, {
|
|
3895
4061
|
status: member.autoRenew ? "active" : "cancel_at_term_end",
|
|
3896
4062
|
memberNumber,
|
|
3897
4063
|
journeyApprovedAt: now,
|
|
@@ -3906,8 +4072,8 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
3906
4072
|
updatedAt: now
|
|
3907
4073
|
}),
|
|
3908
4074
|
...seatActivation.ops
|
|
3909
|
-
], { mutationId: `membership-approve:${
|
|
3910
|
-
{ ns: "networkMembership", id:
|
|
4075
|
+
], { mutationId: `membership-approve:${id11}`, guards: [
|
|
4076
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
|
|
3911
4077
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
|
|
3912
4078
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
3913
4079
|
...seatActivation.guards
|
|
@@ -4006,41 +4172,41 @@ async function refundSubscriptionPayment(secretKey, input) {
|
|
|
4006
4172
|
// src/membership-refund-authority.ts
|
|
4007
4173
|
async function refundNetworkMembership(db, input, secretKey, now) {
|
|
4008
4174
|
membershipClock(now);
|
|
4009
|
-
const
|
|
4010
|
-
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
4175
|
+
const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
|
|
4176
|
+
let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4011
4177
|
requireMembershipScope(member, input);
|
|
4012
|
-
if (member.status === "refunded") return { membershipId:
|
|
4178
|
+
if (member.status === "refunded") return { membershipId: id11, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
4013
4179
|
if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
|
|
4014
4180
|
if (member.status !== "refund_pending") {
|
|
4015
4181
|
const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
|
|
4016
4182
|
requireMembershipScope(entitlement, input);
|
|
4017
4183
|
await db.transact([
|
|
4018
|
-
membershipUpdate("networkMembership",
|
|
4184
|
+
membershipUpdate("networkMembership", id11, {
|
|
4019
4185
|
status: "refund_pending",
|
|
4020
4186
|
refundRequestedAt: now,
|
|
4021
4187
|
revision: Number(member.revision) + 1,
|
|
4022
4188
|
updatedAt: now
|
|
4023
4189
|
}),
|
|
4024
4190
|
membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
|
|
4025
|
-
membershipUpdate("membershipEffect", `${
|
|
4026
|
-
id: `${
|
|
4191
|
+
membershipUpdate("membershipEffect", `${id11}:refund`, {
|
|
4192
|
+
id: `${id11}:refund`,
|
|
4027
4193
|
chapterId: input.chapterId,
|
|
4028
4194
|
environment: input.environment,
|
|
4029
4195
|
runtime: input.runtime,
|
|
4030
|
-
membershipId:
|
|
4196
|
+
membershipId: id11,
|
|
4031
4197
|
kind: "refund_membership",
|
|
4032
4198
|
status: "pending",
|
|
4033
4199
|
attempts: 0,
|
|
4034
4200
|
createdAt: now,
|
|
4035
4201
|
updatedAt: now
|
|
4036
4202
|
})
|
|
4037
|
-
], { mutationId: `membership-refund-request:${
|
|
4038
|
-
{ ns: "networkMembership", id:
|
|
4203
|
+
], { mutationId: `membership-refund-request:${id11}`, guards: [
|
|
4204
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4039
4205
|
{ ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
|
|
4040
4206
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
4041
4207
|
] });
|
|
4042
4208
|
}
|
|
4043
|
-
member = await membershipLoad(db, "networkMembership",
|
|
4209
|
+
member = await membershipLoad(db, "networkMembership", id11);
|
|
4044
4210
|
requireMembershipScope(member, input);
|
|
4045
4211
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
4046
4212
|
const result = await refundSubscriptionPayment(secretKey, {
|
|
@@ -4054,23 +4220,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4054
4220
|
if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
|
|
4055
4221
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4056
4222
|
state = await loadMembershipAuthorityState(db, input);
|
|
4057
|
-
member = await membershipLoad(db, "networkMembership",
|
|
4223
|
+
member = await membershipLoad(db, "networkMembership", id11);
|
|
4058
4224
|
requireMembershipScope(member, input);
|
|
4059
|
-
if (member.status === "refunded") return { membershipId:
|
|
4225
|
+
if (member.status === "refunded") return { membershipId: id11, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
|
|
4060
4226
|
if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
|
|
4061
4227
|
const counted = member.founding === true && member.memberNumber !== void 0;
|
|
4062
4228
|
const counts = state.cohortCounts;
|
|
4063
4229
|
if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
|
|
4064
4230
|
try {
|
|
4065
4231
|
await db.transact([
|
|
4066
|
-
membershipUpdate("networkMembership",
|
|
4232
|
+
membershipUpdate("networkMembership", id11, {
|
|
4067
4233
|
status: "refunded",
|
|
4068
4234
|
refundedCents: result.refundedCents,
|
|
4069
4235
|
autoRenew: false,
|
|
4070
4236
|
revision: Number(member.revision) + 1,
|
|
4071
4237
|
updatedAt: now
|
|
4072
4238
|
}),
|
|
4073
|
-
membershipUpdate("membershipEffect", `${
|
|
4239
|
+
membershipUpdate("membershipEffect", `${id11}:refund`, { status: "complete", updatedAt: now }),
|
|
4074
4240
|
...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
|
|
4075
4241
|
cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
|
|
4076
4242
|
revision: Number(state.revision) + 1,
|
|
@@ -4081,7 +4247,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4081
4247
|
chapterId: input.chapterId,
|
|
4082
4248
|
environment: input.environment,
|
|
4083
4249
|
runtime: input.runtime,
|
|
4084
|
-
membershipId:
|
|
4250
|
+
membershipId: id11,
|
|
4085
4251
|
seatId: member.namedSeatId,
|
|
4086
4252
|
kind: "refund_seat_with_primary",
|
|
4087
4253
|
status: "pending",
|
|
@@ -4089,8 +4255,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4089
4255
|
createdAt: now,
|
|
4090
4256
|
updatedAt: now
|
|
4091
4257
|
})] : []
|
|
4092
|
-
], { mutationId: `membership-refunded:${
|
|
4093
|
-
{ ns: "networkMembership", id:
|
|
4258
|
+
], { mutationId: `membership-refunded:${id11}`, guards: [
|
|
4259
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { status: "refund_pending", revision: member.revision } },
|
|
4094
4260
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4095
4261
|
] });
|
|
4096
4262
|
} catch (error) {
|
|
@@ -4118,8 +4284,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4118
4284
|
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4119
4285
|
requireMembershipScope(member, input);
|
|
4120
4286
|
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4121
|
-
const
|
|
4122
|
-
let seat = await membershipLoad(db, "networkSeat",
|
|
4287
|
+
const id11 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4288
|
+
let seat = await membershipLoad(db, "networkSeat", id11);
|
|
4123
4289
|
if (!seat) {
|
|
4124
4290
|
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4125
4291
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
@@ -4127,7 +4293,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4127
4293
|
membershipFail("named_seat_purchase_unavailable");
|
|
4128
4294
|
}
|
|
4129
4295
|
const attrs = {
|
|
4130
|
-
id:
|
|
4296
|
+
id: id11,
|
|
4131
4297
|
...membershipScope(input),
|
|
4132
4298
|
primaryMembershipId: memberId,
|
|
4133
4299
|
recipientName: name,
|
|
@@ -4152,20 +4318,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4152
4318
|
};
|
|
4153
4319
|
try {
|
|
4154
4320
|
await db.transact([
|
|
4155
|
-
membershipUpdate("networkSeat",
|
|
4321
|
+
membershipUpdate("networkSeat", id11, attrs),
|
|
4156
4322
|
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4157
4323
|
id: claimId,
|
|
4158
4324
|
appId: input.appId,
|
|
4159
4325
|
chapterId: input.chapterId,
|
|
4160
4326
|
environment: input.environment,
|
|
4161
4327
|
runtime: input.runtime,
|
|
4162
|
-
seatId:
|
|
4328
|
+
seatId: id11,
|
|
4163
4329
|
recipientEmailNormalized: email,
|
|
4164
4330
|
createdAt: now
|
|
4165
4331
|
}),
|
|
4166
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId:
|
|
4167
|
-
], { mutationId: `named-seat-reserve:${
|
|
4168
|
-
{ ns: "networkSeat", id:
|
|
4332
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id11, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4333
|
+
], { mutationId: `named-seat-reserve:${id11}`, guards: [
|
|
4334
|
+
{ ns: "networkSeat", id: id11, exists: false },
|
|
4169
4335
|
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4170
4336
|
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4171
4337
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
@@ -4173,7 +4339,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4173
4339
|
} catch (error) {
|
|
4174
4340
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4175
4341
|
}
|
|
4176
|
-
seat = await membershipLoad(db, "networkSeat",
|
|
4342
|
+
seat = await membershipLoad(db, "networkSeat", id11);
|
|
4177
4343
|
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4178
4344
|
}
|
|
4179
4345
|
requireMembershipScope(seat, input);
|
|
@@ -4183,7 +4349,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4183
4349
|
|
|
4184
4350
|
// src/membership-seat-included.ts
|
|
4185
4351
|
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4186
|
-
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4352
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
|
|
4187
4353
|
function namedSeatRecipientState(seat) {
|
|
4188
4354
|
switch (String(seat.status)) {
|
|
4189
4355
|
case "renewal_pending":
|
|
@@ -4281,7 +4447,7 @@ async function grantIncludedNamedSeat(db, input, now) {
|
|
|
4281
4447
|
// src/membership-renewal-authority.ts
|
|
4282
4448
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
4283
4449
|
membershipClock(now);
|
|
4284
|
-
const
|
|
4450
|
+
const id11 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4285
4451
|
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
4286
4452
|
const fact = eventRow?.fact;
|
|
4287
4453
|
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({
|
|
@@ -4294,10 +4460,10 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4294
4460
|
return membershipFail("membership_renewal_evidence_pending");
|
|
4295
4461
|
}
|
|
4296
4462
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4297
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
4463
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4298
4464
|
requireMembershipScope(member, input);
|
|
4299
4465
|
const receipt = () => ({
|
|
4300
|
-
membershipId:
|
|
4466
|
+
membershipId: id11,
|
|
4301
4467
|
status: String(member.status),
|
|
4302
4468
|
termEndsAt: Number(member.termEndsAt),
|
|
4303
4469
|
graceEndsAt: Number(member.graceEndsAt ?? 0),
|
|
@@ -4322,13 +4488,13 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4322
4488
|
const paid = fact.eventType === "invoice.payment_succeeded";
|
|
4323
4489
|
if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
|
|
4324
4490
|
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");
|
|
4325
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
4491
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
4326
4492
|
requireMembershipScope(entitlement, input);
|
|
4327
4493
|
const graceEndsAt = paid ? 0 : start + 14 * 864e5;
|
|
4328
4494
|
const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
|
|
4329
4495
|
const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
|
|
4330
4496
|
const ops = [
|
|
4331
|
-
membershipUpdate("networkMembership",
|
|
4497
|
+
membershipUpdate("networkMembership", id11, {
|
|
4332
4498
|
status: status2,
|
|
4333
4499
|
graceEndsAt,
|
|
4334
4500
|
...paid ? {
|
|
@@ -4339,7 +4505,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4339
4505
|
revision: Number(member.revision) + 1,
|
|
4340
4506
|
updatedAt: now
|
|
4341
4507
|
}),
|
|
4342
|
-
membershipUpdate("networkEntitlement", `${
|
|
4508
|
+
membershipUpdate("networkEntitlement", `${id11}:primary`, {
|
|
4343
4509
|
status: entitlementStatus,
|
|
4344
4510
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
4345
4511
|
sourceEventId: input.eventId,
|
|
@@ -4348,8 +4514,8 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4348
4514
|
})
|
|
4349
4515
|
];
|
|
4350
4516
|
const guards = [
|
|
4351
|
-
{ ns: "networkMembership", id:
|
|
4352
|
-
{ ns: "networkEntitlement", id: `${
|
|
4517
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4518
|
+
{ ns: "networkEntitlement", id: `${id11}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
4353
4519
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
4354
4520
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
4355
4521
|
evidenceGuard
|
|
@@ -4374,15 +4540,15 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4374
4540
|
);
|
|
4375
4541
|
}
|
|
4376
4542
|
try {
|
|
4377
|
-
await db.transact(ops, { mutationId: `membership-renewal:${
|
|
4543
|
+
await db.transact(ops, { mutationId: `membership-renewal:${id11}:${input.eventId}`, guards });
|
|
4378
4544
|
} catch (error) {
|
|
4379
4545
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4380
4546
|
continue;
|
|
4381
4547
|
}
|
|
4382
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
4548
|
+
const saved = await membershipLoad(db, "networkMembership", id11);
|
|
4383
4549
|
requireMembershipScope(saved, input);
|
|
4384
4550
|
return {
|
|
4385
|
-
membershipId:
|
|
4551
|
+
membershipId: id11,
|
|
4386
4552
|
status: String(saved.status),
|
|
4387
4553
|
termEndsAt: Number(saved.termEndsAt),
|
|
4388
4554
|
graceEndsAt: Number(saved.graceEndsAt ?? 0),
|
|
@@ -4408,12 +4574,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4408
4574
|
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
4409
4575
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
4410
4576
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
4411
|
-
const
|
|
4577
|
+
const id11 = await networkMembershipId(input);
|
|
4412
4578
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
4413
|
-
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership",
|
|
4579
|
+
const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4414
4580
|
requireMembershipScope(member, input);
|
|
4415
4581
|
const receipt = (value = member) => ({
|
|
4416
|
-
membershipId:
|
|
4582
|
+
membershipId: id11,
|
|
4417
4583
|
status: String(value.status),
|
|
4418
4584
|
termEndsAt: Number(value.termEndsAt),
|
|
4419
4585
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
@@ -4434,7 +4600,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4434
4600
|
});
|
|
4435
4601
|
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
4436
4602
|
await refundNetworkMembership(db, input, sk, now);
|
|
4437
|
-
const saved2 = await membershipLoad(db, "networkMembership",
|
|
4603
|
+
const saved2 = await membershipLoad(db, "networkMembership", id11);
|
|
4438
4604
|
requireMembershipScope(saved2, input);
|
|
4439
4605
|
return receipt(saved2);
|
|
4440
4606
|
}
|
|
@@ -4449,18 +4615,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4449
4615
|
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";
|
|
4450
4616
|
const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
|
|
4451
4617
|
if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
|
|
4452
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
4618
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
4453
4619
|
requireMembershipScope(entitlement, input);
|
|
4454
4620
|
try {
|
|
4455
4621
|
await db.transact([
|
|
4456
|
-
membershipUpdate("networkMembership",
|
|
4622
|
+
membershipUpdate("networkMembership", id11, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
|
|
4457
4623
|
membershipUpdate("networkEntitlement", String(entitlement.id), {
|
|
4458
4624
|
status: status2 === "lapsed" ? "closed" : entitlement.status,
|
|
4459
4625
|
revision: Number(entitlement.revision) + 1,
|
|
4460
4626
|
updatedAt: now
|
|
4461
4627
|
})
|
|
4462
4628
|
], { guards: [
|
|
4463
|
-
{ ns: "networkMembership", id:
|
|
4629
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
|
|
4464
4630
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
4465
4631
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4466
4632
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
@@ -4470,7 +4636,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4470
4636
|
if (error.code !== "transact_guard_failed") throw error;
|
|
4471
4637
|
continue;
|
|
4472
4638
|
}
|
|
4473
|
-
const saved = await membershipLoad(db, "networkMembership",
|
|
4639
|
+
const saved = await membershipLoad(db, "networkMembership", id11);
|
|
4474
4640
|
requireMembershipScope(saved, input);
|
|
4475
4641
|
return receipt(saved);
|
|
4476
4642
|
}
|
|
@@ -4531,19 +4697,19 @@ async function stopPreviousSubscription(member, target, secret) {
|
|
|
4531
4697
|
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
4532
4698
|
membershipClock(now);
|
|
4533
4699
|
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
4534
|
-
const
|
|
4535
|
-
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([
|
|
4700
|
+
const id11 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
4701
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id11, input.previousSubscriptionId]))}`;
|
|
4536
4702
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4537
|
-
const previous = await membershipLoad(db, "networkMembership",
|
|
4703
|
+
const previous = await membershipLoad(db, "networkMembership", id11);
|
|
4538
4704
|
requireMembershipScope(previous, input);
|
|
4539
4705
|
const receipt = {
|
|
4540
4706
|
applicationId: input.applicationId,
|
|
4541
|
-
membershipId:
|
|
4707
|
+
membershipId: id11,
|
|
4542
4708
|
restartId,
|
|
4543
4709
|
memberNumber: Number(previous.memberNumber),
|
|
4544
4710
|
status: "ready"
|
|
4545
4711
|
};
|
|
4546
|
-
const existing = await membershipLoad(db, "membershipRestart",
|
|
4712
|
+
const existing = await membershipLoad(db, "membershipRestart", id11);
|
|
4547
4713
|
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
4548
4714
|
requireMembershipScope(existing, input);
|
|
4549
4715
|
return receipt;
|
|
@@ -4559,33 +4725,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
4559
4725
|
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
4560
4726
|
id: restartId,
|
|
4561
4727
|
...membershipScope(input),
|
|
4562
|
-
membershipId:
|
|
4728
|
+
membershipId: id11,
|
|
4563
4729
|
providerSubscriptionId: input.previousSubscriptionId,
|
|
4564
4730
|
memberSnapshot: previous,
|
|
4565
4731
|
bindingSnapshot: binding2,
|
|
4566
4732
|
archivedAt: now
|
|
4567
4733
|
}),
|
|
4568
|
-
membershipUpdate("networkMembership",
|
|
4734
|
+
membershipUpdate("networkMembership", id11, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
4569
4735
|
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
4570
4736
|
...seats.guards,
|
|
4571
|
-
{ ns: "networkMembership", id:
|
|
4737
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
4572
4738
|
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
4573
4739
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
4574
4740
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4575
4741
|
] });
|
|
4576
4742
|
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4577
|
-
const reserved = await membershipLoad(db, "networkMembership",
|
|
4743
|
+
const reserved = await membershipLoad(db, "networkMembership", id11);
|
|
4578
4744
|
requireMembershipScope(reserved, input);
|
|
4579
4745
|
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4580
4746
|
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
4581
4747
|
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4582
4748
|
requireMembershipScope(binding, input);
|
|
4583
4749
|
await db.transact([
|
|
4584
|
-
membershipUpdate("membershipRestart",
|
|
4750
|
+
membershipUpdate("membershipRestart", id11, { id: id11, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
4585
4751
|
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
4586
4752
|
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
4587
|
-
{ ns: "membershipRestart", id:
|
|
4588
|
-
{ ns: "networkMembership", id:
|
|
4753
|
+
{ ns: "membershipRestart", id: id11, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
4754
|
+
{ ns: "networkMembership", id: id11, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
4589
4755
|
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
4590
4756
|
] });
|
|
4591
4757
|
return receipt;
|
|
@@ -4595,15 +4761,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
|
4595
4761
|
async function readMembershipAccess(db, input, now) {
|
|
4596
4762
|
membershipClock(now);
|
|
4597
4763
|
await loadMembershipAuthorityState(db, input);
|
|
4598
|
-
const
|
|
4599
|
-
if (!member) return { membershipId:
|
|
4764
|
+
const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
|
|
4765
|
+
if (!member) return { membershipId: id11, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
|
|
4600
4766
|
requireMembershipScope(member, input);
|
|
4601
|
-
const entitlement = await membershipLoad(db, "networkEntitlement", `${
|
|
4767
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
|
|
4602
4768
|
requireMembershipScope(entitlement, input);
|
|
4603
4769
|
const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
4604
|
-
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 ===
|
|
4770
|
+
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 === id11 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
|
|
4605
4771
|
return {
|
|
4606
|
-
membershipId:
|
|
4772
|
+
membershipId: id11,
|
|
4607
4773
|
allowed: allowed2,
|
|
4608
4774
|
status: String(member.status),
|
|
4609
4775
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
@@ -4658,7 +4824,7 @@ async function subscriptionForSeat(sk, seat) {
|
|
|
4658
4824
|
currency: seat.currency,
|
|
4659
4825
|
interval: seat.interval,
|
|
4660
4826
|
environment: seat.environment
|
|
4661
|
-
}, Boolean(seat.providerPaymentIntentId));
|
|
4827
|
+
}, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
|
|
4662
4828
|
if (!productId) reject3();
|
|
4663
4829
|
const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
|
|
4664
4830
|
const items = sub.items?.data;
|
|
@@ -4702,10 +4868,14 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
4702
4868
|
return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
|
|
4703
4869
|
}
|
|
4704
4870
|
async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
4705
|
-
if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject3();
|
|
4871
|
+
if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject3();
|
|
4706
4872
|
const sub = await subscriptionForSeat(sk, seat);
|
|
4707
4873
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
4708
|
-
if (existing)
|
|
4874
|
+
if (existing) {
|
|
4875
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject3();
|
|
4876
|
+
return String(existing.id);
|
|
4877
|
+
}
|
|
4878
|
+
if (seat.combinedInvoice) reject3();
|
|
4709
4879
|
const result = await stripeRequest7({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
4710
4880
|
subscription: seat.providerSubscriptionId,
|
|
4711
4881
|
price: seat.priceId,
|
|
@@ -4923,7 +5093,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4923
5093
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4924
5094
|
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4925
5095
|
requireMembershipScope(seat, input);
|
|
4926
|
-
if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
5096
|
+
if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
|
|
4927
5097
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4928
5098
|
requireMembershipScope(member, input);
|
|
4929
5099
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
@@ -4981,43 +5151,121 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4981
5151
|
}
|
|
4982
5152
|
|
|
4983
5153
|
// src/membership-seat-refund-stripe.ts
|
|
5154
|
+
import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
5155
|
+
|
|
5156
|
+
// src/membership-checkout-seat-refund.ts
|
|
4984
5157
|
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
4985
5158
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4986
5159
|
throw Object.assign(new Error(code), { code });
|
|
4987
5160
|
};
|
|
5161
|
+
var id8 = (value) => typeof value === "string" ? value : value?.id ?? "";
|
|
5162
|
+
async function refundCheckoutNamedSeat(secretKey, seat) {
|
|
5163
|
+
const invoiceRef = seat.combinedInvoice;
|
|
5164
|
+
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();
|
|
5165
|
+
const call = async (method, path, params, key3) => {
|
|
5166
|
+
const result = await stripeRequest8({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
5167
|
+
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
5168
|
+
return result.body;
|
|
5169
|
+
};
|
|
5170
|
+
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
|
|
5171
|
+
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
5172
|
+
if (sub.id !== seat.providerSubscriptionId || id8(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();
|
|
5173
|
+
const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
|
|
5174
|
+
if (invoice.id !== invoiceRef.invoiceId || id8(invoice.subscription ?? invoice.parent?.subscription_details?.subscription) !== sub.id || id8(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();
|
|
5175
|
+
let chargeId = id8(invoice.charge), paymentId = id8(invoice.payment_intent);
|
|
5176
|
+
if (!chargeId && !paymentId) {
|
|
5177
|
+
const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
|
|
5178
|
+
if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
|
|
5179
|
+
const payment = payments.data[0];
|
|
5180
|
+
if (id8(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
|
|
5181
|
+
paymentId = id8(payment.payment.payment_intent);
|
|
5182
|
+
}
|
|
5183
|
+
if (!chargeId) {
|
|
5184
|
+
if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
|
|
5185
|
+
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
|
|
5186
|
+
if (payment.id !== paymentId || id8(payment.customer) !== seat.providerCustomerId || payment.status !== "succeeded" || payment.amount_received !== invoiceRef.totalCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod")) fail4();
|
|
5187
|
+
chargeId = id8(payment.latest_charge);
|
|
5188
|
+
}
|
|
5189
|
+
if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
|
|
5190
|
+
const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
|
|
5191
|
+
if (charge.id !== chargeId || id8(charge.customer) !== seat.providerCustomerId || charge.status !== "succeeded" || charge.livemode !== (seat.environment === "prod") || charge.amount !== invoiceRef.totalCents || charge.currency !== seat.currency || charge.invoice != null && id8(charge.invoice) !== invoice.id || paymentId && id8(charge.payment_intent) !== paymentId || !Number.isSafeInteger(charge.amount_refunded) || charge.amount_refunded < 0 || charge.amount_refunded > charge.amount) fail4();
|
|
5192
|
+
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
5193
|
+
if (items.length > 1) fail4();
|
|
5194
|
+
if (items.length && sub.status !== "canceled") {
|
|
5195
|
+
const item = items[0];
|
|
5196
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
|
|
5197
|
+
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
5198
|
+
if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
|
|
5199
|
+
}
|
|
5200
|
+
if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
|
|
5201
|
+
const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
|
|
5202
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
|
|
5203
|
+
let refundedCents = 0;
|
|
5204
|
+
const refundIds = [];
|
|
5205
|
+
for (const refund of list.data) {
|
|
5206
|
+
if (id8(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();
|
|
5207
|
+
if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
|
|
5208
|
+
if (refund.status === "succeeded") {
|
|
5209
|
+
if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
|
|
5210
|
+
refundedCents += refund.amount;
|
|
5211
|
+
refundIds.push(refund.id);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
|
|
5215
|
+
if (refundedCents < seat.amountCents) {
|
|
5216
|
+
const remaining = seat.amountCents - refundedCents;
|
|
5217
|
+
const refund = await call("POST", "/v1/refunds", {
|
|
5218
|
+
charge: chargeId,
|
|
5219
|
+
amount: remaining,
|
|
5220
|
+
reason: "requested_by_customer",
|
|
5221
|
+
metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
|
|
5222
|
+
}, `combined-seat-refund:${seat.id}:${remaining}`);
|
|
5223
|
+
if (id8(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
|
|
5224
|
+
if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
|
|
5225
|
+
refundedCents += remaining;
|
|
5226
|
+
refundIds.push(refund.id);
|
|
5227
|
+
}
|
|
5228
|
+
return { refundedCents, refundIds };
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
// src/membership-seat-refund-stripe.ts
|
|
5232
|
+
var fail5 = (code = "named_seat_refund_mismatch") => {
|
|
5233
|
+
throw Object.assign(new Error(code), { code });
|
|
5234
|
+
};
|
|
4988
5235
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4989
|
-
if (
|
|
5236
|
+
if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
|
|
5237
|
+
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
|
|
4990
5238
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4991
|
-
const result = await
|
|
4992
|
-
if (!result.ok || !result.body) return
|
|
5239
|
+
const result = await stripeRequest9({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
5240
|
+
if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
|
|
4993
5241
|
return result.body;
|
|
4994
5242
|
};
|
|
4995
5243
|
const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
4996
5244
|
const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
|
|
4997
|
-
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))
|
|
5245
|
+
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();
|
|
4998
5246
|
const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
|
|
4999
|
-
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))
|
|
5247
|
+
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();
|
|
5000
5248
|
const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
|
|
5001
|
-
if (items.length > 1)
|
|
5249
|
+
if (items.length > 1) fail5();
|
|
5002
5250
|
if (items.length && sub.status !== "canceled") {
|
|
5003
5251
|
const item = items[0];
|
|
5004
|
-
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1)
|
|
5252
|
+
if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
|
|
5005
5253
|
const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
|
|
5006
|
-
if (removed.id !== item.id || removed.deleted !== true)
|
|
5254
|
+
if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
|
|
5007
5255
|
}
|
|
5008
5256
|
const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
|
|
5009
|
-
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100)
|
|
5257
|
+
if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
|
|
5010
5258
|
let refundedCents = 0, pendingCents = 0;
|
|
5011
5259
|
const refundIds = [];
|
|
5012
5260
|
for (const refund of list.data) {
|
|
5013
|
-
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))
|
|
5261
|
+
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();
|
|
5014
5262
|
if (refund.status === "succeeded") {
|
|
5015
5263
|
refundedCents += refund.amount;
|
|
5016
5264
|
refundIds.push(refund.id);
|
|
5017
5265
|
} else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
|
|
5018
5266
|
}
|
|
5019
|
-
if (refundedCents + pendingCents > seat.amountCents)
|
|
5020
|
-
if (pendingCents)
|
|
5267
|
+
if (refundedCents + pendingCents > seat.amountCents) fail5();
|
|
5268
|
+
if (pendingCents) fail5("named_seat_refund_pending");
|
|
5021
5269
|
if (refundedCents < seat.amountCents) {
|
|
5022
5270
|
const remaining = seat.amountCents - refundedCents;
|
|
5023
5271
|
const refund = await call("POST", "/v1/refunds", {
|
|
@@ -5026,8 +5274,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
|
5026
5274
|
reason: "requested_by_customer",
|
|
5027
5275
|
metadata
|
|
5028
5276
|
}, `named-seat-refund:${seat.id}:${remaining}`);
|
|
5029
|
-
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id))
|
|
5030
|
-
if (refund.status !== "succeeded")
|
|
5277
|
+
if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
|
|
5278
|
+
if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
|
|
5031
5279
|
refundedCents += remaining;
|
|
5032
5280
|
refundIds.push(refund.id);
|
|
5033
5281
|
}
|
|
@@ -5043,7 +5291,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
5043
5291
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
5044
5292
|
requireMembershipScope(member, input);
|
|
5045
5293
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
5046
|
-
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");
|
|
5294
|
+
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");
|
|
5047
5295
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
5048
5296
|
await db.transact([
|
|
5049
5297
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -5148,14 +5396,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
|
5148
5396
|
async function readNamedSeatOffer(db, input, now) {
|
|
5149
5397
|
membershipClock(now);
|
|
5150
5398
|
await loadMembershipAuthorityState(db, input);
|
|
5151
|
-
const
|
|
5399
|
+
const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
|
|
5152
5400
|
requireMembershipScope(member, input);
|
|
5153
5401
|
const offering = membershipOfferingVersion(member.offeringSnapshot);
|
|
5154
5402
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
5155
5403
|
if (seat) requireMembershipScope(seat, input);
|
|
5156
5404
|
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
5157
5405
|
const offer = {
|
|
5158
|
-
membershipId:
|
|
5406
|
+
membershipId: id11,
|
|
5159
5407
|
amountCents: offering.seatPriceCents,
|
|
5160
5408
|
currency: offering.currency,
|
|
5161
5409
|
interval: offering.interval,
|
|
@@ -5305,7 +5553,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5305
5553
|
};
|
|
5306
5554
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
5307
5555
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
5308
|
-
const
|
|
5556
|
+
const checkout2 = {
|
|
5309
5557
|
...target,
|
|
5310
5558
|
priceId: peer.seatPriceId,
|
|
5311
5559
|
recipientName: String(input.recipientName),
|
|
@@ -5313,7 +5561,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5313
5561
|
purchaserEmail: String(input.purchaserEmail),
|
|
5314
5562
|
purchaserUserId: String(input.purchaserUserId)
|
|
5315
5563
|
};
|
|
5316
|
-
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db,
|
|
5564
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
|
|
5317
5565
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
5318
5566
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
5319
5567
|
if (action === "seat-decline") {
|
|
@@ -5329,7 +5577,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5329
5577
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5330
5578
|
}
|
|
5331
5579
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5332
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
5580
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
|
|
5333
5581
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
5334
5582
|
if (action === "seat-payments") {
|
|
5335
5583
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -5400,9 +5648,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5400
5648
|
const identities = /* @__PURE__ */ new Set();
|
|
5401
5649
|
for (const peer of peers) {
|
|
5402
5650
|
if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
|
|
5403
|
-
const
|
|
5404
|
-
if (identities.has(
|
|
5405
|
-
identities.add(
|
|
5651
|
+
const identity2 = `${peer.appId}:${peer.runtime}`;
|
|
5652
|
+
if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
|
|
5653
|
+
identities.add(identity2);
|
|
5406
5654
|
if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
|
|
5407
5655
|
}
|
|
5408
5656
|
return async (req) => {
|
|
@@ -5433,9 +5681,14 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5433
5681
|
try {
|
|
5434
5682
|
input = JSON.parse(verified.body);
|
|
5435
5683
|
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] ?? []];
|
|
5436
|
-
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]) : SAFE.test(input[key3])))) {
|
|
5684
|
+
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]) : SAFE.test(input[key3])))) {
|
|
5437
5685
|
throw new TypeError("invalid request");
|
|
5438
5686
|
}
|
|
5687
|
+
if (action === "checkouts" && input.namedSeat !== void 0) {
|
|
5688
|
+
const seat = input.namedSeat;
|
|
5689
|
+
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");
|
|
5690
|
+
checkoutNamedSeatSelection(seat, seat.purchaserEmail);
|
|
5691
|
+
}
|
|
5439
5692
|
} catch {
|
|
5440
5693
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
5441
5694
|
}
|
|
@@ -5508,8 +5761,9 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5508
5761
|
offerId: String(input.offerId),
|
|
5509
5762
|
offerDigest: String(input.offerDigest),
|
|
5510
5763
|
quoteDigest: String(input.quoteDigest),
|
|
5511
|
-
priceId: String(input.priceId)
|
|
5512
|
-
|
|
5764
|
+
priceId: String(input.priceId),
|
|
5765
|
+
...input.namedSeat ? { namedSeat: input.namedSeat } : {}
|
|
5766
|
+
}, options.now?.() ?? Date.now(), peer.seatPriceId);
|
|
5513
5767
|
return respond({ ok: true, binding });
|
|
5514
5768
|
}
|
|
5515
5769
|
const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
|
|
@@ -5612,7 +5866,8 @@ async function requestMembershipCheckoutBinding(options) {
|
|
|
5612
5866
|
offerId: options.offerId,
|
|
5613
5867
|
offerDigest: options.offerDigest,
|
|
5614
5868
|
quoteDigest: options.quoteDigest,
|
|
5615
|
-
priceId: options.priceId
|
|
5869
|
+
priceId: options.priceId,
|
|
5870
|
+
...options.namedSeat ? { namedSeat: options.namedSeat } : {}
|
|
5616
5871
|
});
|
|
5617
5872
|
return verifyMembershipCheckoutBinding(result.binding, options);
|
|
5618
5873
|
}
|
|
@@ -5822,7 +6077,7 @@ async function requestMembershipCancellation(options) {
|
|
|
5822
6077
|
}
|
|
5823
6078
|
|
|
5824
6079
|
// src/membership-seat-cancel.ts
|
|
5825
|
-
import { initStripe, stripeRequest as
|
|
6080
|
+
import { initStripe, stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
|
|
5826
6081
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
5827
6082
|
membershipClock(now);
|
|
5828
6083
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -5850,7 +6105,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5850
6105
|
}
|
|
5851
6106
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
5852
6107
|
const request = async (method, path, params) => {
|
|
5853
|
-
const result = await
|
|
6108
|
+
const result = await stripeRequest10(
|
|
5854
6109
|
{ secretKey },
|
|
5855
6110
|
method,
|
|
5856
6111
|
path,
|
|
@@ -5876,9 +6131,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5876
6131
|
if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
|
|
5877
6132
|
if (payment.status !== "canceled") {
|
|
5878
6133
|
if (!["requires_payment_method", "requires_confirmation", "requires_action", "processing"].includes(payment.status)) throw new Error("seat payment outcome unresolved");
|
|
5879
|
-
const
|
|
5880
|
-
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(
|
|
5881
|
-
if (payment.id !==
|
|
6134
|
+
const id11 = payment.id;
|
|
6135
|
+
payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id11)}/cancel`, { cancellation_reason: "abandoned" });
|
|
6136
|
+
if (payment.id !== id11) throw new Error("seat cancellation receipt mismatch");
|
|
5882
6137
|
}
|
|
5883
6138
|
if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
|
|
5884
6139
|
await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
|
|
@@ -5978,11 +6233,11 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5978
6233
|
|
|
5979
6234
|
// src/schema-membership-offers.ts
|
|
5980
6235
|
var attr8 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
5981
|
-
var
|
|
6236
|
+
var id9 = () => attr8("string", false, true, true);
|
|
5982
6237
|
var key = () => attr8("string", false, false, true);
|
|
5983
6238
|
var membershipOfferEntities = {
|
|
5984
6239
|
membershipAuthorityState: { attrs: {
|
|
5985
|
-
id:
|
|
6240
|
+
id: id9(),
|
|
5986
6241
|
chapterId: key(),
|
|
5987
6242
|
environment: key(),
|
|
5988
6243
|
mode: key(),
|
|
@@ -5992,7 +6247,7 @@ var membershipOfferEntities = {
|
|
|
5992
6247
|
updatedAt: attr8("number")
|
|
5993
6248
|
} },
|
|
5994
6249
|
membershipCheckoutOffer: { attrs: {
|
|
5995
|
-
id:
|
|
6250
|
+
id: id9(),
|
|
5996
6251
|
applicationId: key(),
|
|
5997
6252
|
appId: key(),
|
|
5998
6253
|
chapterId: key(),
|
|
@@ -6007,7 +6262,7 @@ var membershipOfferEntities = {
|
|
|
6007
6262
|
authorityRevision: attr8("number")
|
|
6008
6263
|
} },
|
|
6009
6264
|
membershipOfferCheckout: { attrs: {
|
|
6010
|
-
id:
|
|
6265
|
+
id: id9(),
|
|
6011
6266
|
appId: key(),
|
|
6012
6267
|
chapterId: key(),
|
|
6013
6268
|
environment: key(),
|
|
@@ -6018,20 +6273,21 @@ var membershipOfferEntities = {
|
|
|
6018
6273
|
offerDigest: attr8("string"),
|
|
6019
6274
|
quoteDigest: attr8("string"),
|
|
6020
6275
|
priceId: attr8("string"),
|
|
6021
|
-
boundAt: attr8("number")
|
|
6276
|
+
boundAt: attr8("number"),
|
|
6277
|
+
namedSeat: attr8("json", true)
|
|
6022
6278
|
} }
|
|
6023
6279
|
};
|
|
6024
6280
|
var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
|
|
6025
6281
|
|
|
6026
6282
|
// src/schema-membership-authority.ts
|
|
6027
6283
|
var attr9 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
|
|
6028
|
-
var
|
|
6284
|
+
var id10 = () => attr9("string", false, true, true);
|
|
6029
6285
|
var key2 = (optional = false) => attr9("string", optional, false, true);
|
|
6030
6286
|
var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
|
|
6031
6287
|
var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
|
|
6032
6288
|
var membershipAuthorityEntities = {
|
|
6033
6289
|
networkMembership: { attrs: {
|
|
6034
|
-
id:
|
|
6290
|
+
id: id10(),
|
|
6035
6291
|
membershipId: key2(),
|
|
6036
6292
|
...scope(),
|
|
6037
6293
|
personId: key2(true),
|
|
@@ -6071,7 +6327,7 @@ var membershipAuthorityEntities = {
|
|
|
6071
6327
|
restartId: key2(true)
|
|
6072
6328
|
} },
|
|
6073
6329
|
membershipRestart: { attrs: {
|
|
6074
|
-
id:
|
|
6330
|
+
id: id10(),
|
|
6075
6331
|
...scope(),
|
|
6076
6332
|
membershipId: key2(),
|
|
6077
6333
|
restartId: key2(),
|
|
@@ -6081,7 +6337,7 @@ var membershipAuthorityEntities = {
|
|
|
6081
6337
|
createdAt: attr9("number")
|
|
6082
6338
|
} },
|
|
6083
6339
|
membershipPurchaseHistory: { attrs: {
|
|
6084
|
-
id:
|
|
6340
|
+
id: id10(),
|
|
6085
6341
|
...scope(),
|
|
6086
6342
|
membershipId: key2(),
|
|
6087
6343
|
providerSubscriptionId: key2(),
|
|
@@ -6090,7 +6346,7 @@ var membershipAuthorityEntities = {
|
|
|
6090
6346
|
archivedAt: attr9("number")
|
|
6091
6347
|
} },
|
|
6092
6348
|
networkEntitlement: { attrs: {
|
|
6093
|
-
id:
|
|
6349
|
+
id: id10(),
|
|
6094
6350
|
...scope(),
|
|
6095
6351
|
membershipId: key2(),
|
|
6096
6352
|
seatId: key2(true),
|
|
@@ -6107,10 +6363,12 @@ var membershipAuthorityEntities = {
|
|
|
6107
6363
|
journeyApprovedAt: attr9("number", true)
|
|
6108
6364
|
} },
|
|
6109
6365
|
networkSeat: { attrs: {
|
|
6110
|
-
id:
|
|
6366
|
+
id: id10(),
|
|
6111
6367
|
...scope(),
|
|
6112
6368
|
primaryMembershipId: key2(),
|
|
6113
|
-
purchaserUserId: key2(),
|
|
6369
|
+
purchaserUserId: key2(true),
|
|
6370
|
+
purchaserEmailNormalized: key2(true),
|
|
6371
|
+
combinedInvoice: attr9("json", true),
|
|
6114
6372
|
recipientName: attr9("string"),
|
|
6115
6373
|
recipientEmailNormalized: key2(),
|
|
6116
6374
|
recipientClaimId: key2(),
|
|
@@ -6142,9 +6400,9 @@ var membershipAuthorityEntities = {
|
|
|
6142
6400
|
refundedCents: attr9("number", true),
|
|
6143
6401
|
providerRefundIds: attr9("json", true)
|
|
6144
6402
|
} },
|
|
6145
|
-
networkSeatRecipient: { attrs: { id:
|
|
6403
|
+
networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
|
|
6146
6404
|
membershipEffect: { attrs: {
|
|
6147
|
-
id:
|
|
6405
|
+
id: id10(),
|
|
6148
6406
|
chapterId: key2(),
|
|
6149
6407
|
environment: key2(),
|
|
6150
6408
|
runtime: key2(),
|
|
@@ -6269,14 +6527,14 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
6269
6527
|
continue;
|
|
6270
6528
|
}
|
|
6271
6529
|
unknownKeys(raw2, PAGE_KEYS, path, errors);
|
|
6272
|
-
const
|
|
6530
|
+
const id11 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
|
|
6273
6531
|
const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
6274
6532
|
const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
6275
|
-
if (
|
|
6533
|
+
if (id11 && !ID.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
6276
6534
|
if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
6277
|
-
if (ids2.has(
|
|
6535
|
+
if (ids2.has(id11) || knownParentIds.has(id11)) errors.push(`${path}.id duplicates page "${id11}"`);
|
|
6278
6536
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
6279
|
-
ids2.add(
|
|
6537
|
+
ids2.add(id11);
|
|
6280
6538
|
slugs.add(slug);
|
|
6281
6539
|
const navRaw = record(raw2.navigation);
|
|
6282
6540
|
let navigation = { section: "General", order: index2 };
|
|
@@ -6332,7 +6590,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
6332
6590
|
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
6333
6591
|
}
|
|
6334
6592
|
}
|
|
6335
|
-
pages.push({ id:
|
|
6593
|
+
pages.push({ id: id11, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
6336
6594
|
}
|
|
6337
6595
|
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
|
|
6338
6596
|
for (const [index2, page] of pages.entries()) {
|
|
@@ -6538,8 +6796,8 @@ function resourceValue(value, path, errors) {
|
|
|
6538
6796
|
return null;
|
|
6539
6797
|
}
|
|
6540
6798
|
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
6541
|
-
const
|
|
6542
|
-
if (
|
|
6799
|
+
const id11 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
6800
|
+
if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
6543
6801
|
const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
6544
6802
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
6545
6803
|
const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
@@ -6596,7 +6854,7 @@ function resourceValue(value, path, errors) {
|
|
|
6596
6854
|
updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
6597
6855
|
...promotedFrom ? { promotedFrom } : {}
|
|
6598
6856
|
};
|
|
6599
|
-
return { id:
|
|
6857
|
+
return { id: id11, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
6600
6858
|
}
|
|
6601
6859
|
function validateBuilderLibraryDocument(value, options = {}) {
|
|
6602
6860
|
const errors = [];
|
|
@@ -6627,7 +6885,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
6627
6885
|
const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
|
|
6628
6886
|
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
6629
6887
|
const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
|
|
6630
|
-
const ids2 = resources.map(({ id:
|
|
6888
|
+
const ids2 = resources.map(({ id: id11 }) => id11);
|
|
6631
6889
|
if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
|
|
6632
6890
|
for (const resource of resources) {
|
|
6633
6891
|
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
@@ -6714,6 +6972,41 @@ async function createBuilderLibraryEnvelope(document) {
|
|
|
6714
6972
|
return { document, digest: await builderLibraryDigest(document) };
|
|
6715
6973
|
}
|
|
6716
6974
|
|
|
6975
|
+
// src/email-retirement.ts
|
|
6976
|
+
function isRetiredEmailCopy(subject, text6) {
|
|
6977
|
+
const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
|
|
6978
|
+
const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
|
|
6979
|
+
return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6982
|
+
// src/email.ts
|
|
6983
|
+
import { validateEmailCopy } from "@odla-ai/crm";
|
|
6984
|
+
|
|
6985
|
+
// src/email-events.ts
|
|
6986
|
+
var identity = ["firstName", "lastName", "email", "phone", "state"];
|
|
6987
|
+
var policy = ["refundPolicyText", "commitmentText", "normsText"];
|
|
6988
|
+
var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
|
|
6989
|
+
var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
|
|
6990
|
+
var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
|
|
6991
|
+
var CHAPTER_EMAIL_EVENTS = {
|
|
6992
|
+
adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
|
|
6993
|
+
submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
|
|
6994
|
+
paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
|
|
6995
|
+
prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
|
|
6996
|
+
onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
|
|
6997
|
+
namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
|
|
6998
|
+
seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
|
|
6999
|
+
};
|
|
7000
|
+
function chapterEmailEvents(chapter) {
|
|
7001
|
+
const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
|
|
7002
|
+
label: key3,
|
|
7003
|
+
trigger: "Administrator sends a message",
|
|
7004
|
+
audience: "Selected contact",
|
|
7005
|
+
variables: spec.vars
|
|
7006
|
+
}])) : CHAPTER_EMAIL_EVENTS;
|
|
7007
|
+
return { ...base, ...chapter.config.emails?.events };
|
|
7008
|
+
}
|
|
7009
|
+
|
|
6717
7010
|
// src/email.ts
|
|
6718
7011
|
function render(template, vars) {
|
|
6719
7012
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
|
|
@@ -6737,19 +7030,158 @@ function isAlreadySent(priorRows) {
|
|
|
6737
7030
|
function planDelivery(input) {
|
|
6738
7031
|
const tpl = input.group.emailTemplates?.[input.template];
|
|
6739
7032
|
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
6740
|
-
if (tpl.enabled
|
|
7033
|
+
if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
|
|
7034
|
+
if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
|
|
7035
|
+
if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
|
|
6741
7036
|
const vars = groupVars(input.group, input.vars);
|
|
7037
|
+
if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
|
|
7038
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
7039
|
+
const issues = validateEmailCopy(tpl, {
|
|
7040
|
+
allowedVariables: event2?.variables,
|
|
7041
|
+
optionalVariables: event2?.optionalVariables,
|
|
7042
|
+
requiredVariables: event2?.requiresTier ? ["tierName"] : [],
|
|
7043
|
+
values: vars,
|
|
7044
|
+
tierId: vars.tierId
|
|
7045
|
+
});
|
|
7046
|
+
if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
|
|
6742
7047
|
const isProd = input.envName === "prod";
|
|
6743
7048
|
const redirect = !isProd && !!input.group.debugEmail;
|
|
6744
7049
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
6745
7050
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
6746
|
-
const
|
|
7051
|
+
const renderedSubject = render(tpl.subject, vars);
|
|
7052
|
+
const renderedText = render(tpl.text, vars);
|
|
7053
|
+
if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
|
|
7054
|
+
if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
|
|
7055
|
+
const subject = (redirect ? "[dev] " : "") + renderedSubject;
|
|
6747
7056
|
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
6748
7057
|
|
|
6749
|
-
` +
|
|
7058
|
+
` + renderedText : renderedText;
|
|
6750
7059
|
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
6751
7060
|
}
|
|
6752
7061
|
|
|
7062
|
+
// src/email-readiness.ts
|
|
7063
|
+
import { isEmailTierName, validateEmailCopy as validateEmailCopy2 } from "@odla-ai/crm";
|
|
7064
|
+
function validateEmailReadiness(input) {
|
|
7065
|
+
const issues = [];
|
|
7066
|
+
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
7067
|
+
issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
|
|
7068
|
+
}
|
|
7069
|
+
if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
|
|
7070
|
+
for (const tier of input.tiers ?? []) {
|
|
7071
|
+
if (!isEmailTierName(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
|
|
7072
|
+
}
|
|
7073
|
+
const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => isEmailTierName(tier.name, tier.id));
|
|
7074
|
+
if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
|
|
7075
|
+
const events2 = Object.entries(input.events).map(([key3, event2]) => {
|
|
7076
|
+
const template = input.templates[key3];
|
|
7077
|
+
const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : validateEmailCopy2(template, { allowedVariables: event2.variables });
|
|
7078
|
+
if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
|
|
7079
|
+
if (template) {
|
|
7080
|
+
const used = [...`${template.subject ?? ""}
|
|
7081
|
+
${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
7082
|
+
for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
|
|
7083
|
+
problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
7087
|
+
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
7088
|
+
return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
|
|
7089
|
+
});
|
|
7090
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
// src/notify.ts
|
|
7094
|
+
import { validateEmailCopy as validateEmailCopy3 } from "@odla-ai/crm";
|
|
7095
|
+
|
|
7096
|
+
// src/email-event-values.ts
|
|
7097
|
+
import { isEmailTierName as isEmailTierName2 } from "@odla-ai/crm";
|
|
7098
|
+
|
|
7099
|
+
// src/tiers-store.ts
|
|
7100
|
+
function rowToTier(row2, groupId) {
|
|
7101
|
+
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || typeof row2.id !== "string" || !row2.id || row2.groupId !== void 0 && row2.groupId !== groupId || !Number.isSafeInteger(row2.priceCents) || Number(row2.priceCents) < 0 || row2.currency !== void 0 && (typeof row2.currency !== "string" || !/^[a-z]{3}$/.test(row2.currency)) || row2.interval !== void 0 && row2.interval !== "month" && row2.interval !== "year") {
|
|
7102
|
+
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
7103
|
+
}
|
|
7104
|
+
return {
|
|
7105
|
+
id: String(row2.id ?? ""),
|
|
7106
|
+
groupId: String(row2.groupId ?? groupId),
|
|
7107
|
+
name: String(row2.name ?? ""),
|
|
7108
|
+
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
7109
|
+
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
7110
|
+
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
7111
|
+
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
7112
|
+
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
7113
|
+
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
7114
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
7115
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
7116
|
+
active: row2.active !== false
|
|
7117
|
+
};
|
|
7118
|
+
}
|
|
7119
|
+
async function loadTiers(db, group) {
|
|
7120
|
+
const groupId = String(group.id ?? "");
|
|
7121
|
+
let rows3;
|
|
7122
|
+
try {
|
|
7123
|
+
rows3 = (await db.query({
|
|
7124
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
7125
|
+
})).tiers;
|
|
7126
|
+
} catch {
|
|
7127
|
+
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
7128
|
+
}
|
|
7129
|
+
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
7130
|
+
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
7131
|
+
return resolveTiers(group, list);
|
|
7132
|
+
}
|
|
7133
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
7134
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
7135
|
+
}
|
|
7136
|
+
|
|
7137
|
+
// src/notification-variables.ts
|
|
7138
|
+
async function frozenCheckoutVariables(db, app) {
|
|
7139
|
+
const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
|
|
7140
|
+
const offer = intent?.membershipOffer, contract = offer?.contract;
|
|
7141
|
+
const offering = contract?.offering, quote = intent?.paymentQuote;
|
|
7142
|
+
const amount = contract?.dueTodayCents ?? intent?.priceCents;
|
|
7143
|
+
const currency = offering?.currency ?? intent?.currency;
|
|
7144
|
+
let formatted = "";
|
|
7145
|
+
if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
|
|
7146
|
+
formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
|
|
7147
|
+
}
|
|
7148
|
+
return {
|
|
7149
|
+
paymentAmount: formatted,
|
|
7150
|
+
paymentAmountSummary: formatted ? ` of ${formatted}` : "",
|
|
7151
|
+
tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
|
|
7152
|
+
};
|
|
7153
|
+
}
|
|
7154
|
+
async function catalogTierName(db, group, app) {
|
|
7155
|
+
try {
|
|
7156
|
+
return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
|
|
7157
|
+
} catch {
|
|
7158
|
+
return "";
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
async function checkoutVariables(db, group, app) {
|
|
7162
|
+
const frozen = await frozenCheckoutVariables(db, app);
|
|
7163
|
+
return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// src/email-event-values.ts
|
|
7167
|
+
async function emailTierVariables(db, input) {
|
|
7168
|
+
try {
|
|
7169
|
+
const [apps, groups2] = await Promise.all([
|
|
7170
|
+
db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
|
|
7171
|
+
db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
|
|
7172
|
+
]);
|
|
7173
|
+
const app = apps.applications?.[0], group = groups2.groups?.[0];
|
|
7174
|
+
if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
|
|
7175
|
+
const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
|
|
7176
|
+
if (!tierId) return { tierName: "", tier: "" };
|
|
7177
|
+
const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
|
|
7178
|
+
const tierName = isEmailTierName2(name, tierId) ? name.trim() : "";
|
|
7179
|
+
return { tierName, tier: tierName, tierId };
|
|
7180
|
+
} catch {
|
|
7181
|
+
return { tierName: "", tier: "" };
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
|
|
6753
7185
|
// src/notify.ts
|
|
6754
7186
|
function fromAddress(configured, name) {
|
|
6755
7187
|
const display = name?.trim();
|
|
@@ -6767,24 +7199,27 @@ async function sendTemplated(deps, input) {
|
|
|
6767
7199
|
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
6768
7200
|
const override = deps.debugEmail?.trim();
|
|
6769
7201
|
const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
|
|
7202
|
+
const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
|
|
7203
|
+
const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
|
|
6770
7204
|
const decision = planDelivery({
|
|
6771
7205
|
envName: deps.envName,
|
|
6772
7206
|
group,
|
|
6773
7207
|
template: input.template,
|
|
6774
7208
|
to: input.to,
|
|
6775
|
-
vars
|
|
7209
|
+
vars,
|
|
6776
7210
|
cloudflareReady,
|
|
6777
|
-
force: input.force
|
|
7211
|
+
force: input.force,
|
|
7212
|
+
event: event2
|
|
6778
7213
|
});
|
|
6779
7214
|
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
6780
7215
|
const previous = prior[0];
|
|
6781
7216
|
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" };
|
|
6782
7217
|
const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
|
|
6783
|
-
const
|
|
7218
|
+
const id11 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
6784
7219
|
const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
|
|
6785
7220
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
|
|
6786
7221
|
const row2 = {
|
|
6787
|
-
id:
|
|
7222
|
+
id: id11,
|
|
6788
7223
|
groupId: input.group.id,
|
|
6789
7224
|
to: decision.to,
|
|
6790
7225
|
template: input.template,
|
|
@@ -6799,16 +7234,18 @@ async function sendTemplated(deps, input) {
|
|
|
6799
7234
|
error: decision.transport === "log-only" ? "" : "delivery-pending",
|
|
6800
7235
|
...input.applicationId ? { applicationId: input.applicationId } : {}
|
|
6801
7236
|
};
|
|
7237
|
+
if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
|
|
7238
|
+
if (validateEmailCopy3({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
|
|
6802
7239
|
try {
|
|
6803
|
-
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
6804
|
-
mutationId: `email-claim:${
|
|
7240
|
+
const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: row2 }], {
|
|
7241
|
+
mutationId: `email-claim:${id11}:${attempt}`,
|
|
6805
7242
|
guards: [previous ? {
|
|
6806
7243
|
ns: "emailLog",
|
|
6807
|
-
id:
|
|
7244
|
+
id: id11,
|
|
6808
7245
|
exists: true,
|
|
6809
7246
|
equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
|
|
6810
7247
|
...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
|
|
6811
|
-
} : { ns: "emailLog", id:
|
|
7248
|
+
} : { ns: "emailLog", id: id11, exists: false }]
|
|
6812
7249
|
});
|
|
6813
7250
|
if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
|
|
6814
7251
|
} catch (error) {
|
|
@@ -6828,16 +7265,16 @@ async function sendTemplated(deps, input) {
|
|
|
6828
7265
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|
|
6829
7266
|
messageId = response.messageId;
|
|
6830
7267
|
} catch {
|
|
6831
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
6832
|
-
guards: [{ ns: "emailLog", id:
|
|
7268
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
|
|
7269
|
+
guards: [{ ns: "emailLog", id: id11, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
|
|
6833
7270
|
});
|
|
6834
7271
|
return { sent: false, reason: "delivery-uncertain" };
|
|
6835
7272
|
}
|
|
6836
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
6837
|
-
mutationId: `email-confirmed:${
|
|
7273
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
|
|
7274
|
+
mutationId: `email-confirmed:${id11}:${attempt}`,
|
|
6838
7275
|
guards: [{
|
|
6839
7276
|
ns: "emailLog",
|
|
6840
|
-
id:
|
|
7277
|
+
id: id11,
|
|
6841
7278
|
exists: true,
|
|
6842
7279
|
equals: { deliveryState: "sending", deliveryAttempt: attempt }
|
|
6843
7280
|
}]
|
|
@@ -6952,35 +7389,35 @@ function findGiftRef(obj) {
|
|
|
6952
7389
|
...chapterId ? { chapterId } : {}
|
|
6953
7390
|
};
|
|
6954
7391
|
}
|
|
6955
|
-
function normalizeWebhookEvent(
|
|
6956
|
-
const obj =
|
|
6957
|
-
if (
|
|
7392
|
+
function normalizeWebhookEvent(event2, options = {}) {
|
|
7393
|
+
const obj = event2.data?.object ?? {};
|
|
7394
|
+
if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
|
|
6958
7395
|
const gift = findGiftRef(obj);
|
|
6959
7396
|
if (gift.giftIntentId) {
|
|
6960
|
-
const { giftIntentId, providerObjectId, ...
|
|
6961
|
-
if (
|
|
6962
|
-
return { kind: "gift_paid", giftIntentId, providerObjectId, ...
|
|
7397
|
+
const { giftIntentId, providerObjectId, ...identity2 } = gift;
|
|
7398
|
+
if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
7399
|
+
return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
|
|
6963
7400
|
}
|
|
6964
|
-
if (
|
|
6965
|
-
if (
|
|
6966
|
-
return { kind: "ignored", type:
|
|
7401
|
+
if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
|
|
7402
|
+
if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
|
|
7403
|
+
return { kind: "ignored", type: event2.type };
|
|
6967
7404
|
}
|
|
6968
7405
|
const ref = findApplicationRef(obj);
|
|
6969
7406
|
const seatId = obj.metadata?.seatId;
|
|
6970
7407
|
if (typeof seatId === "string" && seatId) {
|
|
6971
|
-
if (!options.requireProviderPayment) return { kind: "ignored", type:
|
|
6972
|
-
if (
|
|
6973
|
-
if (
|
|
6974
|
-
return { kind: "ignored", type:
|
|
7408
|
+
if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7409
|
+
if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
|
|
7410
|
+
if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
|
|
7411
|
+
return { kind: "ignored", type: event2.type };
|
|
6975
7412
|
}
|
|
6976
|
-
if (
|
|
6977
|
-
switch (
|
|
7413
|
+
if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
|
|
7414
|
+
switch (event2.type) {
|
|
6978
7415
|
case "invoice.payment_failed":
|
|
6979
|
-
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type:
|
|
7416
|
+
return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
|
|
6980
7417
|
case "invoice.paid":
|
|
6981
7418
|
case "invoice.payment_succeeded": {
|
|
6982
7419
|
if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
|
|
6983
|
-
return { kind: "ignored", type:
|
|
7420
|
+
return { kind: "ignored", type: event2.type };
|
|
6984
7421
|
}
|
|
6985
7422
|
const renewalAt = membershipInvoicePeriodEnd(obj);
|
|
6986
7423
|
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
@@ -6994,11 +7431,11 @@ function normalizeWebhookEvent(event, options = {}) {
|
|
|
6994
7431
|
case "charge.refunded":
|
|
6995
7432
|
return { kind: "refunded", ...ref };
|
|
6996
7433
|
case "customer.subscription.updated":
|
|
6997
|
-
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type:
|
|
7434
|
+
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
|
|
6998
7435
|
case "customer.subscription.deleted":
|
|
6999
7436
|
return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
|
|
7000
7437
|
default:
|
|
7001
|
-
return { kind: "ignored", type:
|
|
7438
|
+
return { kind: "ignored", type: event2.type };
|
|
7002
7439
|
}
|
|
7003
7440
|
}
|
|
7004
7441
|
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
@@ -7095,14 +7532,14 @@ function validateAdmissionGrantDocument(input) {
|
|
|
7095
7532
|
const seenEmails = /* @__PURE__ */ new Set();
|
|
7096
7533
|
for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
7097
7534
|
const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
|
|
7098
|
-
const
|
|
7535
|
+
const id11 = typeof grant.id === "string" ? grant.id : "";
|
|
7099
7536
|
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
7100
7537
|
const normalized = normalizeAdmissionEmail(email);
|
|
7101
7538
|
const decision = grant.decision;
|
|
7102
7539
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
7103
7540
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
7104
|
-
if (!ID3.test(
|
|
7105
|
-
if (seenIds.has(
|
|
7541
|
+
if (!ID3.test(id11)) errors.push(`document.grants[${index2}].id must be an identifier`);
|
|
7542
|
+
if (seenIds.has(id11)) errors.push(`document.grants repeats id "${id11}"`);
|
|
7106
7543
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
|
|
7107
7544
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
7108
7545
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -7112,10 +7549,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
7112
7549
|
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
7113
7550
|
errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
|
|
7114
7551
|
}
|
|
7115
|
-
seenIds.add(
|
|
7552
|
+
seenIds.add(id11);
|
|
7116
7553
|
seenEmails.add(normalized);
|
|
7117
7554
|
grants.push({
|
|
7118
|
-
id:
|
|
7555
|
+
id: id11,
|
|
7119
7556
|
email,
|
|
7120
7557
|
decision,
|
|
7121
7558
|
...note ? { note } : {},
|
|
@@ -7187,8 +7624,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
7187
7624
|
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
7188
7625
|
const ops = [];
|
|
7189
7626
|
for (const row2 of current) {
|
|
7190
|
-
const
|
|
7191
|
-
if (
|
|
7627
|
+
const id11 = String(row2.id ?? "");
|
|
7628
|
+
if (id11 && !incomingIds.has(id11)) ops.push({ t: "update", ns: "admissionGrants", id: id11, attrs: {
|
|
7192
7629
|
active: false,
|
|
7193
7630
|
revokedAt: receivedAt,
|
|
7194
7631
|
revokedBy: document.source.actor,
|
|
@@ -7197,10 +7634,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
7197
7634
|
} });
|
|
7198
7635
|
}
|
|
7199
7636
|
for (const grant of document.grants) {
|
|
7200
|
-
const
|
|
7201
|
-
const existing = current.find((row2) => row2.id ===
|
|
7202
|
-
ops.push({ t: "update", ns: "admissionGrants", id:
|
|
7203
|
-
id:
|
|
7637
|
+
const id11 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
7638
|
+
const existing = current.find((row2) => row2.id === id11);
|
|
7639
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id11, attrs: {
|
|
7640
|
+
id: id11,
|
|
7204
7641
|
sourceGrantId: grant.id,
|
|
7205
7642
|
sourceScope: "parent",
|
|
7206
7643
|
sourceId: document.source.appId,
|
|
@@ -7221,7 +7658,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
|
7221
7658
|
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
7222
7659
|
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
7223
7660
|
];
|
|
7224
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id:
|
|
7661
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id11, attrs: retract });
|
|
7225
7662
|
}
|
|
7226
7663
|
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
7227
7664
|
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
@@ -7292,10 +7729,10 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
7292
7729
|
}
|
|
7293
7730
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
7294
7731
|
for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
|
|
7295
|
-
const
|
|
7296
|
-
if (
|
|
7297
|
-
if (ids2.has(
|
|
7298
|
-
ids2.add(
|
|
7732
|
+
const id11 = text4(item.id, `${path}.id`, errors, 80);
|
|
7733
|
+
if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
7734
|
+
if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
7735
|
+
ids2.add(id11);
|
|
7299
7736
|
const targetRaw = record3(item.target);
|
|
7300
7737
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
7301
7738
|
if (targetRaw?.type === "tier") {
|
|
@@ -7321,7 +7758,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
7321
7758
|
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
7322
7759
|
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
7323
7760
|
offers.push({
|
|
7324
|
-
id:
|
|
7761
|
+
id: id11,
|
|
7325
7762
|
target,
|
|
7326
7763
|
billingMode: item.billingMode,
|
|
7327
7764
|
stripePriceId,
|
|
@@ -7370,10 +7807,10 @@ async function ensureProjectedPerson(deps, input, options = {}) {
|
|
|
7370
7807
|
const existing = await find();
|
|
7371
7808
|
if (existing) return { record: existing, created: false };
|
|
7372
7809
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
|
|
7373
|
-
const
|
|
7810
|
+
const id11 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
7374
7811
|
try {
|
|
7375
7812
|
const created = await createRecord({ ...deps, db: deps.db }, {
|
|
7376
|
-
id:
|
|
7813
|
+
id: id11,
|
|
7377
7814
|
type: "person",
|
|
7378
7815
|
input: { ...input, email },
|
|
7379
7816
|
...options
|
|
@@ -7796,9 +8233,9 @@ function subAnnualCents(sub) {
|
|
|
7796
8233
|
}
|
|
7797
8234
|
|
|
7798
8235
|
// src/payments-stripe.ts
|
|
7799
|
-
import { stripeRequest as
|
|
8236
|
+
import { stripeRequest as stripeRequest11, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
7800
8237
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7801
|
-
return
|
|
8238
|
+
return stripeRequest11(
|
|
7802
8239
|
{ secretKey: sk },
|
|
7803
8240
|
method,
|
|
7804
8241
|
path,
|
|
@@ -7876,8 +8313,8 @@ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fet
|
|
|
7876
8313
|
return res.ok;
|
|
7877
8314
|
}
|
|
7878
8315
|
async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
|
|
7879
|
-
const
|
|
7880
|
-
return
|
|
8316
|
+
const id11 = await existingClerkUserId(secretKey, email, fetchImpl);
|
|
8317
|
+
return id11 ? updateClerkUserMetadata(secretKey, id11, input, fetchImpl) : false;
|
|
7881
8318
|
}
|
|
7882
8319
|
async function existingClerkUserId(secretKey, email, fetchImpl) {
|
|
7883
8320
|
const auth = { authorization: `Bearer ${secretKey}` };
|
|
@@ -7950,10 +8387,10 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch, source =
|
|
|
7950
8387
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
7951
8388
|
return user ? toRecord(user, source) : null;
|
|
7952
8389
|
}
|
|
7953
|
-
async function clerkGetUser(secretKey,
|
|
7954
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
8390
|
+
async function clerkGetUser(secretKey, id11, fetchImpl = fetch, source = "public") {
|
|
8391
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id11)}`, secretKey, fetchImpl).catch(() => null);
|
|
7955
8392
|
const record4 = data ? toRecord(data, source) : null;
|
|
7956
|
-
return record4?.id ===
|
|
8393
|
+
return record4?.id === id11 ? record4 : null;
|
|
7957
8394
|
}
|
|
7958
8395
|
async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
7959
8396
|
const out = [];
|
|
@@ -7969,8 +8406,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
|
|
|
7969
8406
|
}
|
|
7970
8407
|
return out;
|
|
7971
8408
|
}
|
|
7972
|
-
async function clerkSetRole(secretKey,
|
|
7973
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
8409
|
+
async function clerkSetRole(secretKey, id11, role, fetchImpl = fetch, source = "public") {
|
|
8410
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id11)}/metadata`, {
|
|
7974
8411
|
method: "PATCH",
|
|
7975
8412
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
7976
8413
|
redirect: "manual",
|
|
@@ -7987,7 +8424,7 @@ function planPrivateRoleMigration(input) {
|
|
|
7987
8424
|
if (input.environment !== "dev") blockers.push("dev_only");
|
|
7988
8425
|
if (!input.ladder.length || new Set(input.ladder).size !== input.ladder.length || input.ladder.some((role) => !role || role === "super_admin")) blockers.push("invalid_role_ladder");
|
|
7989
8426
|
const expected = new Set(input.expectedRuntimes);
|
|
7990
|
-
if (!expected.size || expected.size !== input.expectedRuntimes.length || input.expectedRuntimes.some((
|
|
8427
|
+
if (!expected.size || expected.size !== input.expectedRuntimes.length || input.expectedRuntimes.some((id11) => !id11)) blockers.push("runtime_inventory_required");
|
|
7991
8428
|
if (input.runtimes.length !== expected.size || new Set(input.runtimes.map((r) => r.runtime)).size !== expected.size || input.runtimes.some((r) => !expected.has(r.runtime))) blockers.push("runtime_inventory_mismatch");
|
|
7992
8429
|
for (const runtime of input.expectedRuntimes) {
|
|
7993
8430
|
const proof = input.runtimes.find((r) => r.runtime === runtime);
|
|
@@ -8017,7 +8454,7 @@ function planPrivateRoleMigration(input) {
|
|
|
8017
8454
|
if (privateRole === role && !Object.hasOwn(user.publicMetadata, "role")) alreadyCurrent++;
|
|
8018
8455
|
else changes.push({ userId: user.id, role, publicRole, privateRole });
|
|
8019
8456
|
}
|
|
8020
|
-
if (!input.protectedSuperAdminUserIds.some((
|
|
8457
|
+
if (!input.protectedSuperAdminUserIds.some((id11) => ids2.has(id11))) blockers.push("protected_operator_grant_required");
|
|
8021
8458
|
return { ready: blockers.length === 0, blockers, changes, alreadyCurrent, totalUsers: input.users.length };
|
|
8022
8459
|
}
|
|
8023
8460
|
|
|
@@ -8376,44 +8813,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
8376
8813
|
};
|
|
8377
8814
|
}
|
|
8378
8815
|
|
|
8379
|
-
// src/tiers-store.ts
|
|
8380
|
-
function rowToTier(row2, groupId) {
|
|
8381
|
-
if (!row2 || typeof row2 !== "object" || Array.isArray(row2) || typeof row2.id !== "string" || !row2.id || row2.groupId !== void 0 && row2.groupId !== groupId || !Number.isSafeInteger(row2.priceCents) || Number(row2.priceCents) < 0 || row2.currency !== void 0 && (typeof row2.currency !== "string" || !/^[a-z]{3}$/.test(row2.currency)) || row2.interval !== void 0 && row2.interval !== "month" && row2.interval !== "year") {
|
|
8382
|
-
throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
|
|
8383
|
-
}
|
|
8384
|
-
return {
|
|
8385
|
-
id: String(row2.id ?? ""),
|
|
8386
|
-
groupId: String(row2.groupId ?? groupId),
|
|
8387
|
-
name: String(row2.name ?? ""),
|
|
8388
|
-
priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
|
|
8389
|
-
...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
|
|
8390
|
-
...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
|
|
8391
|
-
...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
|
|
8392
|
-
...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
|
|
8393
|
-
sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
|
|
8394
|
-
// A row that never set `active` is offered; only an explicit false retires
|
|
8395
|
-
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
8396
|
-
active: row2.active !== false
|
|
8397
|
-
};
|
|
8398
|
-
}
|
|
8399
|
-
async function loadTiers(db, group) {
|
|
8400
|
-
const groupId = String(group.id ?? "");
|
|
8401
|
-
let rows3;
|
|
8402
|
-
try {
|
|
8403
|
-
rows3 = (await db.query({
|
|
8404
|
-
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
8405
|
-
})).tiers;
|
|
8406
|
-
} catch {
|
|
8407
|
-
throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
|
|
8408
|
-
}
|
|
8409
|
-
if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
|
|
8410
|
-
const list = rows3.map((row2) => rowToTier(row2, groupId));
|
|
8411
|
-
return resolveTiers(group, list);
|
|
8412
|
-
}
|
|
8413
|
-
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
8414
|
-
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
8415
|
-
}
|
|
8416
|
-
|
|
8417
8816
|
// src/signup-control-contract.ts
|
|
8418
8817
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
8419
8818
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
@@ -8633,10 +9032,10 @@ function tiersOf(value, errors) {
|
|
|
8633
9032
|
return [];
|
|
8634
9033
|
}
|
|
8635
9034
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
8636
|
-
const
|
|
8637
|
-
if (
|
|
8638
|
-
if (seen.has(
|
|
8639
|
-
seen.add(
|
|
9035
|
+
const id11 = text5(item.id, `${path}.id`, errors, 80);
|
|
9036
|
+
if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
9037
|
+
if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
|
|
9038
|
+
seen.add(id11);
|
|
8640
9039
|
const name = text5(item.name, `${path}.name`, errors, 160);
|
|
8641
9040
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
8642
9041
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
@@ -8657,7 +9056,7 @@ function tiersOf(value, errors) {
|
|
|
8657
9056
|
}
|
|
8658
9057
|
const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
8659
9058
|
return [{
|
|
8660
|
-
id:
|
|
9059
|
+
id: id11,
|
|
8661
9060
|
name,
|
|
8662
9061
|
available: item.available === true,
|
|
8663
9062
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -8719,17 +9118,17 @@ function policyOf(value, errors, tiers2) {
|
|
|
8719
9118
|
if (input.tierPolicies !== void 0) {
|
|
8720
9119
|
const overrides = recordOf(input.tierPolicies);
|
|
8721
9120
|
if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
|
|
8722
|
-
else for (const [
|
|
8723
|
-
const path = `document.policy.tierPolicies.${
|
|
8724
|
-
const
|
|
8725
|
-
if (!tiers2.some((tier) => tier.id ===
|
|
9121
|
+
else for (const [id11, raw2] of Object.entries(overrides)) {
|
|
9122
|
+
const path = `document.policy.tierPolicies.${id11}`;
|
|
9123
|
+
const policy2 = recordOf(raw2);
|
|
9124
|
+
if (!tiers2.some((tier) => tier.id === id11) || !policy2) {
|
|
8726
9125
|
errors.push(`${path} must name a declared tier and policy`);
|
|
8727
9126
|
continue;
|
|
8728
9127
|
}
|
|
8729
|
-
unknownKeys3(
|
|
8730
|
-
tierPolicies[
|
|
8731
|
-
refundPolicyText: text5(
|
|
8732
|
-
...
|
|
9128
|
+
unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
|
|
9129
|
+
tierPolicies[id11] = {
|
|
9130
|
+
refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
|
|
9131
|
+
...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
|
|
8733
9132
|
};
|
|
8734
9133
|
}
|
|
8735
9134
|
}
|
|
@@ -8784,7 +9183,7 @@ function validateSignupControlDocument(input) {
|
|
|
8784
9183
|
errors.push("document.membershipPage.tiers must follow the available tier display order");
|
|
8785
9184
|
}
|
|
8786
9185
|
const application = applicationOf(root.application, errors);
|
|
8787
|
-
const
|
|
9186
|
+
const policy2 = policyOf(root.policy, errors, tiers2);
|
|
8788
9187
|
let scheduling;
|
|
8789
9188
|
if (root.scheduling !== void 0) {
|
|
8790
9189
|
const checked = validateScheduling(root.scheduling);
|
|
@@ -8804,7 +9203,7 @@ function validateSignupControlDocument(input) {
|
|
|
8804
9203
|
tiers: tiers2,
|
|
8805
9204
|
membershipPage: page.value,
|
|
8806
9205
|
...application ? { application } : {},
|
|
8807
|
-
...
|
|
9206
|
+
...policy2 ? { policy: policy2 } : {},
|
|
8808
9207
|
...scheduling ? { scheduling } : {},
|
|
8809
9208
|
...theme ? { theme } : {},
|
|
8810
9209
|
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
@@ -9052,7 +9451,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
9052
9451
|
id: chapterId,
|
|
9053
9452
|
reason: "provider_identity",
|
|
9054
9453
|
paths: [
|
|
9055
|
-
...paidTierIds.map((
|
|
9454
|
+
...paidTierIds.map((id11) => `tiers[${id11}].stripePriceId`),
|
|
9056
9455
|
"groups.stripePublishableKey",
|
|
9057
9456
|
"applications.stripeCustomerId",
|
|
9058
9457
|
"applications.stripeSubscriptionId",
|
|
@@ -9094,9 +9493,9 @@ async function exportChapterPromotion(input) {
|
|
|
9094
9493
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
9095
9494
|
receiptDigests: [input.digest]
|
|
9096
9495
|
};
|
|
9097
|
-
const artifact = (kind,
|
|
9496
|
+
const artifact = (kind, id11, content) => ({
|
|
9098
9497
|
kind,
|
|
9099
|
-
id:
|
|
9498
|
+
id: id11,
|
|
9100
9499
|
sourceRevision: revision,
|
|
9101
9500
|
provenance: promotionProvenance,
|
|
9102
9501
|
content: canonicalPromotionValue(content)
|
|
@@ -9141,7 +9540,7 @@ var MEMBER_SYSTEM_RECORD_OWNERS = {
|
|
|
9141
9540
|
provider: "originating-provider",
|
|
9142
9541
|
protectedRoles: "odla-platform"
|
|
9143
9542
|
};
|
|
9144
|
-
var rows2 = (group) => group.fields.map((
|
|
9543
|
+
var rows2 = (group) => group.fields.map((id11) => ({ ...group, id: id11 }));
|
|
9145
9544
|
var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
|
|
9146
9545
|
...rows2({
|
|
9147
9546
|
scope: "network",
|
|
@@ -9519,23 +9918,23 @@ function memberSystemAvailability(input) {
|
|
|
9519
9918
|
...!input.bnfFrontendReachable ? { reason: "bnf_frontend_unavailable" } : {}
|
|
9520
9919
|
};
|
|
9521
9920
|
}
|
|
9522
|
-
function classifyMemberStripeEvent(
|
|
9921
|
+
function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
|
|
9523
9922
|
const keys2 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
|
|
9524
|
-
const supplied = keys2.filter((key3) =>
|
|
9525
|
-
if (supplied.some((key3) =>
|
|
9923
|
+
const supplied = keys2.filter((key3) => identity2[key3] !== void 0);
|
|
9924
|
+
if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
|
|
9526
9925
|
if (supplied.length === keys2.length) return "apply";
|
|
9527
9926
|
return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
|
|
9528
9927
|
}
|
|
9529
9928
|
function planMemberSystemEventReplay(events2) {
|
|
9530
9929
|
const seen = /* @__PURE__ */ new Map();
|
|
9531
9930
|
const plan = { accepted: [], replayed: [], collisions: [] };
|
|
9532
|
-
for (const
|
|
9533
|
-
const digest = seen.get(
|
|
9931
|
+
for (const event2 of events2) {
|
|
9932
|
+
const digest = seen.get(event2.id);
|
|
9534
9933
|
if (digest === void 0) {
|
|
9535
|
-
seen.set(
|
|
9536
|
-
plan.accepted.push(
|
|
9537
|
-
} else if (digest ===
|
|
9538
|
-
else plan.collisions.push(
|
|
9934
|
+
seen.set(event2.id, event2.digest);
|
|
9935
|
+
plan.accepted.push(event2.id);
|
|
9936
|
+
} else if (digest === event2.digest) plan.replayed.push(event2.id);
|
|
9937
|
+
else plan.collisions.push(event2.id);
|
|
9539
9938
|
}
|
|
9540
9939
|
return plan;
|
|
9541
9940
|
}
|
|
@@ -9615,6 +10014,7 @@ export {
|
|
|
9615
10014
|
BUILDER_LIBRARY_MANIFEST_PATH,
|
|
9616
10015
|
BUILDER_LIBRARY_PROMOTE_PATH,
|
|
9617
10016
|
BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
10017
|
+
CHAPTER_EMAIL_EVENTS,
|
|
9618
10018
|
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
9619
10019
|
CHAPTER_PROMOTION_ADAPTER,
|
|
9620
10020
|
DEFAULT_CHAPTER_COPY,
|
|
@@ -9671,6 +10071,7 @@ export {
|
|
|
9671
10071
|
canonicalSignupControl,
|
|
9672
10072
|
chapterBrandFromTokens,
|
|
9673
10073
|
chapterDb,
|
|
10074
|
+
chapterEmailEvents,
|
|
9674
10075
|
chapterFlowFieldContract,
|
|
9675
10076
|
clampArray,
|
|
9676
10077
|
classifyMemberStripeEvent,
|
|
@@ -9850,6 +10251,7 @@ export {
|
|
|
9850
10251
|
updateClerkUserMetadataByEmail,
|
|
9851
10252
|
validateAdmissionGrantDocument,
|
|
9852
10253
|
validateBuilderLibraryDocument,
|
|
10254
|
+
validateEmailReadiness,
|
|
9853
10255
|
validateGiftOfferCatalog,
|
|
9854
10256
|
validateMemberContentDocument,
|
|
9855
10257
|
validateMemberSystemAuthority,
|