@odla-ai/chapter 0.48.2 → 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.cjs CHANGED
@@ -204,12 +204,14 @@ __export(src_exports, {
204
204
  renewalPatch: () => renewalPatch,
205
205
  requestMembershipAccess: () => requestMembershipAccess,
206
206
  requestMembershipApproval: () => requestMembershipApproval,
207
+ requestMembershipCancellation: () => requestMembershipCancellation,
207
208
  requestMembershipChapterEffect: () => requestMembershipChapterEffect,
208
209
  requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
209
210
  requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
210
211
  requestMembershipPayment: () => requestMembershipPayment,
211
212
  requestMembershipRefund: () => requestMembershipRefund,
212
213
  requestMembershipRenewal: () => requestMembershipRenewal,
214
+ requestMembershipRestart: () => requestMembershipRestart,
213
215
  requestNamedSeatAcceptance: () => requestNamedSeatAcceptance,
214
216
  requestNamedSeatAccess: () => requestNamedSeatAccess,
215
217
  requestNamedSeatApproval: () => requestNamedSeatApproval,
@@ -241,7 +243,7 @@ __export(src_exports, {
241
243
  slotWindow: () => slotWindow,
242
244
  stageIndex: () => stageIndex,
243
245
  stripeCall: () => stripeCall,
244
- stripeForm: () => import_stripe8.stripeForm,
246
+ stripeForm: () => import_stripe10.stripeForm,
245
247
  subAnnualCents: () => subAnnualCents,
246
248
  submitApplication: () => submitApplication,
247
249
  subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
@@ -262,7 +264,7 @@ __export(src_exports, {
262
264
  verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
263
265
  verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
264
266
  verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
265
- verifyStripeSignature: () => import_stripe8.verifyStripeSignature,
267
+ verifyStripeSignature: () => import_stripe10.verifyStripeSignature,
266
268
  webhookMutationId: () => webhookMutationId
267
269
  });
268
270
  module.exports = __toCommonJS(src_exports);
@@ -307,12 +309,12 @@ function resolveApplication(a) {
307
309
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
308
310
  throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
309
311
  }
310
- const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
312
+ const unsafe = arr.find((field2) => !SCHEMA_FIELD.test(field2));
311
313
  if (unsafe) {
312
314
  throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
313
315
  }
314
316
  }
315
- const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
317
+ const duplicate = [...required, ...optional].find((field2, index2, fields) => fields.indexOf(field2) !== index2);
316
318
  if (duplicate) {
317
319
  throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
318
320
  }
@@ -323,14 +325,14 @@ function resolveApplication(a) {
323
325
  throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
324
326
  }
325
327
  const conditions = a?.conditions ?? {};
326
- for (const [field, declared] of Object.entries(conditions)) {
328
+ for (const [field2, declared] of Object.entries(conditions)) {
327
329
  for (const key3 of ["visibleWhen", "requiredWhen"]) {
328
330
  const expression = declared?.[key3];
329
331
  if (expression === void 0) continue;
330
332
  if (typeof expression !== "string" || !expression.trim()) {
331
- throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
333
+ throw new Error(`defineChapter.application.conditions.${field2}.${key3}: must be a non-empty CEL condition`);
332
334
  }
333
- (0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
335
+ (0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field2}.${key3}`);
334
336
  }
335
337
  }
336
338
  return {
@@ -401,6 +403,13 @@ async function submitApplication(db, chapter, fields, opts) {
401
403
  if (opts.groupId) row2.groupId = opts.groupId;
402
404
  if (opts.runtime) row2.stripeRuntime = opts.runtime;
403
405
  if (chapter.account !== "none") row2.clerkAccountPending = true;
406
+ if (opts.namedSeat) Object.assign(row2, {
407
+ namedSeatId: opts.namedSeat.seatId,
408
+ namedSeatPrimaryApplicationId: opts.namedSeat.primaryApplicationId,
409
+ namedSeatClaimPending: true,
410
+ clerkUserId: opts.namedSeat.userId,
411
+ clerkAccountPending: false
412
+ });
404
413
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
405
414
  row2.emailNormalized = normalizeChapterEmail(fields.email);
406
415
  row2.signupEmailKey = row2.emailNormalized;
@@ -530,6 +539,10 @@ var applicationAttrs = {
530
539
  giftPaidAt: attr("number", { optional: true, indexed: true }),
531
540
  giftRevokedAt: attr("number", { optional: true, indexed: true }),
532
541
  giftCheckoutStartedAt: attr("number", { optional: true, indexed: true }),
542
+ membershipRestartId: attr("string", { optional: true, indexed: true }),
543
+ membershipStatus: attr("string", { optional: true }),
544
+ membershipGraceEndsAt: attr("number", { optional: true }),
545
+ membershipAutoRenew: attr("boolean", { optional: true }),
533
546
  namedSeatId: attr("string", { optional: true, unique: true, indexed: true }),
534
547
  namedSeatPrimaryApplicationId: attr("string", { optional: true }),
535
548
  namedSeatClaimPending: attr("boolean", { optional: true, indexed: true }),
@@ -540,19 +553,19 @@ function applicationsFor(application) {
540
553
  for (const [name, spec] of Object.entries(applicationAttrs)) {
541
554
  attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
542
555
  }
543
- const apply = (field, optional) => {
544
- const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
545
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
546
- if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
547
- Object.defineProperty(attrs, field, {
556
+ const apply = (field2, optional) => {
557
+ const existing = Object.hasOwn(attrs, field2) ? attrs[field2] : void 0;
558
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field2)) throw new Error(`defineChapter.application: field "${field2}" is reserved by the applications schema`);
559
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field2}" is not a configurable string field`);
560
+ Object.defineProperty(attrs, field2, {
548
561
  value: existing ? { ...existing, optional } : attr("string", { optional }),
549
562
  enumerable: true,
550
563
  configurable: true,
551
564
  writable: true
552
565
  });
553
566
  };
554
- for (const field of application.required) apply(field, false);
555
- for (const field of application.optional) apply(field, true);
567
+ for (const field2 of application.required) apply(field2, false);
568
+ for (const field2 of application.optional) apply(field2, true);
556
569
  return { attrs };
557
570
  }
558
571
 
@@ -759,6 +772,19 @@ var giftRedemptionAttemptsEntity = { attrs: {
759
772
  updatedAt: attr4("number")
760
773
  } };
761
774
 
775
+ // src/payment-recovery-schema.ts
776
+ var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
777
+ var paymentRecoveryReviewsEntity = { attrs: {
778
+ id: field("string", true),
779
+ eventId: field("string"),
780
+ actorUserId: field("string"),
781
+ reviewedAt: field("number"),
782
+ previousDigest: field("string"),
783
+ providerDigest: field("string"),
784
+ scope: field("string"),
785
+ previousState: field("json")
786
+ } };
787
+
762
788
  // src/schema-stripe.ts
763
789
  var attr5 = (type, flags = {}) => ({
764
790
  type,
@@ -779,6 +805,10 @@ var stripeEventReceiptsEntity = {
779
805
  dataEnvironment: attr5("string", { indexed: true, optional: true }),
780
806
  runtime: attr5("string", { indexed: true, optional: true }),
781
807
  chapterId: attr5("string", { indexed: true, optional: true }),
808
+ retryReady: attr5("boolean", { optional: true }),
809
+ reviewedDigest: attr5("string", { optional: true }),
810
+ reviewId: attr5("string", { optional: true }),
811
+ collisionDigest: attr5("string", { optional: true }),
782
812
  receivedAt: attr5("number", { indexed: true })
783
813
  }
784
814
  };
@@ -803,6 +833,7 @@ var subscriptionCheckoutIntentsEntity = {
803
833
  requestedCustomerId: attr5("string", { indexed: true, optional: true }),
804
834
  providerCustomerId: attr5("string", { indexed: true, optional: true }),
805
835
  providerSubscriptionId: attr5("string", { unique: true, indexed: true, optional: true }),
836
+ purchaseAttemptId: attr5("string", { optional: true }),
806
837
  status: attr5("string", { indexed: true }),
807
838
  createdAt: attr5("number", { indexed: true }),
808
839
  updatedAt: attr5("number", { indexed: true })
@@ -827,6 +858,20 @@ var membershipQuoteProjectionsEntity = {
827
858
  quote: attr5("json", { optional: true })
828
859
  }
829
860
  };
861
+ var membershipCheckoutHistoryEntity = { attrs: {
862
+ id: attr5("string", { unique: true, indexed: true }),
863
+ applicationId: attr5("string", { indexed: true }),
864
+ appId: attr5("string"),
865
+ chapterId: attr5("string"),
866
+ environment: attr5("string"),
867
+ runtime: attr5("string"),
868
+ skuId: attr5("string"),
869
+ providerSubscriptionId: attr5("string", { indexed: true }),
870
+ applicationSnapshot: attr5("json"),
871
+ intentSnapshot: attr5("json"),
872
+ quoteSnapshot: attr5("json"),
873
+ archivedAt: attr5("number")
874
+ } };
830
875
 
831
876
  // src/schema-network-person.ts
832
877
  var string = (flags = {}) => ({
@@ -960,15 +1005,19 @@ var exceptions = { attrs: {
960
1005
  kind: string2({ indexed: true }),
961
1006
  sourceKey: string2({ indexed: true, optional: true }),
962
1007
  status: string2({ indexed: true }),
1008
+ resolvedDigest: string2({ optional: true }),
1009
+ incomingDigest: string2({ optional: true }),
963
1010
  detail: json2(),
964
1011
  updatedAt: number2({ indexed: true })
965
1012
  } };
966
1013
  var networkCommercialEntities = {
1014
+ paymentRecoveryReviews: paymentRecoveryReviewsEntity,
967
1015
  networkCommercialEvents: events,
968
1016
  networkCommercialCustomers: projection(),
969
1017
  networkCommercialSubscriptions: projection(),
970
1018
  networkCommercialInvoices: projection(),
971
1019
  networkCommercialRefunds: projection(),
1020
+ networkCommercialSeats: projection(),
972
1021
  networkCommercialExceptions: exceptions
973
1022
  };
974
1023
 
@@ -1171,8 +1220,9 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
1171
1220
  entities.tiers = tiers;
1172
1221
  entities.meetings = meetings;
1173
1222
  entities.emailLog = emailLog;
1223
+ entities.paymentRecoveryReviews = paymentRecoveryReviewsEntity;
1174
1224
  entities.stripeEventReceipts = stripeEventReceiptsEntity;
1175
- entities.subscriptionCheckoutIntents = subscriptionCheckoutIntentsEntity;
1225
+ Object.assign(entities, { subscriptionCheckoutIntents: subscriptionCheckoutIntentsEntity, membershipCheckoutHistory: membershipCheckoutHistoryEntity });
1176
1226
  entities.membershipQuoteProjections = membershipQuoteProjectionsEntity;
1177
1227
  entities.namedSeatConsents = namedSeatConsentsEntity;
1178
1228
  entities.admissionGrants = admissionGrantsEntity;
@@ -1710,6 +1760,19 @@ var DEFAULT_COMMON_COPY = {
1710
1760
  unknown: "(unknown)"
1711
1761
  };
1712
1762
  var DEFAULT_JOIN_COPY = {
1763
+ namedSeat: {
1764
+ title: "Your membership invitation",
1765
+ from: "{purchaserName} invited you.",
1766
+ prepaid: "Your {tierName} seat is already paid for.",
1767
+ body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
1768
+ consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
1769
+ accept: "Accept my invitation",
1770
+ accepting: "Confirming your invitation\u2026",
1771
+ loading: "Loading your invitation\u2026",
1772
+ unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
1773
+ retry: "Try again",
1774
+ failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
1775
+ },
1713
1776
  form: {
1714
1777
  submit: "Submit application",
1715
1778
  submitting: "Submitting\u2026",
@@ -1717,6 +1780,8 @@ var DEFAULT_JOIN_COPY = {
1717
1780
  unexpectedFailure: "Something went wrong. Please try again."
1718
1781
  },
1719
1782
  booking: {
1783
+ title: "Book your introduction",
1784
+ intro: "Choose a time to meet your chapter and get to know each other.",
1720
1785
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
1721
1786
  loadFailed: "Times are briefly unavailable. Please try again.",
1722
1787
  slotTaken: "That time was just taken. Here are the current openings.",
@@ -1908,16 +1973,16 @@ function resolveFormationFlowFields(crm, formation) {
1908
1973
  if (!formation.enabled) return [];
1909
1974
  const def = crm.type(formation.type);
1910
1975
  return [...formation.required, ...formation.optional].map((id10, order2) => {
1911
- const field = def.fields[id10];
1976
+ const field2 = def.fields[id10];
1912
1977
  return {
1913
1978
  id: id10,
1914
- label: field.label ?? humanize(id10),
1915
- type: field.type,
1979
+ label: field2.label ?? humanize(id10),
1980
+ type: field2.type,
1916
1981
  required: formation.required.includes(id10),
1917
1982
  order: order2,
1918
1983
  maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
1919
- ...field.options ? { options: [...field.options] } : {},
1920
- ...id10 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1984
+ ...field2.options ? { options: [...field2.options] } : {},
1985
+ ...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
1921
1986
  };
1922
1987
  });
1923
1988
  }
@@ -1953,19 +2018,19 @@ function resolveLeaderFormation(input, crm) {
1953
2018
  const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
1954
2019
  const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
1955
2020
  for (const [name, values] of [["required", required], ["optional", optional]]) {
1956
- if (!Array.isArray(values) || !values.every((field) => typeof field === "string" && field !== "")) {
2021
+ if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
1957
2022
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1958
2023
  }
1959
2024
  }
1960
- const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
2025
+ const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
1961
2026
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1962
- const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
2027
+ const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
1963
2028
  if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
1964
- const missingRequired = Object.entries(def.fields).filter(([, field]) => field.required).map(([field]) => field).filter((field) => !required.includes(field));
2029
+ const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
1965
2030
  if (missingRequired.length) {
1966
2031
  throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
1967
2032
  }
1968
- const unsupportedRequired = required.find((field) => !["string", "email", "url", "enum"].includes(def.fields[field].type));
2033
+ const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
1969
2034
  if (unsupportedRequired) {
1970
2035
  throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
1971
2036
  }
@@ -1978,9 +2043,9 @@ function resolveLeaderFormation(input, crm) {
1978
2043
  if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
1979
2044
  throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
1980
2045
  }
1981
- for (const [field, cap] of Object.entries(input.maxLen ?? {})) {
1982
- if (![...required, ...optional].includes(field) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
1983
- throw new Error(`defineChapter.formation.maxLen.${field}: must name a public field and be an integer from 1 to 32768`);
2046
+ for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
2047
+ if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
2048
+ throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
1984
2049
  }
1985
2050
  }
1986
2051
  return {
@@ -2070,10 +2135,10 @@ function resolveNetwork(config, crm) {
2070
2135
  }
2071
2136
  const def = crm.config.types[type];
2072
2137
  if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
2073
- const unknown = names.filter((field) => !def.fields[field]);
2138
+ const unknown = names.filter((field2) => !def.fields[field2]);
2074
2139
  if (unknown.length > 0) {
2075
2140
  throw new Error(
2076
- `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the leader CRM type`
2141
+ `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
2077
2142
  );
2078
2143
  }
2079
2144
  const nameField = def.nameField ?? "name";
@@ -2161,10 +2226,10 @@ function resolveNetwork(config, crm) {
2161
2226
  if (!def) {
2162
2227
  throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
2163
2228
  }
2164
- const unknown = names.filter((field) => !def.fields[field]);
2229
+ const unknown = names.filter((field2) => !def.fields[field2]);
2165
2230
  if (unknown.length > 0) {
2166
2231
  throw new Error(
2167
- `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the local CRM type`
2232
+ `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
2168
2233
  );
2169
2234
  }
2170
2235
  const nameField = def.nameField ?? "name";
@@ -2482,6 +2547,48 @@ function defineChapter(config) {
2482
2547
  return chapter;
2483
2548
  }
2484
2549
 
2550
+ // src/network-commercial-seats.ts
2551
+ function commercialSeatItems(object2) {
2552
+ const items = object2.items;
2553
+ if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
2554
+ const result = [];
2555
+ for (const item of items.data) {
2556
+ const metadata = item.metadata;
2557
+ if (!metadata?.seatId || !metadata.membershipId) continue;
2558
+ const price = typeof item.price === "string" ? item.price : item.price?.id;
2559
+ 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;
2560
+ result.push({
2561
+ seatId: metadata.seatId,
2562
+ membershipId: metadata.membershipId,
2563
+ subscriptionItemId: item.id,
2564
+ priceId: price,
2565
+ ...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
2566
+ });
2567
+ }
2568
+ return result;
2569
+ }
2570
+ function projectCommercialSeats(payments, subscriptions, refunds) {
2571
+ return payments.map((payment) => {
2572
+ 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 })) : []);
2573
+ const match = matches2.length === 1 ? matches2[0] : void 0;
2574
+ 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);
2575
+ return {
2576
+ ...payment,
2577
+ objectKind: "seat",
2578
+ objectId: payment.seatId,
2579
+ refundedCents,
2580
+ ...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
2581
+ ...match ? {
2582
+ subscriptionId: match.subscription.subscriptionId,
2583
+ subscriptionItemId: match.item.subscriptionItemId,
2584
+ priceId: match.item.priceId,
2585
+ currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
2586
+ cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
2587
+ } : {}
2588
+ };
2589
+ });
2590
+ }
2591
+
2485
2592
  // src/subscription-billing-terms.ts
2486
2593
  var import_stripe = require("@odla-ai/stripe");
2487
2594
 
@@ -2653,6 +2760,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2653
2760
  ...chargeId ? { chargeId } : {},
2654
2761
  ...paymentIntentId ? { paymentIntentId } : {},
2655
2762
  ...price,
2763
+ ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2656
2764
  ...status2 ? { status: status2 } : {},
2657
2765
  ...currency ? { currency } : {},
2658
2766
  ...amountCents !== void 0 ? { amountCents } : {},
@@ -2674,8 +2782,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2674
2782
  var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2675
2783
  function directSource(fact, sources) {
2676
2784
  const mode = fact.livemode ? "live" : "test";
2677
- const supplied = identityFields.filter((field) => fact.identity[field] !== void 0);
2678
- const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field) => source[field] === fact.identity[field]));
2785
+ const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
2786
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
2679
2787
  if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2680
2788
  if (supplied.length > 0 && candidates.length === 0) return "wrong";
2681
2789
  return null;
@@ -2771,11 +2879,11 @@ function projectNetworkCommercialEvents(input, sources) {
2771
2879
  }
2772
2880
  }
2773
2881
  const totalsMap = /* @__PURE__ */ new Map();
2774
- const add = (row2, field, amount) => {
2882
+ const add = (row2, field2, amount) => {
2775
2883
  if (!row2.currency) return;
2776
2884
  const key3 = `${row2.sourceKey}:${row2.currency}`;
2777
2885
  const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2778
- total[field] += amount;
2886
+ total[field2] += amount;
2779
2887
  total.netCents = total.paidCents - total.refundedCents;
2780
2888
  totalsMap.set(key3, total);
2781
2889
  };
@@ -2783,7 +2891,7 @@ function projectNetworkCommercialEvents(input, sources) {
2783
2891
  for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
2784
2892
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2785
2893
  return {
2786
- ...seatPayments.length ? { seatPayments } : {},
2894
+ ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2787
2895
  customers,
2788
2896
  subscriptions,
2789
2897
  invoices,
@@ -3357,8 +3465,8 @@ function createChapterIntegration(chapter, options = {}) {
3357
3465
  }
3358
3466
 
3359
3467
  // src/membership-policy.ts
3360
- var fail2 = (field) => {
3361
- throw new TypeError(`invalid membership policy: ${field}`);
3468
+ var fail2 = (field2) => {
3469
+ throw new TypeError(`invalid membership policy: ${field2}`);
3362
3470
  };
3363
3471
  var object = (value) => {
3364
3472
  if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
@@ -3555,7 +3663,8 @@ async function issueMembershipCommercialOffer(db, input) {
3555
3663
  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));
3556
3664
  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);
3557
3665
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
3558
- const policy = policies.sort((a, b) => b.version - a.version)[0];
3666
+ const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
3667
+ const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3559
3668
  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");
3560
3669
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3561
3670
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
@@ -3593,8 +3702,73 @@ async function issueMembershipCommercialOffer(db, input) {
3593
3702
  return restore(stored, input);
3594
3703
  }
3595
3704
 
3596
- // src/membership-authority-handler.ts
3597
- var import_db = require("@odla-ai/db");
3705
+ // src/membership-cancel-authority.ts
3706
+ var import_stripe2 = require("@odla-ai/stripe");
3707
+
3708
+ // src/commercial-evidence-guard.ts
3709
+ async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
3710
+ const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
3711
+ const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3712
+ if (!row2) return { ns, id: id10, exists: false };
3713
+ if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
3714
+ throw Object.assign(new Error(code), { code });
3715
+ }
3716
+ return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
3717
+ }
3718
+
3719
+ // src/membership-authority-state.ts
3720
+ var membershipFail = (code) => {
3721
+ throw Object.assign(new Error(code), { code });
3722
+ };
3723
+ var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3724
+ var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3725
+ var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3726
+ function requireMembershipScope(row2, input) {
3727
+ if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
3728
+ }
3729
+ async function loadMembershipAuthorityState(db, input) {
3730
+ const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3731
+ 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");
3732
+ return row2;
3733
+ }
3734
+ var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3735
+ var membershipClock = (now) => {
3736
+ if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3737
+ };
3738
+
3739
+ // src/membership-restart-payment.ts
3740
+ async function membershipRestartPayment(db, target, member, now) {
3741
+ const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
3742
+ requireMembershipScope(restart, target);
3743
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
3744
+ requireMembershipScope(entitlement, target);
3745
+ if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
3746
+ if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
3747
+ return {
3748
+ restartId: String(restart.restartId),
3749
+ entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
3750
+ attrs: {
3751
+ memberNumber: member.memberNumber,
3752
+ journeyApprovedAt: member.journeyApprovedAt,
3753
+ founding: member.founding,
3754
+ cohort: member.cohort,
3755
+ ...member.personId ? { personId: member.personId } : {},
3756
+ status: "active",
3757
+ revision: Number(member.revision) + 1,
3758
+ createdAt: member.createdAt,
3759
+ graceEndsAt: 0
3760
+ },
3761
+ ops: [
3762
+ membershipUpdate("membershipRestart", id10, { status: "complete" }),
3763
+ { t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
3764
+ ],
3765
+ guards: [
3766
+ { ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3767
+ { ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
3768
+ { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
3769
+ ]
3770
+ };
3771
+ }
3598
3772
 
3599
3773
  // src/membership-checkout-binding.ts
3600
3774
  var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
@@ -3621,10 +3795,11 @@ async function bindMembershipCheckout(db, input, now) {
3621
3795
  const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
3622
3796
  const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
3623
3797
  if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
3798
+ const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
3624
3799
  const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
3625
3800
  try {
3626
3801
  await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
3627
- mutationId: `membership-checkout:${id10}`,
3802
+ mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
3628
3803
  guards: [
3629
3804
  { ns: "membershipOfferCheckout", id: id10, exists: false },
3630
3805
  { ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
@@ -3639,26 +3814,6 @@ async function bindMembershipCheckout(db, input, now) {
3639
3814
  return verifyMembershipCheckoutBinding(saved, input);
3640
3815
  }
3641
3816
 
3642
- // src/membership-authority-state.ts
3643
- var membershipFail = (code) => {
3644
- throw Object.assign(new Error(code), { code });
3645
- };
3646
- var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3647
- var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3648
- var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3649
- function requireMembershipScope(row2, input) {
3650
- if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
3651
- }
3652
- async function loadMembershipAuthorityState(db, input) {
3653
- const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3654
- 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");
3655
- return row2;
3656
- }
3657
- var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3658
- var membershipClock = (now) => {
3659
- if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3660
- };
3661
-
3662
3817
  // src/membership-payment-authority.ts
3663
3818
  async function networkMembershipId(input) {
3664
3819
  return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
@@ -3676,9 +3831,7 @@ async function confirmMembershipPayment(db, input, now) {
3676
3831
  if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
3677
3832
  const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3678
3833
  if (!eventRow) membershipFail("membership_payment_evidence_pending");
3679
- if (await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) {
3680
- membershipFail("membership_payment_evidence_quarantined");
3681
- }
3834
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
3682
3835
  const fact = eventRow.fact;
3683
3836
  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({
3684
3837
  appId: input.appId,
@@ -3694,8 +3847,9 @@ async function confirmMembershipPayment(db, input, now) {
3694
3847
  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");
3695
3848
  const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
3696
3849
  let member = await membershipLoad(db, "networkMembership", id10);
3697
- if (!member) {
3850
+ if (!member || member.status === "restarting") {
3698
3851
  const offering = offer.contract.offering;
3852
+ const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
3699
3853
  const attrs = {
3700
3854
  id: id10,
3701
3855
  membershipId: id10,
@@ -3727,7 +3881,8 @@ async function confirmMembershipPayment(db, input, now) {
3727
3881
  refundedCents: 0,
3728
3882
  revision: 1,
3729
3883
  createdAt: now,
3730
- updatedAt: now
3884
+ updatedAt: now,
3885
+ ...restart?.attrs
3731
3886
  };
3732
3887
  try {
3733
3888
  await db.transact([
@@ -3737,21 +3892,23 @@ async function confirmMembershipPayment(db, input, now) {
3737
3892
  ...membershipScope(input),
3738
3893
  membershipId: id10,
3739
3894
  kind: "primary",
3740
- status: "pending_approval",
3895
+ status: restart?.attrs.status === "active" ? "active" : "pending_approval",
3741
3896
  termStartedAt: paidAt,
3742
3897
  termEndsAt: endsAt,
3743
3898
  offeringSnapshot: offering,
3744
3899
  sourceEventId: input.eventId,
3745
3900
  revision: 1,
3746
3901
  createdAt: now,
3747
- updatedAt: now
3748
- })
3749
- ], { mutationId: `membership-paid:${id10}`, guards: [
3750
- { ns: "networkMembership", id: id10, exists: false },
3902
+ updatedAt: now,
3903
+ ...restart?.entitlementAttrs
3904
+ }),
3905
+ ...restart?.ops ?? []
3906
+ ], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
3907
+ ...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
3751
3908
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3752
3909
  { ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
3753
3910
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
3754
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
3911
+ evidenceGuard
3755
3912
  ] });
3756
3913
  } catch (error) {
3757
3914
  if (error.code !== "transact_guard_failed") throw error;
@@ -3770,6 +3927,57 @@ async function confirmMembershipPayment(db, input, now) {
3770
3927
  };
3771
3928
  }
3772
3929
 
3930
+ // src/membership-cancel-authority.ts
3931
+ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
3932
+ membershipClock(now);
3933
+ const id10 = await networkMembershipId(target);
3934
+ const state = await loadMembershipAuthorityState(db, target);
3935
+ const member = await membershipLoad(db, "networkMembership", id10);
3936
+ requireMembershipScope(member, target);
3937
+ if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
3938
+ membershipFail("membership_cancellation_unavailable");
3939
+ }
3940
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
3941
+ const read2 = await (0, import_stripe2.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
3942
+ const sub = read2.body;
3943
+ if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
3944
+ ...stripePartitionMetadata(target),
3945
+ applicationId: target.applicationId
3946
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
3947
+ if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
3948
+ const write = await (0, import_stripe2.stripeRequest)(
3949
+ { secretKey: stripeSecret },
3950
+ "POST",
3951
+ path,
3952
+ { cancel_at_period_end: true, proration_behavior: "none" },
3953
+ { idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
3954
+ );
3955
+ if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
3956
+ membershipFail("membership_cancellation_pending");
3957
+ }
3958
+ }
3959
+ const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
3960
+ if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
3961
+ await db.transact([membershipUpdate("networkMembership", id10, {
3962
+ status: status2,
3963
+ autoRenew: false,
3964
+ graceEndsAt: 0,
3965
+ cancellationRequestedAt: now,
3966
+ revision: Number(member.revision) + 1,
3967
+ updatedAt: now
3968
+ })], {
3969
+ guards: [
3970
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
3971
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
3972
+ ]
3973
+ });
3974
+ }
3975
+ return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
3976
+ }
3977
+
3978
+ // src/membership-authority-handler.ts
3979
+ var import_db = require("@odla-ai/db");
3980
+
3773
3981
  // src/membership-seat-activation.ts
3774
3982
  async function namedSeatPrimaryActivation(db, member, now) {
3775
3983
  if (!member.namedSeatId) return { ops: [], guards: [] };
@@ -3862,7 +4070,7 @@ async function approveNetworkMembership(db, input, now) {
3862
4070
  }
3863
4071
 
3864
4072
  // src/payments-refund.ts
3865
- var import_stripe2 = require("@odla-ai/stripe");
4073
+ var import_stripe3 = require("@odla-ai/stripe");
3866
4074
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3867
4075
  var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
3868
4076
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
@@ -3872,7 +4080,7 @@ function fail3(code = "refund_target_mismatch") {
3872
4080
  async function refundSubscriptionPayment(secretKey, input) {
3873
4081
  if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
3874
4082
  const call = async (method, path, params, key3) => {
3875
- const result = await (0, import_stripe2.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4083
+ const result = await (0, import_stripe3.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
3876
4084
  if (!result.ok) fail3("stripe_error");
3877
4085
  if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
3878
4086
  return result.body;
@@ -4043,12 +4251,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4043
4251
  }
4044
4252
 
4045
4253
  // src/membership-provider-lifecycle.ts
4046
- var import_stripe3 = require("@odla-ai/stripe");
4254
+ var import_stripe4 = require("@odla-ai/stripe");
4047
4255
 
4048
4256
  // src/membership-renewal-authority.ts
4049
4257
  async function reconcileMembershipRenewal(db, input, now) {
4050
4258
  membershipClock(now);
4051
4259
  const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4260
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
4052
4261
  const fact = eventRow?.fact;
4053
4262
  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({
4054
4263
  appId: input.appId,
@@ -4056,7 +4265,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4056
4265
  environment: input.environment,
4057
4266
  dataEnvironment: input.environment,
4058
4267
  runtime: input.runtime
4059
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) {
4268
+ }).some(([key3, value]) => fact.identity[key3] !== value)) {
4060
4269
  return membershipFail("membership_renewal_evidence_pending");
4061
4270
  }
4062
4271
  for (let attempt = 0; attempt < 8; attempt++) {
@@ -4070,7 +4279,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4070
4279
  autoRenew: member.autoRenew === true
4071
4280
  });
4072
4281
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.customerId !== member.providerCustomerId || fact.currency !== member.currency) membershipFail("membership_renewal_scope_mismatch");
4073
- if (["refunded", "refund_pending", "canceled", "declined"].includes(String(member.status))) return receipt();
4282
+ if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
4074
4283
  const lines = fact.recurringInvoiceLines;
4075
4284
  if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
4076
4285
  const start = lines[0].startsAt, end = lines[0].endsAt;
@@ -4079,7 +4288,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4079
4288
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
4080
4289
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4081
4290
  if (seat) requireMembershipScope(seat, input);
4082
- const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4291
+ const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4083
4292
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
4084
4293
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
4085
4294
  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");
@@ -4112,7 +4321,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4112
4321
  { ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
4113
4322
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
4114
4323
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
4115
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
4324
+ evidenceGuard
4116
4325
  ];
4117
4326
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4118
4327
  if (billedSeat?.entitlementId) {
@@ -4158,14 +4367,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4158
4367
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4159
4368
  const fact = row2?.fact;
4160
4369
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
4161
- const collisionId = `network-commercial-exception:${input.eventId}:collision`;
4370
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4162
4371
  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({
4163
4372
  appId: input.appId,
4164
4373
  chapterId: input.chapterId,
4165
4374
  environment: input.environment,
4166
4375
  dataEnvironment: input.environment,
4167
4376
  runtime: input.runtime
4168
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4377
+ }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
4169
4378
  const sk = await getVaultSecret(db, stripeSecretName);
4170
4379
  if (!sk) membershipFail("membership_billing_provider_unavailable");
4171
4380
  const id10 = await networkMembershipId(input);
@@ -4179,6 +4388,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4179
4388
  graceEndsAt: Number(value.graceEndsAt ?? 0),
4180
4389
  autoRenew: value.autoRenew === true
4181
4390
  });
4391
+ if (["restarting"].includes(String(member.status))) return receipt();
4182
4392
  if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
4183
4393
  if (fact.eventType === "charge.refunded") {
4184
4394
  if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
@@ -4191,15 +4401,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4191
4401
  alreadyRefundedChargeId: fact.chargeId,
4192
4402
  cancelSubscription: false
4193
4403
  });
4194
- if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4404
+ await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4195
4405
  await refundNetworkMembership(db, input, sk, now);
4196
4406
  const saved2 = await membershipLoad(db, "networkMembership", id10);
4197
4407
  requireMembershipScope(saved2, input);
4198
4408
  return receipt(saved2);
4199
4409
  }
4200
4410
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
4201
- if (["refund_pending", "refunded", "declined", "canceled"].includes(String(member.status))) return receipt();
4202
- const result = await (0, import_stripe3.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4411
+ if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4412
+ const result = await (0, import_stripe4.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4203
4413
  const sub = result.body;
4204
4414
  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)) {
4205
4415
  membershipFail("membership_billing_provider_mismatch");
@@ -4223,7 +4433,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4223
4433
  { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
4224
4434
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4225
4435
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4226
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4436
+ evidenceGuard
4227
4437
  ] });
4228
4438
  } catch (error) {
4229
4439
  if (error.code !== "transact_guard_failed") throw error;
@@ -4236,6 +4446,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4236
4446
  return membershipFail("membership_billing_retry_required");
4237
4447
  }
4238
4448
 
4449
+ // src/membership-restart-seat.ts
4450
+ async function expireRestartedMembershipSeat(db, target, member, now) {
4451
+ const ops = [], guards = [];
4452
+ if (!member.namedSeatId) return { ops, guards };
4453
+ const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
4454
+ requireMembershipScope(seat, target);
4455
+ if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
4456
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
4457
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4458
+ }
4459
+ if (seat.entitlementId) {
4460
+ const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
4461
+ requireMembershipScope(entitlement, target);
4462
+ ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
4463
+ guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
4464
+ }
4465
+ return { ops, guards };
4466
+ }
4467
+
4468
+ // src/membership-restart-authority.ts
4469
+ var import_stripe5 = require("@odla-ai/stripe");
4470
+
4471
+ // src/stripe-event-digest.ts
4472
+ async function stripePayloadDigest(value) {
4473
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
4474
+ return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4475
+ }
4476
+
4477
+ // src/membership-restart-authority.ts
4478
+ function membershipCanRestart(member, now) {
4479
+ const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
4480
+ 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);
4481
+ }
4482
+ async function stopPreviousSubscription(member, target, secret) {
4483
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4484
+ const result = await (0, import_stripe5.stripeRequest)({ secretKey: secret }, "GET", path);
4485
+ const sub = result.body;
4486
+ if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4487
+ ...stripePartitionMetadata(target),
4488
+ applicationId: target.applicationId
4489
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4490
+ if (sub.status === "canceled") return;
4491
+ const canceled = await (0, import_stripe5.stripeRequest)(
4492
+ { secretKey: secret },
4493
+ "DELETE",
4494
+ path,
4495
+ { invoice_now: false, prorate: false },
4496
+ { idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
4497
+ );
4498
+ if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
4499
+ }
4500
+ async function prepareMembershipRestart(db, input, now, stripeSecret) {
4501
+ membershipClock(now);
4502
+ if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
4503
+ const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4504
+ const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
4505
+ const state = await loadMembershipAuthorityState(db, input);
4506
+ const previous = await membershipLoad(db, "networkMembership", id10);
4507
+ requireMembershipScope(previous, input);
4508
+ const receipt = {
4509
+ applicationId: input.applicationId,
4510
+ membershipId: id10,
4511
+ restartId,
4512
+ memberNumber: Number(previous.memberNumber),
4513
+ status: "ready"
4514
+ };
4515
+ const existing = await membershipLoad(db, "membershipRestart", id10);
4516
+ if (existing?.restartId === restartId && existing.status === "ready") {
4517
+ requireMembershipScope(existing, input);
4518
+ return receipt;
4519
+ }
4520
+ if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
4521
+ if (previous.status !== "restarting") {
4522
+ if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
4523
+ const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4524
+ requireMembershipScope(binding2, input);
4525
+ const seats = await expireRestartedMembershipSeat(db, input, previous, now);
4526
+ await db.transact([
4527
+ ...seats.ops,
4528
+ membershipUpdate("membershipPurchaseHistory", restartId, {
4529
+ id: restartId,
4530
+ ...membershipScope(input),
4531
+ membershipId: id10,
4532
+ providerSubscriptionId: input.previousSubscriptionId,
4533
+ memberSnapshot: previous,
4534
+ bindingSnapshot: binding2,
4535
+ archivedAt: now
4536
+ }),
4537
+ membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
4538
+ ], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
4539
+ ...seats.guards,
4540
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
4541
+ { ns: "membershipPurchaseHistory", id: restartId, exists: false },
4542
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
4543
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4544
+ ] });
4545
+ } else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
4546
+ const reserved = await membershipLoad(db, "networkMembership", id10);
4547
+ requireMembershipScope(reserved, input);
4548
+ if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
4549
+ await stopPreviousSubscription(reserved, input, stripeSecret);
4550
+ const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4551
+ requireMembershipScope(binding, input);
4552
+ await db.transact([
4553
+ membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
4554
+ { t: "delete", ns: "membershipOfferCheckout", id: bindingId }
4555
+ ], { mutationId: `membership-restart-ready:${restartId}`, guards: [
4556
+ { ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
4557
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
4558
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
4559
+ ] });
4560
+ return receipt;
4561
+ }
4562
+
4239
4563
  // src/membership-access-authority.ts
4240
4564
  async function readMembershipAccess(db, input, now) {
4241
4565
  membershipClock(now);
@@ -4252,7 +4576,8 @@ async function readMembershipAccess(db, input, now) {
4252
4576
  allowed: allowed2,
4253
4577
  status: String(member.status),
4254
4578
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
4255
- revision: Number(member.revision)
4579
+ revision: Number(member.revision),
4580
+ restartEligible: !allowed2 && membershipCanRestart(member, now)
4256
4581
  };
4257
4582
  }
4258
4583
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -4260,7 +4585,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
4260
4585
  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)) {
4261
4586
  throw new Error("invalid network access receipt");
4262
4587
  }
4263
- return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
4588
+ return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
4264
4589
  }
4265
4590
 
4266
4591
  // src/membership-seat-reservation.ts
@@ -4341,16 +4666,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
4341
4666
  }
4342
4667
 
4343
4668
  // src/membership-seat-stripe.ts
4344
- var import_stripe5 = require("@odla-ai/stripe");
4669
+ var import_stripe7 = require("@odla-ai/stripe");
4345
4670
 
4346
4671
  // src/subscription-provider-contract.ts
4347
- var import_stripe4 = require("@odla-ai/stripe");
4672
+ var import_stripe6 = require("@odla-ai/stripe");
4348
4673
  function subscriptionContractMismatch() {
4349
4674
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4350
4675
  }
4351
4676
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
4352
4677
  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();
4353
- const result = await (0, import_stripe4.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4678
+ const result = await (0, import_stripe6.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4354
4679
  const price = result.body;
4355
4680
  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();
4356
4681
  return typeof price.product === "string" ? price.product : void 0;
@@ -4368,7 +4693,7 @@ var metadataFor = (seat) => ({
4368
4693
  });
4369
4694
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
4370
4695
  async function read(sk, path, params) {
4371
- const response = await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "GET", path, params);
4696
+ const response = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "GET", path, params);
4372
4697
  if (!response.ok) return reject3();
4373
4698
  return response.body;
4374
4699
  }
@@ -4405,11 +4730,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
4405
4730
  }
4406
4731
  await subscriptionForSeat(sk, seat);
4407
4732
  const metadata = metadataFor(seat);
4408
- const result = seat.providerPaymentIntentId ? await (0, import_stripe5.stripeRequest)(
4733
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe7.stripeRequest)(
4409
4734
  { secretKey: sk },
4410
4735
  "GET",
4411
4736
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
4412
- ) : await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
4737
+ ) : await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
4413
4738
  amount: seat.amountCents,
4414
4739
  currency: seat.currency,
4415
4740
  customer: seat.providerCustomerId,
@@ -4427,7 +4752,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4427
4752
  const sub = await subscriptionForSeat(sk, seat);
4428
4753
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
4429
4754
  if (existing) return String(existing.id);
4430
- const result = await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
4755
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
4431
4756
  subscription: seat.providerSubscriptionId,
4432
4757
  price: seat.priceId,
4433
4758
  quantity: 1,
@@ -4505,8 +4830,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4505
4830
  requireMembershipScope(member, input);
4506
4831
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4507
4832
  if (!row2 || !seat.providerPaymentIntentId) membershipFail("named_seat_payment_evidence_pending");
4508
- const collisionId = `network-commercial-exception:${input.eventId}:collision`;
4509
- if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("named_seat_payment_evidence_quarantined");
4833
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
4510
4834
  const fact = row2.fact;
4511
4835
  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({
4512
4836
  appId: input.appId,
@@ -4553,7 +4877,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4553
4877
  { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4554
4878
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4555
4879
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4556
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4880
+ evidenceGuard
4557
4881
  ] });
4558
4882
  } catch (error) {
4559
4883
  if (error.code !== "transact_guard_failed") throw error;
@@ -4702,14 +5026,14 @@ async function recordRecipient(db, input, now, approve) {
4702
5026
  }
4703
5027
 
4704
5028
  // src/membership-seat-refund-stripe.ts
4705
- var import_stripe6 = require("@odla-ai/stripe");
5029
+ var import_stripe8 = require("@odla-ai/stripe");
4706
5030
  var fail4 = (code = "named_seat_refund_mismatch") => {
4707
5031
  throw Object.assign(new Error(code), { code });
4708
5032
  };
4709
5033
  async function refundNamedSeatStripePayment(secretKey, seat) {
4710
5034
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
4711
5035
  const call = async (method, path, params, idempotencyKey) => {
4712
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5036
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4713
5037
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4714
5038
  return result.body;
4715
5039
  };
@@ -4831,7 +5155,7 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
4831
5155
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4832
5156
  if (!row2) return membershipFail("named_seat_refund_evidence_pending");
4833
5157
  const fact = row2.fact;
4834
- if (await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) membershipFail("named_seat_refund_evidence_quarantined");
5158
+ await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
4835
5159
  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");
4836
5160
  await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
4837
5161
  }
@@ -5103,7 +5427,7 @@ function createMembershipAuthorityHandler(options) {
5103
5427
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
5104
5428
  const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
5105
5429
  const action = url.pathname.split("/").at(-1);
5106
- if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
5430
+ if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
5107
5431
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
5108
5432
  if (!peer) return respond({ error: "unauthorized" }, 401);
5109
5433
  let body;
@@ -5125,7 +5449,7 @@ function createMembershipAuthorityHandler(options) {
5125
5449
  let input;
5126
5450
  try {
5127
5451
  input = JSON.parse(verified.body);
5128
- 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] ?? []];
5452
+ 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] ?? []];
5129
5453
  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])))) {
5130
5454
  throw new TypeError("invalid request");
5131
5455
  }
