@odla-ai/chapter 0.48.3 → 0.49.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;
@@ -1441,6 +1489,19 @@ var DEFAULT_COMMON_COPY = {
1441
1489
  unknown: "(unknown)"
1442
1490
  };
1443
1491
  var DEFAULT_JOIN_COPY = {
1492
+ namedSeat: {
1493
+ title: "Your membership invitation",
1494
+ from: "{purchaserName} invited you.",
1495
+ prepaid: "Your {tierName} seat is already paid for.",
1496
+ body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
1497
+ consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
1498
+ accept: "Accept my invitation",
1499
+ accepting: "Confirming your invitation\u2026",
1500
+ loading: "Loading your invitation\u2026",
1501
+ unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
1502
+ retry: "Try again",
1503
+ failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
1504
+ },
1444
1505
  form: {
1445
1506
  submit: "Submit application",
1446
1507
  submitting: "Submitting\u2026",
@@ -1448,6 +1509,8 @@ var DEFAULT_JOIN_COPY = {
1448
1509
  unexpectedFailure: "Something went wrong. Please try again."
1449
1510
  },
1450
1511
  booking: {
1512
+ title: "Book your introduction",
1513
+ intro: "Choose a time to meet your chapter and get to know each other.",
1451
1514
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
1452
1515
  loadFailed: "Times are briefly unavailable. Please try again.",
1453
1516
  slotTaken: "That time was just taken. Here are the current openings.",
@@ -1639,16 +1702,16 @@ function resolveFormationFlowFields(crm, formation) {
1639
1702
  if (!formation.enabled) return [];
1640
1703
  const def = crm.type(formation.type);
1641
1704
  return [...formation.required, ...formation.optional].map((id10, order2) => {
1642
- const field = def.fields[id10];
1705
+ const field2 = def.fields[id10];
1643
1706
  return {
1644
1707
  id: id10,
1645
- label: field.label ?? humanize(id10),
1646
- type: field.type,
1708
+ label: field2.label ?? humanize(id10),
1709
+ type: field2.type,
1647
1710
  required: formation.required.includes(id10),
1648
1711
  order: order2,
1649
1712
  maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
1650
- ...field.options ? { options: [...field.options] } : {},
1651
- ...id10 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1713
+ ...field2.options ? { options: [...field2.options] } : {},
1714
+ ...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
1652
1715
  };
1653
1716
  });
1654
1717
  }
@@ -1684,19 +1747,19 @@ function resolveLeaderFormation(input, crm) {
1684
1747
  const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
1685
1748
  const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
1686
1749
  for (const [name, values] of [["required", required], ["optional", optional]]) {
1687
- if (!Array.isArray(values) || !values.every((field) => typeof field === "string" && field !== "")) {
1750
+ if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
1688
1751
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1689
1752
  }
1690
1753
  }
1691
- const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
1754
+ const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
1692
1755
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1693
- const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
1756
+ const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
1694
1757
  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));
1758
+ const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
1696
1759
  if (missingRequired.length) {
1697
1760
  throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
1698
1761
  }
1699
- const unsupportedRequired = required.find((field) => !["string", "email", "url", "enum"].includes(def.fields[field].type));
1762
+ const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
1700
1763
  if (unsupportedRequired) {
1701
1764
  throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
1702
1765
  }
@@ -1709,9 +1772,9 @@ function resolveLeaderFormation(input, crm) {
1709
1772
  if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
1710
1773
  throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
1711
1774
  }
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`);
1775
+ for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
1776
+ if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
1777
+ throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
1715
1778
  }
1716
1779
  }
1717
1780
  return {
@@ -1801,10 +1864,10 @@ function resolveNetwork(config, crm) {
1801
1864
  }
1802
1865
  const def = crm.config.types[type];
1803
1866
  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]);
1867
+ const unknown = names.filter((field2) => !def.fields[field2]);
1805
1868
  if (unknown.length > 0) {
1806
1869
  throw new Error(
1807
- `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the leader CRM type`
1870
+ `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
1808
1871
  );
1809
1872
  }
1810
1873
  const nameField = def.nameField ?? "name";
