@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.cjs CHANGED
@@ -27,6 +27,7 @@ __export(src_exports, {
27
27
  BUILDER_LIBRARY_MANIFEST_PATH: () => BUILDER_LIBRARY_MANIFEST_PATH,
28
28
  BUILDER_LIBRARY_PROMOTE_PATH: () => BUILDER_LIBRARY_PROMOTE_PATH,
29
29
  BUILDER_LIBRARY_SCHEMA_VERSION: () => BUILDER_LIBRARY_SCHEMA_VERSION,
30
+ CHAPTER_EMAIL_EVENTS: () => CHAPTER_EMAIL_EVENTS,
30
31
  CHAPTER_FLOW_FIELDS_SCHEMA_VERSION: () => CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
31
32
  CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
32
33
  DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
@@ -83,6 +84,7 @@ __export(src_exports, {
83
84
  canonicalSignupControl: () => canonicalSignupControl,
84
85
  chapterBrandFromTokens: () => chapterBrandFromTokens,
85
86
  chapterDb: () => chapterDb,
87
+ chapterEmailEvents: () => chapterEmailEvents,
86
88
  chapterFlowFieldContract: () => chapterFlowFieldContract,
87
89
  clampArray: () => clampArray,
88
90
  classifyMemberStripeEvent: () => classifyMemberStripeEvent,
@@ -251,7 +253,7 @@ __export(src_exports, {
251
253
  slotWindow: () => slotWindow,
252
254
  stageIndex: () => stageIndex,
253
255
  stripeCall: () => stripeCall,
254
- stripeForm: () => import_stripe10.stripeForm,
256
+ stripeForm: () => import_stripe11.stripeForm,
255
257
  subAnnualCents: () => subAnnualCents,
256
258
  submitApplication: () => submitApplication,
257
259
  subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
@@ -262,6 +264,7 @@ __export(src_exports, {
262
264
  updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
263
265
  validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
264
266
  validateBuilderLibraryDocument: () => validateBuilderLibraryDocument,
267
+ validateEmailReadiness: () => validateEmailReadiness,
265
268
  validateGiftOfferCatalog: () => validateGiftOfferCatalog,
266
269
  validateMemberContentDocument: () => validateMemberContentDocument,
267
270
  validateMemberSystemAuthority: () => validateMemberSystemAuthority,
@@ -272,7 +275,7 @@ __export(src_exports, {
272
275
  verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
273
276
  verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
274
277
  verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
275
- verifyStripeSignature: () => import_stripe10.verifyStripeSignature,
278
+ verifyStripeSignature: () => import_stripe11.verifyStripeSignature,
276
279
  webhookMutationId: () => webhookMutationId
277
280
  });
278
281
  module.exports = __toCommonJS(src_exports);
@@ -401,8 +404,8 @@ async function submitApplication(db, chapter, fields, opts) {
401
404
  if (app.requireDisclaimerAck && !acked) {
402
405
  return { ok: false, error: "disclaimerAck is required" };
403
406
  }
404
- const id10 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
405
- const row2 = { id: id10, status: chapter.pipeline.initial, createdAt: opts.now };
407
+ const id11 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
408
+ const row2 = { id: id11, status: chapter.pipeline.initial, createdAt: opts.now };
406
409
  for (const f of [...app.required, ...app.optional]) {
407
410
  if (fieldStates[f]?.visible === false) continue;
408
411
  if (typeof fields[f] === "string") row2[f] = fields[f].trim();
@@ -436,12 +439,12 @@ async function submitApplication(db, chapter, fields, opts) {
436
439
  }
437
440
  if (acked) row2.disclaimerAckAt = opts.now;
438
441
  const ops = [
439
- { t: "update", ns: "applications", id: id10, attrs: row2 }
442
+ { t: "update", ns: "applications", id: id11, attrs: row2 }
440
443
  ];
441
444
  if (opts.admissionGrant?.decision === "waive_interview") {
442
445
  ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
443
446
  consumedAt: opts.now,
444
- consumedApplicationId: id10
447
+ consumedApplicationId: id11
445
448
  } });
446
449
  }
447
450
  try {
@@ -449,7 +452,7 @@ async function submitApplication(db, chapter, fields, opts) {
449
452
  ops,
450
453
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
451
454
  );
452
- return { ok: true, id: id10, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
455
+ return { ok: true, id: id11, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
453
456
  } catch (error) {
454
457
  if (error.code === "unique_violation") return {
455
458
  ok: false,
@@ -1380,6 +1383,7 @@ Warmly,
1380
1383
  ${name}`;
1381
1384
  return {
1382
1385
  adminNotification: {
1386
+ enabled: true,
1383
1387
  subject: `New application: {{firstName}} {{lastName}}`,
1384
1388
  text: `A new application came in for ${name}.
1385
1389
 
@@ -1390,18 +1394,21 @@ Tier: {{tier}}
1390
1394
  Review it: {{adminUrl}}`
1391
1395
  },
1392
1396
  submitConfirmation: {
1397
+ enabled: true,
1393
1398
  subject: `We received your application to ${name}`,
1394
1399
  text: `Hi {{firstName}},
1395
1400
 
1396
1401
  Thanks for applying to ${name}. {{submitNextStep}}${sign}`
1397
1402
  },
1398
1403
  paymentConfirmation: {
1404
+ enabled: true,
1399
1405
  subject: `Welcome to ${name}`,
1400
1406
  text: `Hi {{firstName}},
1401
1407
 
1402
1408
  Your membership payment{{paymentAmountSummary}} is confirmed. {{paymentNextStep}}${sign}`
1403
1409
  },
1404
1410
  prepEmail: {
1411
+ enabled: true,
1405
1412
  subject: `Your ${name} intro call`,
1406
1413
  text: `Hi {{firstName}},
1407
1414
 
@@ -1410,18 +1417,19 @@ You are all set{{paymentAmountSummary}}. Looking forward to our call on {{startA
1410
1417
  Join here: {{meetingLink}}${sign}`
1411
1418
  },
1412
1419
  onboardingInvite: {
1420
+ enabled: true,
1413
1421
  subject: `You're in, ${name}`,
1414
1422
  text: `Hi {{firstName}},
1415
1423
 
1416
1424
  Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
1417
1425
  },
1418
- seatAccepted: SEAT_ACCEPTED_TEMPLATE
1426
+ seatAccepted: { ...SEAT_ACCEPTED_TEMPLATE, enabled: true }
1419
1427
  };
1420
1428
  }
1421
1429
  function buildGroupSeed(config) {
1422
1430
  const prices = config.prices;
1423
1431
  const emails = config.emails;
1424
- const policy = config.policy ?? {};
1432
+ const policy2 = config.policy ?? {};
1425
1433
  const scheduling = {
1426
1434
  ...DEFAULT_SCHEDULING,
1427
1435
  ...config.scheduling ?? {},
@@ -1436,17 +1444,17 @@ function buildGroupSeed(config) {
1436
1444
  interval: prices?.interval ?? "year",
1437
1445
  notificationEmail: emails?.notificationEmail ?? "",
1438
1446
  replyTo: emails?.replyTo ?? emails?.notificationEmail ?? "",
1439
- disclaimerText: policy.disclaimerText ?? "",
1440
- refundPolicyText: policy.refundPolicyText ?? "",
1441
- merchantDisclosureText: policy.merchantDisclosureText ?? "",
1442
- trustCopy: policy.trustCopy ?? "",
1447
+ disclaimerText: policy2.disclaimerText ?? "",
1448
+ refundPolicyText: policy2.refundPolicyText ?? "",
1449
+ merchantDisclosureText: policy2.merchantDisclosureText ?? "",
1450
+ trustCopy: policy2.trustCopy ?? "",
1443
1451
  emailTemplates: emails?.templates ?? defaultEmailTemplates(config.name),
1444
1452
  schedulingJson: scheduling
1445
1453
  };
1446
1454
  if (emails?.debugEmail) row2.debugEmail = emails.debugEmail;
1447
1455
  if (emails?.fromName?.trim()) row2.fromName = emails.fromName.trim();
1448
- if (policy.commitmentText) row2.commitmentText = policy.commitmentText;
1449
- if (policy.normsText) row2.normsText = policy.normsText;
1456
+ if (policy2.commitmentText) row2.commitmentText = policy2.commitmentText;
1457
+ if (policy2.normsText) row2.normsText = policy2.normsText;
1450
1458
  return row2;
1451
1459
  }
1452
1460
 
@@ -1979,9 +1987,9 @@ var APPLICATION_LABELS = {
1979
1987
  phone: "Phone",
1980
1988
  state: "State"
1981
1989
  };
1982
- function humanize(id10) {
1983
- const text6 = id10.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1984
- return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id10;
1990
+ function humanize(id11) {
1991
+ const text6 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1992
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id11;
1985
1993
  }
1986
1994
  function applicationDefs(chapter) {
1987
1995
  try {
@@ -1990,23 +1998,23 @@ function applicationDefs(chapter) {
1990
1998
  return {};
1991
1999
  }
1992
2000
  }
1993
- function applicationType(id10, def) {
2001
+ function applicationType(id11, def) {
1994
2002
  if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
1995
- return id10 === "email" ? "email" : id10 === "linkedin" ? "url" : "string";
2003
+ return id11 === "email" ? "email" : id11 === "linkedin" ? "url" : "string";
1996
2004
  }
1997
2005
  function applicationFlowFields(chapter, application = chapter.application) {
1998
2006
  const defs = applicationDefs(chapter);
1999
- return [...application.required, ...application.optional].map((id10, order2) => {
2000
- const def = defs[id10];
2001
- const conditions = application.conditions[id10];
2002
- const type = applicationType(id10, def);
2007
+ return [...application.required, ...application.optional].map((id11, order2) => {
2008
+ const def = defs[id11];
2009
+ const conditions = application.conditions[id11];
2010
+ const type = applicationType(id11, def);
2003
2011
  return {
2004
- id: id10,
2005
- label: def?.label ?? APPLICATION_LABELS[id10] ?? humanize(id10),
2012
+ id: id11,
2013
+ label: def?.label ?? APPLICATION_LABELS[id11] ?? humanize(id11),
2006
2014
  type,
2007
- required: application.required.includes(id10),
2015
+ required: application.required.includes(id11),
2008
2016
  order: order2,
2009
- maxLength: application.maxLen[id10] ?? application.defaultMaxLen,
2017
+ maxLength: application.maxLen[id11] ?? application.defaultMaxLen,
2010
2018
  ...type === "enum" && def?.options ? { options: [...def.options] } : {},
2011
2019
  ...conditions ? { conditions: { ...conditions } } : {},
2012
2020
  ...type === "email" ? { emailIdentity: "literal" } : {}
@@ -2016,17 +2024,17 @@ function applicationFlowFields(chapter, application = chapter.application) {
2016
2024
  function resolveFormationFlowFields(crm, formation) {
2017
2025
  if (!formation.enabled) return [];
2018
2026
  const def = crm.type(formation.type);
2019
- return [...formation.required, ...formation.optional].map((id10, order2) => {
2020
- const field2 = def.fields[id10];
2027
+ return [...formation.required, ...formation.optional].map((id11, order2) => {
2028
+ const field2 = def.fields[id11];
2021
2029
  return {
2022
- id: id10,
2023
- label: field2.label ?? humanize(id10),
2030
+ id: id11,
2031
+ label: field2.label ?? humanize(id11),
2024
2032
  type: field2.type,
2025
- required: formation.required.includes(id10),
2033
+ required: formation.required.includes(id11),
2026
2034
  order: order2,
2027
- maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
2035
+ maxLength: formation.maxLen[id11] ?? formation.defaultMaxLen,
2028
2036
  ...field2.options ? { options: [...field2.options] } : {},
2029
- ...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
2037
+ ...id11 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
2030
2038
  };
2031
2039
  });
2032
2040
  }
@@ -2330,12 +2338,12 @@ function resolveNetworkPersonRegistry(mode, input, targetIds) {
2330
2338
  if (input.mode !== "shadow") throw new Error('defineChapter.networkPersonRegistry.mode: must be "shadow"');
2331
2339
  const chapterIds = input.chapterIds ? [...input.chapterIds] : [...targetIds];
2332
2340
  if (chapterIds.length === 0) throw new Error("defineChapter.networkPersonRegistry.chapterIds: at least one chapter is required");
2333
- if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id10) => !SLUG2.test(id10))) {
2341
+ if (new Set(chapterIds).size !== chapterIds.length || chapterIds.some((id11) => !SLUG2.test(id11))) {
2334
2342
  throw new Error("defineChapter.networkPersonRegistry.chapterIds: expected distinct lowercase slugs");
2335
2343
  }
2336
- const unknown = chapterIds.filter((id10) => !targetIds.includes(id10));
2344
+ const unknown = chapterIds.filter((id11) => !targetIds.includes(id11));
2337
2345
  if (unknown.length) {
2338
- throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id10) => `"${id10}"`).join(", ")} are not configured network targets`);
2346
+ throw new Error(`defineChapter.networkPersonRegistry.chapterIds: ${unknown.map((id11) => `"${id11}"`).join(", ")} are not configured network targets`);
2339
2347
  }
2340
2348
  return { mode: "shadow", chapterIds };
2341
2349
  }
@@ -2458,9 +2466,9 @@ function isResolvedCrm(x) {
2458
2466
  }
2459
2467
  function defineChapter(config) {
2460
2468
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
2461
- const { id: id10, name } = config;
2462
- if (typeof id10 !== "string" || !SLUG7.test(id10)) {
2463
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id10)}`);
2469
+ const { id: id11, name } = config;
2470
+ if (typeof id11 !== "string" || !SLUG7.test(id11)) {
2471
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id11)}`);
2464
2472
  }
2465
2473
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
2466
2474
  const mode = config.mode ?? "chapter";
@@ -2567,7 +2575,7 @@ function defineChapter(config) {
2567
2575
  };
2568
2576
  const chapter = {
2569
2577
  config,
2570
- id: id10,
2578
+ id: id11,
2571
2579
  name,
2572
2580
  mode,
2573
2581
  brand,
@@ -2755,13 +2763,13 @@ function firstPrice(object2) {
2755
2763
  return { ...priceId ? { priceId } : {}, ...quantity !== void 0 ? { quantity } : {} };
2756
2764
  }
2757
2765
  function normalizeNetworkCommercialStripeEvent(input, digest) {
2758
- const event = raw(input);
2759
- const eventId = providerId(event.id, "evt_") ?? "";
2760
- const eventType = text(event.type) ?? "";
2761
- const object2 = raw(raw(event.data).object);
2766
+ const event2 = raw(input);
2767
+ const eventId = providerId(event2.id, "evt_") ?? "";
2768
+ const eventType = text(event2.type) ?? "";
2769
+ const object2 = raw(raw(event2.data).object);
2762
2770
  const objectId = providerId(object2.id) ?? "";
2763
2771
  const candidates = metadataCandidates(object2);
2764
- const identity = {};
2772
+ const identity2 = {};
2765
2773
  const identityKeys2 = {
2766
2774
  appId: "odlaAppId",
2767
2775
  chapterId: "chapterId",
@@ -2771,7 +2779,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2771
2779
  };
2772
2780
  for (const [target, source] of Object.entries(identityKeys2)) {
2773
2781
  const value = metadataValue(candidates, source);
2774
- if (value) identity[target] = value;
2782
+ if (value) identity2[target] = value;
2775
2783
  }
2776
2784
  let objectKind = "ignored";
2777
2785
  if (eventType.startsWith("customer.subscription.")) objectKind = "subscription";
@@ -2793,11 +2801,11 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2793
2801
  eventId,
2794
2802
  digest,
2795
2803
  eventType,
2796
- eventCreated: integer(event.created) ?? 0,
2797
- livemode: event.livemode === true,
2804
+ eventCreated: integer(event2.created) ?? 0,
2805
+ livemode: event2.livemode === true,
2798
2806
  objectKind,
2799
2807
  objectId,
2800
- identity,
2808
+ identity: identity2,
2801
2809
  ...metadataValue(candidates, "applicationId") ? { applicationId: metadataValue(candidates, "applicationId") } : {},
2802
2810
  ...metadataValue(candidates, "personId") ? { personId: metadataValue(candidates, "personId") } : {},
2803
2811
  ...metadataValue(candidates, "membershipId") ? { membershipId: metadataValue(candidates, "membershipId") } : {},
@@ -2982,14 +2990,14 @@ function facts(snapshots) {
2982
2990
  ["payment_intent.updated", snapshots.payments?.objects ?? []]
2983
2991
  ];
2984
2992
  return groups2.flatMap(([eventType, objects]) => objects.map((object2) => {
2985
- const id10 = typeof object2.id === "string" ? object2.id : "invalid";
2993
+ const id11 = typeof object2.id === "string" ? object2.id : "invalid";
2986
2994
  return normalizeNetworkCommercialStripeEvent({
2987
- id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id10}`,
2995
+ id: `evt_snapshot_${eventType.replaceAll(".", "_")}_${id11}`,
2988
2996
  type: eventType,
2989
2997
  created: typeof object2.created === "number" ? object2.created : 0,
2990
2998
  livemode: object2.livemode === true,
2991
2999
  data: { object: object2 }
2992
- }, `stripe-snapshot:${eventType}:${id10}`);
3000
+ }, `stripe-snapshot:${eventType}:${id11}`);
2993
3001
  }));
2994
3002
  }
2995
3003
  var selected = (row2) => ({
@@ -3644,15 +3652,15 @@ function membershipDiscountPolicyVersion(value) {
3644
3652
  // src/membership-offer.ts
3645
3653
  function createMembershipCommercialOffer(input) {
3646
3654
  const offering = membershipOfferingVersion(input.offering);
3647
- const policy = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
3655
+ const policy2 = input.discountPolicy === void 0 || input.discountPolicy === null ? null : membershipDiscountPolicyVersion(input.discountPolicy);
3648
3656
  const now = input.now;
3649
3657
  if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.approvedFoundingCount) || input.approvedFoundingCount < 0) {
3650
3658
  throw new TypeError("invalid offer clock or founding count");
3651
3659
  }
3652
3660
  if (offering.status !== "published" || now < offering.effectiveFrom || offering.effectiveUntil !== void 0 && now >= offering.effectiveUntil) throw new TypeError("offering is unavailable");
3653
- if (policy && policy.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
3654
- 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);
3655
- const earned = founding ? policy : null;
3661
+ if (policy2 && policy2.chapterId !== offering.chapterId) throw new TypeError("policy belongs to another chapter");
3662
+ 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);
3663
+ const earned = founding ? policy2 : null;
3656
3664
  const initialTermEndsAt = offering.term.kind === "fixed_end" ? offering.term.endsAt : addMembershipCalendarMonths(now, offering.term.months);
3657
3665
  if (initialTermEndsAt <= now) throw new TypeError("membership service term has ended");
3658
3666
  if (offering.term.kind === "rolling_months" && offering.term.months !== (offering.interval === "year" ? 12 : 1)) {
@@ -3698,8 +3706,8 @@ async function hash(value) {
3698
3706
  async function membershipDiscountCouponId(input, policyId) {
3699
3707
  return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
3700
3708
  }
3701
- async function load(db, ns, id10) {
3702
- return (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3709
+ async function load(db, ns, id11) {
3710
+ return (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
3703
3711
  }
3704
3712
  async function membershipOfferIdForRequest(input) {
3705
3713
  return `offer_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.applicationId, input.requestId])}`;
@@ -3744,15 +3752,15 @@ async function previewMembershipCommercialOffer(db, input) {
3744
3752
  }
3745
3753
  if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
3746
3754
  const state = await authoritativeState(db, input);
3747
- const { offering, policy } = await currentPolicy(db, input, false, state);
3748
- const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3755
+ const { offering, policy: policy2 } = await currentPolicy(db, input, false, state);
3756
+ const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
3749
3757
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3750
3758
  const contract = createMembershipCommercialOffer({
3751
3759
  offering,
3752
- discountPolicy: policy,
3760
+ discountPolicy: policy2,
3753
3761
  approvedFoundingCount: Number(approvedFoundingCount),
3754
3762
  now: input.now,
3755
- couponId: await membershipDiscountCouponId(input, policy?.id)
3763
+ couponId: await membershipDiscountCouponId(input, policy2?.id)
3756
3764
  });
3757
3765
  const discount = contract.billingTerms.discount;
3758
3766
  return {
@@ -3781,12 +3789,12 @@ async function currentPolicy(db, input, restartAware, state) {
3781
3789
  });
3782
3790
  if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
3783
3791
  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));
3784
- 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);
3792
+ 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);
3785
3793
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
3786
3794
  const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
3787
- const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3788
- 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");
3789
- return { offering, policy };
3795
+ const policy2 = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3796
+ 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");
3797
+ return { offering, policy: policy2 };
3790
3798
  }
3791
3799
  async function issueMembershipCommercialOffer(db, input) {
3792
3800
  validate(input);
@@ -3794,15 +3802,15 @@ async function issueMembershipCommercialOffer(db, input) {
3794
3802
  const state = await authoritativeState(db, input);
3795
3803
  const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3796
3804
  if (existing) return restore(existing, input);
3797
- const { offering, policy } = await currentPolicy(db, input, true, state);
3798
- const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3805
+ const { offering, policy: policy2 } = await currentPolicy(db, input, true, state);
3806
+ const approvedFoundingCount = policy2 ? state.cohortCounts[policy2.cohortLabel] ?? 0 : 0;
3799
3807
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3800
3808
  const contract = createMembershipCommercialOffer({
3801
3809
  offering,
3802
- discountPolicy: policy,
3810
+ discountPolicy: policy2,
3803
3811
  approvedFoundingCount: Number(approvedFoundingCount),
3804
3812
  now: input.now,
3805
- couponId: await membershipDiscountCouponId(input, policy?.id)
3813
+ couponId: await membershipDiscountCouponId(input, policy2?.id)
3806
3814
  });
3807
3815
  const value = {
3808
3816
  id: input.offerId,
@@ -3836,20 +3844,20 @@ var import_stripe2 = require("@odla-ai/stripe");
3836
3844
 
3837
3845
  // src/commercial-evidence-guard.ts
3838
3846
  async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
3839
- const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
3840
- const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3841
- if (!row2) return { ns, id: id10, exists: false };
3847
+ const ns = "networkCommercialExceptions", id11 = `network-commercial-exception:${eventId}:collision`;
3848
+ const row2 = (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
3849
+ if (!row2) return { ns, id: id11, exists: false };
3842
3850
  if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
3843
3851
  throw Object.assign(new Error(code), { code });
3844
3852
  }
3845
- return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
3853
+ return { ns, id: id11, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
3846
3854
  }
3847
3855
 
3848
3856
  // src/membership-authority-state.ts
3849
3857
  var membershipFail = (code) => {
3850
3858
  throw Object.assign(new Error(code), { code });
3851
3859
  };
3852
- var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3860
+ var membershipLoad = async (db, ns, id11) => (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
3853
3861
  var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3854
3862
  var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3855
3863
  function requireMembershipScope(row2, input) {
@@ -3860,18 +3868,18 @@ async function loadMembershipAuthorityState(db, input) {
3860
3868
  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");
3861
3869
  return row2;
3862
3870
  }
3863
- var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3871
+ var membershipUpdate = (ns, id11, attrs) => ({ t: "update", ns, id: id11, attrs });
3864
3872
  var membershipClock = (now) => {
3865
3873
  if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3866
3874
  };
3867
3875
 
3868
3876
  // src/membership-restart-payment.ts
3869
3877
  async function membershipRestartPayment(db, target, member, now) {
3870
- const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
3878
+ const id11 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id11);
3871
3879
  requireMembershipScope(restart, target);
3872
- const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
3880
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
3873
3881
  requireMembershipScope(entitlement, target);
3874
- if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
3882
+ if (entitlement.membershipId !== id11 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
3875
3883
  if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
3876
3884
  return {
3877
3885
  restartId: String(restart.restartId),
@@ -3888,49 +3896,117 @@ async function membershipRestartPayment(db, target, member, now) {
3888
3896
  graceEndsAt: 0
3889
3897
  },
3890
3898
  ops: [
3891
- membershipUpdate("membershipRestart", id10, { status: "complete" }),
3892
- { t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
3899
+ membershipUpdate("membershipRestart", id11, { status: "complete" }),
3900
+ { t: "retract", ns: "networkMembership", id: id11, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
3893
3901
  ],
3894
3902
  guards: [
3895
- { ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3896
- { ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
3903
+ { ns: "networkMembership", id: id11, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3904
+ { ns: "membershipRestart", id: id11, exists: true, equals: { status: "ready", restartId: restart.restartId } },
3897
3905
  { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
3898
3906
  ]
3899
3907
  };
3900
3908
  }
3901
3909
 
3910
+ // src/checkout-named-seat.ts
3911
+ function checkoutNamedSeatSelection(value, purchaserEmail) {
3912
+ const row2 = value;
3913
+ const name = typeof row2?.recipientName === "string" ? row2.recipientName.trim().replace(/\s+/g, " ") : "";
3914
+ const email = normalizeChapterEmail(typeof row2?.recipientEmail === "string" ? row2.recipientEmail : "");
3915
+ const purchaser = normalizeChapterEmail(purchaserEmail);
3916
+ 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) {
3917
+ throw Object.assign(new Error("Enter a different recipient's name and email."), { code: "invalid_named_seat_recipient" });
3918
+ }
3919
+ return { recipientName: name, recipientEmail: email };
3920
+ }
3921
+ async function checkoutNamedSeatIdentity(target, email) {
3922
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
3923
+ target.appId,
3924
+ target.chapterId,
3925
+ target.environment,
3926
+ target.runtime,
3927
+ email
3928
+ ])));
3929
+ return {
3930
+ seatId: (await membershipOfferIdForRequest({ ...target, requestId: "named_seat" })).replace(/^offer_/, "seat_"),
3931
+ membershipId: (await membershipOfferIdForRequest({ ...target, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"),
3932
+ recipientClaimId: `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`
3933
+ };
3934
+ }
3935
+
3902
3936
  // src/membership-checkout-binding.ts
3903
3937
  var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
3904
3938
  var reject2 = (code) => {
3905
3939
  throw Object.assign(new Error(code), { code });
3906
3940
  };
3907
- var load2 = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3941
+ var load2 = async (db, ns, id11) => (await db.query({ [ns]: { $: { where: { id: id11 }, limit: 1 } } }))[ns]?.[0];
3908
3942
  async function membershipCheckoutBindingId(input) {
3909
3943
  return (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "checkout_");
3910
3944
  }
3911
3945
  async function verifyMembershipCheckoutBinding(value, input) {
3912
3946
  const row2 = value;
3913
3947
  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");
3914
- return { id: row2.id, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: row2.boundAt };
3948
+ if (Boolean(row2.namedSeat) !== Boolean(input.namedSeat)) return reject2("membership_checkout_conflict");
3949
+ if (input.namedSeat) {
3950
+ const expected = {
3951
+ ...checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail),
3952
+ purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail)
3953
+ };
3954
+ const identity2 = await checkoutNamedSeatIdentity(input, expected.recipientEmail);
3955
+ 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");
3956
+ }
3957
+ return {
3958
+ id: row2.id,
3959
+ ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])),
3960
+ boundAt: row2.boundAt,
3961
+ ...row2.namedSeat ? { namedSeat: row2.namedSeat } : {}
3962
+ };
3915
3963
  }
3916
- async function bindMembershipCheckout(db, input, now) {
3964
+ async function bindMembershipCheckout(db, input, now, seatPriceId) {
3917
3965
  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");
3918
3966
  const stateId = `${input.chapterId}:${input.environment}`;
3919
3967
  const state = await load2(db, "membershipAuthorityState", stateId);
3920
3968
  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");
3921
- const id10 = await membershipCheckoutBindingId(input);
3922
- const existing = await load2(db, "membershipOfferCheckout", id10);
3969
+ const id11 = await membershipCheckoutBindingId(input);
3970
+ const existing = await load2(db, "membershipOfferCheckout", id11);
3923
3971
  if (existing) return verifyMembershipCheckoutBinding(existing, input);
3924
3972
  const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
3925
3973
  const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
3926
3974
  if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
3927
- const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
3928
- const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
3975
+ const restart = await load2(db, "membershipRestart", id11.replace(/^checkout_/, "membership_"));
3976
+ let namedSeat;
3977
+ if (input.namedSeat) {
3978
+ if (!seatPriceId || !/^price_[A-Za-z0-9_]+$/.test(seatPriceId) || seatPriceId === input.priceId || restart) return reject2("named_seat_purchase_unavailable");
3979
+ const selection = checkoutNamedSeatSelection(input.namedSeat, input.namedSeat.purchaserEmail);
3980
+ namedSeat = {
3981
+ ...selection,
3982
+ purchaserEmail: normalizeChapterEmail(input.namedSeat.purchaserEmail),
3983
+ ...await checkoutNamedSeatIdentity(input, selection.recipientEmail),
3984
+ priceId: seatPriceId,
3985
+ amountCents: offer.contract.offering.seatPriceCents
3986
+ };
3987
+ }
3988
+ const receipt = { id: id11, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now, ...namedSeat ? { namedSeat } : {} };
3929
3989
  try {
3930
- await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
3931
- mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
3990
+ await db.transact([
3991
+ { t: "update", ns: "membershipOfferCheckout", id: id11, attrs: receipt },
3992
+ ...namedSeat ? [{ t: "update", ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, attrs: {
3993
+ id: namedSeat.recipientClaimId,
3994
+ appId: input.appId,
3995
+ chapterId: input.chapterId,
3996
+ environment: input.environment,
3997
+ runtime: input.runtime,
3998
+ seatId: namedSeat.seatId,
3999
+ recipientEmailNormalized: namedSeat.recipientEmail,
4000
+ createdAt: now
4001
+ } }] : []
4002
+ ], {
4003
+ mutationId: `membership-checkout:${id11}${restart?.restartId ? `:${restart.restartId}` : ""}`,
3932
4004
  guards: [
3933
- { ns: "membershipOfferCheckout", id: id10, exists: false },
4005
+ { ns: "membershipOfferCheckout", id: id11, exists: false },
4006
+ ...namedSeat ? [
4007
+ { ns: "networkSeat", id: namedSeat.seatId, exists: false },
4008
+ { ns: "networkSeatRecipient", id: namedSeat.recipientClaimId, exists: false }
4009
+ ] : [],
3934
4010
  { ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
3935
4011
  { ns: "membershipAuthorityState", id: stateId, exists: true, equals: { mode: "authoritative", revision: state.revision } }
3936
4012
  ]
@@ -3938,11 +4014,78 @@ async function bindMembershipCheckout(db, input, now) {
3938
4014
  } catch (error) {
3939
4015
  if (error.code !== "transact_guard_failed") throw error;
3940
4016
  }
3941
- const saved = await load2(db, "membershipOfferCheckout", id10);
4017
+ const saved = await load2(db, "membershipOfferCheckout", id11);
3942
4018
  if (!saved) return reject2("membership_binding_unconfirmed");
3943
4019
  return verifyMembershipCheckoutBinding(saved, input);
3944
4020
  }
3945
4021
 
4022
+ // src/membership-checkout-seat-payment.ts
4023
+ function checkoutSeatPaymentOperations(target, seat, input) {
4024
+ const { now, paidAt, endsAt } = input;
4025
+ const included = seat.amountCents === 0, entitlementId = `${seat.seatId}:entitlement`;
4026
+ const effectId = `${seat.seatId}:${included ? "invite" : "attach-renewal"}`;
4027
+ return [
4028
+ membershipUpdate("networkSeat", seat.seatId, {
4029
+ id: seat.seatId,
4030
+ ...membershipScope(target),
4031
+ primaryMembershipId: seat.membershipId,
4032
+ purchaserEmailNormalized: seat.purchaserEmail,
4033
+ recipientName: seat.recipientName,
4034
+ recipientEmailNormalized: seat.recipientEmail,
4035
+ recipientClaimId: seat.recipientClaimId,
4036
+ priceId: seat.priceId,
4037
+ primaryPriceId: input.primaryPriceId,
4038
+ amountCents: seat.amountCents,
4039
+ currency: input.offering.currency,
4040
+ interval: input.offering.interval,
4041
+ providerCustomerId: input.customerId,
4042
+ providerSubscriptionId: input.subscriptionId,
4043
+ combinedInvoice: { invoiceId: input.invoiceId, totalCents: input.invoiceTotalCents },
4044
+ status: included ? "pending_acceptance" : "renewal_pending",
4045
+ preparedAt: input.boundAt,
4046
+ purchasedAt: paidAt,
4047
+ termStartedAt: paidAt,
4048
+ termEndsAt: endsAt,
4049
+ acceptanceEndsAt: endsAt,
4050
+ seatPurchaseEndsAt: seatPurchaseDeadline(input.offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
4051
+ offeringSnapshot: input.offering,
4052
+ paymentEventId: input.eventId,
4053
+ ...included ? { entitlementId } : {},
4054
+ revision: 1,
4055
+ createdAt: now,
4056
+ updatedAt: now
4057
+ }),
4058
+ ...included ? [membershipUpdate("networkEntitlement", entitlementId, {
4059
+ id: entitlementId,
4060
+ ...membershipScope(target),
4061
+ membershipId: seat.membershipId,
4062
+ seatId: seat.seatId,
4063
+ kind: "named_seat",
4064
+ status: "pending_acceptance",
4065
+ termStartedAt: paidAt,
4066
+ termEndsAt: endsAt,
4067
+ offeringSnapshot: input.offering,
4068
+ sourceEventId: input.eventId,
4069
+ revision: 1,
4070
+ createdAt: now,
4071
+ updatedAt: now
4072
+ })] : [],
4073
+ membershipUpdate("membershipEffect", effectId, {
4074
+ id: effectId,
4075
+ chapterId: target.chapterId,
4076
+ environment: target.environment,
4077
+ runtime: target.runtime,
4078
+ membershipId: seat.membershipId,
4079
+ seatId: seat.seatId,
4080
+ kind: included ? "invite_named_recipient" : "attach_seat_renewal",
4081
+ status: "pending",
4082
+ attempts: 0,
4083
+ createdAt: now,
4084
+ updatedAt: now
4085
+ })
4086
+ ];
4087
+ }
4088
+
3946
4089
  // src/membership-payment-authority.ts
3947
4090
  async function networkMembershipId(input) {
3948
4091
  return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
@@ -3958,11 +4101,14 @@ async function confirmMembershipPayment(db, input, now) {
3958
4101
  { ...input, offerId: String(binding.offerId), now, lifetimeMs: 6e4 }
3959
4102
  );
3960
4103
  if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
4104
+ const { namedSeat } = await verifyMembershipCheckoutBinding(binding, binding);
4105
+ if (namedSeat && namedSeat.amountCents !== offer.contract.offering.seatPriceCents) membershipFail("membership_checkout_conflict");
4106
+ const totalCents = offer.contract.dueTodayCents + (namedSeat?.amountCents ?? 0);
3961
4107
  const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3962
4108
  if (!eventRow) membershipFail("membership_payment_evidence_pending");
3963
4109
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
3964
4110
  const fact = eventRow.fact;
3965
- 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({
4111
+ 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({
3966
4112
  appId: input.appId,
3967
4113
  chapterId: input.chapterId,
3968
4114
  environment: input.environment,
@@ -3974,14 +4120,14 @@ async function confirmMembershipPayment(db, input, now) {
3974
4120
  const paidAt = fact.invoicePaidAt;
3975
4121
  const endsAt = offer.contract.initialTermEndsAt ?? fact.invoicePeriodEndsAt;
3976
4122
  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");
3977
- const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
3978
- let member = await membershipLoad(db, "networkMembership", id10);
4123
+ const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
4124
+ let member = await membershipLoad(db, "networkMembership", id11);
3979
4125
  if (!member || member.status === "restarting") {
3980
4126
  const offering = offer.contract.offering;
3981
4127
  const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
3982
4128
  const attrs = {
3983
- id: id10,
3984
- membershipId: id10,
4129
+ id: id11,
4130
+ membershipId: id11,
3985
4131
  ...membershipScope(input),
3986
4132
  baseSkuId: input.skuId,
3987
4133
  status: "paid_pending_approval",
@@ -3997,8 +4143,9 @@ async function confirmMembershipPayment(db, input, now) {
3997
4143
  founding: offer.contract.founding,
3998
4144
  cohort: offer.contract.founding ? offer.contract.discountPolicy.cohortLabel : "standard",
3999
4145
  baseAmountCents: offering.basePriceCents,
4000
- amountCents: offer.contract.dueTodayCents,
4146
+ amountCents: totalCents,
4001
4147
  renewalAmountCents: offer.contract.renewalCents,
4148
+ ...namedSeat ? { namedSeatId: namedSeat.seatId } : {},
4002
4149
  currency: offering.currency,
4003
4150
  interval: offering.interval,
4004
4151
  initialTermStartedAt: paidAt,
@@ -4015,11 +4162,11 @@ async function confirmMembershipPayment(db, input, now) {
4015
4162
  };
4016
4163
  try {
4017
4164
  await db.transact([
4018
- membershipUpdate("networkMembership", id10, attrs),
4165
+ membershipUpdate("networkMembership", id11, attrs),
4019
4166
  membershipUpdate("networkEntitlement", entitlementId, {
4020
4167
  id: entitlementId,
4021
4168
  ...membershipScope(input),
4022
- membershipId: id10,
4169
+ membershipId: id11,
4023
4170
  kind: "primary",
4024
4171
  status: restart?.attrs.status === "active" ? "active" : "pending_approval",
4025
4172
  termStartedAt: paidAt,
@@ -4031,23 +4178,45 @@ async function confirmMembershipPayment(db, input, now) {
4031
4178
  updatedAt: now,
4032
4179
  ...restart?.entitlementAttrs
4033
4180
  }),
4034
- ...restart?.ops ?? []
4035
- ], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
4036
- ...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
4181
+ ...restart?.ops ?? [],
4182
+ ...namedSeat ? checkoutSeatPaymentOperations(input, namedSeat, {
4183
+ offering,
4184
+ primaryPriceId: String(binding.priceId),
4185
+ invoiceId: fact.invoiceId,
4186
+ invoiceTotalCents: totalCents,
4187
+ subscriptionId: input.providerSubscriptionId,
4188
+ customerId: input.providerCustomerId,
4189
+ eventId: input.eventId,
4190
+ paidAt: Number(paidAt),
4191
+ endsAt: Number(endsAt),
4192
+ boundAt: Number(binding.boundAt),
4193
+ now
4194
+ }) : []
4195
+ ], { mutationId: `membership-paid:${id11}${restart ? `:${restart.restartId}` : ""}`, guards: [
4196
+ ...restart?.guards ?? [{ ns: "networkMembership", id: id11, exists: false }],
4197
+ ...namedSeat ? [
4198
+ { ns: "networkSeat", id: namedSeat.seatId, exists: false },
4199
+ {
4200
+ ns: "networkSeatRecipient",
4201
+ id: namedSeat.recipientClaimId,
4202
+ exists: true,
4203
+ equals: { seatId: namedSeat.seatId, recipientEmailNormalized: namedSeat.recipientEmail }
4204
+ }
4205
+ ] : [],
4037
4206
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4038
- { ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
4207
+ { ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest, quoteDigest: binding.quoteDigest } },
4039
4208
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
4040
4209
  evidenceGuard
4041
4210
  ] });
4042
4211
  } catch (error) {
4043
4212
  if (error.code !== "transact_guard_failed") throw error;
4044
4213
  }
4045
- member = await membershipLoad(db, "networkMembership", id10);
4214
+ member = await membershipLoad(db, "networkMembership", id11);
4046
4215
  }
4047
4216
  requireMembershipScope(member, input);
4048
4217
  if (member.purchaseOfferDigest !== offer.digest || member.providerInitialInvoiceId !== fact.invoiceId || member.providerSubscriptionId !== fact.subscriptionId || member.providerCustomerId !== fact.customerId) membershipFail("membership_purchase_conflict");
4049
4218
  return {
4050
- membershipId: id10,
4219
+ membershipId: id11,
4051
4220
  status: String(member.status),
4052
4221
  cohort: String(member.cohort),
4053
4222
  initialTermStartedAt: Number(member.initialTermStartedAt),
@@ -4059,9 +4228,9 @@ async function confirmMembershipPayment(db, input, now) {
4059
4228
  // src/membership-cancel-authority.ts
4060
4229
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
4061
4230
  membershipClock(now);
4062
- const id10 = await networkMembershipId(target);
4231
+ const id11 = await networkMembershipId(target);
4063
4232
  const state = await loadMembershipAuthorityState(db, target);
4064
- const member = await membershipLoad(db, "networkMembership", id10);
4233
+ const member = await membershipLoad(db, "networkMembership", id11);
4065
4234
  requireMembershipScope(member, target);
4066
4235
  if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
4067
4236
  membershipFail("membership_cancellation_unavailable");
@@ -4079,7 +4248,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
4079
4248
  "POST",
4080
4249
  path,
4081
4250
  { cancel_at_period_end: true, proration_behavior: "none" },
4082
- { idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
4251
+ { idempotencyKey: `membership-cancel-at-term:${id11}:${member.providerSubscriptionId}` }
4083
4252
  );
4084
4253
  if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
4085
4254
  membershipFail("membership_cancellation_pending");
@@ -4087,7 +4256,7 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
4087
4256
  }
4088
4257
  const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
4089
4258
  if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
4090
- await db.transact([membershipUpdate("networkMembership", id10, {
4259
+ await db.transact([membershipUpdate("networkMembership", id11, {
4091
4260
  status: status2,
4092
4261
  autoRenew: false,
4093
4262
  graceEndsAt: 0,
@@ -4096,12 +4265,12 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
4096
4265
  updatedAt: now
4097
4266
  })], {
4098
4267
  guards: [
4099
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
4268
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
4100
4269
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
4101
4270
  ]
4102
4271
  });
4103
4272
  }
4104
- return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
4273
+ return { membershipId: id11, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
4105
4274
  }
4106
4275
 
4107
4276
  // src/membership-authority-handler.ts
@@ -4140,17 +4309,17 @@ async function namedSeatPrimaryActivation(db, member, now) {
4140
4309
  // src/membership-approval-authority.ts
4141
4310
  async function approveNetworkMembership(db, input, now) {
4142
4311
  membershipClock(now);
4143
- const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
4312
+ const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
4144
4313
  for (let attempt = 0; attempt < 8; attempt++) {
4145
4314
  const state = await loadMembershipAuthorityState(db, input);
4146
- const member = await membershipLoad(db, "networkMembership", id10);
4315
+ const member = await membershipLoad(db, "networkMembership", id11);
4147
4316
  requireMembershipScope(member, input);
4148
4317
  const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
4149
4318
  requireMembershipScope(entitlement, input);
4150
- if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
4319
+ if (entitlement.membershipId !== id11 || entitlement.kind !== "primary") membershipFail("membership_entitlement_conflict");
4151
4320
  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) {
4152
4321
  return {
4153
- membershipId: id10,
4322
+ membershipId: id11,
4154
4323
  status: String(member.status),
4155
4324
  cohort: String(member.cohort),
4156
4325
  memberNumber: Number(member.memberNumber),
@@ -4170,7 +4339,7 @@ async function approveNetworkMembership(db, input, now) {
4170
4339
  const seatActivation = await namedSeatPrimaryActivation(db, member, now);
4171
4340
  try {
4172
4341
  await db.transact([
4173
- membershipUpdate("networkMembership", id10, {
4342
+ membershipUpdate("networkMembership", id11, {
4174
4343
  status: member.autoRenew ? "active" : "cancel_at_term_end",
4175
4344
  memberNumber,
4176
4345
  journeyApprovedAt: now,
@@ -4185,8 +4354,8 @@ async function approveNetworkMembership(db, input, now) {
4185
4354
  updatedAt: now
4186
4355
  }),
4187
4356
  ...seatActivation.ops
4188
- ], { mutationId: `membership-approve:${id10}`, guards: [
4189
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
4357
+ ], { mutationId: `membership-approve:${id11}`, guards: [
4358
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision, status: "paid_pending_approval" }, absent: ["memberNumber"] },
4190
4359
  { ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision, status: "pending_approval" } },
4191
4360
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
4192
4361
  ...seatActivation.guards
@@ -4285,41 +4454,41 @@ async function refundSubscriptionPayment(secretKey, input) {
4285
4454
  // src/membership-refund-authority.ts
4286
4455
  async function refundNetworkMembership(db, input, secretKey, now) {
4287
4456
  membershipClock(now);
4288
- const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
4289
- let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id10);
4457
+ const id11 = await networkMembershipId(input), entitlementId = `${id11}:primary`;
4458
+ let state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4290
4459
  requireMembershipScope(member, input);
4291
- if (member.status === "refunded") return { membershipId: id10, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
4460
+ if (member.status === "refunded") return { membershipId: id11, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
4292
4461
  if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending"].includes(String(member.status))) membershipFail("membership_refund_not_eligible");
4293
4462
  if (member.status !== "refund_pending") {
4294
4463
  const entitlement = await membershipLoad(db, "networkEntitlement", entitlementId);
4295
4464
  requireMembershipScope(entitlement, input);
4296
4465
  await db.transact([
4297
- membershipUpdate("networkMembership", id10, {
4466
+ membershipUpdate("networkMembership", id11, {
4298
4467
  status: "refund_pending",
4299
4468
  refundRequestedAt: now,
4300
4469
  revision: Number(member.revision) + 1,
4301
4470
  updatedAt: now
4302
4471
  }),
4303
4472
  membershipUpdate("networkEntitlement", entitlementId, { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }),
4304
- membershipUpdate("membershipEffect", `${id10}:refund`, {
4305
- id: `${id10}:refund`,
4473
+ membershipUpdate("membershipEffect", `${id11}:refund`, {
4474
+ id: `${id11}:refund`,
4306
4475
  chapterId: input.chapterId,
4307
4476
  environment: input.environment,
4308
4477
  runtime: input.runtime,
4309
- membershipId: id10,
4478
+ membershipId: id11,
4310
4479
  kind: "refund_membership",
4311
4480
  status: "pending",
4312
4481
  attempts: 0,
4313
4482
  createdAt: now,
4314
4483
  updatedAt: now
4315
4484
  })
4316
- ], { mutationId: `membership-refund-request:${id10}`, guards: [
4317
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
4485
+ ], { mutationId: `membership-refund-request:${id11}`, guards: [
4486
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
4318
4487
  { ns: "networkEntitlement", id: entitlementId, exists: true, equals: { revision: entitlement.revision } },
4319
4488
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
4320
4489
  ] });
4321
4490
  }
4322
- member = await membershipLoad(db, "networkMembership", id10);
4491
+ member = await membershipLoad(db, "networkMembership", id11);
4323
4492
  requireMembershipScope(member, input);
4324
4493
  if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
4325
4494
  const result = await refundSubscriptionPayment(secretKey, {
@@ -4333,23 +4502,23 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4333
4502
  if (!result.subscriptionCanceled || result.refundedCents !== member.amountCents) membershipFail("membership_refund_unconfirmed");
4334
4503
  for (let attempt = 0; attempt < 8; attempt++) {
4335
4504
  state = await loadMembershipAuthorityState(db, input);
4336
- member = await membershipLoad(db, "networkMembership", id10);
4505
+ member = await membershipLoad(db, "networkMembership", id11);
4337
4506
  requireMembershipScope(member, input);
4338
- if (member.status === "refunded") return { membershipId: id10, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
4507
+ if (member.status === "refunded") return { membershipId: id11, status: "refunded", refundedCents: Number(member.refundedCents), subscriptionCanceled: true };
4339
4508
  if (member.status !== "refund_pending") membershipFail("membership_refund_retry_required");
4340
4509
  const counted = member.founding === true && member.memberNumber !== void 0;
4341
4510
  const counts = state.cohortCounts;
4342
4511
  if (!counts || counted && (!Number.isSafeInteger(counts[String(member.cohort)]) || counts[String(member.cohort)] < 1)) membershipFail("invalid_cohort_count");
4343
4512
  try {
4344
4513
  await db.transact([
4345
- membershipUpdate("networkMembership", id10, {
4514
+ membershipUpdate("networkMembership", id11, {
4346
4515
  status: "refunded",
4347
4516
  refundedCents: result.refundedCents,
4348
4517
  autoRenew: false,
4349
4518
  revision: Number(member.revision) + 1,
4350
4519
  updatedAt: now
4351
4520
  }),
4352
- membershipUpdate("membershipEffect", `${id10}:refund`, { status: "complete", updatedAt: now }),
4521
+ membershipUpdate("membershipEffect", `${id11}:refund`, { status: "complete", updatedAt: now }),
4353
4522
  ...counted ? [membershipUpdate("membershipAuthorityState", String(state.id), {
4354
4523
  cohortCounts: { ...counts, [String(member.cohort)]: counts[String(member.cohort)] - 1 },
4355
4524
  revision: Number(state.revision) + 1,
@@ -4360,7 +4529,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4360
4529
  chapterId: input.chapterId,
4361
4530
  environment: input.environment,
4362
4531
  runtime: input.runtime,
4363
- membershipId: id10,
4532
+ membershipId: id11,
4364
4533
  seatId: member.namedSeatId,
4365
4534
  kind: "refund_seat_with_primary",
4366
4535
  status: "pending",
@@ -4368,8 +4537,8 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4368
4537
  createdAt: now,
4369
4538
  updatedAt: now
4370
4539
  })] : []
4371
- ], { mutationId: `membership-refunded:${id10}`, guards: [
4372
- { ns: "networkMembership", id: id10, exists: true, equals: { status: "refund_pending", revision: member.revision } },
4540
+ ], { mutationId: `membership-refunded:${id11}`, guards: [
4541
+ { ns: "networkMembership", id: id11, exists: true, equals: { status: "refund_pending", revision: member.revision } },
4373
4542
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4374
4543
  ] });
4375
4544
  } catch (error) {
@@ -4397,8 +4566,8 @@ async function reserveNamedMembershipSeat(db, input, now) {
4397
4566
  const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
4398
4567
  requireMembershipScope(member, input);
4399
4568
  if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
4400
- const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4401
- let seat = await membershipLoad(db, "networkSeat", id10);
4569
+ const id11 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4570
+ let seat = await membershipLoad(db, "networkSeat", id11);
4402
4571
  if (!seat) {
4403
4572
  const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4404
4573
  const purchaseEndsAt = seatPurchaseDeadline(offering, member);
@@ -4406,7 +4575,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4406
4575
  membershipFail("named_seat_purchase_unavailable");
4407
4576
  }
4408
4577
  const attrs = {
4409
- id: id10,
4578
+ id: id11,
4410
4579
  ...membershipScope(input),
4411
4580
  primaryMembershipId: memberId,
4412
4581
  recipientName: name,
@@ -4431,20 +4600,20 @@ async function reserveNamedMembershipSeat(db, input, now) {
4431
4600
  };
4432
4601
  try {
4433
4602
  await db.transact([
4434
- membershipUpdate("networkSeat", id10, attrs),
4603
+ membershipUpdate("networkSeat", id11, attrs),
4435
4604
  membershipUpdate("networkSeatRecipient", claimId, {
4436
4605
  id: claimId,
4437
4606
  appId: input.appId,
4438
4607
  chapterId: input.chapterId,
4439
4608
  environment: input.environment,
4440
4609
  runtime: input.runtime,
4441
- seatId: id10,
4610
+ seatId: id11,
4442
4611
  recipientEmailNormalized: email,
4443
4612
  createdAt: now
4444
4613
  }),
4445
- membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
4446
- ], { mutationId: `named-seat-reserve:${id10}`, guards: [
4447
- { ns: "networkSeat", id: id10, exists: false },
4614
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id11, revision: Number(member.revision) + 1, updatedAt: now })
4615
+ ], { mutationId: `named-seat-reserve:${id11}`, guards: [
4616
+ { ns: "networkSeat", id: id11, exists: false },
4448
4617
  { ns: "networkSeatRecipient", id: claimId, exists: false },
4449
4618
  { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
4450
4619
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
@@ -4452,7 +4621,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4452
4621
  } catch (error) {
4453
4622
  if (error.code !== "transact_guard_failed") throw error;
4454
4623
  }
4455
- seat = await membershipLoad(db, "networkSeat", id10);
4624
+ seat = await membershipLoad(db, "networkSeat", id11);
4456
4625
  if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
4457
4626
  }
4458
4627
  requireMembershipScope(seat, input);
@@ -4462,7 +4631,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4462
4631
 
4463
4632
  // src/membership-seat-included.ts
4464
4633
  var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
4465
- var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
4634
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean((seat.providerPaymentIntentId || seat.combinedInvoice) && seat.providerSubscriptionItemId);
4466
4635
  function namedSeatRecipientState(seat) {
4467
4636
  switch (String(seat.status)) {
4468
4637
  case "renewal_pending":
@@ -4560,7 +4729,7 @@ async function grantIncludedNamedSeat(db, input, now) {
4560
4729
  // src/membership-renewal-authority.ts
4561
4730
  async function reconcileMembershipRenewal(db, input, now) {
4562
4731
  membershipClock(now);
4563
- const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4732
+ const id11 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4564
4733
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
4565
4734
  const fact = eventRow?.fact;
4566
4735
  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({
@@ -4573,10 +4742,10 @@ async function reconcileMembershipRenewal(db, input, now) {
4573
4742
  return membershipFail("membership_renewal_evidence_pending");
4574
4743
  }
4575
4744
  for (let attempt = 0; attempt < 8; attempt++) {
4576
- const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id10);
4745
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4577
4746
  requireMembershipScope(member, input);
4578
4747
  const receipt = () => ({
4579
- membershipId: id10,
4748
+ membershipId: id11,
4580
4749
  status: String(member.status),
4581
4750
  termEndsAt: Number(member.termEndsAt),
4582
4751
  graceEndsAt: Number(member.graceEndsAt ?? 0),
@@ -4601,13 +4770,13 @@ async function reconcileMembershipRenewal(db, input, now) {
4601
4770
  const paid = fact.eventType === "invoice.payment_succeeded";
4602
4771
  if (!paid && (fact.status !== "open" || !Number.isSafeInteger(fact.amountPaidCents) || Number(fact.amountPaidCents) < 0 || Number(fact.amountPaidCents) >= amount)) membershipFail("membership_renewal_failure_mismatch");
4603
4772
  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");
4604
- const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
4773
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
4605
4774
  requireMembershipScope(entitlement, input);
4606
4775
  const graceEndsAt = paid ? 0 : start + 14 * 864e5;
4607
4776
  const status2 = paid ? member.journeyApprovedAt ? member.autoRenew ? "active" : "cancel_at_term_end" : "paid_pending_approval" : now < graceEndsAt ? "payment_grace" : "lapsed";
4608
4777
  const entitlementStatus = member.journeyApprovedAt && (paid || now < graceEndsAt) ? "active" : member.journeyApprovedAt ? "closed" : "pending_approval";
4609
4778
  const ops = [
4610
- membershipUpdate("networkMembership", id10, {
4779
+ membershipUpdate("networkMembership", id11, {
4611
4780
  status: status2,
4612
4781
  graceEndsAt,
4613
4782
  ...paid ? {
@@ -4618,7 +4787,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4618
4787
  revision: Number(member.revision) + 1,
4619
4788
  updatedAt: now
4620
4789
  }),
4621
- membershipUpdate("networkEntitlement", `${id10}:primary`, {
4790
+ membershipUpdate("networkEntitlement", `${id11}:primary`, {
4622
4791
  status: entitlementStatus,
4623
4792
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4624
4793
  sourceEventId: input.eventId,
@@ -4627,8 +4796,8 @@ async function reconcileMembershipRenewal(db, input, now) {
4627
4796
  })
4628
4797
  ];
4629
4798
  const guards = [
4630
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
4631
- { ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
4799
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
4800
+ { ns: "networkEntitlement", id: `${id11}:primary`, exists: true, equals: { revision: entitlement.revision } },
4632
4801
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
4633
4802
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
4634
4803
  evidenceGuard
@@ -4653,15 +4822,15 @@ async function reconcileMembershipRenewal(db, input, now) {
4653
4822
  );
4654
4823
  }
4655
4824
  try {
4656
- await db.transact(ops, { mutationId: `membership-renewal:${id10}:${input.eventId}`, guards });
4825
+ await db.transact(ops, { mutationId: `membership-renewal:${id11}:${input.eventId}`, guards });
4657
4826
  } catch (error) {
4658
4827
  if (error.code !== "transact_guard_failed") throw error;
4659
4828
  continue;
4660
4829
  }
4661
- const saved = await membershipLoad(db, "networkMembership", id10);
4830
+ const saved = await membershipLoad(db, "networkMembership", id11);
4662
4831
  requireMembershipScope(saved, input);
4663
4832
  return {
4664
- membershipId: id10,
4833
+ membershipId: id11,
4665
4834
  status: String(saved.status),
4666
4835
  termEndsAt: Number(saved.termEndsAt),
4667
4836
  graceEndsAt: Number(saved.graceEndsAt ?? 0),
@@ -4687,12 +4856,12 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4687
4856
  }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
4688
4857
  const sk = await getVaultSecret(db, stripeSecretName);
4689
4858
  if (!sk) membershipFail("membership_billing_provider_unavailable");
4690
- const id10 = await networkMembershipId(input);
4859
+ const id11 = await networkMembershipId(input);
4691
4860
  for (let attempt = 0; attempt < 8; attempt++) {
4692
- const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id10);
4861
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4693
4862
  requireMembershipScope(member, input);
4694
4863
  const receipt = (value = member) => ({
4695
- membershipId: id10,
4864
+ membershipId: id11,
4696
4865
  status: String(value.status),
4697
4866
  termEndsAt: Number(value.termEndsAt),
4698
4867
  graceEndsAt: Number(value.graceEndsAt ?? 0),
@@ -4713,7 +4882,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4713
4882
  });
4714
4883
  await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4715
4884
  await refundNetworkMembership(db, input, sk, now);
4716
- const saved2 = await membershipLoad(db, "networkMembership", id10);
4885
+ const saved2 = await membershipLoad(db, "networkMembership", id11);
4717
4886
  requireMembershipScope(saved2, input);
4718
4887
  return receipt(saved2);
4719
4888
  }
@@ -4728,18 +4897,18 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4728
4897
  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";
4729
4898
  const graceEndsAt = autoRenew ? Number(member.graceEndsAt ?? 0) : 0;
4730
4899
  if (member.autoRenew === autoRenew && member.status === status2 && Number(member.graceEndsAt ?? 0) === graceEndsAt) return receipt();
4731
- const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
4900
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
4732
4901
  requireMembershipScope(entitlement, input);
4733
4902
  try {
4734
4903
  await db.transact([
4735
- membershipUpdate("networkMembership", id10, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
4904
+ membershipUpdate("networkMembership", id11, { status: status2, autoRenew, graceEndsAt, revision: Number(member.revision) + 1, updatedAt: now }),
4736
4905
  membershipUpdate("networkEntitlement", String(entitlement.id), {
4737
4906
  status: status2 === "lapsed" ? "closed" : entitlement.status,
4738
4907
  revision: Number(entitlement.revision) + 1,
4739
4908
  updatedAt: now
4740
4909
  })
4741
4910
  ], { guards: [
4742
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
4911
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
4743
4912
  { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
4744
4913
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4745
4914
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
@@ -4749,7 +4918,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4749
4918
  if (error.code !== "transact_guard_failed") throw error;
4750
4919
  continue;
4751
4920
  }
4752
- const saved = await membershipLoad(db, "networkMembership", id10);
4921
+ const saved = await membershipLoad(db, "networkMembership", id11);
4753
4922
  requireMembershipScope(saved, input);
4754
4923
  return receipt(saved);
4755
4924
  }
@@ -4810,19 +4979,19 @@ async function stopPreviousSubscription(member, target, secret) {
4810
4979
  async function prepareMembershipRestart(db, input, now, stripeSecret) {
4811
4980
  membershipClock(now);
4812
4981
  if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
4813
- const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4814
- const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
4982
+ const id11 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4983
+ const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id11, input.previousSubscriptionId]))}`;
4815
4984
  const state = await loadMembershipAuthorityState(db, input);
4816
- const previous = await membershipLoad(db, "networkMembership", id10);
4985
+ const previous = await membershipLoad(db, "networkMembership", id11);
4817
4986
  requireMembershipScope(previous, input);
4818
4987
  const receipt = {
4819
4988
  applicationId: input.applicationId,
4820
- membershipId: id10,
4989
+ membershipId: id11,
4821
4990
  restartId,
4822
4991
  memberNumber: Number(previous.memberNumber),
4823
4992
  status: "ready"
4824
4993
  };
4825
- const existing = await membershipLoad(db, "membershipRestart", id10);
4994
+ const existing = await membershipLoad(db, "membershipRestart", id11);
4826
4995
  if (existing?.restartId === restartId && existing.status === "ready") {
4827
4996
  requireMembershipScope(existing, input);
4828
4997
  return receipt;
@@ -4838,33 +5007,33 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
4838
5007
  membershipUpdate("membershipPurchaseHistory", restartId, {
4839
5008
  id: restartId,
4840
5009
  ...membershipScope(input),
4841
- membershipId: id10,
5010
+ membershipId: id11,
4842
5011
  providerSubscriptionId: input.previousSubscriptionId,
4843
5012
  memberSnapshot: previous,
4844
5013
  bindingSnapshot: binding2,
4845
5014
  archivedAt: now
4846
5015
  }),
4847
- membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
5016
+ membershipUpdate("networkMembership", id11, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
4848
5017
  ], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
4849
5018
  ...seats.guards,
4850
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
5019
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: previous.revision, status: previous.status } },
4851
5020
  { ns: "membershipPurchaseHistory", id: restartId, exists: false },
4852
5021
  { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
4853
5022
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4854
5023
  ] });
4855
5024
  } else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
4856
- const reserved = await membershipLoad(db, "networkMembership", id10);
5025
+ const reserved = await membershipLoad(db, "networkMembership", id11);
4857
5026
  requireMembershipScope(reserved, input);
4858
5027
  if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
4859
5028
  await stopPreviousSubscription(reserved, input, stripeSecret);
4860
5029
  const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4861
5030
  requireMembershipScope(binding, input);
4862
5031
  await db.transact([
4863
- membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
5032
+ membershipUpdate("membershipRestart", id11, { id: id11, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
4864
5033
  { t: "delete", ns: "membershipOfferCheckout", id: bindingId }
4865
5034
  ], { mutationId: `membership-restart-ready:${restartId}`, guards: [
4866
- { ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
4867
- { ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
5035
+ { ns: "membershipRestart", id: id11, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
5036
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
4868
5037
  { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
4869
5038
  ] });
4870
5039
  return receipt;
@@ -4874,15 +5043,15 @@ async function prepareMembershipRestart(db, input, now, stripeSecret) {
4874
5043
  async function readMembershipAccess(db, input, now) {
4875
5044
  membershipClock(now);
4876
5045
  await loadMembershipAuthorityState(db, input);
4877
- const id10 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id10);
4878
- if (!member) return { membershipId: id10, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
5046
+ const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
5047
+ if (!member) return { membershipId: id11, allowed: false, status: "unpaid", accessEndsAt: 0, revision: 0 };
4879
5048
  requireMembershipScope(member, input);
4880
- const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
5049
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id11}:primary`);
4881
5050
  requireMembershipScope(entitlement, input);
4882
5051
  const endsAt = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
4883
- 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;
5052
+ 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;
4884
5053
  return {
4885
- membershipId: id10,
5054
+ membershipId: id11,
4886
5055
  allowed: allowed2,
4887
5056
  status: String(member.status),
4888
5057
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
@@ -4937,7 +5106,7 @@ async function subscriptionForSeat(sk, seat) {
4937
5106
  currency: seat.currency,
4938
5107
  interval: seat.interval,
4939
5108
  environment: seat.environment
4940
- }, Boolean(seat.providerPaymentIntentId));
5109
+ }, Boolean(seat.providerPaymentIntentId || seat.combinedInvoice));
4941
5110
  if (!productId) reject3();
4942
5111
  const sub = await read(sk, `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}?expand[]=discounts`);
4943
5112
  const items = sub.items?.data;
@@ -4981,10 +5150,14 @@ async function createNamedSeatStripePayment(sk, seat, now) {
4981
5150
  return { paymentIntentId: pi.id, clientSecret: pi.client_secret, status: pi.status };
4982
5151
  }
4983
5152
  async function attachNamedSeatStripeRenewal(sk, seat) {
4984
- if (!seat.providerPaymentIntentId || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject3();
5153
+ if (!seat.providerPaymentIntentId && !seat.combinedInvoice || !["renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) reject3();
4985
5154
  const sub = await subscriptionForSeat(sk, seat);
4986
5155
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
4987
- if (existing) return String(existing.id);
5156
+ if (existing) {
5157
+ if (!/^si_[A-Za-z0-9_]+$/.test(existing.id)) reject3();
5158
+ return String(existing.id);
5159
+ }
5160
+ if (seat.combinedInvoice) reject3();
4988
5161
  const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
4989
5162
  subscription: seat.providerSubscriptionId,
4990
5163
  price: seat.priceId,
@@ -5202,7 +5375,7 @@ async function recordRecipient(db, input, now, approve) {
5202
5375
  const state = await loadMembershipAuthorityState(db, input);
5203
5376
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
5204
5377
  requireMembershipScope(seat, input);
5205
- if (!seat.purchaserUserId || seat.purchaserUserId === input.recipientUserId) membershipFail("named_seat_recipient_must_differ_from_purchaser");
5378
+ if (!seat.purchaserUserId && !seat.purchaserEmailNormalized || seat.purchaserUserId === input.recipientUserId || seat.purchaserEmailNormalized === normalizeChapterEmail(input.verifiedRecipientEmail)) membershipFail("named_seat_recipient_must_differ_from_purchaser");
5206
5379
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5207
5380
  requireMembershipScope(member, input);
5208
5381
  const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
@@ -5260,43 +5433,121 @@ async function recordRecipient(db, input, now, approve) {
5260
5433
  }
5261
5434
 
5262
5435
  // src/membership-seat-refund-stripe.ts
5436
+ var import_stripe9 = require("@odla-ai/stripe");
5437
+
5438
+ // src/membership-checkout-seat-refund.ts
5263
5439
  var import_stripe8 = require("@odla-ai/stripe");
5264
5440
  var fail4 = (code = "named_seat_refund_mismatch") => {
5265
5441
  throw Object.assign(new Error(code), { code });
5266
5442
  };
5443
+ var id8 = (value) => typeof value === "string" ? value : value?.id ?? "";
5444
+ async function refundCheckoutNamedSeat(secretKey, seat) {
5445
+ const invoiceRef = seat.combinedInvoice;
5446
+ 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();
5447
+ const call = async (method, path, params, key3) => {
5448
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5449
+ if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
5450
+ return result.body;
5451
+ };
5452
+ const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId };
5453
+ const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
5454
+ 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();
5455
+ const invoice = await call("GET", `/v1/invoices/${encodeURIComponent(invoiceRef.invoiceId)}`);
5456
+ 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();
5457
+ let chargeId = id8(invoice.charge), paymentId = id8(invoice.payment_intent);
5458
+ if (!chargeId && !paymentId) {
5459
+ const payments = await call("GET", "/v1/invoice_payments", { invoice: invoice.id, status: "paid", limit: 100 });
5460
+ if (!Array.isArray(payments.data) || payments.data.length !== 1 || payments.has_more !== false) fail4();
5461
+ const payment = payments.data[0];
5462
+ if (id8(payment.invoice) !== invoice.id || payment.status !== "paid" || payment.payment?.type !== "payment_intent") fail4();
5463
+ paymentId = id8(payment.payment.payment_intent);
5464
+ }
5465
+ if (!chargeId) {
5466
+ if (!/^pi_[A-Za-z0-9_]+$/.test(paymentId)) fail4();
5467
+ const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentId)}`);
5468
+ 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();
5469
+ chargeId = id8(payment.latest_charge);
5470
+ }
5471
+ if (!/^ch_[A-Za-z0-9_]+$/.test(chargeId)) fail4();
5472
+ const charge = await call("GET", `/v1/charges/${encodeURIComponent(chargeId)}`);
5473
+ 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();
5474
+ const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
5475
+ if (items.length > 1) fail4();
5476
+ if (items.length && sub.status !== "canceled") {
5477
+ const item = items[0];
5478
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.quantity !== 1 || item.price?.id !== seat.priceId) fail4();
5479
+ const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
5480
+ if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
5481
+ }
5482
+ if (charge.amount_refunded === charge.amount && charge.refunded === true) return { refundedCents: seat.amountCents, refundIds: [] };
5483
+ const list = await call("GET", "/v1/refunds", { charge: chargeId, limit: 100 });
5484
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
5485
+ let refundedCents = 0;
5486
+ const refundIds = [];
5487
+ for (const refund of list.data) {
5488
+ 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();
5489
+ if (["pending", "requires_action"].includes(refund.status)) fail4("named_seat_refund_pending");
5490
+ if (refund.status === "succeeded") {
5491
+ if (refund.metadata?.seatId !== seat.id || refund.metadata?.membershipId !== seat.primaryMembershipId) fail4("named_seat_refund_allocation_ambiguous");
5492
+ refundedCents += refund.amount;
5493
+ refundIds.push(refund.id);
5494
+ }
5495
+ }
5496
+ if (refundedCents !== charge.amount_refunded || refundedCents > seat.amountCents) fail4();
5497
+ if (refundedCents < seat.amountCents) {
5498
+ const remaining = seat.amountCents - refundedCents;
5499
+ const refund = await call("POST", "/v1/refunds", {
5500
+ charge: chargeId,
5501
+ amount: remaining,
5502
+ reason: "requested_by_customer",
5503
+ metadata: { ...metadata, membershipId: seat.primaryMembershipId, seatId: seat.id }
5504
+ }, `combined-seat-refund:${seat.id}:${remaining}`);
5505
+ if (id8(refund.charge) !== chargeId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
5506
+ if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
5507
+ refundedCents += remaining;
5508
+ refundIds.push(refund.id);
5509
+ }
5510
+ return { refundedCents, refundIds };
5511
+ }
5512
+
5513
+ // src/membership-seat-refund-stripe.ts
5514
+ var fail5 = (code = "named_seat_refund_mismatch") => {
5515
+ throw Object.assign(new Error(code), { code });
5516
+ };
5267
5517
  async function refundNamedSeatStripePayment(secretKey, seat) {
5268
- if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
5518
+ if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
5519
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
5269
5520
  const call = async (method, path, params, idempotencyKey) => {
5270
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5271
- if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
5521
+ const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5522
+ if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
5272
5523
  return result.body;
5273
5524
  };
5274
5525
  const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
5275
5526
  const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
5276
- 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();
5527
+ 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();
5277
5528
  const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
5278
- 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();
5529
+ 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();
5279
5530
  const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
5280
- if (items.length > 1) fail4();
5531
+ if (items.length > 1) fail5();
5281
5532
  if (items.length && sub.status !== "canceled") {
5282
5533
  const item = items[0];
5283
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail4();
5534
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail5();
5284
5535
  const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
5285
- if (removed.id !== item.id || removed.deleted !== true) fail4("named_seat_renewal_removal_unconfirmed");
5536
+ if (removed.id !== item.id || removed.deleted !== true) fail5("named_seat_renewal_removal_unconfirmed");
5286
5537
  }
5287
5538
  const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
5288
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail4("named_seat_refund_history_incomplete");
5539
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail5("named_seat_refund_history_incomplete");
5289
5540
  let refundedCents = 0, pendingCents = 0;
5290
5541
  const refundIds = [];
5291
5542
  for (const refund of list.data) {
5292
- 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();
5543
+ 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();
5293
5544
  if (refund.status === "succeeded") {
5294
5545
  refundedCents += refund.amount;
5295
5546
  refundIds.push(refund.id);
5296
5547
  } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
5297
5548
  }
5298
- if (refundedCents + pendingCents > seat.amountCents) fail4();
5299
- if (pendingCents) fail4("named_seat_refund_pending");
5549
+ if (refundedCents + pendingCents > seat.amountCents) fail5();
5550
+ if (pendingCents) fail5("named_seat_refund_pending");
5300
5551
  if (refundedCents < seat.amountCents) {
5301
5552
  const remaining = seat.amountCents - refundedCents;
5302
5553
  const refund = await call("POST", "/v1/refunds", {
@@ -5305,8 +5556,8 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
5305
5556
  reason: "requested_by_customer",
5306
5557
  metadata
5307
5558
  }, `named-seat-refund:${seat.id}:${remaining}`);
5308
- if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail4();
5309
- if (refund.status !== "succeeded") fail4("named_seat_refund_pending");
5559
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail5();
5560
+ if (refund.status !== "succeeded") fail5("named_seat_refund_pending");
5310
5561
  refundedCents += remaining;
5311
5562
  refundIds.push(refund.id);
5312
5563
  }
@@ -5322,7 +5573,7 @@ async function requestNamedSeatRefund(db, input, now) {
5322
5573
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5323
5574
  requireMembershipScope(member, input);
5324
5575
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
5325
- 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");
5576
+ 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");
5326
5577
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
5327
5578
  await db.transact([
5328
5579
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -5427,14 +5678,14 @@ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
5427
5678
  async function readNamedSeatOffer(db, input, now) {
5428
5679
  membershipClock(now);
5429
5680
  await loadMembershipAuthorityState(db, input);
5430
- const id10 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id10);
5681
+ const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
5431
5682
  requireMembershipScope(member, input);
5432
5683
  const offering = membershipOfferingVersion(member.offeringSnapshot);
5433
5684
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
5434
5685
  if (seat) requireMembershipScope(seat, input);
5435
5686
  const purchaseEndsAt = seatPurchaseDeadline(offering, member);
5436
5687
  const offer = {
5437
- membershipId: id10,
5688
+ membershipId: id11,
5438
5689
  amountCents: offering.seatPriceCents,
5439
5690
  currency: offering.currency,
5440
5691
  interval: offering.interval,
@@ -5584,7 +5835,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5584
5835
  };
5585
5836
  if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
5586
5837
  if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
5587
- const checkout = {
5838
+ const checkout2 = {
5588
5839
  ...target,
5589
5840
  priceId: peer.seatPriceId,
5590
5841
  recipientName: String(input.recipientName),
@@ -5592,7 +5843,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5592
5843
  purchaserEmail: String(input.purchaserEmail),
5593
5844
  purchaserUserId: String(input.purchaserUserId)
5594
5845
  };
5595
- if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
5846
+ if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout2, now))) return grantIncludedNamedSeat(db, checkout2, now);
5596
5847
  const secret = await getVaultSecret(db, stripeSecretName);
5597
5848
  if (!secret) throw new Error("network Stripe provider is unavailable");
5598
5849
  if (action === "seat-decline") {
@@ -5608,7 +5859,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5608
5859
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
5609
5860
  }
5610
5861
  if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
5611
- if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
5862
+ if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout2, secret, now);
5612
5863
  const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
5613
5864
  if (action === "seat-payments") {
5614
5865
  const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
@@ -5679,9 +5930,9 @@ function createMembershipAuthorityHandler(options) {
5679
5930
  const identities = /* @__PURE__ */ new Set();
5680
5931
  for (const peer of peers) {
5681
5932
  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");
5682
- const identity = `${peer.appId}:${peer.runtime}`;
5683
- if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
5684
- identities.add(identity);
5933
+ const identity2 = `${peer.appId}:${peer.runtime}`;
5934
+ if (identities.has(identity2)) throw new TypeError("ambiguous authority peer");
5935
+ identities.add(identity2);
5685
5936
  if (peer.seatPriceId !== void 0 && !/^price_[A-Za-z0-9]+$/.test(peer.seatPriceId)) throw new TypeError("invalid seat Price");
5686
5937
  }
5687
5938
  return async (req) => {
@@ -5712,9 +5963,14 @@ function createMembershipAuthorityHandler(options) {
5712
5963
  try {
5713
5964
  input = JSON.parse(verified.body);
5714
5965
  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] ?? []];
5715
- 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])))) {
5966
+ 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])))) {
5716
5967
  throw new TypeError("invalid request");
5717
5968
  }
5969
+ if (action === "checkouts" && input.namedSeat !== void 0) {
5970
+ const seat = input.namedSeat;
5971
+ 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");
5972
+ checkoutNamedSeatSelection(seat, seat.purchaserEmail);
5973
+ }
5718
5974
  } catch {
5719
5975
  return respond({ error: "invalid_offer_request" }, 400);
5720
5976
  }
@@ -5787,8 +6043,9 @@ function createMembershipAuthorityHandler(options) {
5787
6043
  offerId: String(input.offerId),
5788
6044
  offerDigest: String(input.offerDigest),
5789
6045
  quoteDigest: String(input.quoteDigest),
5790
- priceId: String(input.priceId)
5791
- }, options.now?.() ?? Date.now());
6046
+ priceId: String(input.priceId),
6047
+ ...input.namedSeat ? { namedSeat: input.namedSeat } : {}
6048
+ }, options.now?.() ?? Date.now(), peer.seatPriceId);
5792
6049
  return respond({ ok: true, binding });
5793
6050
  }