@@ -5150,6 +5474,15 @@ function createMembershipAuthorityHandler(options) {
5150
5474
  applicationId: String(input.applicationId),
5151
5475
  skuId: String(input.skuId)
5152
5476
  };
5477
+ if (action === "restarts" || action === "cancel-renewal") {
5478
+ const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
5479
+ if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
5480
+ if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
5481
+ return respond({ ok: true, restart: await prepareMembershipRestart(db, {
5482
+ ...target,
5483
+ previousSubscriptionId: String(input.previousSubscriptionId)
5484
+ }, options.now?.() ?? Date.now(), stripeSecret) });
5485
+ }
5153
5486
  if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
5154
5487
  if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
5155
5488
  db,
@@ -5423,7 +5756,7 @@ async function requestMembershipRefund(options) {
5423
5756
  async function requestMembershipRenewal(options) {
5424
5757
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
5425
5758
  const row2 = result.renewal;
5426
- 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)) {
5759
+ 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)) {
5427
5760
  throw new Error("invalid membership renewal receipt");
5428
5761
  }
5429
5762
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -5465,9 +5798,25 @@ async function requestMembershipAccess(options) {
5465
5798
  const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
5466
5799
  return verifyMembershipAccessReceipt(result.access, options, Date.now());
5467
5800
  }