@@ -1892,10 +1955,10 @@ function resolveNetwork(config, crm) {
1892
1955
  if (!def) {
1893
1956
  throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
1894
1957
  }
1895
- const unknown = names.filter((field) => !def.fields[field]);
1958
+ const unknown = names.filter((field2) => !def.fields[field2]);
1896
1959
  if (unknown.length > 0) {
1897
1960
  throw new Error(
1898
- `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the local CRM type`
1961
+ `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
1899
1962
  );
1900
1963
  }
1901
1964
  const nameField = def.nameField ?? "name";
@@ -2213,6 +2276,48 @@ function defineChapter(config) {
2213
2276
  return chapter;
2214
2277
  }
2215
2278
 
2279
+ // src/network-commercial-seats.ts
2280
+ function commercialSeatItems(object2) {
2281
+ const items = object2.items;
2282
+ if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
2283
+ const result = [];
2284
+ for (const item of items.data) {
2285
+ const metadata = item.metadata;
2286
+ if (!metadata?.seatId || !metadata.membershipId) continue;
2287
+ const price = typeof item.price === "string" ? item.price : item.price?.id;
2288
+ 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;
2289
+ result.push({
2290
+ seatId: metadata.seatId,
2291
+ membershipId: metadata.membershipId,
2292
+ subscriptionItemId: item.id,
2293
+ priceId: price,
2294
+ ...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
2295
+ });
2296
+ }
2297
+ return result;
2298
+ }
2299
+ function projectCommercialSeats(payments, subscriptions, refunds) {
2300
+ return payments.map((payment) => {
2301
+ 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 })) : []);
2302
+ const match = matches2.length === 1 ? matches2[0] : void 0;
2303
+ 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);
2304
+ return {
2305
+ ...payment,
2306
+ objectKind: "seat",
2307
+ objectId: payment.seatId,
2308
+ refundedCents,
2309
+ ...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
2310
+ ...match ? {
2311
+ subscriptionId: match.subscription.subscriptionId,
2312
+ subscriptionItemId: match.item.subscriptionItemId,
2313
+ priceId: match.item.priceId,
2314
+ currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
2315
+ cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
2316
+ } : {}
2317
+ };
2318
+ });
2319
+ }
2320
+
2216
2321
  // src/subscription-billing-terms.ts
2217
2322
  import { stripeRequest } from "@odla-ai/stripe";
2218
2323
 
@@ -2384,6 +2489,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2384
2489
  ...chargeId ? { chargeId } : {},
2385
2490
  ...paymentIntentId ? { paymentIntentId } : {},
2386
2491
  ...price,
2492
+ ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2387
2493
  ...status2 ? { status: status2 } : {},
2388
2494
  ...currency ? { currency } : {},
2389
2495
  ...amountCents !== void 0 ? { amountCents } : {},
@@ -2405,8 +2511,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2405
2511
  var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2406
2512
  function directSource(fact, sources) {
2407
2513
  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]));
2514
+ const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
2515
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
2410
2516
  if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2411
2517
  if (supplied.length > 0 && candidates.length === 0) return "wrong";
2412
2518
  return null;
@@ -2502,11 +2608,11 @@ function projectNetworkCommercialEvents(input, sources) {
2502
2608
  }
2503
2609
  }
2504
2610
  const totalsMap = /* @__PURE__ */ new Map();
2505
- const add = (row2, field, amount) => {
2611
+ const add = (row2, field2, amount) => {
2506
2612
  if (!row2.currency) return;
2507
2613
  const key3 = `${row2.sourceKey}:${row2.currency}`;
2508
2614
  const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2509
- total[field] += amount;
2615
+ total[field2] += amount;
2510
2616
  total.netCents = total.paidCents - total.refundedCents;
2511
2617
  totalsMap.set(key3, total);
2512
2618
  };
@@ -2514,7 +2620,7 @@ function projectNetworkCommercialEvents(input, sources) {
2514
2620
  for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
2515
2621
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2516
2622
  return {
2517
- ...seatPayments.length ? { seatPayments } : {},
2623
+ ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2518
2624
  customers,
2519
2625
  subscriptions,
2520
2626
  invoices,
@@ -3088,8 +3194,8 @@ function createChapterIntegration(chapter, options = {}) {
3088
3194
  }
3089
3195
 
3090
3196
  // src/membership-policy.ts
3091
- var fail2 = (field) => {
3092
- throw new TypeError(`invalid membership policy: ${field}`);
3197
+ var fail2 = (field2) => {
3198
+ throw new TypeError(`invalid membership policy: ${field2}`);
3093
3199
  };
3094
3200
  var object = (value) => {
3095
3201
  if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
@@ -3286,7 +3392,8 @@ async function issueMembershipCommercialOffer(db, input) {
3286
3392
  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
3393
  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);
3288
3394
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
3289
- const policy = policies.sort((a, b) => b.version - a.version)[0];
3395
+ const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
3396
+ const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3290
3397
  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");
3291
3398
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3292
3399
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
@@ -3324,8 +3431,73 @@ async function issueMembershipCommercialOffer(db, input) {
3324
3431
  return restore(stored, input);
3325
3432
  }
3326
3433
 
3327
- // src/membership-authority-handler.ts
3328
- import { FEDERATION_HEADERS, verifyFederatedRequest } from "@odla-ai/db";
3434
+ // src/membership-cancel-authority.ts
3435
+ import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
3436
+
3437
+ // src/commercial-evidence-guard.ts
3438
+ async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
3439
+ const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
3440
+ const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3441
+ if (!row2) return { ns, id: id10, exists: false };
3442
+ if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
3443
+ throw Object.assign(new Error(code), { code });
3444
+ }
3445
+ return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
3446
+ }
3447
+
3448
+ // src/membership-authority-state.ts
3449
+ var membershipFail = (code) => {
3450
+ throw Object.assign(new Error(code), { code });
3451
+ };
3452
+ var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3453
+ var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3454
+ var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3455
+ function requireMembershipScope(row2, input) {
3456
+ if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
3457
+ }
3458
+ async function loadMembershipAuthorityState(db, input) {
3459
+ const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3460
+ 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");
3461
+ return row2;
3462
+ }
3463
+ var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3464
+ var membershipClock = (now) => {
3465
+ if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3466
+ };
3467
+
3468
+ // src/membership-restart-payment.ts
3469
+ async function membershipRestartPayment(db, target, member, now) {
3470
+ const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
3471
+ requireMembershipScope(restart, target);
3472
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
3473
+ requireMembershipScope(entitlement, target);
3474
+ if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
3475
+ if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
3476
+ return {
3477
+ restartId: String(restart.restartId),
3478
+ entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
3479
+ attrs: {
3480
+ memberNumber: member.memberNumber,
3481
+ journeyApprovedAt: member.journeyApprovedAt,
3482
+ founding: member.founding,
3483
+ cohort: member.cohort,
3484
+ ...member.personId ? { personId: member.personId } : {},
3485
+ status: "active",
3486
+ revision: Number(member.revision) + 1,
3487
+ createdAt: member.createdAt,
3488
+ graceEndsAt: 0
3489
+ },
3490
+ ops: [
3491
+ membershipUpdate("membershipRestart", id10, { status: "complete" }),
3492
+ { t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
3493
+ ],
3494
+ guards: [
3495
+ { ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3496
+ { ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
3497
+ { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
3498
+ ]
3499
+ };
3500
+ }
3329
3501
 
3330
3502
  // src/membership-checkout-binding.ts
3331
3503
  var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
@@ -3352,10 +3524,11 @@ async function bindMembershipCheckout(db, input, now) {
3352
3524
  const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
3353
3525
  const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
3354
3526
  if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
3527
+ const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
3355
3528
  const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
3356
3529
  try {
3357
3530
  await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
3358
- mutationId: `membership-checkout:${id10}`,
3531
+ mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
3359
3532
  guards: [
3360
3533
  { ns: "membershipOfferCheckout", id: id10, exists: false },
3361
3534
  { ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
@@ -3370,26 +3543,6 @@ async function bindMembershipCheckout(db, input, now) {
3370
3543
  return verifyMembershipCheckoutBinding(saved, input);
3371
3544
  }
3372
3545
 
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
3546
  // src/membership-payment-authority.ts
3394
3547
  async function networkMembershipId(input) {
3395
3548
  return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
@@ -3407,9 +3560,7 @@ async function confirmMembershipPayment(db, input, now) {
3407
3560
  if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
3408
3561
  const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3409
3562
  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
- }
3563
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
3413
3564
  const fact = eventRow.fact;
3414
3565
  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
3566
  appId: input.appId,
@@ -3425,8 +3576,9 @@ async function confirmMembershipPayment(db, input, now) {
3425
3576
  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
3577
  const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
3427
3578
  let member = await membershipLoad(db, "networkMembership", id10);
3428
- if (!member) {
3579
+ if (!member || member.status === "restarting") {
3429
3580
  const offering = offer.contract.offering;
3581
+ const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
3430
3582
  const attrs = {
3431
3583
  id: id10,
3432
3584
  membershipId: id10,
@@ -3458,7 +3610,8 @@ async function confirmMembershipPayment(db, input, now) {
3458
3610
  refundedCents: 0,
3459
3611
  revision: 1,
3460
3612
  createdAt: now,
3461
- updatedAt: now
3613
+ updatedAt: now,
3614
+ ...restart?.attrs
3462
3615
  };
3463
3616
  try {
3464
3617
  await db.transact([
@@ -3468,21 +3621,23 @@ async function confirmMembershipPayment(db, input, now) {
3468
3621
  ...membershipScope(input),
3469
3622
  membershipId: id10,
3470
3623
  kind: "primary",
3471
- status: "pending_approval",
3624
+ status: restart?.attrs.status === "active" ? "active" : "pending_approval",
3472
3625
  termStartedAt: paidAt,
3473
3626
  termEndsAt: endsAt,
3474
3627
  offeringSnapshot: offering,
3475
3628
  sourceEventId: input.eventId,
3476
3629
  revision: 1,
3477
3630
  createdAt: now,
3478
- updatedAt: now
3479
- })
3480
- ], { mutationId: `membership-paid:${id10}`, guards: [
3481
- { ns: "networkMembership", id: id10, exists: false },
3631
+ updatedAt: now,
3632
+ ...restart?.entitlementAttrs
3633
+ }),
3634
+ ...restart?.ops ?? []
3635
+ ], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
3636
+ ...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
3482
3637
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3483
3638
  { ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
3484
3639
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
3485
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
3640
+ evidenceGuard
3486
3641
  ] });
3487
3642
  } catch (error) {
3488
3643
  if (error.code !== "transact_guard_failed") throw error;
@@ -3501,6 +3656,57 @@ async function confirmMembershipPayment(db, input, now) {
3501
3656
  };
3502
3657
  }
3503
3658
 
3659
+ // src/membership-cancel-authority.ts
3660
+ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
3661
+ membershipClock(now);
3662
+ const id10 = await networkMembershipId(target);
3663
+ const state = await loadMembershipAuthorityState(db, target);
3664
+ const member = await membershipLoad(db, "networkMembership", id10);
3665
+ requireMembershipScope(member, target);
3666
+ if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
3667
+ membershipFail("membership_cancellation_unavailable");
3668
+ }
3669
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
3670
+ const read2 = await stripeRequest2({ secretKey: stripeSecret }, "GET", path);
3671
+ const sub = read2.body;
3672
+ if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
3673
+ ...stripePartitionMetadata(target),
3674
+ applicationId: target.applicationId
3675
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
3676
+ if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
3677
+ const write = await stripeRequest2(
3678
+ { secretKey: stripeSecret },
3679
+ "POST",
3680
+ path,
3681
+ { cancel_at_period_end: true, proration_behavior: "none" },
3682
+ { idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
3683
+ );
3684
+ if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
3685
+ membershipFail("membership_cancellation_pending");
3686
+ }
3687
+ }
3688
+ const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
3689
+ if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
3690
+ await db.transact([membershipUpdate("networkMembership", id10, {
3691
+ status: status2,
3692
+ autoRenew: false,
3693
+ graceEndsAt: 0,
3694
+ cancellationRequestedAt: now,
3695
+ revision: Number(member.revision) + 1,
3696
+ updatedAt: now
3697
+ })], {
3698
+ guards: [
3699
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
3700
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
3701
+ ]
3702
+ });
3703
+ }
3704
+ return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
3705
+ }
3706
+
3707
+ // src/membership-authority-handler.ts
3708
+ import { FEDERATION_HEADERS, verifyFederatedRequest } from "@odla-ai/db";
3709
+
3504
3710
  // src/membership-seat-activation.ts
3505
3711
  async function namedSeatPrimaryActivation(db, member, now) {
3506
3712
  if (!member.namedSeatId) return { ops: [], guards: [] };
@@ -3593,7 +3799,7 @@ async function approveNetworkMembership(db, input, now) {
3593
3799
  }
3594
3800
 
3595
3801
  // src/payments-refund.ts
3596
- import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
3802
+ import { stripeRequest as stripeRequest3 } from "@odla-ai/stripe";
3597
3803
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3598
3804
  var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
3599
3805
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
@@ -3603,7 +3809,7 @@ function fail3(code = "refund_target_mismatch") {
3603
3809
  async function refundSubscriptionPayment(secretKey, input) {
3604
3810
  if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
3605
3811
  const call = async (method, path, params, key3) => {
3606
- const result = await stripeRequest2({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
3812
+ const result = await stripeRequest3({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
3607
3813
  if (!result.ok) fail3("stripe_error");
3608
3814
  if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
3609
3815
  return result.body;
@@ -3774,12 +3980,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
3774
3980
  }
3775
3981
 
3776
3982
  // src/membership-provider-lifecycle.ts
3777
- import { stripeRequest as stripeRequest3 } from "@odla-ai/stripe";
3983
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
3778
3984
 
3779
3985
  // src/membership-renewal-authority.ts
3780
3986
  async function reconcileMembershipRenewal(db, input, now) {
3781
3987
  membershipClock(now);
3782
3988
  const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3989
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
3783
3990
  const fact = eventRow?.fact;
3784
3991
  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
3992
  appId: input.appId,
@@ -3787,7 +3994,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3787
3994
  environment: input.environment,
3788
3995
  dataEnvironment: input.environment,
3789
3996
  runtime: input.runtime
3790
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) {
3997
+ }).some(([key3, value]) => fact.identity[key3] !== value)) {
3791
3998
  return membershipFail("membership_renewal_evidence_pending");
3792
3999
  }
3793
4000
  for (let attempt = 0; attempt < 8; attempt++) {
@@ -3801,7 +4008,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3801
4008
  autoRenew: member.autoRenew === true
3802
4009
  });
3803
4010
  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();
4011
+ if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
3805
4012
  const lines = fact.recurringInvoiceLines;
3806
4013
  if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
3807
4014
  const start = lines[0].startsAt, end = lines[0].endsAt;
@@ -3810,7 +4017,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3810
4017
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
3811
4018
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
3812
4019
  if (seat) requireMembershipScope(seat, input);
3813
- const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4020
+ const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
3814
4021
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
3815
4022
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
3816
4023
  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 +4050,7 @@ async function reconcileMembershipRenewal(db, input, now) {
3843
4050
  { ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
3844
4051
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
3845
4052
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
3846
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
4053
+ evidenceGuard
3847
4054
  ];
3848
4055
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
3849
4056
  if (billedSeat?.entitlementId) {
@@ -3889,14 +4096,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3889
4096
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3890
4097
  const fact = row2?.fact;
3891
4098
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
3892
- const collisionId = `network-commercial-exception:${input.eventId}:collision`;
4099
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3893
4100
  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
4101
  appId: input.appId,
3895
4102
  chapterId: input.chapterId,
3896
4103
  environment: input.environment,
3897
4104
  dataEnvironment: input.environment,
3898
4105
  runtime: input.runtime
3899
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4106
+ }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
3900
4107
  const sk = await getVaultSecret(db, stripeSecretName);
3901
4108
  if (!sk) membershipFail("membership_billing_provider_unavailable");
3902
4109
  const id10 = await networkMembershipId(input);
@@ -3910,6 +4117,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3910
4117
  graceEndsAt: Number(value.graceEndsAt ?? 0),
3911
4118
  autoRenew: value.autoRenew === true
3912
4119
  });
4120
+ if (["restarting"].includes(String(member.status))) return receipt();
3913
4121
  if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
3914
4122
  if (fact.eventType === "charge.refunded") {
3915
4123
  if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
@@ -3922,15 +4130,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3922
4130
  alreadyRefundedChargeId: fact.chargeId,
3923
4131
  cancelSubscription: false
3924
4132
  });
3925
- if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4133
+ await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3926
4134
  await refundNetworkMembership(db, input, sk, now);
3927
4135
  const saved2 = await membershipLoad(db, "networkMembership", id10);
3928
4136
  requireMembershipScope(saved2, input);
3929
4137
  return receipt(saved2);
3930
4138
  }
3931
4139
  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))}`);
4140
+ if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4141
+ const result = await stripeRequest4({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
3934
4142
  const sub = result.body;
3935
4143
  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
4144
  membershipFail("membership_billing_provider_mismatch");
@@ -3954,7 +4162,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3954
4162
  { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
3955
4163
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3956
4164
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
3957
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4165
+ evidenceGuard
3958
4166
  ] });
3959
4167
  } catch (error) {
3960
4168
  if (error.code !== "transact_guard_failed") throw error;
@@ -3967,6 +4175,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
3967
4175
  return membershipFail("membership_billing_retry_required");
3968
4176
  }
3969
4177
 
4178
+ // src/membership-restart-seat.ts
4179
+ async function expireRestartedMembershipSeat(db, target, member, now) {
4180
+ const ops = [], guards = [];
4181
+ if (!member.namedSeatId) return { ops, guards };
4182
+ const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
4183
+ requireMembershipScope(seat, target);
4184
+ if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
4185
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
4186
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4187
+ }
4188
+ if (seat.entitlementId) {
4189
+ const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
4190
+ requireMembershipScope(entitlement, target);
4191
+ ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
4192
+ guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
4193
+ }
4194
+ return { ops, guards };
4195
+ }
4196
+
4197
+ // src/membership-restart-authority.ts
4198
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4199
+
4200
+ // src/stripe-event-digest.ts
4201
+ async function stripePayloadDigest(value) {
4202
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
4203
+ return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4204
+ }
4205
+
4206
+ // src/membership-restart-authority.ts
4207
+ function membershipCanRestart(member, now) {
4208
+ const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
4209
+ 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);
4210
+ }
4211
+ async function stopPreviousSubscription(member, target, secret) {
4212
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4213
+ const result = await stripeRequest5({ secretKey: secret }, "GET", path);
4214
+ const sub = result.body;
4215
+ if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4216
+ ...stripePartitionMetadata(target),
4217
+ applicationId: target.applicationId
4218
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4219
+ if (sub.status === "canceled") return;
4220
+ const canceled = await stripeRequest5(
4221
+ { secretKey: secret },
4222
+ "DELETE",
4223
+ path,
4224
+ { invoice_now: false, prorate: false },
4225
+ { idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
4226
+ );
4227
+ if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
4228
+ }
4229
+ async function prepareMembershipRestart(db, input, now, stripeSecret) {
4230
+ membershipClock(now);
4231
+ if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
4232
+ const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4233
+ const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
4234
+ const state = await loadMembershipAuthorityState(db, input);
4235
+ const previous = await membershipLoad(db, "networkMembership", id10);
4236
+ requireMembershipScope(previous, input);
4237
+ const receipt = {
4238
+ applicationId: input.applicationId,
4239
+ membershipId: id10,
4240
+ restartId,
4241
+ memberNumber: Number(previous.memberNumber),
4242
+ status: "ready"
4243
+ };
4244
+ const existing = await membershipLoad(db, "membershipRestart", id10);
4245
+ if (existing?.restartId === restartId && existing.status === "ready") {
4246
+ requireMembershipScope(existing, input);
4247
+ return receipt;
4248
+ }
4249
+ if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
4250
+ if (previous.status !== "restarting") {
4251
+ if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
4252
+ const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4253
+ requireMembershipScope(binding2, input);
4254
+ const seats = await expireRestartedMembershipSeat(db, input, previous, now);
4255
+ await db.transact([
4256
+ ...seats.ops,
4257
+ membershipUpdate("membershipPurchaseHistory", restartId, {
4258
+ id: restartId,
4259
+ ...membershipScope(input),
4260
+ membershipId: id10,
4261
+ providerSubscriptionId: input.previousSubscriptionId,
4262
+ memberSnapshot: previous,
4263
+ bindingSnapshot: binding2,
4264
+ archivedAt: now
4265
+ }),
4266
+ membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
4267
+ ], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
4268
+ ...seats.guards,
4269
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
4270
+ { ns: "membershipPurchaseHistory", id: restartId, exists: false },
4271
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
4272
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4273
+ ] });
4274
+ } else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
4275
+ const reserved = await membershipLoad(db, "networkMembership", id10);
4276
+ requireMembershipScope(reserved, input);
4277
+ if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
4278
+ await stopPreviousSubscription(reserved, input, stripeSecret);
4279
+ const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4280
+ requireMembershipScope(binding, input);
4281
+ await db.transact([
4282
+ membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
4283
+ { t: "delete", ns: "membershipOfferCheckout", id: bindingId }
4284
+ ], { mutationId: `membership-restart-ready:${restartId}`, guards: [
4285
+ { ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
4286
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
4287
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
4288
+ ] });
4289
+ return receipt;
4290
+ }
4291
+
3970
4292
  // src/membership-access-authority.ts
