@odla-ai/chapter 0.48.3 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,12 +38,12 @@ function resolveApplication(a) {
38
38
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
39
39
  throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
40
40
  }
41
- const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
41
+ const unsafe = arr.find((field2) => !SCHEMA_FIELD.test(field2));
42
42
  if (unsafe) {
43
43
  throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
44
44
  }
45
45
  }
46
- const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
46
+ const duplicate = [...required, ...optional].find((field2, index2, fields) => fields.indexOf(field2) !== index2);
47
47
  if (duplicate) {
48
48
  throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
49
49
  }
@@ -54,14 +54,14 @@ function resolveApplication(a) {
54
54
  throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
55
55
  }
56
56
  const conditions = a?.conditions ?? {};
57
- for (const [field, declared] of Object.entries(conditions)) {
57
+ for (const [field2, declared] of Object.entries(conditions)) {
58
58
  for (const key3 of ["visibleWhen", "requiredWhen"]) {
59
59
  const expression = declared?.[key3];
60
60
  if (expression === void 0) continue;
61
61
  if (typeof expression !== "string" || !expression.trim()) {
62
- throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
62
+ throw new Error(`defineChapter.application.conditions.${field2}.${key3}: must be a non-empty CEL condition`);
63
63
  }
64
- assertFieldCondition(expression, `defineChapter.application.conditions.${field}.${key3}`);
64
+ assertFieldCondition(expression, `defineChapter.application.conditions.${field2}.${key3}`);
65
65
  }
66
66
  }
67
67
  return {
@@ -132,6 +132,13 @@ async function submitApplication(db, chapter, fields, opts) {
132
132
  if (opts.groupId) row2.groupId = opts.groupId;
133
133
  if (opts.runtime) row2.stripeRuntime = opts.runtime;
134
134
  if (chapter.account !== "none") row2.clerkAccountPending = true;
135
+ if (opts.namedSeat) Object.assign(row2, {
136
+ namedSeatId: opts.namedSeat.seatId,
137
+ namedSeatPrimaryApplicationId: opts.namedSeat.primaryApplicationId,
138
+ namedSeatClaimPending: true,
139
+ clerkUserId: opts.namedSeat.userId,
140
+ clerkAccountPending: false
141
+ });
135
142
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
136
143
  row2.emailNormalized = normalizeChapterEmail(fields.email);
137
144
  row2.signupEmailKey = row2.emailNormalized;
@@ -261,6 +268,10 @@ var applicationAttrs = {
261
268
  giftPaidAt: attr("number", { optional: true, indexed: true }),
262
269
  giftRevokedAt: attr("number", { optional: true, indexed: true }),
263
270
  giftCheckoutStartedAt: attr("number", { optional: true, indexed: true }),
271
+ membershipRestartId: attr("string", { optional: true, indexed: true }),
272
+ membershipStatus: attr("string", { optional: true }),
273
+ membershipGraceEndsAt: attr("number", { optional: true }),
274
+ membershipAutoRenew: attr("boolean", { optional: true }),
264
275
  namedSeatId: attr("string", { optional: true, unique: true, indexed: true }),
265
276
  namedSeatPrimaryApplicationId: attr("string", { optional: true }),
266
277
  namedSeatClaimPending: attr("boolean", { optional: true, indexed: true }),
@@ -271,19 +282,19 @@ function applicationsFor(application) {
271
282
  for (const [name, spec] of Object.entries(applicationAttrs)) {
272
283
  attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
273
284
  }
274
- const apply = (field, optional) => {
275
- const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
276
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
277
- if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
278
- Object.defineProperty(attrs, field, {
285
+ const apply = (field2, optional) => {
286
+ const existing = Object.hasOwn(attrs, field2) ? attrs[field2] : void 0;
287
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field2)) throw new Error(`defineChapter.application: field "${field2}" is reserved by the applications schema`);
288
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field2}" is not a configurable string field`);
289
+ Object.defineProperty(attrs, field2, {
279
290
  value: existing ? { ...existing, optional } : attr("string", { optional }),
280
291
  enumerable: true,
281
292
  configurable: true,
282
293
  writable: true
283
294
  });
284
295
  };
285
- for (const field of application.required) apply(field, false);
286
- for (const field of application.optional) apply(field, true);
296
+ for (const field2 of application.required) apply(field2, false);
297
+ for (const field2 of application.optional) apply(field2, true);
287
298
  return { attrs };
288
299
  }
289
300
 
@@ -490,6 +501,19 @@ var giftRedemptionAttemptsEntity = { attrs: {
490
501
  updatedAt: attr4("number")
491
502
  } };
492
503
 
504
+ // src/payment-recovery-schema.ts
505
+ var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
506
+ var paymentRecoveryReviewsEntity = { attrs: {
507
+ id: field("string", true),
508
+ eventId: field("string"),
509
+ actorUserId: field("string"),
510
+ reviewedAt: field("number"),
511
+ previousDigest: field("string"),
512
+ providerDigest: field("string"),
513
+ scope: field("string"),
514
+ previousState: field("json")
515
+ } };
516
+
493
517
  // src/schema-stripe.ts
494
518
  var attr5 = (type, flags = {}) => ({
495
519
  type,
@@ -510,6 +534,10 @@ var stripeEventReceiptsEntity = {
510
534
  dataEnvironment: attr5("string", { indexed: true, optional: true }),
511
535
  runtime: attr5("string", { indexed: true, optional: true }),
512
536
  chapterId: attr5("string", { indexed: true, optional: true }),
537
+ retryReady: attr5("boolean", { optional: true }),
538
+ reviewedDigest: attr5("string", { optional: true }),
539
+ reviewId: attr5("string", { optional: true }),
540
+ collisionDigest: attr5("string", { optional: true }),
513
541
  receivedAt: attr5("number", { indexed: true })
514
542
  }
515
543
  };
@@ -534,6 +562,7 @@ var subscriptionCheckoutIntentsEntity = {
534
562
  requestedCustomerId: attr5("string", { indexed: true, optional: true }),
535
563
  providerCustomerId: attr5("string", { indexed: true, optional: true }),
536
564
  providerSubscriptionId: attr5("string", { unique: true, indexed: true, optional: true }),
565
+ purchaseAttemptId: attr5("string", { optional: true }),
537
566
  status: attr5("string", { indexed: true }),
538
567
  createdAt: attr5("number", { indexed: true }),
539
568
  updatedAt: attr5("number", { indexed: true })
@@ -558,6 +587,20 @@ var membershipQuoteProjectionsEntity = {
558
587
  quote: attr5("json", { optional: true })
559
588
  }
560
589
  };
590
+ var membershipCheckoutHistoryEntity = { attrs: {
591
+ id: attr5("string", { unique: true, indexed: true }),
592
+ applicationId: attr5("string", { indexed: true }),
593
+ appId: attr5("string"),
594
+ chapterId: attr5("string"),
595
+ environment: attr5("string"),
596
+ runtime: attr5("string"),
597
+ skuId: attr5("string"),
598
+ providerSubscriptionId: attr5("string", { indexed: true }),
599
+ applicationSnapshot: attr5("json"),
600
+ intentSnapshot: attr5("json"),
601
+ quoteSnapshot: attr5("json"),
602
+ archivedAt: attr5("number")
603
+ } };
561
604
 
562
605
  // src/schema-network-person.ts
563
606
  var string = (flags = {}) => ({
@@ -691,15 +734,19 @@ var exceptions = { attrs: {
691
734
  kind: string2({ indexed: true }),
692
735
  sourceKey: string2({ indexed: true, optional: true }),
693
736
  status: string2({ indexed: true }),
737
+ resolvedDigest: string2({ optional: true }),
738
+ incomingDigest: string2({ optional: true }),
694
739
  detail: json2(),
695
740
  updatedAt: number2({ indexed: true })
696
741
  } };
697
742
  var networkCommercialEntities = {
743
+ paymentRecoveryReviews: paymentRecoveryReviewsEntity,
698
744
  networkCommercialEvents: events,
699
745
  networkCommercialCustomers: projection(),
700
746
  networkCommercialSubscriptions: projection(),
701
747
  networkCommercialInvoices: projection(),
702
748
  networkCommercialRefunds: projection(),
749
+ networkCommercialSeats: projection(),
703
750
  networkCommercialExceptions: exceptions
704
751
  };
705
752
 
@@ -902,8 +949,9 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
902
949
  entities.tiers = tiers;
903
950
  entities.meetings = meetings;
904
951
  entities.emailLog = emailLog;
952
+ entities.paymentRecoveryReviews = paymentRecoveryReviewsEntity;
905
953
  entities.stripeEventReceipts = stripeEventReceiptsEntity;
906
- entities.subscriptionCheckoutIntents = subscriptionCheckoutIntentsEntity;
954
+ Object.assign(entities, { subscriptionCheckoutIntents: subscriptionCheckoutIntentsEntity, membershipCheckoutHistory: membershipCheckoutHistoryEntity });
907
955
  entities.membershipQuoteProjections = membershipQuoteProjectionsEntity;
908
956
  entities.namedSeatConsents = namedSeatConsentsEntity;
909
957
  entities.admissionGrants = admissionGrantsEntity;
@@ -1394,6 +1442,7 @@ var DEFAULT_ADMIN_COPY = {
1394
1442
  },
1395
1443
  records: {
1396
1444
  workflowMissing: "This record is not linked to an application workflow.",
1445
+ invitation: "Invitation",
1397
1446
  comms: "Comms",
1398
1447
  commsHistory: "Comms history",
1399
1448
  scheduling: "Scheduling",
@@ -1441,6 +1490,19 @@ var DEFAULT_COMMON_COPY = {
1441
1490
  unknown: "(unknown)"
1442
1491
  };
1443
1492
  var DEFAULT_JOIN_COPY = {
1493
+ namedSeat: {
1494
+ title: "Your membership invitation",
1495
+ from: "{purchaserName} invited you.",
1496
+ prepaid: "Your {tierName} seat is already paid for.",
1497
+ body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
1498
+ consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
1499
+ accept: "Accept my invitation",
1500
+ accepting: "Confirming your invitation\u2026",
1501
+ loading: "Loading your invitation\u2026",
1502
+ unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
1503
+ retry: "Try again",
1504
+ failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
1505
+ },
1444
1506
  form: {
1445
1507
  submit: "Submit application",
1446
1508
  submitting: "Submitting\u2026",
@@ -1448,6 +1510,8 @@ var DEFAULT_JOIN_COPY = {
1448
1510
  unexpectedFailure: "Something went wrong. Please try again."
1449
1511
  },
1450
1512
  booking: {
1513
+ title: "Book your introduction",
1514
+ intro: "Choose a time to meet your chapter and get to know each other.",
1451
1515
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
1452
1516
  loadFailed: "Times are briefly unavailable. Please try again.",
1453
1517
  slotTaken: "That time was just taken. Here are the current openings.",
@@ -1463,7 +1527,9 @@ var DEFAULT_JOIN_COPY = {
1463
1527
  processing: "Processing\u2026",
1464
1528
  payAndContinue: "Pay and continue",
1465
1529
  incomplete: "The payment did not complete. Please try again.",
1466
- consent: "I have read and agree to the pricing and refund policy above."
1530
+ consent: "I have read and agree to the pricing and refund policy above.",
1531
+ foundingMember: "Founding member rate",
1532
+ foundingDiscount: "Founding discount"
1467
1533
  },
1468
1534
  done: {
1469
1535
  label: "You're booked",
@@ -1639,16 +1705,16 @@ function resolveFormationFlowFields(crm, formation) {
1639
1705
  if (!formation.enabled) return [];
1640
1706
  const def = crm.type(formation.type);
1641
1707
  return [...formation.required, ...formation.optional].map((id10, order2) => {
1642
- const field = def.fields[id10];
1708
+ const field2 = def.fields[id10];
1643
1709
  return {
1644
1710
  id: id10,
1645
- label: field.label ?? humanize(id10),
1646
- type: field.type,
1711
+ label: field2.label ?? humanize(id10),
1712
+ type: field2.type,
1647
1713
  required: formation.required.includes(id10),
1648
1714
  order: order2,
1649
1715
  maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
1650
- ...field.options ? { options: [...field.options] } : {},
1651
- ...id10 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1716
+ ...field2.options ? { options: [...field2.options] } : {},
1717
+ ...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
1652
1718
  };
1653
1719
  });
1654
1720
  }
@@ -1684,19 +1750,19 @@ function resolveLeaderFormation(input, crm) {
1684
1750
  const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
1685
1751
  const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
1686
1752
  for (const [name, values] of [["required", required], ["optional", optional]]) {
1687
- if (!Array.isArray(values) || !values.every((field) => typeof field === "string" && field !== "")) {
1753
+ if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
1688
1754
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1689
1755
  }
1690
1756
  }
1691
- const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
1757
+ const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
1692
1758
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1693
- const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
1759
+ const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
1694
1760
  if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
1695
- const missingRequired = Object.entries(def.fields).filter(([, field]) => field.required).map(([field]) => field).filter((field) => !required.includes(field));
1761
+ const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
1696
1762
  if (missingRequired.length) {
1697
1763
  throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
1698
1764
  }
1699
- const unsupportedRequired = required.find((field) => !["string", "email", "url", "enum"].includes(def.fields[field].type));
1765
+ const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
1700
1766
  if (unsupportedRequired) {
1701
1767
  throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
1702
1768
  }
@@ -1709,9 +1775,9 @@ function resolveLeaderFormation(input, crm) {
1709
1775
  if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
1710
1776
  throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
1711
1777
  }
1712
- for (const [field, cap] of Object.entries(input.maxLen ?? {})) {
1713
- if (![...required, ...optional].includes(field) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
1714
- throw new Error(`defineChapter.formation.maxLen.${field}: must name a public field and be an integer from 1 to 32768`);
1778
+ for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
1779
+ if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
1780
+ throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
1715
1781
  }
1716
1782
  }
1717
1783
  return {
@@ -1801,10 +1867,10 @@ function resolveNetwork(config, crm) {
1801
1867
  }
1802
1868
  const def = crm.config.types[type];
1803
1869
  if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
1804
- const unknown = names.filter((field) => !def.fields[field]);
1870
+ const unknown = names.filter((field2) => !def.fields[field2]);
1805
1871
  if (unknown.length > 0) {
1806
1872
  throw new Error(
1807
- `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the leader CRM type`
1873
+ `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
1808
1874
  );
1809
1875
  }
1810
1876
  const nameField = def.nameField ?? "name";
@@ -1892,10 +1958,10 @@ function resolveNetwork(config, crm) {
1892
1958
  if (!def) {
1893
1959
  throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
1894
1960
  }
1895
- const unknown = names.filter((field) => !def.fields[field]);
1961
+ const unknown = names.filter((field2) => !def.fields[field2]);
1896
1962
  if (unknown.length > 0) {
1897
1963
  throw new Error(
1898
- `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the local CRM type`
1964
+ `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
1899
1965
  );
1900
1966
  }
1901
1967
  const nameField = def.nameField ?? "name";
@@ -2213,6 +2279,48 @@ function defineChapter(config) {
2213
2279
  return chapter;
2214
2280
  }
2215
2281
 
2282
+ // src/network-commercial-seats.ts
2283
+ function commercialSeatItems(object2) {
2284
+ const items = object2.items;
2285
+ if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
2286
+ const result = [];
2287
+ for (const item of items.data) {
2288
+ const metadata = item.metadata;
2289
+ if (!metadata?.seatId || !metadata.membershipId) continue;
2290
+ const price = typeof item.price === "string" ? item.price : item.price?.id;
2291
+ if (typeof metadata.seatId !== "string" || !/^seat_[a-f0-9]{64}$/.test(metadata.seatId) || typeof metadata.membershipId !== "string" || !/^membership_[a-f0-9]{64}$/.test(metadata.membershipId) || typeof item.id !== "string" || !/^si_[A-Za-z0-9_]+$/.test(item.id) || typeof price !== "string" || !/^price_[A-Za-z0-9_]+$/.test(price) || item.quantity !== 1) continue;
2292
+ result.push({
2293
+ seatId: metadata.seatId,
2294
+ membershipId: metadata.membershipId,
2295
+ subscriptionItemId: item.id,
2296
+ priceId: price,
2297
+ ...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
2298
+ });
2299
+ }
2300
+ return result;
2301
+ }
2302
+ function projectCommercialSeats(payments, subscriptions, refunds) {
2303
+ return payments.map((payment) => {
2304
+ const matches2 = subscriptions.flatMap((subscription) => subscription.sourceKey === payment.sourceKey && subscription.customerId === payment.customerId && subscription.applicationId === payment.applicationId ? (subscription.seatSubscriptionItems ?? []).filter((item) => item.seatId === payment.seatId && item.membershipId === payment.membershipId).map((item) => ({ subscription, item })) : []);
2305
+ const match = matches2.length === 1 ? matches2[0] : void 0;
2306
+ const refundedCents = refunds.filter((refund) => refund.sourceKey === payment.sourceKey && refund.currency === payment.currency && refund.paymentIntentId === payment.paymentIntentId && refund.status === "succeeded").reduce((sum, refund) => sum + Number(refund.amountCents ?? 0), 0);
2307
+ return {
2308
+ ...payment,
2309
+ objectKind: "seat",
2310
+ objectId: payment.seatId,
2311
+ refundedCents,
2312
+ ...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
2313
+ ...match ? {
2314
+ subscriptionId: match.subscription.subscriptionId,
2315
+ subscriptionItemId: match.item.subscriptionItemId,
2316
+ priceId: match.item.priceId,
2317
+ currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
2318
+ cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
2319
+ } : {}
2320
+ };
2321
+ });
2322
+ }
2323
+
2216
2324
  // src/subscription-billing-terms.ts
2217
2325
  import { stripeRequest } from "@odla-ai/stripe";
2218
2326
 
@@ -2384,6 +2492,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2384
2492
  ...chargeId ? { chargeId } : {},
2385
2493
  ...paymentIntentId ? { paymentIntentId } : {},
2386
2494
  ...price,
2495
+ ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2387
2496
  ...status2 ? { status: status2 } : {},
2388
2497
  ...currency ? { currency } : {},
2389
2498
  ...amountCents !== void 0 ? { amountCents } : {},
@@ -2405,8 +2514,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2405
2514
  var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2406
2515
  function directSource(fact, sources) {
2407
2516
  const mode = fact.livemode ? "live" : "test";
2408
- const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
2409
- const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
2517
+ const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
2518
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
2410
2519
  if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2411
2520
  if (supplied.length > 0 && candidates.length === 0) return "wrong";
2412
2521
  return null;
@@ -2502,11 +2611,11 @@ function projectNetworkCommercialEvents(input, sources) {
2502
2611
  }
2503
2612
  }
2504
2613
  const totalsMap = /* @__PURE__ */ new Map();
2505
- const add = (row2, field, amount) => {
2614
+ const add = (row2, field2, amount) => {
2506
2615
  if (!row2.currency) return;
2507
2616
  const key3 = `${row2.sourceKey}:${row2.currency}`;
2508
2617
  const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2509
- total[field] += amount;
2618
+ total[field2] += amount;
2510
2619
  total.netCents = total.paidCents - total.refundedCents;
2511
2620
  totalsMap.set(key3, total);
2512
2621
  };
@@ -2514,7 +2623,7 @@ function projectNetworkCommercialEvents(input, sources) {
2514
2623
  for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
2515
2624
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2516
2625
  return {
2517
- ...seatPayments.length ? { seatPayments } : {},
2626
+ ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2518
2627
  customers,
2519
2628
  subscriptions,
2520
2629
  invoices,
@@ -2977,7 +3086,7 @@ function findTier(tiers2, tierId) {
2977
3086
  if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
2978
3087
  return tiers2.find((tier) => tier.id === tierId) ?? null;
2979
3088
  }
2980
- function joinConfigTiers(tiers2, defaults = {}) {
3089
+ function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
2981
3090
  return tiers2.map((tier) => ({
2982
3091
  id: tier.id,
2983
3092
  name: tier.name,
@@ -2985,7 +3094,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
2985
3094
  currency: tier.currency ?? defaults.currency ?? "usd",
2986
3095
  interval: tier.interval ?? defaults.interval ?? "year",
2987
3096
  blurb: tier.blurb ?? "",
2988
- free: tierIsFree(tier)
3097
+ free: tierIsFree(tier),
3098
+ ...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
2989
3099
  }));
2990
3100
  }
2991
3101
 
@@ -3088,8 +3198,8 @@ function createChapterIntegration(chapter, options = {}) {
3088
3198
  }
3089
3199
 
3090
3200
  // src/membership-policy.ts
3091
- var fail2 = (field) => {
3092
- throw new TypeError(`invalid membership policy: ${field}`);
3201
+ var fail2 = (field2) => {
3202
+ throw new TypeError(`invalid membership policy: ${field2}`);
3093
3203
  };
3094
3204
  var object = (value) => {
3095
3205
  if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
@@ -3271,23 +3381,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
3271
3381
  if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
3272
3382
  return restore({ ...value, approvedFoundingCount: 0 }, expected);
3273
3383
  }
3274
- async function issueMembershipCommercialOffer(db, input) {
3275
- validate(input);
3276
- const stateId = `${input.chapterId}:${input.environment}`;
3277
- const state = await load(db, "membershipAuthorityState", stateId);
3384
+ async function previewMembershipCommercialOffer(db, input) {
3385
+ for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
3386
+ if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
3387
+ }
3388
+ if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
3389
+ const state = await authoritativeState(db, input);
3390
+ const { offering, policy } = await currentPolicy(db, input, false, state);
3391
+ const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3392
+ if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3393
+ const contract = createMembershipCommercialOffer({
3394
+ offering,
3395
+ discountPolicy: policy,
3396
+ approvedFoundingCount: Number(approvedFoundingCount),
3397
+ now: input.now,
3398
+ couponId: await couponId(input, policy?.id)
3399
+ });
3400
+ const discount = contract.billingTerms.discount;
3401
+ return {
3402
+ skuId: input.skuId,
3403
+ offeringVersionId: offering.id,
3404
+ basePriceCents: offering.basePriceCents,
3405
+ currency: offering.currency,
3406
+ interval: offering.interval,
3407
+ founding: contract.founding,
3408
+ ...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
3409
+ dueTodayCents: contract.dueTodayCents,
3410
+ renewalCents: contract.renewalCents,
3411
+ ...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
3412
+ ...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
3413
+ };
3414
+ }
3415
+ async function authoritativeState(db, input) {
3416
+ const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3278
3417
  if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
3279
- const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3280
- if (existing) return restore(existing, input);
3418
+ return state;
3419
+ }
3420
+ async function currentPolicy(db, input, restartAware, state) {
3281
3421
  const rows3 = await db.query({
3282
3422
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
3283
3423
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
3284
3424
  });
3285
3425
  if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
3286
- const offerings = rows3.membershipOfferingVersion.map((row3) => membershipOfferingVersion(row3.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));
3287
- const policies = rows3.discountPolicyVersion.map((row3) => membershipDiscountPolicyVersion(row3.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
3426
+ 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));
3427
+ 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);
3288
3428
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
3289
- const policy = policies.sort((a, b) => b.version - a.version)[0];
3429
+ const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
3430
+ const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3290
3431
  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");
3432
+ return { offering, policy };
3433
+ }
3434
+ async function issueMembershipCommercialOffer(db, input) {
3435
+ validate(input);
3436
+ const stateId = `${input.chapterId}:${input.environment}`;
3437
+ const state = await authoritativeState(db, input);
3438
+ const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3439
+ if (existing) return restore(existing, input);
3440
+ const { offering, policy } = await currentPolicy(db, input, true, state);
3291
3441
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3292
3442
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3293
3443
  const contract = createMembershipCommercialOffer({
@@ -3324,8 +3474,73 @@ async function issueMembershipCommercialOffer(db, input) {
3324
3474
  return restore(stored, input);
3325
3475
  }
3326
3476
 
3327
- // src/membership-authority-handler.ts
3328
- import { FEDERATION_HEADERS, verifyFederatedRequest } from "@odla-ai/db";
3477
+ // src/membership-cancel-authority.ts
3478
+ import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
3479
+
3480
+ // src/commercial-evidence-guard.ts
3481
+ async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
3482
+ const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
3483
+ const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3484
+ if (!row2) return { ns, id: id10, exists: false };
3485
+ if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
3486
+ throw Object.assign(new Error(code), { code });
3487
+ }
3488
+ return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
3489
+ }
3490
+
3491
+ // src/membership-authority-state.ts
3492
+ var membershipFail = (code) => {
3493
+ throw Object.assign(new Error(code), { code });
3494
+ };
3495
+ var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3496
+ var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3497
+ var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3498
+ function requireMembershipScope(row2, input) {
3499
+ if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
3500
+ }
3501
+ async function loadMembershipAuthorityState(db, input) {
3502
+ const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3503
+ 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");
3504
+ return row2;
3505
+ }
3506
+ var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3507
+ var membershipClock = (now) => {
3508
+ if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3509
+ };
3510
+
3511
+ // src/membership-restart-payment.ts
3512
+ async function membershipRestartPayment(db, target, member, now) {
3513
+ const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
3514
+ requireMembershipScope(restart, target);
3515
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
3516
+ requireMembershipScope(entitlement, target);
3517
+ if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
3518
+ if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
3519
+ return {
3520
+ restartId: String(restart.restartId),
3521
+ entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
3522
+ attrs: {
3523
+ memberNumber: member.memberNumber,
3524
+ journeyApprovedAt: member.journeyApprovedAt,
3525
+ founding: member.founding,
3526
+ cohort: member.cohort,
3527
+ ...member.personId ? { personId: member.personId } : {},
3528
+ status: "active",
3529
+ revision: Number(member.revision) + 1,
3530
+ createdAt: member.createdAt,
3531
+ graceEndsAt: 0
3532
+ },
3533
+ ops: [
3534
+ membershipUpdate("membershipRestart", id10, { status: "complete" }),
3535
+ { t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
3536
+ ],
3537
+ guards: [
3538
+ { ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3539
+ { ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
3540
+ { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
3541
+ ]
3542
+ };
3543
+ }
3329
3544
 
3330
3545
  // src/membership-checkout-binding.ts
3331
3546
  var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
@@ -3352,10 +3567,11 @@ async function bindMembershipCheckout(db, input, now) {
3352
3567
  const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
3353
3568
  const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
3354
3569
  if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
3570
+ const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
3355
3571
  const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
3356
3572
  try {
3357
3573
  await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
3358
- mutationId: `membership-checkout:${id10}`,
3574
+ mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
3359
3575
  guards: [
3360
3576
  { ns: "membershipOfferCheckout", id: id10, exists: false },
3361
3577
  { ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
@@ -3370,26 +3586,6 @@ async function bindMembershipCheckout(db, input, now) {
3370
3586
  return verifyMembershipCheckoutBinding(saved, input);
3371
3587
  }
3372
3588
 
3373
- // src/membership-authority-state.ts
3374
- var membershipFail = (code) => {
3375
- throw Object.assign(new Error(code), { code });
3376
- };
3377
- var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3378
- var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3379
- var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3380
- function requireMembershipScope(row2, input) {
3381
- if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
3382
- }
3383
- async function loadMembershipAuthorityState(db, input) {
3384
- const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3385
- 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");
3386
- return row2;
3387
- }
3388
- var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3389
- var membershipClock = (now) => {
3390
- if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3391
- };
3392
-
3393
3589
  // src/membership-payment-authority.ts
3394
3590
  async function networkMembershipId(input) {
3395
3591
  return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
@@ -3407,9 +3603,7 @@ async function confirmMembershipPayment(db, input, now) {
3407
3603
  if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
3408
3604
  const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3409
3605
  if (!eventRow) membershipFail("membership_payment_evidence_pending");
3410
- if (await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) {
3411
- membershipFail("membership_payment_evidence_quarantined");
3412
- }
3606
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
3413
3607
  const fact = eventRow.fact;
3414
3608
  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({
3415
3609
  appId: input.appId,
@@ -3425,8 +3619,9 @@ async function confirmMembershipPayment(db, input, now) {
3425
3619
  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");
3426
3620
  const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
3427
3621
  let member = await membershipLoad(db, "networkMembership", id10);
3428
- if (!member) {
3622
+ if (!member || member.status === "restarting") {
3429
3623
  const offering = offer.contract.offering;
3624
+ const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
3430
3625
  const attrs = {
3431
3626
  id: id10,
3432
3627
  membershipId: id10,
@@ -3458,7 +3653,8 @@ async function confirmMembershipPayment(db, input, now) {
3458
3653
  refundedCents: 0,
3459
3654
  revision: 1,
3460
3655
  createdAt: now,
3461
- updatedAt: now
3656
+ updatedAt: now,
3657
+ ...restart?.attrs
3462
3658
  };
3463
3659
  try {
3464
3660
  await db.transact([
@@ -3468,21 +3664,23 @@ async function confirmMembershipPayment(db, input, now) {
3468
3664
  ...membershipScope(input),
3469
3665
  membershipId: id10,
3470
3666
  kind: "primary",
3471
- status: "pending_approval",
3667
+ status: restart?.attrs.status === "active" ? "active" : "pending_approval",
3472
3668
  termStartedAt: paidAt,
3473
3669
  termEndsAt: endsAt,
3474
3670
  offeringSnapshot: offering,
3475
3671
  sourceEventId: input.eventId,
3476
3672
  revision: 1,
3477
3673
  createdAt: now,
3478
- updatedAt: now
3479
- })
3480
- ], { mutationId: `membership-paid:${id10}`, guards: [
3481
- { ns: "networkMembership", id: id10, exists: false },
3674
+ updatedAt: now,
3675
+ ...restart?.entitlementAttrs
3676
+ }),
3677
+ ...restart?.ops ?? []
3678
+ ], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
3679
+ ...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
3482
3680
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3483
3681
  { ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
3484
3682
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
3485
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
3683
+ evidenceGuard
3486
3684
  ] });
3487
3685
  } catch (error) {
3488
3686
  if (error.code !== "transact_guard_failed") throw error;
@@ -3501,6 +3699,57 @@ async function confirmMembershipPayment(db, input, now) {
3501
3699
  };
3502
3700
  }
3503
3701
 
3702
+ // src/membership-cancel-authority.ts
3703
+ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
3704
+ membershipClock(now);
3705
+ const id10 = await networkMembershipId(target);
3706
+ const state = await loadMembershipAuthorityState(db, target);
3707
+ const member = await membershipLoad(db, "networkMembership", id10);
3708
+ requireMembershipScope(member, target);
3709
+ if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
3710
+ membershipFail("membership_cancellation_unavailable");
3711
+ }
3712
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
3713
+ const read2 = await stripeRequest2({ secretKey: stripeSecret }, "GET", path);
3714
+ const sub = read2.body;
3715
+ if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
3716
+ ...stripePartitionMetadata(target),
3717
+ applicationId: target.applicationId
3718
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
3719
+ if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
3720
+ const write = await stripeRequest2(
3721
+ { secretKey: stripeSecret },
3722
+ "POST",
3723
+ path,
3724
+ { cancel_at_period_end: true, proration_behavior: "none" },
3725
+ { idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
3726
+ );
3727
+ if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
3728
+ membershipFail("membership_cancellation_pending");
3729
+ }
3730
+ }
3731
+ const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
3732
+ if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
3733
+ await db.transact([membershipUpdate("networkMembership", id10, {
3734
+ status: status2,
3735
+ autoRenew: false,
3736
+ graceEndsAt: 0,
3737
+ cancellationRequestedAt: now,
3738
+ revision: Number(member.revision) + 1,
3739
+ updatedAt: now
3740
+ })], {
3741
+ guards: [
3742
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
3743
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
3744
+ ]
3745
+ });
3746
+ }
3747
+ return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
3748
+ }
3749
+
3750
+ // src/membership-authority-handler.ts
3751
+ import { FEDERATION_HEADERS, verifyFederatedRequest } from "@odla-ai/db";
3752
+
3504
3753
  // src/membership-seat-activation.ts
3505
3754
  async function namedSeatPrimaryActivation(db, member, now) {
3506
3755
  if (!member.namedSeatId) return { ops: [], guards: [] };
@@ -3593,7 +3842,7 @@ async function approveNetworkMembership(db, input, now) {
3593
3842
  }
3594
3843
 
3595
3844
  // src/payments-refund.ts
3596
- import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
3845
+ import { stripeRequest as stripeRequest3 } from "@odla-ai/stripe";
3597
3846
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3598
3847
  var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
3599
3848
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
@@ -3603,7 +3852,7 @@ function fail3(code = "refund_target_mismatch") {
3603
3852
  async function refundSubscriptionPayment(secretKey, input) {
3604
3853
  if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
3605
3854
  const call = async (method, path, params, key3) => {
3606
- const result = await stripeRequest2({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
3855
+ const result = await stripeRequest3({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
3607
3856
  if (!result.ok) fail3("stripe_error");
3608
3857
  if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
3609
3858
  return result.body;
@@ -3774,12 +4023,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
3774
4023
  }
3775
4024
 
3776
4025
  // src/membership-provider-lifecycle.ts
3777
- import { stripeRequest as stripeRequest3 } from "@odla-ai/stripe";
4026
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
3778
4027
 
3779
4028
  // src/membership-renewal-authority.ts
3780
4029
  async function reconcileMembershipRenewal(db, input, now) {
3781
4030
  membershipClock(now);
3782
4031
  const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4032
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
3783
4033
  const fact = eventRow?.fact;
3784
4034
  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({
3785
4035
  appId: input.appId,
@@ -3787,7 +4037,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3787
4037
  environment: input.environment,
3788
4038
  dataEnvironment: input.environment,
3789
4039
  runtime: input.runtime
3790
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) {
4040
+ }).some(([key3, value]) => fact.identity[key3] !== value)) {
3791
4041
  return membershipFail("membership_renewal_evidence_pending");
3792
4042
  }
3793
4043
  for (let attempt = 0; attempt < 8; attempt++) {
@@ -3801,7 +4051,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3801
4051
  autoRenew: member.autoRenew === true
3802
4052
  });
3803
4053
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.customerId !== member.providerCustomerId || fact.currency !== member.currency) membershipFail("membership_renewal_scope_mismatch");
3804
- if (["refunded", "refund_pending", "canceled", "declined"].includes(String(member.status))) return receipt();
4054
+ if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
3805
4055
  const lines = fact.recurringInvoiceLines;
3806
4056
  if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
3807
4057
  const start = lines[0].startsAt, end = lines[0].endsAt;
@@ -3810,7 +4060,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3810
4060
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
3811
4061
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
3812
4062
  if (seat) requireMembershipScope(seat, input);
3813
- const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4063
+ const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
3814
4064
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
3815
4065
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
3816
4066
  if (!Number.isSafeInteger(amount) || amount <= 0 || fact.invoiceTotalCents !== amount || JSON.stringify(lines.map((line) => line.priceId).sort()) !== JSON.stringify(prices)) membershipFail("membership_renewal_amount_mismatch");
@@ -3843,7 +4093,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3843
4093
  { ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
3844
4094
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
3845
4095
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
3846
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
4096
+ evidenceGuard
3847
4097
  ];
3848
4098
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
3849
4099
  if (billedSeat?.entitlementId) {
@@ -3889,14 +4139,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3889
4139
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3890
4140
  const fact = row2?.fact;
3891
4141
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
3892
- const collisionId = `network-commercial-exception:${input.eventId}:collision`;
4142
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3893
4143
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
3894
4144
  appId: input.appId,
3895
4145
  chapterId: input.chapterId,
3896
4146
  environment: input.environment,
3897
4147
  dataEnvironment: input.environment,
3898
4148
  runtime: input.runtime
3899
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4149
+ }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
3900
4150
  const sk = await getVaultSecret(db, stripeSecretName);
3901
4151
  if (!sk) membershipFail("membership_billing_provider_unavailable");
3902
4152
  const id10 = await networkMembershipId(input);
@@ -3910,6 +4160,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3910
4160
  graceEndsAt: Number(value.graceEndsAt ?? 0),
3911
4161
  autoRenew: value.autoRenew === true
3912
4162
  });
4163
+ if (["restarting"].includes(String(member.status))) return receipt();
3913
4164
  if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
3914
4165
  if (fact.eventType === "charge.refunded") {
3915
4166
  if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
@@ -3922,15 +4173,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3922
4173
  alreadyRefundedChargeId: fact.chargeId,
3923
4174
  cancelSubscription: false
3924
4175
  });
3925
- if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4176
+ await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3926
4177
  await refundNetworkMembership(db, input, sk, now);
3927
4178
  const saved2 = await membershipLoad(db, "networkMembership", id10);
3928
4179
  requireMembershipScope(saved2, input);
3929
4180
  return receipt(saved2);
3930
4181
  }
3931
4182
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
3932
- if (["refund_pending", "refunded", "declined", "canceled"].includes(String(member.status))) return receipt();
3933
- const result = await stripeRequest3({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4183
+ if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4184
+ const result = await stripeRequest4({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
3934
4185
  const sub = result.body;
3935
4186
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
3936
4187
  membershipFail("membership_billing_provider_mismatch");
@@ -3954,7 +4205,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3954
4205
  { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
3955
4206
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3956
4207
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
3957
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4208
+ evidenceGuard
3958
4209
  ] });
3959
4210
  } catch (error) {
3960
4211
  if (error.code !== "transact_guard_failed") throw error;
@@ -3967,6 +4218,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3967
4218
  return membershipFail("membership_billing_retry_required");
3968
4219
  }
3969
4220
 
4221
+ // src/membership-restart-seat.ts
4222
+ async function expireRestartedMembershipSeat(db, target, member, now) {
4223
+ const ops = [], guards = [];
4224
+ if (!member.namedSeatId) return { ops, guards };
4225
+ const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
4226
+ requireMembershipScope(seat, target);
4227
+ if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
4228
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
4229
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4230
+ }
4231
+ if (seat.entitlementId) {
4232
+ const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
4233
+ requireMembershipScope(entitlement, target);
4234
+ ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
4235
+ guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
4236
+ }
4237
+ return { ops, guards };
4238
+ }
4239
+
4240
+ // src/membership-restart-authority.ts
4241
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4242
+
4243
+ // src/stripe-event-digest.ts
4244
+ async function stripePayloadDigest(value) {
4245
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
4246
+ return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4247
+ }
4248
+
4249
+ // src/membership-restart-authority.ts
4250
+ function membershipCanRestart(member, now) {
4251
+ const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
4252
+ return Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && Number.isSafeInteger(member.journeyApprovedAt) && (["refunded", "canceled", "lapsed"].includes(String(member.status)) || ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && Number.isSafeInteger(end) && end <= now);
4253
+ }
4254
+ async function stopPreviousSubscription(member, target, secret) {
4255
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4256
+ const result = await stripeRequest5({ secretKey: secret }, "GET", path);
4257
+ const sub = result.body;
4258
+ if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4259
+ ...stripePartitionMetadata(target),
4260
+ applicationId: target.applicationId
4261
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4262
+ if (sub.status === "canceled") return;
4263
+ const canceled = await stripeRequest5(
4264
+ { secretKey: secret },
4265
+ "DELETE",
4266
+ path,
4267
+ { invoice_now: false, prorate: false },
4268
+ { idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
4269
+ );
4270
+ if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
4271
+ }
4272
+ async function prepareMembershipRestart(db, input, now, stripeSecret) {
4273
+ membershipClock(now);
4274
+ if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
4275
+ const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4276
+ const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
4277
+ const state = await loadMembershipAuthorityState(db, input);
4278
+ const previous = await membershipLoad(db, "networkMembership", id10);
4279
+ requireMembershipScope(previous, input);
4280
+ const receipt = {
4281
+ applicationId: input.applicationId,
4282
+ membershipId: id10,
4283
+ restartId,
4284
+ memberNumber: Number(previous.memberNumber),
4285
+ status: "ready"
4286
+ };
4287
+ const existing = await membershipLoad(db, "membershipRestart", id10);
4288
+ if (existing?.restartId === restartId && existing.status === "ready") {
4289
+ requireMembershipScope(existing, input);
4290
+ return receipt;
4291
+ }
4292
+ if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
4293
+ if (previous.status !== "restarting") {
4294
+ if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
4295
+ const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4296
+ requireMembershipScope(binding2, input);
4297
+ const seats = await expireRestartedMembershipSeat(db, input, previous, now);
4298
+ await db.transact([
4299
+ ...seats.ops,
4300
+ membershipUpdate("membershipPurchaseHistory", restartId, {
4301
+ id: restartId,
4302
+ ...membershipScope(input),
4303
+ membershipId: id10,
4304
+ providerSubscriptionId: input.previousSubscriptionId,
4305
+ memberSnapshot: previous,
4306
+ bindingSnapshot: binding2,
4307
+ archivedAt: now
4308
+ }),
4309
+ membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
4310
+ ], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
4311
+ ...seats.guards,
4312
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
4313
+ { ns: "membershipPurchaseHistory", id: restartId, exists: false },
4314
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
4315
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4316
+ ] });
4317
+ } else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
4318
+ const reserved = await membershipLoad(db, "networkMembership", id10);
4319
+ requireMembershipScope(reserved, input);
4320
+ if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
4321
+ await stopPreviousSubscription(reserved, input, stripeSecret);
4322
+ const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4323
+ requireMembershipScope(binding, input);
4324
+ await db.transact([
4325
+ membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
4326
+ { t: "delete", ns: "membershipOfferCheckout", id: bindingId }
4327
+ ], { mutationId: `membership-restart-ready:${restartId}`, guards: [
4328
+ { ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
4329
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
4330
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
4331
+ ] });
4332
+ return receipt;
4333
+ }
4334
+
3970
4335
  // src/membership-access-authority.ts
3971
4336
  async function readMembershipAccess(db, input, now) {
3972
4337
  membershipClock(now);
@@ -3983,7 +4348,8 @@ async function readMembershipAccess(db, input, now) {
3983
4348
  allowed: allowed2,
3984
4349
  status: String(member.status),
3985
4350
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
3986
- revision: Number(member.revision)
4351
+ revision: Number(member.revision),
4352
+ restartEligible: !allowed2 && membershipCanRestart(member, now)
3987
4353
  };
3988
4354
  }
3989
4355
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -3991,7 +4357,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
3991
4357
  if (!row2 || row2.membershipId !== await networkMembershipId(input) || typeof row2.allowed !== "boolean" || typeof row2.status !== "string" || !Number.isSafeInteger(row2.revision) || row2.revision < 0 || !Number.isSafeInteger(row2.accessEndsAt) || row2.accessEndsAt < 0 || row2.allowed && (!["active", "cancel_at_term_end", "payment_grace"].includes(row2.status) || row2.accessEndsAt <= now || row2.revision < 1)) {
3992
4358
  throw new Error("invalid network access receipt");
3993
4359
  }
3994
- return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
4360
+ return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
3995
4361
  }
3996
4362
 
3997
4363
  // src/membership-seat-reservation.ts
@@ -4072,16 +4438,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
4072
4438
  }
4073
4439
 
4074
4440
  // src/membership-seat-stripe.ts
4075
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4441
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
4076
4442
 
4077
4443
  // src/subscription-provider-contract.ts
4078
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4444
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4079
4445
  function subscriptionContractMismatch() {
4080
4446
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4081
4447
  }
4082
4448
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
4083
4449
  if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
4084
- const result = await stripeRequest4({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4450
+ const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4085
4451
  const price = result.body;
4086
4452
  if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
4087
4453
  return typeof price.product === "string" ? price.product : void 0;
@@ -4099,7 +4465,7 @@ var metadataFor = (seat) => ({
4099
4465
  });
4100
4466
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
4101
4467
  async function read(sk, path, params) {
4102
- const response = await stripeRequest5({ secretKey: sk }, "GET", path, params);
4468
+ const response = await stripeRequest7({ secretKey: sk }, "GET", path, params);
4103
4469
  if (!response.ok) return reject3();
4104
4470
  return response.body;
4105
4471
  }
@@ -4136,11 +4502,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
4136
4502
  }
4137
4503
  await subscriptionForSeat(sk, seat);
4138
4504
  const metadata = metadataFor(seat);
4139
- const result = seat.providerPaymentIntentId ? await stripeRequest5(
4505
+ const result = seat.providerPaymentIntentId ? await stripeRequest7(
4140
4506
  { secretKey: sk },
4141
4507
  "GET",
4142
4508
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
4143
- ) : await stripeRequest5({ secretKey: sk }, "POST", "/v1/payment_intents", {
4509
+ ) : await stripeRequest7({ secretKey: sk }, "POST", "/v1/payment_intents", {
4144
4510
  amount: seat.amountCents,
4145
4511
  currency: seat.currency,
4146
4512
  customer: seat.providerCustomerId,
@@ -4158,7 +4524,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4158
4524
  const sub = await subscriptionForSeat(sk, seat);
4159
4525
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
4160
4526
  if (existing) return String(existing.id);
4161
- const result = await stripeRequest5({ secretKey: sk }, "POST", "/v1/subscription_items", {
4527
+ const result = await stripeRequest7({ secretKey: sk }, "POST", "/v1/subscription_items", {
4162
4528
  subscription: seat.providerSubscriptionId,
4163
4529
  price: seat.priceId,
4164
4530
  quantity: 1,
@@ -4236,8 +4602,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4236
4602
  requireMembershipScope(member, input);
4237
4603
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4238
4604
  if (!row2 || !seat.providerPaymentIntentId) membershipFail("named_seat_payment_evidence_pending");
4239
- const collisionId = `network-commercial-exception:${input.eventId}:collision`;
4240
- if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("named_seat_payment_evidence_quarantined");
4605
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
4241
4606
  const fact = row2.fact;
4242
4607
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || fact.eventType !== "payment_intent.succeeded" || fact.objectKind !== "payment_intent" || fact.status !== "succeeded" || fact.livemode !== (input.environment === "prod") || fact.paymentIntentId !== seat.providerPaymentIntentId || fact.seatId !== seat.id || fact.membershipId !== member.id || fact.customerId !== seat.providerCustomerId || fact.applicationId !== input.applicationId || fact.amountCents !== seat.amountCents || fact.amountReceivedCents !== seat.amountCents || fact.currency !== seat.currency || !fact.identity || Object.entries({
4243
4608
  appId: input.appId,
@@ -4259,7 +4624,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4259
4624
  membershipUpdate("networkSeat", input.seatId, {
4260
4625
  status: status2,
4261
4626
  purchasedAt: paidAt,
4262
- acceptanceEndsAt: Math.min(paidAt + 30 * 864e5, Number(member.termEndsAt)),
4627
+ acceptanceEndsAt: Number(member.termEndsAt),
4263
4628
  termStartedAt: paidAt,
4264
4629
  termEndsAt: member.termEndsAt,
4265
4630
  paymentEventId: input.eventId,
@@ -4284,7 +4649,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4284
4649
  { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4285
4650
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4286
4651
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4287
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4652
+ evidenceGuard
4288
4653
  ] });
4289
4654
  } catch (error) {
4290
4655
  if (error.code !== "transact_guard_failed") throw error;
@@ -4307,7 +4672,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
4307
4672
  if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
4308
4673
  const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
4309
4674
  requireMembershipScope(member, target);
4310
- if (now >= Number(seat.acceptanceEndsAt) || now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
4675
+ if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
4311
4676
  const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
4312
4677
  const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
4313
4678
  try {
@@ -4382,7 +4747,7 @@ async function recordRecipient(db, input, now, approve) {
4382
4747
  requireMembershipScope(entitlement, input);
4383
4748
  if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !seat.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
4384
4749
  if (seat.acceptedAt === void 0) {
4385
- if (approve || now >= Number(seat.acceptanceEndsAt) || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
4750
+ if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
4386
4751
  membershipFail("named_seat_recipient_verification_required");
4387
4752
  }
4388
4753
  } else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
@@ -4433,14 +4798,14 @@ async function recordRecipient(db, input, now, approve) {
4433
4798
  }
4434
4799
 
4435
4800
  // src/membership-seat-refund-stripe.ts
4436
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4801
+ import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
4437
4802
  var fail4 = (code = "named_seat_refund_mismatch") => {
4438
4803
  throw Object.assign(new Error(code), { code });
4439
4804
  };
4440
4805
  async function refundNamedSeatStripePayment(secretKey, seat) {
4441
4806
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
4442
4807
  const call = async (method, path, params, idempotencyKey) => {
4443
- const result = await stripeRequest6({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4808
+ const result = await stripeRequest8({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4444
4809
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4445
4810
  return result.body;
4446
4811
  };
@@ -4495,7 +4860,7 @@ async function requestNamedSeatRefund(db, input, now) {
4495
4860
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4496
4861
  requireMembershipScope(member, input);
4497
4862
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
4498
- if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "expired" && (seat.acceptedAt !== void 0 || !Number.isSafeInteger(seat.acceptanceEndsAt) || now < Number(seat.acceptanceEndsAt)) || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
4863
+ if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
4499
4864
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
4500
4865
  await db.transact([
4501
4866
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -4562,11 +4927,24 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
4562
4927
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4563
4928
  if (!row2) return membershipFail("named_seat_refund_evidence_pending");
4564
4929
  const fact = row2.fact;
4565
- if (await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) membershipFail("named_seat_refund_evidence_quarantined");
4930
+ await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
4566
4931
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || fact.eventType !== "charge.refunded" || fact.seatId !== seat.id || fact.membershipId !== seat.primaryMembershipId || fact.paymentIntentId !== seat.providerPaymentIntentId || fact.amountCents !== seat.amountCents || fact.currency !== seat.currency || fact.customerId !== seat.providerCustomerId || fact.applicationId !== input.applicationId || fact.livemode !== (input.environment === "prod") || !fact.identity || Object.entries({ appId: input.appId, chapterId: input.chapterId, environment: input.environment, dataEnvironment: input.environment, runtime: input.runtime }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("named_seat_refund_evidence_mismatch");
4567
4932
  await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
4568
4933
  }
4569
4934
 
4935
+ // src/membership-seat-operator-refund.ts
4936
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
4937
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
4938
+ requireMembershipScope(seat, input);
4939
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
4940
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
4941
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
4942
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
4943
+ throw new Error("named seat refund unconfirmed");
4944
+ }
4945
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
4946
+ }
4947
+
4570
4948
  // src/membership-seat-offer.ts
4571
4949
  async function namedSeatOfferDigest(offer) {
4572
4950
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -4579,7 +4957,7 @@ async function namedSeatOfferDigest(offer) {
4579
4957
  "named-only",
4580
4958
  "full-upfront",
4581
4959
  "auto-renew-coterm",
4582
- "30-day-unaccepted-full-refund"
4960
+ "no-automatic-refund"
4583
4961
  ])));
4584
4962
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4585
4963
  }
@@ -4619,7 +4997,7 @@ async function readNamedSeatInvitation(db, input, now) {
4619
4997
  await loadMembershipAuthorityState(db, target);
4620
4998
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4621
4999
  requireMembershipScope(member, target);
4622
- if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt) || !seat.acceptedAt && now >= Number(seat.acceptanceEndsAt)) membershipFail("named_seat_invitation_unavailable");
5000
+ if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
4623
5001
  return {
4624
5002
  seatId: input.seatId,
4625
5003
  primaryApplicationId: target.applicationId,
@@ -4649,7 +5027,7 @@ async function readMembershipChapterEffect(db, input, now) {
4649
5027
  requireMembershipScope(member, input);
4650
5028
  if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
4651
5029
  let applicable;
4652
- if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(seat.acceptanceEndsAt) && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
5030
+ if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
4653
5031
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
4654
5032
  else applicable = (await readNamedSeatAccess(db, {
4655
5033
  ...input,
@@ -4678,6 +5056,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
4678
5056
  "seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
4679
5057
  "seat-payments": ["seatId", "eventId"],
4680
5058
  "seat-refunds": ["seatId", "eventId"],
5059
+ "seat-operator-refund": ["seatId"],
4681
5060
  "seat-accept": recipient,
4682
5061
  "seat-approve": recipient,
4683
5062
  "seat-decline": recipient,
@@ -4746,6 +5125,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
4746
5125
  if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
4747
5126
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
4748
5127
  }
5128
+ if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
4749
5129
  if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
4750
5130
  ...target,
4751
5131
  priceId: peer.seatPriceId,
@@ -4834,7 +5214,7 @@ function createMembershipAuthorityHandler(options) {
4834
5214
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
4835
5215
  const sender = req.headers.get(FEDERATION_HEADERS.sender);
4836
5216
  const action = url.pathname.split("/").at(-1);
4837
- if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
5217
+ if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
4838
5218
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
4839
5219
  if (!peer) return respond({ error: "unauthorized" }, 401);
4840
5220
  let body;
@@ -4856,13 +5236,23 @@ function createMembershipAuthorityHandler(options) {
4856
5236
  let input;
4857
5237
  try {
4858
5238
  input = JSON.parse(verified.body);
4859
- const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
5239
+ 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] ?? []];
4860
5240
  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])))) {
4861
5241
  throw new TypeError("invalid request");
4862
5242
  }
4863
5243
  } catch {
4864
5244
  return respond({ error: "invalid_offer_request" }, 400);
4865
5245
  }
5246
+ if (action === "offer-preview") {
5247
+ return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
5248
+ appId: peer.appId,
5249
+ chapterId: peer.chapterId,
5250
+ environment: peer.environment,
5251
+ runtime: peer.runtime,
5252
+ skuId: String(input.skuId),
5253
+ now: options.now?.() ?? Date.now()
5254
+ }) });
5255
+ }
4866
5256
  if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
4867
5257
  return respond({ ok: true, seat: await handleMembershipSeatAction(
4868
5258
  action,
@@ -4881,6 +5271,15 @@ function createMembershipAuthorityHandler(options) {
4881
5271
  applicationId: String(input.applicationId),
4882
5272
  skuId: String(input.skuId)
4883
5273
  };
5274
+ if (action === "restarts" || action === "cancel-renewal") {
5275
+ const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
5276
+ if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
5277
+ if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
5278
+ return respond({ ok: true, restart: await prepareMembershipRestart(db, {
5279
+ ...target,
5280
+ previousSubscriptionId: String(input.previousSubscriptionId)
5281
+ }, options.now?.() ?? Date.now(), stripeSecret) });
5282
+ }
4884
5283
  if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
4885
5284
  if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
4886
5285
  db,
@@ -5004,6 +5403,12 @@ async function requestMembershipCommercialOffer(options) {
5004
5403
  lifetimeMs: 6e4
5005
5404
  });
5006
5405
  }
5406
+ async function requestMembershipOfferPreview(options) {
5407
+ const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
5408
+ const preview = result.preview;
5409
+ if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
5410
+ return preview;
5411
+ }
5007
5412
  async function requestMembershipCheckoutBinding(options) {
5008
5413
  const result = await requestAuthority(options, "checkouts", {
5009
5414
  applicationId: options.applicationId,
@@ -5042,6 +5447,12 @@ async function requestNamedSeatDecline(options) {
5042
5447
  if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
5043
5448
  return seat;
5044
5449
  }
5450
+ async function requestNamedSeatOperatorRefund(options) {
5451
+ const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
5452
+ const seat = result.seat;
5453
+ if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
5454
+ return seat;
5455
+ }
5045
5456
  async function requestNamedSeatReservation(options) {
5046
5457
  const result = await requestAuthority(options, "seat-reservations", {
5047
5458
  applicationId: options.applicationId,
@@ -5154,7 +5565,7 @@ async function requestMembershipRefund(options) {
5154
5565
  async function requestMembershipRenewal(options) {
5155
5566
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
5156
5567
  const row2 = result.renewal;
5157
- if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined"].includes(row2.status)) {
5568
+ if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
5158
5569
  throw new Error("invalid membership renewal receipt");
5159
5570
  }
5160
5571
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -5196,9 +5607,25 @@ async function requestMembershipAccess(options) {
5196
5607
  const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
5197
5608
  return verifyMembershipAccessReceipt(result.access, options, Date.now());
5198
5609
  }
5610
+ async function requestMembershipRestart(options) {
5611
+ const result = await requestAuthority(options, "restarts", {
5612
+ applicationId: options.applicationId,
5613
+ skuId: options.skuId,
5614
+ previousSubscriptionId: options.previousSubscriptionId
5615
+ });
5616
+ const row2 = result.restart;
5617
+ if (!row2 || row2.applicationId !== options.applicationId || row2.status !== "ready" || row2.membershipId !== await networkMembershipId(options) || !/^restart_[a-f0-9]{64}$/.test(row2.restartId) || !Number.isSafeInteger(row2.memberNumber) || row2.memberNumber < 1) throw new Error("invalid membership restart receipt");
5618
+ return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
5619
+ }
5620
+ async function requestMembershipCancellation(options) {
5621
+ const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
5622
+ const row2 = result.renewal;
5623
+ if (!row2 || row2.membershipId !== await networkMembershipId(options) || row2.autoRenew !== false || row2.graceEndsAt !== 0 || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !["cancel_at_term_end", "lapsed", "paid_pending_approval"].includes(row2.status)) throw new Error("invalid membership cancellation receipt");
5624
+ return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
5625
+ }
5199
5626
 
5200
5627
  // src/membership-seat-cancel.ts
5201
- import { initStripe, stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5628
+ import { initStripe, stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
5202
5629
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5203
5630
  membershipClock(now);
5204
5631
  const state = await loadMembershipAuthorityState(db, target);
@@ -5226,7 +5653,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5226
5653
  }
5227
5654
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
5228
5655
  const request = async (method, path, params) => {
5229
- const result = await stripeRequest7(
5656
+ const result = await stripeRequest9(
5230
5657
  { secretKey },
5231
5658
  method,
5232
5659
  path,
@@ -5270,7 +5697,7 @@ async function drainMembershipEffects(db, options, now) {
5270
5697
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
5271
5698
  await loadMembershipAuthorityState(db, scope2);
5272
5699
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
5273
- let expired = 0, pending = 0, complete = 0, more = false;
5700
+ let pending = 0, complete = 0, more = false;
5274
5701
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
5275
5702
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
5276
5703
  more ||= seats.length > limit;
@@ -5288,26 +5715,6 @@ async function drainMembershipEffects(db, options, now) {
5288
5715
  }
5289
5716
  }
5290
5717
  }
5291
- for (const status2 of ["renewal_pending", "pending_acceptance"]) {
5292
- const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
5293
- more ||= seats.length > limit;
5294
- for (const seat of seats.slice(0, limit)) {
5295
- if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
5296
- if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
5297
- try {
5298
- await requestNamedSeatRefund(db, {
5299
- ...scope2,
5300
- applicationId: String(seat.applicationId),
5301
- skuId: String(seat.skuId),
5302
- seatId: String(seat.id),
5303
- reason: "expired"
5304
- }, now);
5305
- expired++;
5306
- } catch {
5307
- pending++;
5308
- }
5309
- }
5310
- }
5311
5718
  const effects = (await db.query({ membershipEffect: { $: { where: {
5312
5719
  chapterId: scope2.chapterId,
5313
5720
  environment: scope2.environment,
@@ -5369,7 +5776,7 @@ async function drainMembershipEffects(db, options, now) {
5369
5776
  pending++;
5370
5777
  }
5371
5778
  }
5372
- return { complete, pending, expired, more };
5779
+ return { complete, pending, more };
5373
5780
  }
5374
5781
 
5375
5782
  // src/schema-membership-offers.ts
@@ -5463,7 +5870,27 @@ var membershipAuthorityEntities = {
5463
5870
  cancellationRequestedAt: attr9("number", true),
5464
5871
  graceEndsAt: attr9("number", true),
5465
5872
  ...times(),
5466
- refundRequestedAt: attr9("number", true)
5873
+ refundRequestedAt: attr9("number", true),
5874
+ restartId: key2(true)
5875
+ } },
5876
+ membershipRestart: { attrs: {
5877
+ id: id9(),
5878
+ ...scope(),
5879
+ membershipId: key2(),
5880
+ restartId: key2(),
5881
+ previousSubscriptionId: key2(),
5882
+ memberNumber: attr9("number"),
5883
+ status: key2(),
5884
+ createdAt: attr9("number")
5885
+ } },
5886
+ membershipPurchaseHistory: { attrs: {
5887
+ id: id9(),
5888
+ ...scope(),
5889
+ membershipId: key2(),
5890
+ providerSubscriptionId: key2(),
5891
+ memberSnapshot: attr9("json"),
5892
+ bindingSnapshot: attr9("json"),
5893
+ archivedAt: attr9("number")
5467
5894
  } },
5468
5895
  networkEntitlement: { attrs: {
5469
5896
  id: id9(),
@@ -6894,9 +7321,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
6894
7321
  }
6895
7322
  const nameField = def.nameField ?? "name";
6896
7323
  const input = {};
6897
- for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
6898
- const value = record4.fields?.[field];
6899
- if (value !== void 0) input[field] = value;
7324
+ for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
7325
+ const value = record4.fields?.[field2];
7326
+ if (value !== void 0) input[field2] = value;
6900
7327
  }
6901
7328
  if (input[nameField] === void 0) input[nameField] = record4.name;
6902
7329
  return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
@@ -7165,9 +7592,9 @@ function subAnnualCents(sub) {
7165
7592
  }
7166
7593
 
7167
7594
  // src/payments-stripe.ts
7168
- import { stripeRequest as stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7595
+ import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7169
7596
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7170
- return stripeRequest8(
7597
+ return stripeRequest10(
7171
7598
  { secretKey: sk },
7172
7599
  method,
7173
7600
  path,
@@ -7477,7 +7904,8 @@ function applicationSummary(app) {
7477
7904
  status: app.status,
7478
7905
  createdAt: app.createdAt ?? null,
7479
7906
  meetingLink: app.meetingLink ?? null,
7480
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
7907
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
7908
+ ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
7481
7909
  renewalAt: app.renewalAt ?? null,
7482
7910
  canceled: app.canceled === true,
7483
7911
  tierId: app.tierId ?? null
@@ -7659,7 +8087,7 @@ function isValidTimeZone(tz) {
7659
8087
  function resolveScheduling(config) {
7660
8088
  const result = validateScheduling(config);
7661
8089
  if (result.ok) return result.value;
7662
- const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
8090
+ const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
7663
8091
  throw new Error(`scheduling: ${detail}`);
7664
8092
  }
7665
8093
  function validateScheduling(config) {
@@ -8806,7 +9234,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
8806
9234
  MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
8807
9235
  );
8808
9236
  function projectMemberSelf(record4) {
8809
- return Object.fromEntries(Object.entries(record4).filter(([field]) => MEMBER_SELF_READABLE_FIELDS.has(field)));
9237
+ return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
8810
9238
  }
8811
9239
  function classifyMemberSystemWrite(intent) {
8812
9240
  if (intent.actor === "bnf") return "reject_bnf_surface";
@@ -9132,6 +9560,7 @@ export {
9132
9560
  planMemberSystemEventReplay,
9133
9561
  planMemberSystemMigration,
9134
9562
  planPrivateRoleMigration,
9563
+ previewMembershipCommercialOffer,
9135
9564
  projectApplicant,
9136
9565
  projectBuilderLibraryDelivery,
9137
9566
  projectMemberContent,
@@ -9160,12 +9589,15 @@ export {
9160
9589
  renewalPatch,
9161
9590
  requestMembershipAccess,
9162
9591
  requestMembershipApproval,
9592
+ requestMembershipCancellation,
9163
9593
  requestMembershipChapterEffect,
9164
9594
  requestMembershipCheckoutBinding,
9165
9595
  requestMembershipCommercialOffer,
9596
+ requestMembershipOfferPreview,
9166
9597
  requestMembershipPayment,
9167
9598
  requestMembershipRefund,
9168
9599
  requestMembershipRenewal,
9600
+ requestMembershipRestart,
9169
9601
  requestNamedSeatAcceptance,
9170
9602
  requestNamedSeatAccess,
9171
9603
  requestNamedSeatApproval,
@@ -9173,6 +9605,7 @@ export {
9173
9605
  requestNamedSeatDecline,
9174
9606
  requestNamedSeatInvitation,
9175
9607
  requestNamedSeatOffer,
9608
+ requestNamedSeatOperatorRefund,
9176
9609
  requestNamedSeatPayment,
9177
9610
  requestNamedSeatRefund2 as requestNamedSeatRefund,
9178
9611
  requestNamedSeatReservation,