5801
+ async function requestMembershipRestart(options) {
5802
+ const result = await requestAuthority(options, "restarts", {
5803
+ applicationId: options.applicationId,
5804
+ skuId: options.skuId,
5805
+ previousSubscriptionId: options.previousSubscriptionId
5806
+ });
5807
+ const row2 = result.restart;
5808
+ 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");
5809
+ return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
5810
+ }
5811
+ async function requestMembershipCancellation(options) {
5812
+ const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
5813
+ const row2 = result.renewal;
5814
+ 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");
5815
+ return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
5816
+ }
5468
5817
 
5469
5818
  // src/membership-seat-cancel.ts
5470
- var import_stripe7 = require("@odla-ai/stripe");
5819
+ var import_stripe9 = require("@odla-ai/stripe");
5471
5820
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5472
5821
  membershipClock(now);
5473
5822
  const state = await loadMembershipAuthorityState(db, target);
@@ -5495,7 +5844,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5495
5844
  }
5496
5845
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
5497
5846
  const request = async (method, path, params) => {
5498
- const result = await (0, import_stripe7.stripeRequest)(
5847
+ const result = await (0, import_stripe9.stripeRequest)(
5499
5848
  { secretKey },
5500
5849
  method,
5501
5850
  path,
@@ -5508,7 +5857,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5508
5857
  let payment;
5509
5858
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
5510
5859
  else {
5511
- const snapshot = await listStripeObjects((0, import_stripe7.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
5860
+ const snapshot = await listStripeObjects((0, import_stripe9.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
5512
5861
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
5513
5862
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
5514
5863
  payment = matches2[0];
@@ -5732,7 +6081,27 @@ var membershipAuthorityEntities = {
5732
6081
  cancellationRequestedAt: attr9("number", true),
5733
6082
  graceEndsAt: attr9("number", true),
5734
6083
  ...times(),
5735
- refundRequestedAt: attr9("number", true)
6084
+ refundRequestedAt: attr9("number", true),
6085
+ restartId: key2(true)
6086
+ } },
6087
+ membershipRestart: { attrs: {
6088
+ id: id9(),
6089
+ ...scope(),
6090
+ membershipId: key2(),
6091
+ restartId: key2(),
6092
+ previousSubscriptionId: key2(),
6093
+ memberNumber: attr9("number"),
6094
+ status: key2(),
6095
+ createdAt: attr9("number")
6096
+ } },
6097
+ membershipPurchaseHistory: { attrs: {
6098
+ id: id9(),
6099
+ ...scope(),
6100
+ membershipId: key2(),
6101
+ providerSubscriptionId: key2(),
6102
+ memberSnapshot: attr9("json"),
6103
+ bindingSnapshot: attr9("json"),
6104
+ archivedAt: attr9("number")
5736
6105
  } },
5737
6106
  networkEntitlement: { attrs: {
5738
6107
  id: id9(),
@@ -6531,7 +6900,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
6531
6900
  }
6532
6901
 
6533
6902
  // src/payments.ts
6534
- var import_stripe8 = require("@odla-ai/stripe");
6903
+ var import_stripe10 = require("@odla-ai/stripe");
6535
6904
  function paymentsReady(group, hasSecretKey) {
6536
6905
  return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
6537
6906
  }
@@ -7158,9 +7527,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
7158
7527
  }
7159
7528
  const nameField = def.nameField ?? "name";
7160
7529
  const input = {};
7161
- for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
7162
- const value = record4.fields?.[field];
7163
- if (value !== void 0) input[field] = value;
7530
+ for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
7531
+ const value = record4.fields?.[field2];
7532
+ if (value !== void 0) input[field2] = value;
7164
7533
  }
7165
7534
  if (input[nameField] === void 0) input[nameField] = record4.name;
7166
7535
  return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
@@ -7429,9 +7798,9 @@ function subAnnualCents(sub) {
7429
7798
  }
7430
7799
 
7431
7800
  // src/payments-stripe.ts
7432
- var import_stripe9 = require("@odla-ai/stripe");
7801
+ var import_stripe11 = require("@odla-ai/stripe");
7433
7802
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7434
- return (0, import_stripe9.stripeRequest)(
7803
+ return (0, import_stripe11.stripeRequest)(
7435
7804
  { secretKey: sk },
7436
7805
  method,
7437
7806
  path,
@@ -7741,7 +8110,8 @@ function applicationSummary(app) {
7741
8110
  status: app.status,
7742
8111
  createdAt: app.createdAt ?? null,
7743
8112
  meetingLink: app.meetingLink ?? null,
7744
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
8113
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
8114
+ ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
7745
8115
  renewalAt: app.renewalAt ?? null,
7746
8116
  canceled: app.canceled === true,
7747
8117
  tierId: app.tierId ?? null
@@ -7923,7 +8293,7 @@ function isValidTimeZone(tz) {
7923
8293
  function resolveScheduling(config) {
7924
8294
  const result = validateScheduling(config);
7925
8295
  if (result.ok) return result.value;
7926
- const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
8296
+ const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
7927
8297
  throw new Error(`scheduling: ${detail}`);
7928
8298
  }
7929
8299
  function validateScheduling(config) {
@@ -9068,7 +9438,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
9068
9438
  MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
9069
9439
  );
9070
9440
  function projectMemberSelf(record4) {
9071
- return Object.fromEntries(Object.entries(record4).filter(([field]) => MEMBER_SELF_READABLE_FIELDS.has(field)));
9441
+ return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
9072
9442
  }
9073
9443
  function classifyMemberSystemWrite(intent) {
9074
9444
  if (intent.actor === "bnf") return "reject_bnf_surface";