5794
6051
  const offerId = await membershipOfferIdForRequest({ ...peer, applicationId: String(input.applicationId), requestId: String(input.requestId) });
@@ -5891,7 +6148,8 @@ async function requestMembershipCheckoutBinding(options) {
5891
6148
  offerId: options.offerId,
5892
6149
  offerDigest: options.offerDigest,
5893
6150
  quoteDigest: options.quoteDigest,
5894
- priceId: options.priceId
6151
+ priceId: options.priceId,
6152
+ ...options.namedSeat ? { namedSeat: options.namedSeat } : {}
5895
6153
  });
5896
6154
  return verifyMembershipCheckoutBinding(result.binding, options);
5897
6155
  }
@@ -6101,7 +6359,7 @@ async function requestMembershipCancellation(options) {
6101
6359
  }
6102
6360
 
6103
6361
  // src/membership-seat-cancel.ts
6104
- var import_stripe9 = require("@odla-ai/stripe");
6362
+ var import_stripe10 = require("@odla-ai/stripe");
6105
6363
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6106
6364
  membershipClock(now);
6107
6365
  const state = await loadMembershipAuthorityState(db, target);
@@ -6129,7 +6387,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6129
6387
  }
6130
6388
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
6131
6389
  const request = async (method, path, params) => {
6132
- const result = await (0, import_stripe9.stripeRequest)(
6390
+ const result = await (0, import_stripe10.stripeRequest)(
6133
6391
  { secretKey },
6134
6392
  method,
6135
6393
  path,
@@ -6142,7 +6400,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6142
6400
  let payment;
6143
6401
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
6144
6402
  else {
6145
- const snapshot = await listStripeObjects((0, import_stripe9.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
6403
+ const snapshot = await listStripeObjects((0, import_stripe10.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
6146
6404
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
6147
6405
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
6148
6406
  payment = matches2[0];
@@ -6155,9 +6413,9 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6155
6413
  if (payment.status === "succeeded" || payment.amount_received > 0) throw new Error("paid seat requires payment evidence and full refund");
6156
6414
  if (payment.status !== "canceled") {
6157
6415
  if (!["requires_payment_method", "requires_confirmation", "requires_action", "processing"].includes(payment.status)) throw new Error("seat payment outcome unresolved");
6158
- const id10 = payment.id;
6159
- payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id10)}/cancel`, { cancellation_reason: "abandoned" });
6160
- if (payment.id !== id10) throw new Error("seat cancellation receipt mismatch");
6416
+ const id11 = payment.id;
6417
+ payment = await request("POST", `/v1/payment_intents/${encodeURIComponent(id11)}/cancel`, { cancellation_reason: "abandoned" });
6418
+ if (payment.id !== id11) throw new Error("seat cancellation receipt mismatch");
6161
6419
  }
6162
6420
  if (!valid(payment) || payment.status !== "canceled" || payment.amount_received !== 0) throw new Error("seat cancellation unconfirmed");
6163
6421
  await db.transact([membershipUpdate("networkSeat", target.seatId, { status: "canceled", revision: Number(seat.revision) + 1, updatedAt: now })], {
@@ -6257,11 +6515,11 @@ async function drainMembershipEffects(db, options, now) {
6257
6515
 
6258
6516
  // src/schema-membership-offers.ts
6259
6517
  var attr8 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
6260
- var id8 = () => attr8("string", false, true, true);
6518
+ var id9 = () => attr8("string", false, true, true);
6261
6519
  var key = () => attr8("string", false, false, true);
6262
6520
  var membershipOfferEntities = {
6263
6521
  membershipAuthorityState: { attrs: {
6264
- id: id8(),
6522
+ id: id9(),
6265
6523
  chapterId: key(),
6266
6524
  environment: key(),
6267
6525
  mode: key(),
@@ -6271,7 +6529,7 @@ var membershipOfferEntities = {
6271
6529
  updatedAt: attr8("number")
6272
6530
  } },
6273
6531
  membershipCheckoutOffer: { attrs: {
6274
- id: id8(),
6532
+ id: id9(),
6275
6533
  applicationId: key(),
6276
6534
  appId: key(),
6277
6535
  chapterId: key(),
@@ -6286,7 +6544,7 @@ var membershipOfferEntities = {
6286
6544
  authorityRevision: attr8("number")
6287
6545
  } },
6288
6546
  membershipOfferCheckout: { attrs: {
6289
- id: id8(),
6547
+ id: id9(),
6290
6548
  appId: key(),
6291
6549
  chapterId: key(),
6292
6550
  environment: key(),
@@ -6297,20 +6555,21 @@ var membershipOfferEntities = {
6297
6555
  offerDigest: attr8("string"),
6298
6556
  quoteDigest: attr8("string"),
6299
6557
  priceId: attr8("string"),
6300
- boundAt: attr8("number")
6558
+ boundAt: attr8("number"),
6559
+ namedSeat: attr8("json", true)
6301
6560
  } }
6302
6561
  };
6303
6562
  var membershipOfferRules = Object.fromEntries(Object.keys(membershipOfferEntities).map((ns) => [ns, { view: "false", create: "false", update: "false", delete: "false" }]));
6304
6563
 
6305
6564
  // src/schema-membership-authority.ts
6306
6565
  var attr9 = (type, optional = false, unique = false, indexed = false) => ({ type, optional, unique, indexed });
6307
- var id9 = () => attr9("string", false, true, true);
6566
+ var id10 = () => attr9("string", false, true, true);
6308
6567
  var key2 = (optional = false) => attr9("string", optional, false, true);
6309
6568
  var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
6310
6569
  var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
6311
6570
  var membershipAuthorityEntities = {
6312
6571
  networkMembership: { attrs: {
6313
- id: id9(),
6572
+ id: id10(),
6314
6573
  membershipId: key2(),
6315
6574
  ...scope(),
6316
6575
  personId: key2(true),
@@ -6350,7 +6609,7 @@ var membershipAuthorityEntities = {
6350
6609
  restartId: key2(true)
6351
6610
  } },
6352
6611
  membershipRestart: { attrs: {
6353
- id: id9(),
6612
+ id: id10(),
6354
6613
  ...scope(),
6355
6614
  membershipId: key2(),
6356
6615
  restartId: key2(),
@@ -6360,7 +6619,7 @@ var membershipAuthorityEntities = {
6360
6619
  createdAt: attr9("number")
6361
6620
  } },
6362
6621
  membershipPurchaseHistory: { attrs: {
6363
- id: id9(),
6622
+ id: id10(),
6364
6623
  ...scope(),
6365
6624
  membershipId: key2(),
6366
6625
  providerSubscriptionId: key2(),
@@ -6369,7 +6628,7 @@ var membershipAuthorityEntities = {
6369
6628
  archivedAt: attr9("number")
6370
6629
  } },
6371
6630
  networkEntitlement: { attrs: {
6372
- id: id9(),
6631
+ id: id10(),
6373
6632
  ...scope(),
6374
6633
  membershipId: key2(),
6375
6634
  seatId: key2(true),
@@ -6386,10 +6645,12 @@ var membershipAuthorityEntities = {
6386
6645
  journeyApprovedAt: attr9("number", true)
6387
6646
  } },
6388
6647
  networkSeat: { attrs: {
6389
- id: id9(),
6648
+ id: id10(),
6390
6649
  ...scope(),
6391
6650
  primaryMembershipId: key2(),
6392
- purchaserUserId: key2(),
6651
+ purchaserUserId: key2(true),
6652
+ purchaserEmailNormalized: key2(true),
6653
+ combinedInvoice: attr9("json", true),
6393
6654
  recipientName: attr9("string"),
6394
6655
  recipientEmailNormalized: key2(),
6395
6656
  recipientClaimId: key2(),
@@ -6421,9 +6682,9 @@ var membershipAuthorityEntities = {
6421
6682
  refundedCents: attr9("number", true),
6422
6683
  providerRefundIds: attr9("json", true)
6423
6684
  } },
6424
- networkSeatRecipient: { attrs: { id: id9(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
6685
+ networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
6425
6686
  membershipEffect: { attrs: {
6426
- id: id9(),
6687
+ id: id10(),
6427
6688
  chapterId: key2(),
6428
6689
  environment: key2(),
6429
6690
  runtime: key2(),
@@ -6548,14 +6809,14 @@ function validateMemberContentDocument(input, options = {}) {
6548
6809
  continue;
6549
6810
  }
6550
6811
  unknownKeys(raw2, PAGE_KEYS, path, errors);
6551
- const id10 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
6812
+ const id11 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
6552
6813
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
6553
6814
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
6554
- if (id10 && !ID.test(id10)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6815
+ if (id11 && !ID.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6555
6816
  if (slug && !SLUG8.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
6556
- if (ids2.has(id10) || knownParentIds.has(id10)) errors.push(`${path}.id duplicates page "${id10}"`);
6817
+ if (ids2.has(id11) || knownParentIds.has(id11)) errors.push(`${path}.id duplicates page "${id11}"`);
6557
6818
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
6558
- ids2.add(id10);
6819
+ ids2.add(id11);
6559
6820
  slugs.add(slug);
6560
6821
  const navRaw = record(raw2.navigation);
6561
6822
  let navigation = { section: "General", order: index2 };
@@ -6611,7 +6872,7 @@ function validateMemberContentDocument(input, options = {}) {
6611
6872
  else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
6612
6873
  }
6613
6874
  }
6614
- pages.push({ id: id10, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
6875
+ pages.push({ id: id11, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
6615
6876
  }
6616
6877
  const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids2]);
6617
6878
  for (const [index2, page] of pages.entries()) {
@@ -6817,8 +7078,8 @@ function resourceValue(value, path, errors) {
6817
7078
  return null;
6818
7079
  }
6819
7080
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
6820
- const id10 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
6821
- if (id10 && !ID2.test(id10)) errors.push(`${path}.id must be lowercase kebab-case`);
7081
+ const id11 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
7082
+ if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
6822
7083
  const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
6823
7084
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
6824
7085
  const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
@@ -6875,7 +7136,7 @@ function resourceValue(value, path, errors) {
6875
7136
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
6876
7137
  ...promotedFrom ? { promotedFrom } : {}
6877
7138
  };
6878
- return { id: id10, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
7139
+ return { id: id11, kind, title, ...summary ? { summary } : {}, tags, scope: scope2, status: status2, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
6879
7140
  }
6880
7141
  function validateBuilderLibraryDocument(value, options = {}) {
6881
7142
  const errors = [];
@@ -6906,7 +7167,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
6906
7167
  const inputs = Array.isArray(raw2.resources) ? raw2.resources : [];
6907
7168
  if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
6908
7169
  const resources = inputs.map((item, index2) => resourceValue(item, `document.resources[${index2}]`, errors)).filter(Boolean);
6909
- const ids2 = resources.map(({ id: id10 }) => id10);
7170
+ const ids2 = resources.map(({ id: id11 }) => id11);
6910
7171
  if (new Set(ids2).size !== ids2.length) errors.push("document.resources ids must be unique");
6911
7172
  for (const resource of resources) {
6912
7173
  if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
@@ -6993,6 +7254,41 @@ async function createBuilderLibraryEnvelope(document) {
6993
7254
  return { document, digest: await builderLibraryDigest(document) };
6994
7255
  }
6995
7256
 
7257
+ // src/email-retirement.ts
7258
+ function isRetiredEmailCopy(subject, text6) {
7259
+ const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
7260
+ const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7261
+ return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
7262
+ }
7263
+
7264
+ // src/email.ts
7265
+ var import_crm5 = require("@odla-ai/crm");
7266
+
7267
+ // src/email-events.ts
7268
+ var identity = ["firstName", "lastName", "email", "phone", "state"];
7269
+ var policy = ["refundPolicyText", "commitmentText", "normsText"];
7270
+ var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
7271
+ var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
7272
+ var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
7273
+ var CHAPTER_EMAIL_EVENTS = {
7274
+ adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
7275
+ submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
7276
+ paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
7277
+ prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
7278
+ onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
7279
+ namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
7280
+ seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
7281
+ };
7282
+ function chapterEmailEvents(chapter) {
7283
+ const base = chapter.mode === "hub" ? Object.fromEntries(Object.entries(chapter.crm.config.templates ?? {}).map(([key3, spec]) => [key3, {
7284
+ label: key3,
7285
+ trigger: "Administrator sends a message",
7286
+ audience: "Selected contact",
7287
+ variables: spec.vars
7288
+ }])) : CHAPTER_EMAIL_EVENTS;
7289
+ return { ...base, ...chapter.config.emails?.events };
7290
+ }
7291
+
6996
7292
  // src/email.ts
6997
7293
  function render(template, vars) {
6998
7294
  return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
@@ -7016,19 +7312,158 @@ function isAlreadySent(priorRows) {
7016
7312
  function planDelivery(input) {
7017
7313
  const tpl = input.group.emailTemplates?.[input.template];
7018
7314
  if (!tpl) return { deliver: false, reason: "template-missing" };
7019
- if (tpl.enabled === false && !input.force) return { deliver: false, reason: "disabled" };
7315
+ if (typeof tpl.enabled !== "boolean") return { deliver: false, reason: "template-invalid", issues: ["Choose explicitly whether this email is enabled."] };
7316
+ if (tpl.enabled !== true && !input.force) return { deliver: false, reason: "disabled" };
7317
+ if (!/^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(input.to)) return { deliver: false, reason: "recipient-invalid" };
7020
7318
  const vars = groupVars(input.group, input.vars);
7319
+ if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
7320
+ const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
7321
+ const issues = (0, import_crm5.validateEmailCopy)(tpl, {
7322
+ allowedVariables: event2?.variables,
7323
+ optionalVariables: event2?.optionalVariables,
7324
+ requiredVariables: event2?.requiresTier ? ["tierName"] : [],
7325
+ values: vars,
7326
+ tierId: vars.tierId
7327
+ });
7328
+ if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
7021
7329
  const isProd = input.envName === "prod";
7022
7330
  const redirect = !isProd && !!input.group.debugEmail;
7023
7331
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
7024
7332
  const to = redirect ? input.group.debugEmail : input.to;
7025
- const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
7333
+ const renderedSubject = render(tpl.subject, vars);
7334
+ const renderedText = render(tpl.text, vars);
7335
+ if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
7336
+ if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7337
+ const subject = (redirect ? "[dev] " : "") + renderedSubject;
7026
7338
  const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
7027
7339
 
7028
- ` + render(tpl.text, vars) : render(tpl.text, vars);
7340
+ ` + renderedText : renderedText;
7029
7341
  return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
7030
7342
  }
7031
7343
 
7344
+ // src/email-readiness.ts
7345
+ var import_crm6 = require("@odla-ai/crm");
7346
+ function validateEmailReadiness(input) {
7347
+ const issues = [];
7348
+ for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
7349
+ issues.push({ code: "event-undeclared", message: `Stored email ${key3} has no declared event. Review its trigger before using it.` });
7350
+ }
7351
+ if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
7352
+ for (const tier of input.tiers ?? []) {
7353
+ if (!(0, import_crm6.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
7354
+ }
7355
+ const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm6.isEmailTierName)(tier.name, tier.id));
7356
+ if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
7357
+ const events2 = Object.entries(input.events).map(([key3, event2]) => {
7358
+ const template = input.templates[key3];
7359
+ const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm6.validateEmailCopy)(template, { allowedVariables: event2.variables });
7360
+ if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
7361
+ if (template) {
7362
+ const used = [...`${template.subject ?? ""}
7363
+ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
7364
+ for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
7365
+ problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
7366
+ }
7367
+ }
7368
+ if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
7369
+ else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
7370
+ return { ...event2, key: key3, status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready", issues: problems };
7371
+ });
7372
+ return { checkedAt: input.now ?? Date.now(), events: events2, issues };
7373
+ }
7374
+
7375
+ // src/notify.ts
7376
+ var import_crm8 = require("@odla-ai/crm");
7377
+
7378
+ // src/email-event-values.ts
7379
+ var import_crm7 = require("@odla-ai/crm");
7380
+
7381
+ // src/tiers-store.ts
7382
+ function rowToTier(row2, groupId) {
7383
+ 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") {
7384
+ throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
7385
+ }
7386
+ return {
7387
+ id: String(row2.id ?? ""),
7388
+ groupId: String(row2.groupId ?? groupId),
7389
+ name: String(row2.name ?? ""),
7390
+ priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
7391
+ ...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
7392
+ ...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
7393
+ ...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
7394
+ ...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
7395
+ sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
7396
+ // A row that never set `active` is offered; only an explicit false retires
7397
+ // a tier, so a half-seeded row cannot silently hide a membership.
7398
+ active: row2.active !== false
7399
+ };
7400
+ }
7401
+ async function loadTiers(db, group) {
7402
+ const groupId = String(group.id ?? "");
7403
+ let rows3;
7404
+ try {
7405
+ rows3 = (await db.query({
7406
+ tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
7407
+ })).tiers;
7408
+ } catch {
7409
+ throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
7410
+ }
7411
+ if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
7412
+ const list = rows3.map((row2) => rowToTier(row2, groupId));
7413
+ return resolveTiers(group, list);
7414
+ }
7415
+ async function loadOfferableTiers(db, group, hasSecretKey) {
7416
+ return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
7417
+ }
7418
+
7419
+ // src/notification-variables.ts
7420
+ async function frozenCheckoutVariables(db, app) {
7421
+ const intent = (await db.query({ subscriptionCheckoutIntents: { $: { where: { id: String(app.id) }, limit: 1 } } })).subscriptionCheckoutIntents?.[0];
7422
+ const offer = intent?.membershipOffer, contract = offer?.contract;
7423
+ const offering = contract?.offering, quote = intent?.paymentQuote;
7424
+ const amount = contract?.dueTodayCents ?? intent?.priceCents;
7425
+ const currency = offering?.currency ?? intent?.currency;
7426
+ let formatted = "";
7427
+ if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
7428
+ formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
7429
+ }
7430
+ return {
7431
+ paymentAmount: formatted,
7432
+ paymentAmountSummary: formatted ? ` of ${formatted}` : "",
7433
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
7434
+ };
7435
+ }
7436
+ async function catalogTierName(db, group, app) {
7437
+ try {
7438
+ return findTier(await loadTiers(db, group), typeof app.tierId === "string" ? app.tierId : null)?.name ?? "";
7439
+ } catch {
7440
+ return "";
7441
+ }
7442
+ }
7443
+ async function checkoutVariables(db, group, app) {
7444
+ const frozen = await frozenCheckoutVariables(db, app);
7445
+ return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
7446
+ }
7447
+
7448
+ // src/email-event-values.ts
7449
+ async function emailTierVariables(db, input) {
7450
+ try {
7451
+ const [apps, groups2] = await Promise.all([
7452
+ db.query({ applications: { $: { where: { id: input.applicationId }, limit: 1 } } }),
7453
+ db.query({ groups: { $: { where: { id: input.groupId }, limit: 1 } } })
7454
+ ]);
7455
+ const app = apps.applications?.[0], group = groups2.groups?.[0];
7456
+ if (!app || !group || app.groupId !== input.groupId) return { tierName: "", tier: "" };
7457
+ const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
7458
+ if (!tierId) return { tierName: "", tier: "" };
7459
+ const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
7460
+ const tierName = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
7461
+ return { tierName, tier: tierName, tierId };
7462
+ } catch {
7463
+ return { tierName: "", tier: "" };
7464
+ }
7465
+ }
7466
+
7032
7467
  // src/notify.ts
7033
7468
  function fromAddress(configured, name) {
7034
7469
  const display = name?.trim();
@@ -7046,24 +7481,27 @@ async function sendTemplated(deps, input) {
7046
7481
  const cloudflareReady = Boolean(deps.sender && deps.from);
7047
7482
  const override = deps.debugEmail?.trim();
7048
7483
  const group = deps.envName !== "prod" && deps.debugEmail !== void 0 ? { ...input.group, debugEmail: override && /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/.test(override) ? override : void 0 } : input.group;
7484
+ const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
7485
+ const vars = event2?.requiresTier && input.applicationId ? { ...input.vars, ...await emailTierVariables(deps.db, { applicationId: input.applicationId, groupId: group.id, tierId: input.tierId }) } : input.vars;
7049
7486
  const decision = planDelivery({
7050
7487
  envName: deps.envName,
7051
7488
  group,
7052
7489
  template: input.template,
7053
7490
  to: input.to,
7054
- vars: input.vars,
7491
+ vars,
7055
7492
  cloudflareReady,
7056
- force: input.force
7493
+ force: input.force,
7494
+ event: event2
7057
7495
  });
7058
7496
  if (!decision.deliver) return { sent: false, reason: decision.reason };
7059
7497
  const previous = prior[0];
7060
7498
  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" };
7061
7499
  const hash2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.dedupeKey));
7062
- const id10 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
7500
+ const id11 = previous ? String(previous.id) : `email_${Array.from(new Uint8Array(hash2), (b) => b.toString(16).padStart(2, "0")).join("")}`;
7063
7501
  const attempt = Number(previous?.deliveryAttempt ?? 0) + 1;
7064
7502
  if (!Number.isSafeInteger(attempt) || attempt < 1) return { sent: false, reason: "delivery-state-invalid" };
7065
7503
  const row2 = {
7066
- id: id10,
7504
+ id: id11,
7067
7505
  groupId: input.group.id,
7068
7506
  to: decision.to,
7069
7507
  template: input.template,
@@ -7078,16 +7516,18 @@ async function sendTemplated(deps, input) {
7078
7516
  error: decision.transport === "log-only" ? "" : "delivery-pending",
7079
7517
  ...input.applicationId ? { applicationId: input.applicationId } : {}
7080
7518
  };
7519
+ if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
7520
+ if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
7081
7521
  try {
7082
- const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id10, attrs: row2 }], {
7083
- mutationId: `email-claim:${id10}:${attempt}`,
7522
+ const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: row2 }], {
7523
+ mutationId: `email-claim:${id11}:${attempt}`,
7084
7524
  guards: [previous ? {
7085
7525
  ns: "emailLog",
7086
- id: id10,
7526
+ id: id11,
7087
7527
  exists: true,
7088
7528
  equals: { deliveryState: "retry_ready", ...previous.deliveryAttempt !== void 0 ? { deliveryAttempt: previous.deliveryAttempt } : {} },
7089
7529
  ...previous.deliveryAttempt === void 0 ? { absent: ["deliveryAttempt"] } : {}
7090
- } : { ns: "emailLog", id: id10, exists: false }]
7530
+ } : { ns: "emailLog", id: id11, exists: false }]
7091
7531
  });
