@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/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 id10 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
126
- const row2 = { id: id10, status: chapter.pipeline.initial, createdAt: opts.now };
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: id10, attrs: row2 }
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: id10
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: id10, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
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 policy = config.policy ?? {};
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: policy.disclaimerText ?? "",
1161
- refundPolicyText: policy.refundPolicyText ?? "",
1162
- merchantDisclosureText: policy.merchantDisclosureText ?? "",
1163
- trustCopy: policy.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 (policy.commitmentText) row2.commitmentText = policy.commitmentText;
1170
- if (policy.normsText) row2.normsText = policy.normsText;
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(id10) {
1704
- const text6 = id10.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1705
- return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id10;
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(id10, def) {
1719
+ function applicationType(id11, def) {
1715
1720
  if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
1716
- return id10 === "email" ? "email" : id10 === "linkedin" ? "url" : "string";
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((id10, order2) => {
1721
- const def = defs[id10];
1722
- const conditions = application.conditions[id10];
1723
- const type = applicationType(id10, def);
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: id10,
1726
- label: def?.label ?? APPLICATION_LABELS[id10] ?? humanize(id10),
1730
+ id: id11,
1731
+ label: def?.label ?? APPLICATION_LABELS[id11] ?? humanize(id11),
1727
1732
  type,
1728
- required: application.required.includes(id10),
1733
+ required: application.required.includes(id11),
1729
1734
  order: order2,
1730
- maxLength: application.maxLen[id10] ?? application.defaultMaxLen,
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((id10, order2) => {
1741
- const field2 = def.fields[id10];
1745
+ return [...formation.required, ...formation.optional].map((id11, order2) => {
1746
+ const field2 = def.fields[id11];
1742
1747
  return {
1743
- id: id10,
1744
- label: field2.label ?? humanize(id10),
1748
+ id: id11,
1749
+ label: field2.label ?? humanize(id11),
1745
1750
  type: field2.type,
1746
- required: formation.required.includes(id10),
1751
+ required: formation.required.includes(id11),
1747
1752
  order: order2,
1748
- maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
1753
+ maxLength: formation.maxLen[id11] ?? formation.defaultMaxLen,
1749
1754
  ...field2.options ? { options: [...field2.options] } : {},
1750
- ...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
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((id10) => !SLUG2.test(id10))) {
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((id10) => !targetIds.includes(id10));
2062
+ const unknown = chapterIds.filter((id11) => !targetIds.includes(id11));
2058
2063
  if (unknown.length) {
2059
- throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id10) => `"${id10}"`).join(", ")} are not configured network targets`);
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: id10, name } = config;
2183
- if (typeof id10 !== "string" || !SLUG7.test(id10)) {
2184
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id10)}`);
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: id10,
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 event = raw(input);
2480
- const eventId = providerId(event.id, "evt_") ?? "";
2481
- const eventType = text(event.type) ?? "";
2482
- const object2 = raw(raw(event.data).object);
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 identity = {};
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) identity[target] = 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(event.created) ?? 0,
2518
- livemode: event.livemode === true,
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 id10 = typeof object2.id === "string" ? object2.id : "invalid";
2711
+ const id11 = typeof object2.id === "string" ? object2.id : "invalid";
2707
2712
  return normalizeNetworkCommercialStripeEvent({
2708
- id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id10}`,
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}:${id10}`);
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 policy = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
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 (policy && policy.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
3375
- const founding = Boolean(policy && policy.status === "published" && offering.basePriceCents > 0 && policy.cohortEligibleSkuIds.includes(offering.baseSkuId) && now >= policy.qualifyingStartsAt && now < policy.qualifyingEndsAt && input.approvedFoundingCount < policy.softTarget);
3376
- const earned = founding ? policy : null;
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, id10) {
3423
- return (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
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 = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
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: policy,
3478
+ discountPolicy: policy2,
3474
3479
  approvedFoundingCount: Number(approvedFoundingCount),
3475
3480
  now: input.now,
3476
- couponId: await membershipDiscountCouponId(input, policy?.id)
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((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
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 policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3509
- if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy && policies.filter((item) => item.version === policy.version).length !== 1) reject("ambiguous_membership_policy");
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 = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
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: policy,
3528
+ discountPolicy: policy2,
3524
3529
  approvedFoundingCount: Number(approvedFoundingCount),
3525
3530
  now: input.now,
3526
- couponId: await membershipDiscountCouponId(input, policy?.id)
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", id10 = `network-commercial-exception:${eventId}:collision`;
3561
- const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3562
- if (!row2) return { ns, id: id10, exists: false };
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: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
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, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
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, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
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 id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
3596
+ const id11 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id11);
3592
3597
  requireMembershipScope(restart, target);
3593
- const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
3598
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
3594
3599
  requireMembershipScope(entitlement, target);
3595
- if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
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", id10, { status: "complete" }),
3613
- { t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
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: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3617
- { ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
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, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
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
- return { id: row2.id, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: row2.boundAt };
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 id10 = await membershipCheckoutBindingId(input);
3643
- const existing = await load2(db, "membershipOfferCheckout", id10);
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", id10.replace(/^checkout_/, "membership_"));
3649
- const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
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([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
3652
- mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
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: id10, exists: false },
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", id10);
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 !== offer.contract.dueTodayCents || fact.amountPaidCents !== offer.contract.dueTodayCents || Object.entries({
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 id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
3699
- let member = await membershipLoad(db, "networkMembership", id10);
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: id10,
3705
- membershipId: id10,
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: offer.contract.dueTodayCents,
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", id10, attrs),
3883
+ membershipUpdate("networkMembership", id11, attrs),
3740
3884
  membershipUpdate("networkEntitlement", entitlementId, {
3741
3885
  id: entitlementId,
3742
3886
  ...membershipScope(input),
3743
- membershipId: id10,
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
- ], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
3757
- ...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
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", id10);
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: id10,
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 id10 = await networkMembershipId(target);
3949
+ const id11 = await networkMembershipId(target);
3784
3950
  const state = await loadMembershipAuthorityState(db, target);
3785
- const member = await membershipLoad(db, "networkMembership", id10);
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:${id10}:${member.providerSubscriptionId}` }
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", id10, {
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: id10, exists: true, equals: { revision: member.revision } },
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: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
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 id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
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", id10);
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 !== id10 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
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: id10,
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", id10, {
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:${id10}`, guards: [
3910
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
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 id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
4010
- let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id10);
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: id10, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
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", id10, {
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", `${id10}:refund`, {
4026
- id: `${id10}:refund`,
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: id10,
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:${id10}`, guards: [
4038
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
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", id10);
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", id10);
4223
+ member = await membershipLoad(db, "networkMembership", id11);
4058
4224
  requireMembershipScope(member, input);
4059
- if (member.status === "refunded") return { membershipId: id10, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
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", id10, {
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", `${id10}:refund`, { status: "complete", updatedAt: now }),
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: id10,
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:${id10}`, guards: [
4093
- { ns: "networkMembership", id: id10, exists: true, equals: { status: "refund_pending", revision: member.revision } },
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 id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4122
- let seat = await membershipLoad(db, "networkSeat", id10);
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: id10,
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", id10, attrs),
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: id10,
4328
+ seatId: id11,
4163
4329
  recipientEmailNormalized: email,
4164
4330
  createdAt: now
4165
4331
  }),
4166
- membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
4167
- ], { mutationId: `named-seat-reserve:${id10}`, guards: [
4168
- { ns: "networkSeat", id: id10, exists: false },
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", id10);
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 id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
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", id10);
4463
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4298
4464
  requireMembershipScope(member, input);
4299
4465
  const receipt = () => ({
4300
- membershipId: id10,
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", `${id10}:primary`);
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", id10, {
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", `${id10}:primary`, {
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: id10, exists: true, equals: { revision: member.revision } },
4352
- { ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
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:${id10}:${input.eventId}`, guards });
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", id10);
4548
+ const saved = await membershipLoad(db, "networkMembership", id11);
4383
4549
  requireMembershipScope(saved, input);
4384
4550
  return {
4385
- membershipId: id10,
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 id10 = await networkMembershipId(input);
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", id10);
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: id10,
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", id10);
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", `${id10}:primary`);
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", id10, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
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: id10, exists: true, equals: { revision: member.revision } },
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", id10);
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 id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4535
- const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
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", id10);
4703
+ const previous = await membershipLoad(db, "networkMembership", id11);
4538
4704
  requireMembershipScope(previous, input);
4539
4705
  const receipt = {
4540
4706
  applicationId: input.applicationId,
4541
- membershipId: id10,
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", id10);
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: id10,
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", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
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: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
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", id10);
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", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
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: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
4588
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
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 id10 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id10);
4599
- if (!member) return { membershipId: id10, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
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", `${id10}:primary`);
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 === id10 && entitlement.kind === "primary" && Number.isSafeInteger(endsAt) && endsAt > now && Number(member.termStartedAt) <= now;
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: id10,
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) return String(existing.id);
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 (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
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 stripeRequest8({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4992
- if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
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)) fail4();
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)) fail4();
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) fail4();
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) fail4();
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) fail4("named_seat_renewal_removal_unconfirmed");
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) fail4("named_seat_refund_history_incomplete");
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)) fail4();
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) fail4();
5020
- if (pendingCents) fail4("named_seat_refund_pending");
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)) fail4();
5030
- if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
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 id10 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id10);
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: id10,
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 checkout = {
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, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
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, checkout, secret, now);
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 identity = `${peer.appId}:${peer.runtime}`;
5404
- if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
5405
- identities.add(identity);
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
- }, options.now?.() ?? Date.now());
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 stripeRequest9 } from "@odla-ai/stripe";
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 stripeRequest9(
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 id10 = payment.id;
5880
- payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id10)}/cancel`, { cancellation_reason: "abandoned" });
5881
- if (payment.id !== id10) throw new Error("seat cancellation receipt mismatch");
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 id8 = () => attr8("string", false, true, true);
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: id8(),
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: id8(),
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: id8(),
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 id9 = () => attr9("string", false, true, true);
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: id9(),
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: id9(),
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: id9(),
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: id9(),
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: id9(),
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: id9(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
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: id9(),
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 id10 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
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 (id10 && !ID.test(id10)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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(id10) || knownParentIds.has(id10)) errors.push(`${path}.id duplicates page "${id10}"`);
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(id10);
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: id10, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
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 id10 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
6542
- if (id10 && !ID2.test(id10)) errors.push(`${path}.id must be lowercase kebab-case`);
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: id10, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
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: id10 }) => id10);
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 === false && !input.force) return { deliver: false, reason: "disabled" };
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 subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
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
- ` + render(tpl.text, vars) : render(tpl.text, vars);
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: input.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 id10 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
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: id10,
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: id10, attrs: row2 }], {
6804
- mutationId: `email-claim:${id10}:${attempt}`,
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: id10,
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: id10, exists: false }]
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: id10, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
6832
- guards: [{ ns: "emailLog", id: id10, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
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: id10, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
6837
- mutationId: `email-confirmed:${id10}:${attempt}`,
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: id10,
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(event, options = {}) {
6956
- const obj = event.data?.object ?? {};
6957
- if (event.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event.type };
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, ...identity } = gift;
6961
- if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
6962
- return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity };
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 (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity, reason: "refunded" };
6965
- if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity, reason: "canceled" };
6966
- return { kind: "ignored", type: event.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: event.type };
6972
- if (event.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
6973
- if (event.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
6974
- return { kind: "ignored", type: event.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 (event.type === "invoice.paid" && options.requireProviderPayment || event.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event.type };
6977
- switch (event.type) {
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: event.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: event.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: event.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: event.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 id10 = typeof grant.id === "string" ? grant.id : "";
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(id10)) errors.push(`document.grants[${index2}].id must be an identifier`);
7105
- if (seenIds.has(id10)) errors.push(`document.grants repeats id "${id10}"`);
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(id10);
7552
+ seenIds.add(id11);
7116
7553
  seenEmails.add(normalized);
7117
7554
  grants.push({
7118
- id: id10,
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 id10 = String(row2.id ?? "");
7191
- if (id10 && !incomingIds.has(id10)) ops.push({ t: "update", ns: "admissionGrants", id: id10, attrs: {
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 id10 = `admission:parent:${document.source.appId}:${grant.id}`;
7201
- const existing = current.find((row2) => row2.id === id10);
7202
- ops.push({ t: "update", ns: "admissionGrants", id: id10, attrs: {
7203
- id: id10,
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: id10, attrs: retract });
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 id10 = text4(item.id, `${path}.id`, errors, 80);
7296
- if (id10 && !ID4.test(id10)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7297
- if (ids2.has(id10)) errors.push(`${path}.id duplicates "${id10}"`);
7298
- ids2.add(id10);
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: id10,
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 id10 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
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: id10,
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 stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
8236
+ import { stripeRequest as stripeRequest11, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7800
8237
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7801
- return stripeRequest10(
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 id10 = await existingClerkUserId(secretKey, email, fetchImpl);
7880
- return id10 ? updateClerkUserMetadata(secretKey, id10, input, fetchImpl) : false;
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, id10, fetchImpl = fetch, source = "public") {
7954
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id10)}`, secretKey, fetchImpl).catch(() => null);
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 === id10 ? record4 : null;
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, id10, role, fetchImpl = fetch, source = "public") {
7973
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id10)}/metadata`, {
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((id10) => !id10)) blockers.push("runtime_inventory_required");
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((id10) => ids2.has(id10))) blockers.push("protected_operator_grant_required");
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 id10 = text5(item.id, `${path}.id`, errors, 80);
8637
- if (id10 && !TIER.test(id10)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8638
- if (seen.has(id10)) errors.push(`${path}.id duplicates "${id10}"`);
8639
- seen.add(id10);
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: id10,
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 [id10, raw2] of Object.entries(overrides)) {
8723
- const path = `document.policy.tierPolicies.${id10}`;
8724
- const policy = recordOf(raw2);
8725
- if (!tiers2.some((tier) => tier.id === id10) || !policy) {
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(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
8730
- tierPolicies[id10] = {
8731
- refundPolicyText: text5(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
8732
- ...policy.trustCopy !== void 0 ? { trustCopy: text5(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
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 policy = policyOf(root.policy, errors, tiers2);
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
- ...policy ? { policy } : {},
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((id10) => `tiers[${id10}].stripePriceId`),
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, id10, content) => ({
9496
+ const artifact = (kind, id11, content) => ({
9098
9497
  kind,
9099
- id: id10,
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((id10) => ({ ...group, id: id10 }));
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(identity, target, hasProvenLegacyLocalLink) {
9921
+ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
9523
9922
  const keys2 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
9524
- const supplied = keys2.filter((key3) => identity[key3] !== void 0);
9525
- if (supplied.some((key3) => identity[key3] !== target[key3])) return "ignore_sibling";
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 event of events2) {
9533
- const digest = seen.get(event.id);
9931
+ for (const event2 of events2) {
9932
+ const digest = seen.get(event2.id);
9534
9933
  if (digest === void 0) {
9535
- seen.set(event.id, event.digest);
9536
- plan.accepted.push(event.id);
9537
- } else if (digest === event.digest) plan.replayed.push(event.id);
9538
- else plan.collisions.push(event.id);
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,