3971
4293
  async function readMembershipAccess(db, input, now) {
3972
4294
  membershipClock(now);
@@ -3983,7 +4305,8 @@ async function readMembershipAccess(db, input, now) {
3983
4305
  allowed: allowed2,
3984
4306
  status: String(member.status),
3985
4307
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
3986
- revision: Number(member.revision)
4308
+ revision: Number(member.revision),
4309
+ restartEligible: !allowed2 && membershipCanRestart(member, now)
3987
4310
  };
3988
4311
  }
3989
4312
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -3991,7 +4314,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
3991
4314
  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
4315
  throw new Error("invalid network access receipt");
3993
4316
  }
3994
- return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
4317
+ return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
3995
4318
  }
3996
4319
 
3997
4320
  // src/membership-seat-reservation.ts
@@ -4072,16 +4395,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
4072
4395
  }
4073
4396
 
4074
4397
  // src/membership-seat-stripe.ts
4075
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4398
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
4076
4399
 
4077
4400
  // src/subscription-provider-contract.ts
4078
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4401
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4079
4402
  function subscriptionContractMismatch() {
4080
4403
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4081
4404
  }
4082
4405
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
4083
4406
  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)}`);
4407
+ const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4085
4408
  const price = result.body;
4086
4409
  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
4410
  return typeof price.product === "string" ? price.product : void 0;
@@ -4099,7 +4422,7 @@ var metadataFor = (seat) => ({
4099
4422
  });
4100
4423
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
4101
4424
  async function read(sk, path, params) {
4102
- const response = await stripeRequest5({ secretKey: sk }, "GET", path, params);
4425
+ const response = await stripeRequest7({ secretKey: sk }, "GET", path, params);
4103
4426
  if (!response.ok) return reject3();
4104
4427
  return response.body;
4105
4428
  }
@@ -4136,11 +4459,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
4136
4459
  }
4137
4460
  await subscriptionForSeat(sk, seat);
4138
4461
  const metadata = metadataFor(seat);
4139
- const result = seat.providerPaymentIntentId ? await stripeRequest5(
4462
+ const result = seat.providerPaymentIntentId ? await stripeRequest7(
4140
4463
  { secretKey: sk },
4141
4464
  "GET",
4142
4465
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
4143
- ) : await stripeRequest5({ secretKey: sk }, "POST", "/v1/payment_intents", {
4466
+ ) : await stripeRequest7({ secretKey: sk }, "POST", "/v1/payment_intents", {
4144
4467
  amount: seat.amountCents,
4145
4468
  currency: seat.currency,
4146
4469
  customer: seat.providerCustomerId,
@@ -4158,7 +4481,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4158
4481
  const sub = await subscriptionForSeat(sk, seat);
4159
4482
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
4160
4483
  if (existing) return String(existing.id);
4161
- const result = await stripeRequest5({ secretKey: sk }, "POST", "/v1/subscription_items", {
4484
+ const result = await stripeRequest7({ secretKey: sk }, "POST", "/v1/subscription_items", {
4162
4485
  subscription: seat.providerSubscriptionId,
4163
4486
  price: seat.priceId,
4164
4487
  quantity: 1,
@@ -4236,8 +4559,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4236
4559
  requireMembershipScope(member, input);
4237
4560
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4238
4561
  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");
4562
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
4241
4563
  const fact = row2.fact;
4242
4564
  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
4565
  appId: input.appId,
@@ -4284,7 +4606,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4284
4606
  { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4285
4607
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4286
4608
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4287
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4609
+ evidenceGuard
4288
4610
  ] });
4289
4611
  } catch (error) {
4290
4612
  if (error.code !== "transact_guard_failed") throw error;
@@ -4433,14 +4755,14 @@ async function recordRecipient(db, input, now, approve) {
4433
4755
  }
4434
4756
 
4435
4757
  // src/membership-seat-refund-stripe.ts
4436
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4758
+ import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
4437
4759
  var fail4 = (code = "named_seat_refund_mismatch") => {
4438
4760
  throw Object.assign(new Error(code), { code });
4439
4761
  };
4440
4762
  async function refundNamedSeatStripePayment(secretKey, seat) {
4441
4763
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
4442
4764
  const call = async (method, path, params, idempotencyKey) => {
4443
- const result = await stripeRequest6({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4765
+ const result = await stripeRequest8({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4444
4766
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4445
4767
  return result.body;
4446
4768
  };
@@ -4562,7 +4884,7 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
4562
4884
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4563
4885
  if (!row2) return membershipFail("named_seat_refund_evidence_pending");
4564
4886
  const fact = row2.fact;
4565
- if (await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) membershipFail("named_seat_refund_evidence_quarantined");
4887
+ await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
4566
4888
  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
4889
  await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
4568
4890
  }
@@ -4834,7 +5156,7 @@ function createMembershipAuthorityHandler(options) {
4834
5156
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
4835
5157
  const sender = req.headers.get(FEDERATION_HEADERS.sender);
4836
5158
  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);
5159
+ if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
4838
5160
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
4839
5161
  if (!peer) return respond({ error: "unauthorized" }, 401);
4840
5162
  let body;
@@ -4856,7 +5178,7 @@ function createMembershipAuthorityHandler(options) {
4856
5178
  let input;
4857
5179
  try {
4858
5180
  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] ?? []];
5181
+ 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"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
4860
5182
  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
5183
  throw new TypeError("invalid request");
4862
5184
  }
@@ -4881,6 +5203,15 @@ function createMembershipAuthorityHandler(options) {
4881
5203
  applicationId: String(input.applicationId),
4882
5204
  skuId: String(input.skuId)
4883
5205
  };
5206
+ if (action === "restarts" || action === "cancel-renewal") {
5207
+ const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
5208
+ if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
5209
+ if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
5210
+ return respond({ ok: true, restart: await prepareMembershipRestart(db, {
5211
+ ...target,
5212
+ previousSubscriptionId: String(input.previousSubscriptionId)
5213
+ }, options.now?.() ?? Date.now(), stripeSecret) });
5214
+ }
4884
5215
  if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
4885
5216
  if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
4886
5217
  db,
@@ -5154,7 +5485,7 @@ async function requestMembershipRefund(options) {
5154
5485
  async function requestMembershipRenewal(options) {
5155
5486
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
5156
5487
  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)) {
5488
+ 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
5489
  throw new Error("invalid membership renewal receipt");
5159
5490
  }
5160
5491
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -5196,9 +5527,25 @@ async function requestMembershipAccess(options) {
5196
5527
  const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
5197
5528
  return verifyMembershipAccessReceipt(result.access, options, Date.now());
5198
5529
  }
5530
+ async function requestMembershipRestart(options) {
5531
+ const result = await requestAuthority(options, "restarts", {
5532
+ applicationId: options.applicationId,
5533
+ skuId: options.skuId,
5534
+ previousSubscriptionId: options.previousSubscriptionId
5535
+ });
5536
+ const row2 = result.restart;
5537
+ 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");
5538
+ return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
5539
+ }
5540
+ async function requestMembershipCancellation(options) {
5541
+ const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
5542
+ const row2 = result.renewal;
5543
+ 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");
5544
+ return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
5545
+ }
5199
5546
 
5200
5547
  // src/membership-seat-cancel.ts
5201
- import { initStripe, stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5548
+ import { initStripe, stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
5202
5549
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5203
5550
  membershipClock(now);
5204
5551
  const state = await loadMembershipAuthorityState(db, target);
@@ -5226,7 +5573,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5226
5573
  }
5227
5574
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
5228
5575
  const request = async (method, path, params) => {
5229
- const result = await stripeRequest7(
5576
+ const result = await stripeRequest9(
5230
5577
  { secretKey },
5231
5578
  method,
5232
5579
  path,
@@ -5463,7 +5810,27 @@ var membershipAuthorityEntities = {
5463
5810
  cancellationRequestedAt: attr9("number", true),
5464
5811
  graceEndsAt: attr9("number", true),
5465
5812
  ...times(),
5466
- refundRequestedAt: attr9("number", true)
5813
+ refundRequestedAt: attr9("number", true),
5814
+ restartId: key2(true)
5815
+ } },
5816
+ membershipRestart: { attrs: {
5817
+ id: id9(),
5818
+ ...scope(),
5819
+ membershipId: key2(),
5820
+ restartId: key2(),
5821
+ previousSubscriptionId: key2(),
5822
+ memberNumber: attr9("number"),
5823
+ status: key2(),
5824
+ createdAt: attr9("number")
5825
+ } },
5826
+ membershipPurchaseHistory: { attrs: {
5827
+ id: id9(),
5828
+ ...scope(),
5829
+ membershipId: key2(),
5830
+ providerSubscriptionId: key2(),
5831
+ memberSnapshot: attr9("json"),
5832
+ bindingSnapshot: attr9("json"),
5833
+ archivedAt: attr9("number")
5467
5834
  } },
5468
5835
  networkEntitlement: { attrs: {
5469
5836
  id: id9(),
@@ -6894,9 +7261,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
6894
7261
  }
6895
7262
  const nameField = def.nameField ?? "name";
6896
7263
  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;
7264
+ for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
7265
+ const value = record4.fields?.[field2];
7266
+ if (value !== void 0) input[field2] = value;
6900
7267
  }
6901
7268
  if (input[nameField] === void 0) input[nameField] = record4.name;
6902
7269
  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 +7532,9 @@ function subAnnualCents(sub) {
7165
7532
  }
7166
7533
 
7167
7534
  // src/payments-stripe.ts
7168
- import { stripeRequest as stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7535
+ import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7169
7536
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7170
- return stripeRequest8(
7537
+ return stripeRequest10(
7171
7538
  { secretKey: sk },
7172
7539
  method,
7173
7540
  path,
@@ -7477,7 +7844,8 @@ function applicationSummary(app) {
7477
7844
  status: app.status,
7478
7845
  createdAt: app.createdAt ?? null,
7479
7846
  meetingLink: app.meetingLink ?? null,
7480
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
7847
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
7848
+ ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
7481
7849
  renewalAt: app.renewalAt ?? null,
7482
7850
  canceled: app.canceled === true,
7483
7851
  tierId: app.tierId ?? null
@@ -7659,7 +8027,7 @@ function isValidTimeZone(tz) {
7659
8027
  function resolveScheduling(config) {
7660
8028
  const result = validateScheduling(config);
7661
8029
  if (result.ok) return result.value;
7662
- const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
8030
+ const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
7663
8031
  throw new Error(`scheduling: ${detail}`);
7664
8032
  }
7665
8033
  function validateScheduling(config) {
@@ -8806,7 +9174,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
8806
9174
  MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
8807
9175
  );
8808
9176
  function projectMemberSelf(record4) {
8809
- return Object.fromEntries(Object.entries(record4).filter(([field]) => MEMBER_SELF_READABLE_FIELDS.has(field)));
9177
+ return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
8810
9178
  }
8811
9179
  function classifyMemberSystemWrite(intent) {
8812
9180
  if (intent.actor === "bnf") return "reject_bnf_surface";
@@ -9160,12 +9528,14 @@ export {
9160
9528
  renewalPatch,
9161
9529
  requestMembershipAccess,
9162
9530
  requestMembershipApproval,
9531
+ requestMembershipCancellation,
9163
9532
  requestMembershipChapterEffect,
9164
9533
  requestMembershipCheckoutBinding,
9165
9534
  requestMembershipCommercialOffer,
9166
9535
  requestMembershipPayment,
9167
9536
  requestMembershipRefund,
9168
9537
  requestMembershipRenewal,
9538
+ requestMembershipRestart,
9169
9539
  requestNamedSeatAcceptance,
9170
9540
  requestNamedSeatAccess,
9171
9541
  requestNamedSeatApproval,