7092
7532
  if (claim.duplicate) return { sent: false, reason: "delivery-uncertain" };
7093
7533
  } catch (error) {
@@ -7107,16 +7547,16 @@ async function sendTemplated(deps, input) {
7107
7547
  if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
7108
7548
  messageId = response.messageId;
7109
7549
  } catch {
7110
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id10, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
7111
- guards: [{ ns: "emailLog", id: id10, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
7550
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: { deliveryState: "uncertain", error: "delivery-uncertain" } }], {
7551
+ guards: [{ ns: "emailLog", id: id11, exists: true, equals: { deliveryState: "sending", deliveryAttempt: attempt } }]
7112
7552
  });
7113
7553
  return { sent: false, reason: "delivery-uncertain" };
7114
7554
  }
7115
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id10, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
7116
- mutationId: `email-confirmed:${id10}:${attempt}`,
7555
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: { deliveryState: "sent", error: "", messageId, sentAt: deps.now() } }], {
7556
+ mutationId: `email-confirmed:${id11}:${attempt}`,
7117
7557
  guards: [{
7118
7558
  ns: "emailLog",
7119
- id: id10,
7559
+ id: id11,
7120
7560
  exists: true,
7121
7561
  equals: { deliveryState: "sending", deliveryAttempt: attempt }
7122
7562
  }]
@@ -7172,7 +7612,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
7172
7612
  }
7173
7613
 
7174
7614
  // src/payments.ts
7175
- var import_stripe10 = require("@odla-ai/stripe");
7615
+ var import_stripe11 = require("@odla-ai/stripe");
7176
7616
  function paymentsReady(group, hasSecretKey) {
7177
7617
  return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
7178
7618
  }
@@ -7231,35 +7671,35 @@ function findGiftRef(obj) {
7231
7671
  ...chapterId ? { chapterId } : {}
7232
7672
  };
7233
7673
  }
7234
- function normalizeWebhookEvent(event, options = {}) {
7235
- const obj = event.data?.object ?? {};
7236
- 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 };
7674
+ function normalizeWebhookEvent(event2, options = {}) {
7675
+ const obj = event2.data?.object ?? {};
7676
+ 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 };
7237
7677
  const gift = findGiftRef(obj);
7238
7678
  if (gift.giftIntentId) {
7239
- const { giftIntentId, providerObjectId, ...identity } = gift;
7240
- if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
7241
- return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity };
7679
+ const { giftIntentId, providerObjectId, ...identity2 } = gift;
7680
+ if (event2.type === "payment_intent.succeeded" || event2.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
7681
+ return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity2 };
7242
7682
  }
7243
- if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity, reason: "refunded" };
7244
- if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity, reason: "canceled" };
7245
- return { kind: "ignored", type: event.type };
7683
+ if (event2.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "refunded" };
7684
+ if (event2.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity2, reason: "canceled" };
7685
+ return { kind: "ignored", type: event2.type };
7246
7686
  }
7247
7687
  const ref = findApplicationRef(obj);
7248
7688
  const seatId = obj.metadata?.seatId;
7249
7689
  if (typeof seatId === "string" && seatId) {
7250
- if (!options.requireProviderPayment) return { kind: "ignored", type: event.type };
7251
- if (event.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
7252
- if (event.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
7253
- return { kind: "ignored", type: event.type };
7690
+ if (!options.requireProviderPayment) return { kind: "ignored", type: event2.type };
7691
+ if (event2.type === "payment_intent.succeeded") return { kind: "seat_paid", seatId, ...ref };
7692
+ if (event2.type === "charge.refunded") return { kind: "seat_refunded", seatId, ...ref };
7693
+ return { kind: "ignored", type: event2.type };
7254
7694
  }
7255
- if (event.type === "invoice.paid" && options.requireProviderPayment || event.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event.type };
7256
- switch (event.type) {
7695
+ if (event2.type === "invoice.paid" && options.requireProviderPayment || event2.type === "invoice.payment_succeeded" && !options.requireProviderPayment) return { kind: "ignored", type: event2.type };
7696
+ switch (event2.type) {
7257
7697
  case "invoice.payment_failed":
7258
- return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event.type };
7698
+ return options.requireProviderPayment && obj.billing_reason === "subscription_cycle" ? { kind: "renewal_failed", ...ref } : { kind: "ignored", type: event2.type };
7259
7699
  case "invoice.paid":
7260
7700
  case "invoice.payment_succeeded": {
7261
7701
  if (obj.billing_reason !== "subscription_create" && obj.billing_reason !== "subscription_cycle") {
7262
- return { kind: "ignored", type: event.type };
7702
+ return { kind: "ignored", type: event2.type };
7263
7703
  }
7264
7704
  const renewalAt = membershipInvoicePeriodEnd(obj);
7265
7705
  const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
@@ -7273,11 +7713,11 @@ function normalizeWebhookEvent(event, options = {}) {
7273
7713
  case "charge.refunded":
7274
7714
  return { kind: "refunded", ...ref };
7275
7715
  case "customer.subscription.updated":
7276
- return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event.type };
7716
+ return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "ignored", type: event2.type };
7277
7717
  case "customer.subscription.deleted":
7278
7718
  return options.requireProviderPayment ? { kind: "subscription_changed", ...ref } : { kind: "canceled", ...ref };
7279
7719
  default:
7280
- return { kind: "ignored", type: event.type };
7720
+ return { kind: "ignored", type: event2.type };
7281
7721
  }
7282
7722
  }
7283
7723
  function firstPaymentPatch(currentStatus, renewalAt) {
@@ -7374,14 +7814,14 @@ function validateAdmissionGrantDocument(input) {
7374
7814
  const seenEmails = /* @__PURE__ */ new Set();
7375
7815
  for (const [index2, raw2] of rawGrants.slice(0, MAX_GRANTS).entries()) {
7376
7816
  const grant = raw2 && typeof raw2 === "object" ? raw2 : {};
7377
- const id10 = typeof grant.id === "string" ? grant.id : "";
7817
+ const id11 = typeof grant.id === "string" ? grant.id : "";
7378
7818
  const email = typeof grant.email === "string" ? grant.email.trim() : "";
7379
7819
  const normalized = normalizeAdmissionEmail(email);
7380
7820
  const decision = grant.decision;
7381
7821
  const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
7382
7822
  const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
7383
- if (!ID3.test(id10)) errors.push(`document.grants[${index2}].id must be an identifier`);
7384
- if (seenIds.has(id10)) errors.push(`document.grants repeats id "${id10}"`);
7823
+ if (!ID3.test(id11)) errors.push(`document.grants[${index2}].id must be an identifier`);
7824
+ if (seenIds.has(id11)) errors.push(`document.grants repeats id "${id11}"`);
7385
7825
  if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
7386
7826
  if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
7387
7827
  if (decision !== "waive_interview" && decision !== "require_interview") {
@@ -7391,10 +7831,10 @@ function validateAdmissionGrantDocument(input) {
7391
7831
  if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
7392
7832
  errors.push(`document.grants[${index2}].expiresAt must be epoch milliseconds`);
7393
7833
  }
7394
- seenIds.add(id10);
7834
+ seenIds.add(id11);
7395
7835
  seenEmails.add(normalized);
7396
7836
  grants.push({
7397
- id: id10,
7837
+ id: id11,
7398
7838
  email,
7399
7839
  decision,
7400
7840
  ...note ? { note } : {},
@@ -7466,8 +7906,8 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
7466
7906
  const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
7467
7907
  const ops = [];
7468
7908
  for (const row2 of current) {
7469
- const id10 = String(row2.id ?? "");
7470
- if (id10 && !incomingIds.has(id10)) ops.push({ t: "update", ns: "admissionGrants", id: id10, attrs: {
7909
+ const id11 = String(row2.id ?? "");
7910
+ if (id11 && !incomingIds.has(id11)) ops.push({ t: "update", ns: "admissionGrants", id: id11, attrs: {
7471
7911
  active: false,
7472
7912
  revokedAt: receivedAt,
7473
7913
  revokedBy: document.source.actor,
@@ -7476,10 +7916,10 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
7476
7916
  } });
7477
7917
  }
7478
7918
  for (const grant of document.grants) {
7479
- const id10 = `admission:parent:${document.source.appId}:${grant.id}`;
7480
- const existing = current.find((row2) => row2.id === id10);
7481
- ops.push({ t: "update", ns: "admissionGrants", id: id10, attrs: {
7482
- id: id10,
7919
+ const id11 = `admission:parent:${document.source.appId}:${grant.id}`;
7920
+ const existing = current.find((row2) => row2.id === id11);
7921
+ ops.push({ t: "update", ns: "admissionGrants", id: id11, attrs: {
7922
+ id: id11,
7483
7923
  sourceGrantId: grant.id,
7484
7924
  sourceScope: "parent",
7485
7925
  sourceId: document.source.appId,
@@ -7500,7 +7940,7 @@ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
7500
7940
  ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
7501
7941
  ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
7502
7942
  ];
7503
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id10, attrs: retract });
7943
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id11, attrs: retract });
7504
7944
  }
7505
7945
  if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
7506
7946
  const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
@@ -7571,10 +8011,10 @@ function validateGiftOfferCatalog(input, tierIds) {
7571
8011
  }
7572
8012
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
7573
8013
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7574
- const id10 = text4(item.id, `${path}.id`, errors, 80);
7575
- if (id10 && !ID4.test(id10)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7576
- if (ids2.has(id10)) errors.push(`${path}.id duplicates "${id10}"`);
7577
- ids2.add(id10);
8014
+ const id11 = text4(item.id, `${path}.id`, errors, 80);
8015
+ if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8016
+ if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
8017
+ ids2.add(id11);
7578
8018
  const targetRaw = record3(item.target);
7579
8019
  let target = { type: "entitlement", entitlementKey: "invalid" };
7580
8020
  if (targetRaw?.type === "tier") {
@@ -7600,7 +8040,7 @@ function validateGiftOfferCatalog(input, tierIds) {
7600
8040
  if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
7601
8041
  if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
7602
8042
  offers.push({
7603
- id: id10,
8043
+ id: id11,
7604
8044
  target,
7605
8045
  billingMode: item.billingMode,
7606
8046
  stripePriceId,
@@ -7625,10 +8065,10 @@ function findGiftOffer(catalog, offerId, now) {
7625
8065
  }
7626
8066
 
7627
8067
  // src/network.ts
7628
- var import_crm7 = require("@odla-ai/crm");
8068
+ var import_crm11 = require("@odla-ai/crm");
7629
8069
 
7630
8070
  // src/crm-person-projection.ts
7631
- var import_crm5 = require("@odla-ai/crm");
8071
+ var import_crm9 = require("@odla-ai/crm");
7632
8072
  async function ensureProjectedPerson(deps, input, options = {}) {
7633
8073
  const email = String(input.email ?? "").trim().toLowerCase();
7634
8074
  if (!email) throw new Error("crm_projection_email_required");
@@ -7644,10 +8084,10 @@ async function ensureProjectedPerson(deps, input, options = {}) {
7644
8084
  const existing = await find();
7645
8085
  if (existing) return { record: existing, created: false };
7646
8086
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
7647
- const id10 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
8087
+ const id11 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
7648
8088
  try {
7649
- const created = await (0, import_crm5.createRecord)({ ...deps, db: deps.db }, {
7650
- id: id10,
8089
+ const created = await (0, import_crm9.createRecord)({ ...deps, db: deps.db }, {
8090
+ id: id11,
7651
8091
  type: "person",
7652
8092
  input: { ...input, email },
7653
8093
  ...options
@@ -7662,7 +8102,7 @@ async function ensureProjectedPerson(deps, input, options = {}) {
7662
8102
  }
7663
8103
 
7664
8104
  // src/applicant-projection.ts
7665
- var import_crm6 = require("@odla-ai/crm");
8105
+ var import_crm10 = require("@odla-ai/crm");
7666
8106
 
7667
8107
  // src/crm-application-fields.ts
7668
8108
  function applicationCrmFields(crm, app) {
@@ -7681,10 +8121,10 @@ async function upsertPerson(deps, opts) {
7681
8121
  const email = opts.email.toLowerCase();
7682
8122
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
7683
8123
  const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
7684
- if (!created) await (0, import_crm6.updateRecord)(crmDeps, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
8124
+ if (!created) await (0, import_crm10.updateRecord)(crmDeps, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
7685
8125
  const initial = deps.crm.initialStage("person");
7686
8126
  if (!created && record4.stage == null && initial) {
7687
- await (0, import_crm6.setStage)(crmDeps, {
8127
+ await (0, import_crm10.setStage)(crmDeps, {
7688
8128
  id: String(record4.id),
7689
8129
  to: initial,
7690
8130
  authorId: "system",
@@ -7807,7 +8247,7 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
7807
8247
  return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
7808
8248
  }
7809
8249
  async function findSharedRecord(deps, record4, tag) {
7810
- const structured = await (0, import_crm7.getRecordByOrigin)(
8250
+ const structured = await (0, import_crm11.getRecordByOrigin)(
7811
8251
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
7812
8252
  record4.sourceId,
7813
8253
  record4.sourceRecordId
@@ -7859,15 +8299,15 @@ async function projectSharedRecord(deps, shared, options = {}) {
7859
8299
  const existing = await findSharedRecord(deps, record4, tag);
7860
8300
  let recordId;
7861
8301
  if (existing && typeof existing.id === "string") {
7862
- await (0, import_crm7.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
8302
+ await (0, import_crm11.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
7863
8303
  recordId = existing.id;
7864
8304
  } else if (record4.type === "person" && typeof record4.input.email === "string") {
7865
8305
  const projected = await ensureProjectedPerson(deps, record4.input);
7866
8306
  recordId = String(projected.record.id);
7867
- if (!projected.created) await (0, import_crm7.updateRecord)(crmDeps, { id: recordId, input: record4.input });
8307
+ if (!projected.created) await (0, import_crm11.updateRecord)(crmDeps, { id: recordId, input: record4.input });
7868
8308
  } else {
7869
8309
  recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
7870
- await (0, import_crm7.createRecord)({ ...crmDeps, newId: () => recordId }, {
8310
+ await (0, import_crm11.createRecord)({ ...crmDeps, newId: () => recordId }, {
7871
8311
  type: record4.type,
7872
8312
  input: record4.input,
7873
8313
  mutationId: `share-create:${tag}`
@@ -7877,7 +8317,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
7877
8317
  [{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
7878
8318
  { mutationId: `share-map:${tag}:${recordId}` }
7879
8319
  );
7880
- await (0, import_crm7.upsertRecordOrigin)(crmDeps, {
8320
+ await (0, import_crm11.upsertRecordOrigin)(crmDeps, {
7881
8321
  recordId,
7882
8322
  sourceId: record4.sourceId,
7883
8323
  sourceRecordId: record4.sourceRecordId,
@@ -7888,7 +8328,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
7888
8328
  }
7889
8329
 
7890
8330
  // src/crm-sync.ts
7891
- var import_crm8 = require("@odla-ai/crm");
8331
+ var import_crm12 = require("@odla-ai/crm");
7892
8332
 
7893
8333
  // src/crm-account-projection.ts
7894
8334
  async function syncAccountToCrm(deps, userId) {
@@ -7973,9 +8413,9 @@ async function syncApplicationToCrm(deps, opts) {
7973
8413
  const stage = opts.stage || void 0;
7974
8414
  const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
7975
8415
  const recordId = String(existing.id);
7976
- if (!created) await (0, import_crm8.updateRecord)(recordDeps, { id: recordId, input });
8416
+ if (!created) await (0, import_crm12.updateRecord)(recordDeps, { id: recordId, input });
7977
8417
  if (!created && stage && existing.stage !== stage) {
7978
- await (0, import_crm8.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
8418
+ await (0, import_crm12.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
7979
8419
  }
7980
8420
  const billing = billingColumns(opts.app);
7981
8421
  await deps.db.transact([
@@ -7983,7 +8423,7 @@ async function syncApplicationToCrm(deps, opts) {
7983
8423
  // Optional typed dates are removed with a retract, never an invalid null.
7984
8424
  ...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
7985
8425
  ]);
7986
- await (0, import_crm8.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
8426
+ await (0, import_crm12.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
7987
8427
  return recordId;
7988
8428
  }
7989
8429
  async function backfillCrm(deps) {
@@ -8070,9 +8510,9 @@ function subAnnualCents(sub) {
8070
8510
  }
8071
8511
 
8072
8512
  // src/payments-stripe.ts
8073
- var import_stripe11 = require("@odla-ai/stripe");
8513
+ var import_stripe12 = require("@odla-ai/stripe");
8074
8514
  async function stripeCall(sk, method, path, params, idempotencyKey) {
8075
- return (0, import_stripe11.stripeRequest)(
8515
+ return (0, import_stripe12.stripeRequest)(
8076
8516
  { secretKey: sk },
8077
8517
  method,
8078
8518
  path,
@@ -8150,8 +8590,8 @@ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fet
8150
8590
  return res.ok;
8151
8591
  }
8152
8592
  async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
8153
- const id10 = await existingClerkUserId(secretKey, email, fetchImpl);
8154
- return id10 ? updateClerkUserMetadata(secretKey, id10, input, fetchImpl) : false;
8593
+ const id11 = await existingClerkUserId(secretKey, email, fetchImpl);
8594
+ return id11 ? updateClerkUserMetadata(secretKey, id11, input, fetchImpl) : false;
8155
8595
  }
8156
8596
  async function existingClerkUserId(secretKey, email, fetchImpl) {
8157
8597
  const auth = { authorization: `Bearer ${secretKey}` };
@@ -8224,10 +8664,10 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch, source =
8224
8664
  const user = Array.isArray(data) ? data[0] : void 0;
8225
8665
  return user ? toRecord(user, source) : null;
8226
8666
  }
8227
- async function clerkGetUser(secretKey, id10, fetchImpl = fetch, source = "public") {
8228
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id10)}`, secretKey, fetchImpl).catch(() => null);
8667
+ async function clerkGetUser(secretKey, id11, fetchImpl = fetch, source = "public") {
8668
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id11)}`, secretKey, fetchImpl).catch(() => null);
8229
8669
  const record4 = data ? toRecord(data, source) : null;
8230
- return record4?.id === id10 ? record4 : null;
8670
+ return record4?.id === id11 ? record4 : null;
8231
8671
  }
8232
8672
  async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
8233
8673
  const out = [];
@@ -8243,8 +8683,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch, source = "public") {
8243
8683
  }
8244
8684
  return out;
8245
8685
  }
8246
- async function clerkSetRole(secretKey, id10, role, fetchImpl = fetch, source = "public") {
8247
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id10)}/metadata`, {
8686
+ async function clerkSetRole(secretKey, id11, role, fetchImpl = fetch, source = "public") {
8687
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id11)}/metadata`, {
8248
8688
  method: "PATCH",
8249
8689
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
8250
8690
  redirect: "manual",
@@ -8261,7 +8701,7 @@ function planPrivateRoleMigration(input) {
8261
8701
  if (input.environment !== "dev") blockers.push("dev_only");
8262
8702
  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");
8263
8703
  const expected = new Set(input.expectedRuntimes);
8264
- if (!expected.size || expected.size !== input.expectedRuntimes.length || input.expectedRuntimes.some((id10) => !id10)) blockers.push("runtime_inventory_required");
8704
+ if (!expected.size || expected.size !== input.expectedRuntimes.length || input.expectedRuntimes.some((id11) => !id11)) blockers.push("runtime_inventory_required");
8265
8705
  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");
8266
8706
  for (const runtime of input.expectedRuntimes) {
8267
8707
  const proof = input.runtimes.find((r) => r.runtime === runtime);
@@ -8291,7 +8731,7 @@ function planPrivateRoleMigration(input) {
8291
8731
  if (privateRole === role && !Object.hasOwn(user.publicMetadata, "role")) alreadyCurrent++;
8292
8732
  else changes.push({ userId: user.id, role, publicRole, privateRole });
8293
8733
  }
8294
- if (!input.protectedSuperAdminUserIds.some((id10) => ids2.has(id10))) blockers.push("protected_operator_grant_required");
8734
+ if (!input.protectedSuperAdminUserIds.some((id11) => ids2.has(id11))) blockers.push("protected_operator_grant_required");
8295
8735
  return { ready: blockers.length === 0, blockers, changes, alreadyCurrent, totalUsers: input.users.length };
8296
8736
  }
8297
8737
 
@@ -8650,44 +9090,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
8650
9090
  };
8651
9091
  }
8652
9092
 
8653
- // src/tiers-store.ts
8654
- function rowToTier(row2, groupId) {
8655
- 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") {
8656
- throw Object.assign(new Error("stored membership tier is invalid"), { code: "tiers_unavailable" });
8657
- }
8658
- return {
8659
- id: String(row2.id ?? ""),
8660
- groupId: String(row2.groupId ?? groupId),
8661
- name: String(row2.name ?? ""),
8662
- priceCents: typeof row2.priceCents === "number" ? row2.priceCents : 0,
8663
- ...typeof row2.currency === "string" && row2.currency ? { currency: row2.currency } : {},
8664
- ...row2.interval === "year" || row2.interval === "month" ? { interval: row2.interval } : {},
8665
- ...typeof row2.stripePriceId === "string" && row2.stripePriceId ? { stripePriceId: row2.stripePriceId } : {},
8666
- ...typeof row2.blurb === "string" ? { blurb: row2.blurb } : {},
8667
- sortOrder: typeof row2.sortOrder === "number" ? row2.sortOrder : 0,
8668
- // A row that never set `active` is offered; only an explicit false retires
8669
- // a tier, so a half-seeded row cannot silently hide a membership.
8670
- active: row2.active !== false
8671
- };
8672
- }
8673
- async function loadTiers(db, group) {
8674
- const groupId = String(group.id ?? "");
8675
- let rows3;
8676
- try {
8677
- rows3 = (await db.query({
8678
- tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
8679
- })).tiers;
8680
- } catch {
8681
- throw Object.assign(new Error("membership tiers are unavailable"), { code: "tiers_unavailable" });
8682
- }
8683
- if (!Array.isArray(rows3)) throw Object.assign(new Error("membership tiers response is invalid"), { code: "tiers_unavailable" });
8684
- const list = rows3.map((row2) => rowToTier(row2, groupId));
8685
- return resolveTiers(group, list);
8686
- }
8687
- async function loadOfferableTiers(db, group, hasSecretKey) {
8688
- return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
8689
- }
8690
-
8691
9093
  // src/signup-control-contract.ts
8692
9094
  var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
8693
9095
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
@@ -8907,10 +9309,10 @@ function tiersOf(value, errors) {
8907
9309
  return [];
8908
9310
  }
8909
9311
  unknownKeys3(item, TIER_KEYS2, path, errors);
8910
- const id10 = text5(item.id, `${path}.id`, errors, 80);
8911
- if (id10 && !TIER.test(id10)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8912
- if (seen.has(id10)) errors.push(`${path}.id duplicates "${id10}"`);
8913
- seen.add(id10);
9312
+ const id11 = text5(item.id, `${path}.id`, errors, 80);
9313
+ if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9314
+ if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
9315
+ seen.add(id11);
8914
9316
  const name = text5(item.name, `${path}.name`, errors, 160);
8915
9317
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
8916
9318
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
@@ -8931,7 +9333,7 @@ function tiersOf(value, errors) {
8931
9333
  }
8932
9334
  const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
8933
9335
  return [{
8934
- id: id10,
9336
+ id: id11,
8935
9337
  name,
8936
9338
  available: item.available === true,
8937
9339
  sortOrder: Number(item.sortOrder) || 0,
@@ -8993,17 +9395,17 @@ function policyOf(value, errors, tiers2) {
8993
9395
  if (input.tierPolicies !== void 0) {
8994
9396
  const overrides = recordOf(input.tierPolicies);
8995
9397
  if (!overrides || Object.keys(overrides).length > 20) errors.push("document.policy.tierPolicies must be a bounded tier map");
8996
- else for (const [id10, raw2] of Object.entries(overrides)) {
8997
- const path = `document.policy.tierPolicies.${id10}`;
8998
- const policy = recordOf(raw2);
8999
- if (!tiers2.some((tier) => tier.id === id10) || !policy) {
9398
+ else for (const [id11, raw2] of Object.entries(overrides)) {
9399
+ const path = `document.policy.tierPolicies.${id11}`;
9400
+ const policy2 = recordOf(raw2);
9401
+ if (!tiers2.some((tier) => tier.id === id11) || !policy2) {
9000
9402
  errors.push(`${path} must name a declared tier and policy`);
9001
9403
  continue;
9002
9404
  }
9003
- unknownKeys3(policy, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
9004
- tierPolicies[id10] = {
9005
- refundPolicyText: text5(policy.refundPolicyText, `${path}.refundPolicyText`, errors),
9006
- ...policy.trustCopy !== void 0 ? { trustCopy: text5(policy.trustCopy, `${path}.trustCopy`, errors) } : {}
9405
+ unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
9406
+ tierPolicies[id11] = {
9407
+ refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9408
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9007
9409
  };
9008
9410
  }
9009
9411
  }
@@ -9058,7 +9460,7 @@ function validateSignupControlDocument(input) {
9058
9460
  errors.push("document.membershipPage.tiers must follow the available tier display order");
9059
9461
  }
9060
9462
  const application = applicationOf(root.application, errors);
9061
- const policy = policyOf(root.policy, errors, tiers2);
9463
+ const policy2 = policyOf(root.policy, errors, tiers2);
9062
9464
  let scheduling;
9063
9465
  if (root.scheduling !== void 0) {
9064
9466
  const checked = validateScheduling(root.scheduling);
@@ -9078,7 +9480,7 @@ function validateSignupControlDocument(input) {
9078
9480
  tiers: tiers2,
9079
9481
  membershipPage: page.value,
9080
9482
  ...application ? { application } : {},
9081
- ...policy ? { policy } : {},
9483
+ ...policy2 ? { policy: policy2 } : {},
9082
9484
  ...scheduling ? { scheduling } : {},
9083
9485
  ...theme ? { theme } : {},
9084
9486
  ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
@@ -9324,7 +9726,7 @@ var exclusions = (chapterId, paidTierIds) => [
9324
9726
  id: chapterId,
9325
9727
  reason: "provider_identity",
9326
9728
  paths: [
9327
- ...paidTierIds.map((id10) => `tiers[${id10}].stripePriceId`),
9729
+ ...paidTierIds.map((id11) => `tiers[${id11}].stripePriceId`),
9328
9730
  "groups.stripePublishableKey",
9329
9731
  "applications.stripeCustomerId",
9330
9732
  "applications.stripeSubscriptionId",
@@ -9366,9 +9768,9 @@ async function exportChapterPromotion(input) {
9366
9768
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
9367
9769
  receiptDigests: [input.digest]
9368
9770
  };
9369
- const artifact = (kind, id10, content) => ({
9771
+ const artifact = (kind, id11, content) => ({
9370
9772
  kind,
9371
- id: id10,
9773
+ id: id11,
9372
9774
  sourceRevision: revision,
9373
9775
  provenance: promotionProvenance,
9374
9776
  content: (0, import_promotion.canonicalPromotionValue)(content)
@@ -9413,7 +9815,7 @@ var MEMBER_SYSTEM_RECORD_OWNERS = {
9413
9815
  provider: "originating-provider",
9414
9816
  protectedRoles: "odla-platform"
9415
9817
  };
9416
- var rows2 = (group) => group.fields.map((id10) => ({ ...group, id: id10 }));
9818
+ var rows2 = (group) => group.fields.map((id11) => ({ ...group, id: id11 }));
9417
9819
  var MEMBER_SYSTEM_AUTHORITY_MATRIX = [
9418
9820
  ...rows2({
9419
9821
  scope: "network",
@@ -9791,23 +10193,23 @@ function memberSystemAvailability(input) {
9791
10193
  ...!input.bnfFrontendReachable ? { reason: "bnf_frontend_unavailable" } : {}
9792
10194
  };
9793
10195
  }
9794
- function classifyMemberStripeEvent(identity, target, hasProvenLegacyLocalLink) {
10196
+ function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
9795
10197
  const keys2 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
9796
- const supplied = keys2.filter((key3) => identity[key3] !== void 0);
9797
- if (supplied.some((key3) => identity[key3] !== target[key3])) return "ignore_sibling";
10198
+ const supplied = keys2.filter((key3) => identity2[key3] !== void 0);
10199
+ if (supplied.some((key3) => identity2[key3] !== target[key3])) return "ignore_sibling";
9798
10200
  if (supplied.length === keys2.length) return "apply";
9799
10201
  return hasProvenLegacyLocalLink ? "apply_legacy" : "quarantine_unmatched";
9800
10202
  }
9801
10203
  function planMemberSystemEventReplay(events2) {
9802
10204
  const seen = /* @__PURE__ */ new Map();
9803
10205
  const plan = { accepted: [], replayed: [], collisions: [] };
9804
- for (const event of events2) {
9805
- const digest = seen.get(event.id);
10206
+ for (const event2 of events2) {
10207
+ const digest = seen.get(event2.id);
9806
10208
  if (digest === void 0) {
9807
- seen.set(event.id, event.digest);
9808
- plan.accepted.push(event.id);
9809
- } else if (digest === event.digest) plan.replayed.push(event.id);
9810
- else plan.collisions.push(event.id);
10209
+ seen.set(event2.id, event2.digest);
10210
+ plan.accepted.push(event2.id);
10211
+ } else if (digest === event2.digest) plan.replayed.push(event2.id);
10212
+ else plan.collisions.push(event2.id);
9811
10213
  }
9812
10214
  return plan;
9813
10215
  }