@odla-ai/chapter 0.48.3 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -176,6 +176,7 @@ __export(src_exports, {
176
176
  planMemberSystemEventReplay: () => planMemberSystemEventReplay,
177
177
  planMemberSystemMigration: () => planMemberSystemMigration,
178
178
  planPrivateRoleMigration: () => planPrivateRoleMigration,
179
+ previewMembershipCommercialOffer: () => previewMembershipCommercialOffer,
179
180
  projectApplicant: () => projectApplicant,
180
181
  projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
181
182
  projectMemberContent: () => projectMemberContent,
@@ -204,12 +205,15 @@ __export(src_exports, {
204
205
  renewalPatch: () => renewalPatch,
205
206
  requestMembershipAccess: () => requestMembershipAccess,
206
207
  requestMembershipApproval: () => requestMembershipApproval,
208
+ requestMembershipCancellation: () => requestMembershipCancellation,
207
209
  requestMembershipChapterEffect: () => requestMembershipChapterEffect,
208
210
  requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
209
211
  requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
212
+ requestMembershipOfferPreview: () => requestMembershipOfferPreview,
210
213
  requestMembershipPayment: () => requestMembershipPayment,
211
214
  requestMembershipRefund: () => requestMembershipRefund,
212
215
  requestMembershipRenewal: () => requestMembershipRenewal,
216
+ requestMembershipRestart: () => requestMembershipRestart,
213
217
  requestNamedSeatAcceptance: () => requestNamedSeatAcceptance,
214
218
  requestNamedSeatAccess: () => requestNamedSeatAccess,
215
219
  requestNamedSeatApproval: () => requestNamedSeatApproval,
@@ -217,6 +221,7 @@ __export(src_exports, {
217
221
  requestNamedSeatDecline: () => requestNamedSeatDecline,
218
222
  requestNamedSeatInvitation: () => requestNamedSeatInvitation,
219
223
  requestNamedSeatOffer: () => requestNamedSeatOffer,
224
+ requestNamedSeatOperatorRefund: () => requestNamedSeatOperatorRefund,
220
225
  requestNamedSeatPayment: () => requestNamedSeatPayment,
221
226
  requestNamedSeatRefund: () => requestNamedSeatRefund2,
222
227
  requestNamedSeatReservation: () => requestNamedSeatReservation,
@@ -241,7 +246,7 @@ __export(src_exports, {
241
246
  slotWindow: () => slotWindow,
242
247
  stageIndex: () => stageIndex,
243
248
  stripeCall: () => stripeCall,
244
- stripeForm: () => import_stripe8.stripeForm,
249
+ stripeForm: () => import_stripe10.stripeForm,
245
250
  subAnnualCents: () => subAnnualCents,
246
251
  submitApplication: () => submitApplication,
247
252
  subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
@@ -262,7 +267,7 @@ __export(src_exports, {
262
267
  verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
263
268
  verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
264
269
  verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
265
- verifyStripeSignature: () => import_stripe8.verifyStripeSignature,
270
+ verifyStripeSignature: () => import_stripe10.verifyStripeSignature,
266
271
  webhookMutationId: () => webhookMutationId
267
272
  });
268
273
  module.exports = __toCommonJS(src_exports);
@@ -307,12 +312,12 @@ function resolveApplication(a) {
307
312
  if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
308
313
  throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
309
314
  }
310
- const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
315
+ const unsafe = arr.find((field2) => !SCHEMA_FIELD.test(field2));
311
316
  if (unsafe) {
312
317
  throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
313
318
  }
314
319
  }
315
- const duplicate = [...required, ...optional].find((field, index2, fields) => fields.indexOf(field) !== index2);
320
+ const duplicate = [...required, ...optional].find((field2, index2, fields) => fields.indexOf(field2) !== index2);
316
321
  if (duplicate) {
317
322
  throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
318
323
  }
@@ -323,14 +328,14 @@ function resolveApplication(a) {
323
328
  throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
324
329
  }
325
330
  const conditions = a?.conditions ?? {};
326
- for (const [field, declared] of Object.entries(conditions)) {
331
+ for (const [field2, declared] of Object.entries(conditions)) {
327
332
  for (const key3 of ["visibleWhen", "requiredWhen"]) {
328
333
  const expression = declared?.[key3];
329
334
  if (expression === void 0) continue;
330
335
  if (typeof expression !== "string" || !expression.trim()) {
331
- throw new Error(`defineChapter.application.conditions.${field}.${key3}: must be a non-empty CEL condition`);
336
+ throw new Error(`defineChapter.application.conditions.${field2}.${key3}: must be a non-empty CEL condition`);
332
337
  }
333
- (0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key3}`);
338
+ (0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field2}.${key3}`);
334
339
  }
335
340
  }
336
341
  return {
@@ -401,6 +406,13 @@ async function submitApplication(db, chapter, fields, opts) {
401
406
  if (opts.groupId) row2.groupId = opts.groupId;
402
407
  if (opts.runtime) row2.stripeRuntime = opts.runtime;
403
408
  if (chapter.account !== "none") row2.clerkAccountPending = true;
409
+ if (opts.namedSeat) Object.assign(row2, {
410
+ namedSeatId: opts.namedSeat.seatId,
411
+ namedSeatPrimaryApplicationId: opts.namedSeat.primaryApplicationId,
412
+ namedSeatClaimPending: true,
413
+ clerkUserId: opts.namedSeat.userId,
414
+ clerkAccountPending: false
415
+ });
404
416
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
405
417
  row2.emailNormalized = normalizeChapterEmail(fields.email);
406
418
  row2.signupEmailKey = row2.emailNormalized;
@@ -530,6 +542,10 @@ var applicationAttrs = {
530
542
  giftPaidAt: attr("number", { optional: true, indexed: true }),
531
543
  giftRevokedAt: attr("number", { optional: true, indexed: true }),
532
544
  giftCheckoutStartedAt: attr("number", { optional: true, indexed: true }),
545
+ membershipRestartId: attr("string", { optional: true, indexed: true }),
546
+ membershipStatus: attr("string", { optional: true }),
547
+ membershipGraceEndsAt: attr("number", { optional: true }),
548
+ membershipAutoRenew: attr("boolean", { optional: true }),
533
549
  namedSeatId: attr("string", { optional: true, unique: true, indexed: true }),
534
550
  namedSeatPrimaryApplicationId: attr("string", { optional: true }),
535
551
  namedSeatClaimPending: attr("boolean", { optional: true, indexed: true }),
@@ -540,19 +556,19 @@ function applicationsFor(application) {
540
556
  for (const [name, spec] of Object.entries(applicationAttrs)) {
541
557
  attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
542
558
  }
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, {
559
+ const apply = (field2, optional) => {
560
+ const existing = Object.hasOwn(attrs, field2) ? attrs[field2] : void 0;
561
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field2)) throw new Error(`defineChapter.application: field "${field2}" is reserved by the applications schema`);
562
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field2}" is not a configurable string field`);
563
+ Object.defineProperty(attrs, field2, {
548
564
  value: existing ? { ...existing, optional } : attr("string", { optional }),
549
565
  enumerable: true,
550
566
  configurable: true,
551
567
  writable: true
552
568
  });
553
569
  };
554
- for (const field of application.required) apply(field, false);
555
- for (const field of application.optional) apply(field, true);
570
+ for (const field2 of application.required) apply(field2, false);
571
+ for (const field2 of application.optional) apply(field2, true);
556
572
  return { attrs };
557
573
  }
558
574
 
@@ -759,6 +775,19 @@ var giftRedemptionAttemptsEntity = { attrs: {
759
775
  updatedAt: attr4("number")
760
776
  } };
761
777
 
778
+ // src/payment-recovery-schema.ts
779
+ var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
780
+ var paymentRecoveryReviewsEntity = { attrs: {
781
+ id: field("string", true),
782
+ eventId: field("string"),
783
+ actorUserId: field("string"),
784
+ reviewedAt: field("number"),
785
+ previousDigest: field("string"),
786
+ providerDigest: field("string"),
787
+ scope: field("string"),
788
+ previousState: field("json")
789
+ } };
790
+
762
791
  // src/schema-stripe.ts
763
792
  var attr5 = (type, flags = {}) => ({
764
793
  type,
@@ -779,6 +808,10 @@ var stripeEventReceiptsEntity = {
779
808
  dataEnvironment: attr5("string", { indexed: true, optional: true }),
780
809
  runtime: attr5("string", { indexed: true, optional: true }),
781
810
  chapterId: attr5("string", { indexed: true, optional: true }),
811
+ retryReady: attr5("boolean", { optional: true }),
812
+ reviewedDigest: attr5("string", { optional: true }),
813
+ reviewId: attr5("string", { optional: true }),
814
+ collisionDigest: attr5("string", { optional: true }),
782
815
  receivedAt: attr5("number", { indexed: true })
783
816
  }
784
817
  };
@@ -803,6 +836,7 @@ var subscriptionCheckoutIntentsEntity = {
803
836
  requestedCustomerId: attr5("string", { indexed: true, optional: true }),
804
837
  providerCustomerId: attr5("string", { indexed: true, optional: true }),
805
838
  providerSubscriptionId: attr5("string", { unique: true, indexed: true, optional: true }),
839
+ purchaseAttemptId: attr5("string", { optional: true }),
806
840
  status: attr5("string", { indexed: true }),
807
841
  createdAt: attr5("number", { indexed: true }),
808
842
  updatedAt: attr5("number", { indexed: true })
@@ -827,6 +861,20 @@ var membershipQuoteProjectionsEntity = {
827
861
  quote: attr5("json", { optional: true })
828
862
  }
829
863
  };
864
+ var membershipCheckoutHistoryEntity = { attrs: {
865
+ id: attr5("string", { unique: true, indexed: true }),
866
+ applicationId: attr5("string", { indexed: true }),
867
+ appId: attr5("string"),
868
+ chapterId: attr5("string"),
869
+ environment: attr5("string"),
870
+ runtime: attr5("string"),
871
+ skuId: attr5("string"),
872
+ providerSubscriptionId: attr5("string", { indexed: true }),
873
+ applicationSnapshot: attr5("json"),
874
+ intentSnapshot: attr5("json"),
875
+ quoteSnapshot: attr5("json"),
876
+ archivedAt: attr5("number")
877
+ } };
830
878
 
831
879
  // src/schema-network-person.ts
832
880
  var string = (flags = {}) => ({
@@ -960,15 +1008,19 @@ var exceptions = { attrs: {
960
1008
  kind: string2({ indexed: true }),
961
1009
  sourceKey: string2({ indexed: true, optional: true }),
962
1010
  status: string2({ indexed: true }),
1011
+ resolvedDigest: string2({ optional: true }),
1012
+ incomingDigest: string2({ optional: true }),
963
1013
  detail: json2(),
964
1014
  updatedAt: number2({ indexed: true })
965
1015
  } };
966
1016
  var networkCommercialEntities = {
1017
+ paymentRecoveryReviews: paymentRecoveryReviewsEntity,
967
1018
  networkCommercialEvents: events,
968
1019
  networkCommercialCustomers: projection(),
969
1020
  networkCommercialSubscriptions: projection(),
970
1021
  networkCommercialInvoices: projection(),
971
1022
  networkCommercialRefunds: projection(),
1023
+ networkCommercialSeats: projection(),
972
1024
  networkCommercialExceptions: exceptions
973
1025
  };
974
1026
 
@@ -1171,8 +1223,9 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
1171
1223
  entities.tiers = tiers;
1172
1224
  entities.meetings = meetings;
1173
1225
  entities.emailLog = emailLog;
1226
+ entities.paymentRecoveryReviews = paymentRecoveryReviewsEntity;
1174
1227
  entities.stripeEventReceipts = stripeEventReceiptsEntity;
1175
- entities.subscriptionCheckoutIntents = subscriptionCheckoutIntentsEntity;
1228
+ Object.assign(entities, { subscriptionCheckoutIntents: subscriptionCheckoutIntentsEntity, membershipCheckoutHistory: membershipCheckoutHistoryEntity });
1176
1229
  entities.membershipQuoteProjections = membershipQuoteProjectionsEntity;
1177
1230
  entities.namedSeatConsents = namedSeatConsentsEntity;
1178
1231
  entities.admissionGrants = admissionGrantsEntity;
@@ -1663,6 +1716,7 @@ var DEFAULT_ADMIN_COPY = {
1663
1716
  },
1664
1717
  records: {
1665
1718
  workflowMissing: "This record is not linked to an application workflow.",
1719
+ invitation: "Invitation",
1666
1720
  comms: "Comms",
1667
1721
  commsHistory: "Comms history",
1668
1722
  scheduling: "Scheduling",
@@ -1710,6 +1764,19 @@ var DEFAULT_COMMON_COPY = {
1710
1764
  unknown: "(unknown)"
1711
1765
  };
1712
1766
  var DEFAULT_JOIN_COPY = {
1767
+ namedSeat: {
1768
+ title: "Your membership invitation",
1769
+ from: "{purchaserName} invited you.",
1770
+ prepaid: "Your {tierName} seat is already paid for.",
1771
+ body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
1772
+ consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
1773
+ accept: "Accept my invitation",
1774
+ accepting: "Confirming your invitation\u2026",
1775
+ loading: "Loading your invitation\u2026",
1776
+ unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
1777
+ retry: "Try again",
1778
+ failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
1779
+ },
1713
1780
  form: {
1714
1781
  submit: "Submit application",
1715
1782
  submitting: "Submitting\u2026",
@@ -1717,6 +1784,8 @@ var DEFAULT_JOIN_COPY = {
1717
1784
  unexpectedFailure: "Something went wrong. Please try again."
1718
1785
  },
1719
1786
  booking: {
1787
+ title: "Book your introduction",
1788
+ intro: "Choose a time to meet your chapter and get to know each other.",
1720
1789
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
1721
1790
  loadFailed: "Times are briefly unavailable. Please try again.",
1722
1791
  slotTaken: "That time was just taken. Here are the current openings.",
@@ -1732,7 +1801,9 @@ var DEFAULT_JOIN_COPY = {
1732
1801
  processing: "Processing\u2026",
1733
1802
  payAndContinue: "Pay and continue",
1734
1803
  incomplete: "The payment did not complete. Please try again.",
1735
- consent: "I have read and agree to the pricing and refund policy above."
1804
+ consent: "I have read and agree to the pricing and refund policy above.",
1805
+ foundingMember: "Founding member rate",
1806
+ foundingDiscount: "Founding discount"
1736
1807
  },
1737
1808
  done: {
1738
1809
  label: "You're booked",
@@ -1908,16 +1979,16 @@ function resolveFormationFlowFields(crm, formation) {
1908
1979
  if (!formation.enabled) return [];
1909
1980
  const def = crm.type(formation.type);
1910
1981
  return [...formation.required, ...formation.optional].map((id10, order2) => {
1911
- const field = def.fields[id10];
1982
+ const field2 = def.fields[id10];
1912
1983
  return {
1913
1984
  id: id10,
1914
- label: field.label ?? humanize(id10),
1915
- type: field.type,
1985
+ label: field2.label ?? humanize(id10),
1986
+ type: field2.type,
1916
1987
  required: formation.required.includes(id10),
1917
1988
  order: order2,
1918
1989
  maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
1919
- ...field.options ? { options: [...field.options] } : {},
1920
- ...id10 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1990
+ ...field2.options ? { options: [...field2.options] } : {},
1991
+ ...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
1921
1992
  };
1922
1993
  });
1923
1994
  }
@@ -1953,19 +2024,19 @@ function resolveLeaderFormation(input, crm) {
1953
2024
  const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
1954
2025
  const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
1955
2026
  for (const [name, values] of [["required", required], ["optional", optional]]) {
1956
- if (!Array.isArray(values) || !values.every((field) => typeof field === "string" && field !== "")) {
2027
+ if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
1957
2028
  throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
1958
2029
  }
1959
2030
  }
1960
- const duplicates = [...required, ...optional].filter((field, index2, fields) => fields.indexOf(field) !== index2);
2031
+ const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
1961
2032
  if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
1962
- const unknown = [...required, ...optional].filter((field) => !def.fields[field]);
2033
+ const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
1963
2034
  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));
2035
+ const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
1965
2036
  if (missingRequired.length) {
1966
2037
  throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
1967
2038
  }
1968
- const unsupportedRequired = required.find((field) => !["string", "email", "url", "enum"].includes(def.fields[field].type));
2039
+ const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
1969
2040
  if (unsupportedRequired) {
1970
2041
  throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
1971
2042
  }
@@ -1978,9 +2049,9 @@ function resolveLeaderFormation(input, crm) {
1978
2049
  if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
1979
2050
  throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
1980
2051
  }
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`);
2052
+ for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
2053
+ if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
2054
+ throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
1984
2055
  }
1985
2056
  }
1986
2057
  return {
@@ -2070,10 +2141,10 @@ function resolveNetwork(config, crm) {
2070
2141
  }
2071
2142
  const def = crm.config.types[type];
2072
2143
  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]);
2144
+ const unknown = names.filter((field2) => !def.fields[field2]);
2074
2145
  if (unknown.length > 0) {
2075
2146
  throw new Error(
2076
- `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the leader CRM type`
2147
+ `defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
2077
2148
  );
2078
2149
  }
2079
2150
  const nameField = def.nameField ?? "name";
@@ -2161,10 +2232,10 @@ function resolveNetwork(config, crm) {
2161
2232
  if (!def) {
2162
2233
  throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
2163
2234
  }
2164
- const unknown = names.filter((field) => !def.fields[field]);
2235
+ const unknown = names.filter((field2) => !def.fields[field2]);
2165
2236
  if (unknown.length > 0) {
2166
2237
  throw new Error(
2167
- `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field) => `"${field}"`).join(", ")} not declared on the local CRM type`
2238
+ `defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
2168
2239
  );
2169
2240
  }
2170
2241
  const nameField = def.nameField ?? "name";
@@ -2482,6 +2553,48 @@ function defineChapter(config) {
2482
2553
  return chapter;
2483
2554
  }
2484
2555
 
2556
+ // src/network-commercial-seats.ts
2557
+ function commercialSeatItems(object2) {
2558
+ const items = object2.items;
2559
+ if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
2560
+ const result = [];
2561
+ for (const item of items.data) {
2562
+ const metadata = item.metadata;
2563
+ if (!metadata?.seatId || !metadata.membershipId) continue;
2564
+ const price = typeof item.price === "string" ? item.price : item.price?.id;
2565
+ 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;
2566
+ result.push({
2567
+ seatId: metadata.seatId,
2568
+ membershipId: metadata.membershipId,
2569
+ subscriptionItemId: item.id,
2570
+ priceId: price,
2571
+ ...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
2572
+ });
2573
+ }
2574
+ return result;
2575
+ }
2576
+ function projectCommercialSeats(payments, subscriptions, refunds) {
2577
+ return payments.map((payment) => {
2578
+ 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 })) : []);
2579
+ const match = matches2.length === 1 ? matches2[0] : void 0;
2580
+ 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);
2581
+ return {
2582
+ ...payment,
2583
+ objectKind: "seat",
2584
+ objectId: payment.seatId,
2585
+ refundedCents,
2586
+ ...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
2587
+ ...match ? {
2588
+ subscriptionId: match.subscription.subscriptionId,
2589
+ subscriptionItemId: match.item.subscriptionItemId,
2590
+ priceId: match.item.priceId,
2591
+ currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
2592
+ cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
2593
+ } : {}
2594
+ };
2595
+ });
2596
+ }
2597
+
2485
2598
  // src/subscription-billing-terms.ts
2486
2599
  var import_stripe = require("@odla-ai/stripe");
2487
2600
 
@@ -2653,6 +2766,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2653
2766
  ...chargeId ? { chargeId } : {},
2654
2767
  ...paymentIntentId ? { paymentIntentId } : {},
2655
2768
  ...price,
2769
+ ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2656
2770
  ...status2 ? { status: status2 } : {},
2657
2771
  ...currency ? { currency } : {},
2658
2772
  ...amountCents !== void 0 ? { amountCents } : {},
@@ -2674,8 +2788,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2674
2788
  var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
2675
2789
  function directSource(fact, sources) {
2676
2790
  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]));
2791
+ const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
2792
+ const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
2679
2793
  if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
2680
2794
  if (supplied.length > 0 && candidates.length === 0) return "wrong";
2681
2795
  return null;
@@ -2771,11 +2885,11 @@ function projectNetworkCommercialEvents(input, sources) {
2771
2885
  }
2772
2886
  }
2773
2887
  const totalsMap = /* @__PURE__ */ new Map();
2774
- const add = (row2, field, amount) => {
2888
+ const add = (row2, field2, amount) => {
2775
2889
  if (!row2.currency) return;
2776
2890
  const key3 = `${row2.sourceKey}:${row2.currency}`;
2777
2891
  const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
2778
- total[field] += amount;
2892
+ total[field2] += amount;
2779
2893
  total.netCents = total.paidCents - total.refundedCents;
2780
2894
  totalsMap.set(key3, total);
2781
2895
  };
@@ -2783,7 +2897,7 @@ function projectNetworkCommercialEvents(input, sources) {
2783
2897
  for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
2784
2898
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2785
2899
  return {
2786
- ...seatPayments.length ? { seatPayments } : {},
2900
+ ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2787
2901
  customers,
2788
2902
  subscriptions,
2789
2903
  invoices,
@@ -3246,7 +3360,7 @@ function findTier(tiers2, tierId) {
3246
3360
  if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
3247
3361
  return tiers2.find((tier) => tier.id === tierId) ?? null;
3248
3362
  }
3249
- function joinConfigTiers(tiers2, defaults = {}) {
3363
+ function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
3250
3364
  return tiers2.map((tier) => ({
3251
3365
  id: tier.id,
3252
3366
  name: tier.name,
@@ -3254,7 +3368,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
3254
3368
  currency: tier.currency ?? defaults.currency ?? "usd",
3255
3369
  interval: tier.interval ?? defaults.interval ?? "year",
3256
3370
  blurb: tier.blurb ?? "",
3257
- free: tierIsFree(tier)
3371
+ free: tierIsFree(tier),
3372
+ ...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
3258
3373
  }));
3259
3374
  }
3260
3375
 
@@ -3357,8 +3472,8 @@ function createChapterIntegration(chapter, options = {}) {
3357
3472
  }
3358
3473
 
3359
3474
  // src/membership-policy.ts
3360
- var fail2 = (field) => {
3361
- throw new TypeError(`invalid membership policy: ${field}`);
3475
+ var fail2 = (field2) => {
3476
+ throw new TypeError(`invalid membership policy: ${field2}`);
3362
3477
  };
3363
3478
  var object = (value) => {
3364
3479
  if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
@@ -3540,23 +3655,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
3540
3655
  if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
3541
3656
  return restore({ ...value, approvedFoundingCount: 0 }, expected);
3542
3657
  }
3543
- async function issueMembershipCommercialOffer(db, input) {
3544
- validate(input);
3545
- const stateId = `${input.chapterId}:${input.environment}`;
3546
- const state = await load(db, "membershipAuthorityState", stateId);
3658
+ async function previewMembershipCommercialOffer(db, input) {
3659
+ for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
3660
+ if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
3661
+ }
3662
+ if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
3663
+ const state = await authoritativeState(db, input);
3664
+ const { offering, policy } = await currentPolicy(db, input, false, state);
3665
+ const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3666
+ if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3667
+ const contract = createMembershipCommercialOffer({
3668
+ offering,
3669
+ discountPolicy: policy,
3670
+ approvedFoundingCount: Number(approvedFoundingCount),
3671
+ now: input.now,
3672
+ couponId: await couponId(input, policy?.id)
3673
+ });
3674
+ const discount = contract.billingTerms.discount;
3675
+ return {
3676
+ skuId: input.skuId,
3677
+ offeringVersionId: offering.id,
3678
+ basePriceCents: offering.basePriceCents,
3679
+ currency: offering.currency,
3680
+ interval: offering.interval,
3681
+ founding: contract.founding,
3682
+ ...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
3683
+ dueTodayCents: contract.dueTodayCents,
3684
+ renewalCents: contract.renewalCents,
3685
+ ...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
3686
+ ...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
3687
+ };
3688
+ }
3689
+ async function authoritativeState(db, input) {
3690
+ const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3547
3691
  if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
3548
- const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3549
- if (existing) return restore(existing, input);
3692
+ return state;
3693
+ }
3694
+ async function currentPolicy(db, input, restartAware, state) {
3550
3695
  const rows3 = await db.query({
3551
3696
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
3552
3697
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
3553
3698
  });
3554
3699
  if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
3555
- 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
- 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);
3700
+ const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
3701
+ const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
3557
3702
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
3558
- const policy = policies.sort((a, b) => b.version - a.version)[0];
3703
+ const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
3704
+ const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3559
3705
  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");
3706
+ return { offering, policy };
3707
+ }
3708
+ async function issueMembershipCommercialOffer(db, input) {
3709
+ validate(input);
3710
+ const stateId = `${input.chapterId}:${input.environment}`;
3711
+ const state = await authoritativeState(db, input);
3712
+ const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3713
+ if (existing) return restore(existing, input);
3714
+ const { offering, policy } = await currentPolicy(db, input, true, state);
3560
3715
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3561
3716
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3562
3717
  const contract = createMembershipCommercialOffer({
@@ -3593,8 +3748,73 @@ async function issueMembershipCommercialOffer(db, input) {
3593
3748
  return restore(stored, input);
3594
3749
  }
3595
3750
 
3596
- // src/membership-authority-handler.ts
3597
- var import_db = require("@odla-ai/db");
3751
+ // src/membership-cancel-authority.ts
3752
+ var import_stripe2 = require("@odla-ai/stripe");
3753
+
3754
+ // src/commercial-evidence-guard.ts
3755
+ async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
3756
+ const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
3757
+ const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3758
+ if (!row2) return { ns, id: id10, exists: false };
3759
+ if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
3760
+ throw Object.assign(new Error(code), { code });
3761
+ }
3762
+ return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
3763
+ }
3764
+
3765
+ // src/membership-authority-state.ts
3766
+ var membershipFail = (code) => {
3767
+ throw Object.assign(new Error(code), { code });
3768
+ };
3769
+ var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
3770
+ var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
3771
+ var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
3772
+ function requireMembershipScope(row2, input) {
3773
+ if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
3774
+ }
3775
+ async function loadMembershipAuthorityState(db, input) {
3776
+ const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3777
+ 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");
3778
+ return row2;
3779
+ }
3780
+ var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
3781
+ var membershipClock = (now) => {
3782
+ if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
3783
+ };
3784
+
3785
+ // src/membership-restart-payment.ts
3786
+ async function membershipRestartPayment(db, target, member, now) {
3787
+ const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
3788
+ requireMembershipScope(restart, target);
3789
+ const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
3790
+ requireMembershipScope(entitlement, target);
3791
+ if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
3792
+ if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
3793
+ return {
3794
+ restartId: String(restart.restartId),
3795
+ entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
3796
+ attrs: {
3797
+ memberNumber: member.memberNumber,
3798
+ journeyApprovedAt: member.journeyApprovedAt,
3799
+ founding: member.founding,
3800
+ cohort: member.cohort,
3801
+ ...member.personId ? { personId: member.personId } : {},
3802
+ status: "active",
3803
+ revision: Number(member.revision) + 1,
3804
+ createdAt: member.createdAt,
3805
+ graceEndsAt: 0
3806
+ },
3807
+ ops: [
3808
+ membershipUpdate("membershipRestart", id10, { status: "complete" }),
3809
+ { t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
3810
+ ],
3811
+ guards: [
3812
+ { ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
3813
+ { ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
3814
+ { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
3815
+ ]
3816
+ };
3817
+ }
3598
3818
 
3599
3819
  // src/membership-checkout-binding.ts
3600
3820
  var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
@@ -3621,10 +3841,11 @@ async function bindMembershipCheckout(db, input, now) {
3621
3841
  const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
3622
3842
  const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
3623
3843
  if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
3844
+ const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
3624
3845
  const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
3625
3846
  try {
3626
3847
  await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
3627
- mutationId: `membership-checkout:${id10}`,
3848
+ mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
3628
3849
  guards: [
3629
3850
  { ns: "membershipOfferCheckout", id: id10, exists: false },
3630
3851
  { ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
@@ -3639,26 +3860,6 @@ async function bindMembershipCheckout(db, input, now) {
3639
3860
  return verifyMembershipCheckoutBinding(saved, input);
3640
3861
  }
3641
3862
 
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
3863
  // src/membership-payment-authority.ts
3663
3864
  async function networkMembershipId(input) {
3664
3865
  return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
@@ -3676,9 +3877,7 @@ async function confirmMembershipPayment(db, input, now) {
3676
3877
  if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
3677
3878
  const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3678
3879
  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
- }
3880
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
3682
3881
  const fact = eventRow.fact;
3683
3882
  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
3883
  appId: input.appId,
@@ -3694,8 +3893,9 @@ async function confirmMembershipPayment(db, input, now) {
3694
3893
  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
3894
  const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
3696
3895
  let member = await membershipLoad(db, "networkMembership", id10);
3697
- if (!member) {
3896
+ if (!member || member.status === "restarting") {
3698
3897
  const offering = offer.contract.offering;
3898
+ const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
3699
3899
  const attrs = {
3700
3900
  id: id10,
3701
3901
  membershipId: id10,
@@ -3727,7 +3927,8 @@ async function confirmMembershipPayment(db, input, now) {
3727
3927
  refundedCents: 0,
3728
3928
  revision: 1,
3729
3929
  createdAt: now,
3730
- updatedAt: now
3930
+ updatedAt: now,
3931
+ ...restart?.attrs
3731
3932
  };
3732
3933
  try {
3733
3934
  await db.transact([
@@ -3737,21 +3938,23 @@ async function confirmMembershipPayment(db, input, now) {
3737
3938
  ...membershipScope(input),
3738
3939
  membershipId: id10,
3739
3940
  kind: "primary",
3740
- status: "pending_approval",
3941
+ status: restart?.attrs.status === "active" ? "active" : "pending_approval",
3741
3942
  termStartedAt: paidAt,
3742
3943
  termEndsAt: endsAt,
3743
3944
  offeringSnapshot: offering,
3744
3945
  sourceEventId: input.eventId,
3745
3946
  revision: 1,
3746
3947
  createdAt: now,
3747
- updatedAt: now
3748
- })
3749
- ], { mutationId: `membership-paid:${id10}`, guards: [
3750
- { ns: "networkMembership", id: id10, exists: false },
3948
+ updatedAt: now,
3949
+ ...restart?.entitlementAttrs
3950
+ }),
3951
+ ...restart?.ops ?? []
3952
+ ], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
3953
+ ...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
3751
3954
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3752
3955
  { ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
3753
3956
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
3754
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
3957
+ evidenceGuard
3755
3958
  ] });
3756
3959
  } catch (error) {
3757
3960
  if (error.code !== "transact_guard_failed") throw error;
@@ -3770,6 +3973,57 @@ async function confirmMembershipPayment(db, input, now) {
3770
3973
  };
3771
3974
  }
3772
3975
 
3976
+ // src/membership-cancel-authority.ts
3977
+ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
3978
+ membershipClock(now);
3979
+ const id10 = await networkMembershipId(target);
3980
+ const state = await loadMembershipAuthorityState(db, target);
3981
+ const member = await membershipLoad(db, "networkMembership", id10);
3982
+ requireMembershipScope(member, target);
3983
+ if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
3984
+ membershipFail("membership_cancellation_unavailable");
3985
+ }
3986
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
3987
+ const read2 = await (0, import_stripe2.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
3988
+ const sub = read2.body;
3989
+ if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
3990
+ ...stripePartitionMetadata(target),
3991
+ applicationId: target.applicationId
3992
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
3993
+ if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
3994
+ const write = await (0, import_stripe2.stripeRequest)(
3995
+ { secretKey: stripeSecret },
3996
+ "POST",
3997
+ path,
3998
+ { cancel_at_period_end: true, proration_behavior: "none" },
3999
+ { idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
4000
+ );
4001
+ if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
4002
+ membershipFail("membership_cancellation_pending");
4003
+ }
4004
+ }
4005
+ const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
4006
+ if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
4007
+ await db.transact([membershipUpdate("networkMembership", id10, {
4008
+ status: status2,
4009
+ autoRenew: false,
4010
+ graceEndsAt: 0,
4011
+ cancellationRequestedAt: now,
4012
+ revision: Number(member.revision) + 1,
4013
+ updatedAt: now
4014
+ })], {
4015
+ guards: [
4016
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
4017
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
4018
+ ]
4019
+ });
4020
+ }
4021
+ return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
4022
+ }
4023
+
4024
+ // src/membership-authority-handler.ts
4025
+ var import_db = require("@odla-ai/db");
4026
+
3773
4027
  // src/membership-seat-activation.ts
3774
4028
  async function namedSeatPrimaryActivation(db, member, now) {
3775
4029
  if (!member.namedSeatId) return { ops: [], guards: [] };
@@ -3862,7 +4116,7 @@ async function approveNetworkMembership(db, input, now) {
3862
4116
  }
3863
4117
 
3864
4118
  // src/payments-refund.ts
3865
- var import_stripe2 = require("@odla-ai/stripe");
4119
+ var import_stripe3 = require("@odla-ai/stripe");
3866
4120
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3867
4121
  var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
3868
4122
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
@@ -3872,7 +4126,7 @@ function fail3(code = "refund_target_mismatch") {
3872
4126
  async function refundSubscriptionPayment(secretKey, input) {
3873
4127
  if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
3874
4128
  const call = async (method, path, params, key3) => {
3875
- const result = await (0, import_stripe2.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4129
+ const result = await (0, import_stripe3.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
3876
4130
  if (!result.ok) fail3("stripe_error");
3877
4131
  if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
3878
4132
  return result.body;
@@ -4043,12 +4297,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4043
4297
  }
4044
4298
 
4045
4299
  // src/membership-provider-lifecycle.ts
4046
- var import_stripe3 = require("@odla-ai/stripe");
4300
+ var import_stripe4 = require("@odla-ai/stripe");
4047
4301
 
4048
4302
  // src/membership-renewal-authority.ts
4049
4303
  async function reconcileMembershipRenewal(db, input, now) {
4050
4304
  membershipClock(now);
4051
4305
  const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4306
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
4052
4307
  const fact = eventRow?.fact;
4053
4308
  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
4309
  appId: input.appId,
@@ -4056,7 +4311,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4056
4311
  environment: input.environment,
4057
4312
  dataEnvironment: input.environment,
4058
4313
  runtime: input.runtime
4059
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) {
4314
+ }).some(([key3, value]) => fact.identity[key3] !== value)) {
4060
4315
  return membershipFail("membership_renewal_evidence_pending");
4061
4316
  }
4062
4317
  for (let attempt = 0; attempt < 8; attempt++) {
@@ -4070,7 +4325,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4070
4325
  autoRenew: member.autoRenew === true
4071
4326
  });
4072
4327
  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();
4328
+ if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
4074
4329
  const lines = fact.recurringInvoiceLines;
4075
4330
  if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
4076
4331
  const start = lines[0].startsAt, end = lines[0].endsAt;
@@ -4079,7 +4334,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4079
4334
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
4080
4335
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4081
4336
  if (seat) requireMembershipScope(seat, input);
4082
- const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4337
+ const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
4083
4338
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
4084
4339
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
4085
4340
  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 +4367,7 @@ async function reconcileMembershipRenewal(db, input, now) {
4112
4367
  { ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
4113
4368
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
4114
4369
  { ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
4115
- { ns: "networkCommercialExceptions", id: `network-commercial-exception:${input.eventId}:collision`, exists: false }
4370
+ evidenceGuard
4116
4371
  ];
4117
4372
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4118
4373
  if (billedSeat?.entitlementId) {
@@ -4158,14 +4413,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4158
4413
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4159
4414
  const fact = row2?.fact;
4160
4415
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
4161
- const collisionId = `network-commercial-exception:${input.eventId}:collision`;
4416
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4162
4417
  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
4418
  appId: input.appId,
4164
4419
  chapterId: input.chapterId,
4165
4420
  environment: input.environment,
4166
4421
  dataEnvironment: input.environment,
4167
4422
  runtime: input.runtime
4168
- }).some(([key3, value]) => fact.identity[key3] !== value) || await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4423
+ }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
4169
4424
  const sk = await getVaultSecret(db, stripeSecretName);
4170
4425
  if (!sk) membershipFail("membership_billing_provider_unavailable");
4171
4426
  const id10 = await networkMembershipId(input);
@@ -4179,6 +4434,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4179
4434
  graceEndsAt: Number(value.graceEndsAt ?? 0),
4180
4435
  autoRenew: value.autoRenew === true
4181
4436
  });
4437
+ if (["restarting"].includes(String(member.status))) return receipt();
4182
4438
  if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
4183
4439
  if (fact.eventType === "charge.refunded") {
4184
4440
  if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
@@ -4191,15 +4447,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4191
4447
  alreadyRefundedChargeId: fact.chargeId,
4192
4448
  cancelSubscription: false
4193
4449
  });
4194
- if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("membership_billing_evidence_pending");
4450
+ await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4195
4451
  await refundNetworkMembership(db, input, sk, now);
4196
4452
  const saved2 = await membershipLoad(db, "networkMembership", id10);
4197
4453
  requireMembershipScope(saved2, input);
4198
4454
  return receipt(saved2);
4199
4455
  }
4200
4456
  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))}`);
4457
+ if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4458
+ const result = await (0, import_stripe4.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4203
4459
  const sub = result.body;
4204
4460
  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
4461
  membershipFail("membership_billing_provider_mismatch");
@@ -4223,7 +4479,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4223
4479
  { ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
4224
4480
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4225
4481
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4226
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4482
+ evidenceGuard
4227
4483
  ] });
4228
4484
  } catch (error) {
4229
4485
  if (error.code !== "transact_guard_failed") throw error;
@@ -4236,6 +4492,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4236
4492
  return membershipFail("membership_billing_retry_required");
4237
4493
  }
4238
4494
 
4495
+ // src/membership-restart-seat.ts
4496
+ async function expireRestartedMembershipSeat(db, target, member, now) {
4497
+ const ops = [], guards = [];
4498
+ if (!member.namedSeatId) return { ops, guards };
4499
+ const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
4500
+ requireMembershipScope(seat, target);
4501
+ if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
4502
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
4503
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
4504
+ }
4505
+ if (seat.entitlementId) {
4506
+ const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
4507
+ requireMembershipScope(entitlement, target);
4508
+ ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
4509
+ guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
4510
+ }
4511
+ return { ops, guards };
4512
+ }
4513
+
4514
+ // src/membership-restart-authority.ts
4515
+ var import_stripe5 = require("@odla-ai/stripe");
4516
+
4517
+ // src/stripe-event-digest.ts
4518
+ async function stripePayloadDigest(value) {
4519
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
4520
+ return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4521
+ }
4522
+
4523
+ // src/membership-restart-authority.ts
4524
+ function membershipCanRestart(member, now) {
4525
+ const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
4526
+ 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);
4527
+ }
4528
+ async function stopPreviousSubscription(member, target, secret) {
4529
+ const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4530
+ const result = await (0, import_stripe5.stripeRequest)({ secretKey: secret }, "GET", path);
4531
+ const sub = result.body;
4532
+ if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4533
+ ...stripePartitionMetadata(target),
4534
+ applicationId: target.applicationId
4535
+ }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4536
+ if (sub.status === "canceled") return;
4537
+ const canceled = await (0, import_stripe5.stripeRequest)(
4538
+ { secretKey: secret },
4539
+ "DELETE",
4540
+ path,
4541
+ { invoice_now: false, prorate: false },
4542
+ { idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
4543
+ );
4544
+ if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
4545
+ }
4546
+ async function prepareMembershipRestart(db, input, now, stripeSecret) {
4547
+ membershipClock(now);
4548
+ if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
4549
+ const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
4550
+ const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
4551
+ const state = await loadMembershipAuthorityState(db, input);
4552
+ const previous = await membershipLoad(db, "networkMembership", id10);
4553
+ requireMembershipScope(previous, input);
4554
+ const receipt = {
4555
+ applicationId: input.applicationId,
4556
+ membershipId: id10,
4557
+ restartId,
4558
+ memberNumber: Number(previous.memberNumber),
4559
+ status: "ready"
4560
+ };
4561
+ const existing = await membershipLoad(db, "membershipRestart", id10);
4562
+ if (existing?.restartId === restartId && existing.status === "ready") {
4563
+ requireMembershipScope(existing, input);
4564
+ return receipt;
4565
+ }
4566
+ if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
4567
+ if (previous.status !== "restarting") {
4568
+ if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
4569
+ const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4570
+ requireMembershipScope(binding2, input);
4571
+ const seats = await expireRestartedMembershipSeat(db, input, previous, now);
4572
+ await db.transact([
4573
+ ...seats.ops,
4574
+ membershipUpdate("membershipPurchaseHistory", restartId, {
4575
+ id: restartId,
4576
+ ...membershipScope(input),
4577
+ membershipId: id10,
4578
+ providerSubscriptionId: input.previousSubscriptionId,
4579
+ memberSnapshot: previous,
4580
+ bindingSnapshot: binding2,
4581
+ archivedAt: now
4582
+ }),
4583
+ membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
4584
+ ], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
4585
+ ...seats.guards,
4586
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
4587
+ { ns: "membershipPurchaseHistory", id: restartId, exists: false },
4588
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
4589
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4590
+ ] });
4591
+ } else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
4592
+ const reserved = await membershipLoad(db, "networkMembership", id10);
4593
+ requireMembershipScope(reserved, input);
4594
+ if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
4595
+ await stopPreviousSubscription(reserved, input, stripeSecret);
4596
+ const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
4597
+ requireMembershipScope(binding, input);
4598
+ await db.transact([
4599
+ membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
4600
+ { t: "delete", ns: "membershipOfferCheckout", id: bindingId }
4601
+ ], { mutationId: `membership-restart-ready:${restartId}`, guards: [
4602
+ { ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
4603
+ { ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
4604
+ { ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
4605
+ ] });
4606
+ return receipt;
4607
+ }
4608
+
4239
4609
  // src/membership-access-authority.ts
4240
4610
  async function readMembershipAccess(db, input, now) {
4241
4611
  membershipClock(now);
@@ -4252,7 +4622,8 @@ async function readMembershipAccess(db, input, now) {
4252
4622
  allowed: allowed2,
4253
4623
  status: String(member.status),
4254
4624
  accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
4255
- revision: Number(member.revision)
4625
+ revision: Number(member.revision),
4626
+ restartEligible: !allowed2 && membershipCanRestart(member, now)
4256
4627
  };
4257
4628
  }
4258
4629
  async function verifyMembershipAccessReceipt(value, input, now) {
@@ -4260,7 +4631,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
4260
4631
  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
4632
  throw new Error("invalid network access receipt");
4262
4633
  }
4263
- return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
4634
+ return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
4264
4635
  }
4265
4636
 
4266
4637
  // src/membership-seat-reservation.ts
@@ -4341,16 +4712,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
4341
4712
  }
4342
4713
 
4343
4714
  // src/membership-seat-stripe.ts
4344
- var import_stripe5 = require("@odla-ai/stripe");
4715
+ var import_stripe7 = require("@odla-ai/stripe");
4345
4716
 
4346
4717
  // src/subscription-provider-contract.ts
4347
- var import_stripe4 = require("@odla-ai/stripe");
4718
+ var import_stripe6 = require("@odla-ai/stripe");
4348
4719
  function subscriptionContractMismatch() {
4349
4720
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4350
4721
  }
4351
4722
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
4352
4723
  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)}`);
4724
+ const result = await (0, import_stripe6.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4354
4725
  const price = result.body;
4355
4726
  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
4727
  return typeof price.product === "string" ? price.product : void 0;
@@ -4368,7 +4739,7 @@ var metadataFor = (seat) => ({
4368
4739
  });
4369
4740
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
4370
4741
  async function read(sk, path, params) {
4371
- const response = await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "GET", path, params);
4742
+ const response = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "GET", path, params);
4372
4743
  if (!response.ok) return reject3();
4373
4744
  return response.body;
4374
4745
  }
@@ -4405,11 +4776,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
4405
4776
  }
4406
4777
  await subscriptionForSeat(sk, seat);
4407
4778
  const metadata = metadataFor(seat);
4408
- const result = seat.providerPaymentIntentId ? await (0, import_stripe5.stripeRequest)(
4779
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe7.stripeRequest)(
4409
4780
  { secretKey: sk },
4410
4781
  "GET",
4411
4782
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
4412
- ) : await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
4783
+ ) : await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
4413
4784
  amount: seat.amountCents,
4414
4785
  currency: seat.currency,
4415
4786
  customer: seat.providerCustomerId,
@@ -4427,7 +4798,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
4427
4798
  const sub = await subscriptionForSeat(sk, seat);
4428
4799
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
4429
4800
  if (existing) return String(existing.id);
4430
- const result = await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
4801
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
4431
4802
  subscription: seat.providerSubscriptionId,
4432
4803
  price: seat.priceId,
4433
4804
  quantity: 1,
@@ -4505,8 +4876,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4505
4876
  requireMembershipScope(member, input);
4506
4877
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4507
4878
  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");
4879
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
4510
4880
  const fact = row2.fact;
4511
4881
  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
4882
  appId: input.appId,
@@ -4528,7 +4898,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4528
4898
  membershipUpdate("networkSeat", input.seatId, {
4529
4899
  status: status2,
4530
4900
  purchasedAt: paidAt,
4531
- acceptanceEndsAt: Math.min(paidAt + 30 * 864e5, Number(member.termEndsAt)),
4901
+ acceptanceEndsAt: Number(member.termEndsAt),
4532
4902
  termStartedAt: paidAt,
4533
4903
  termEndsAt: member.termEndsAt,
4534
4904
  paymentEventId: input.eventId,
@@ -4553,7 +4923,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4553
4923
  { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
4554
4924
  { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4555
4925
  { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
4556
- { ns: "networkCommercialExceptions", id: collisionId, exists: false }
4926
+ evidenceGuard
4557
4927
  ] });
4558
4928
  } catch (error) {
4559
4929
  if (error.code !== "transact_guard_failed") throw error;
@@ -4576,7 +4946,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
4576
4946
  if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
4577
4947
  const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
4578
4948
  requireMembershipScope(member, target);
4579
- if (now >= Number(seat.acceptanceEndsAt) || now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
4949
+ if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
4580
4950
  const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
4581
4951
  const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
4582
4952
  try {
@@ -4651,7 +5021,7 @@ async function recordRecipient(db, input, now, approve) {
4651
5021
  requireMembershipScope(entitlement, input);
4652
5022
  if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !seat.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
4653
5023
  if (seat.acceptedAt === void 0) {
4654
- if (approve || now >= Number(seat.acceptanceEndsAt) || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
5024
+ if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
4655
5025
  membershipFail("named_seat_recipient_verification_required");
4656
5026
  }
4657
5027
  } else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
@@ -4702,14 +5072,14 @@ async function recordRecipient(db, input, now, approve) {
4702
5072
  }
4703
5073
 
4704
5074
  // src/membership-seat-refund-stripe.ts
4705
- var import_stripe6 = require("@odla-ai/stripe");
5075
+ var import_stripe8 = require("@odla-ai/stripe");
4706
5076
  var fail4 = (code = "named_seat_refund_mismatch") => {
4707
5077
  throw Object.assign(new Error(code), { code });
4708
5078
  };
4709
5079
  async function refundNamedSeatStripePayment(secretKey, seat) {
4710
5080
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
4711
5081
  const call = async (method, path, params, idempotencyKey) => {
4712
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5082
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4713
5083
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4714
5084
  return result.body;
4715
5085
  };
@@ -4764,7 +5134,7 @@ async function requestNamedSeatRefund(db, input, now) {
4764
5134
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4765
5135
  requireMembershipScope(member, input);
4766
5136
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
4767
- if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "expired" && (seat.acceptedAt !== void 0 || !Number.isSafeInteger(seat.acceptanceEndsAt) || now < Number(seat.acceptanceEndsAt)) || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
5137
+ if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
4768
5138
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
4769
5139
  await db.transact([
4770
5140
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -4831,11 +5201,24 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
4831
5201
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4832
5202
  if (!row2) return membershipFail("named_seat_refund_evidence_pending");
4833
5203
  const fact = row2.fact;
4834
- if (await membershipLoad(db, "networkCommercialExceptions", `network-commercial-exception:${input.eventId}:collision`)) membershipFail("named_seat_refund_evidence_quarantined");
5204
+ await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
4835
5205
  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
5206
  await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
4837
5207
  }
4838
5208
 
5209
+ // src/membership-seat-operator-refund.ts
5210
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
5211
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
5212
+ requireMembershipScope(seat, input);
5213
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
5214
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
5215
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
5216
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
5217
+ throw new Error("named seat refund unconfirmed");
5218
+ }
5219
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
5220
+ }
5221
+
4839
5222
  // src/membership-seat-offer.ts
4840
5223
  async function namedSeatOfferDigest(offer) {
4841
5224
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -4848,7 +5231,7 @@ async function namedSeatOfferDigest(offer) {
4848
5231
  "named-only",
4849
5232
  "full-upfront",
4850
5233
  "auto-renew-coterm",
4851
- "30-day-unaccepted-full-refund"
5234
+ "no-automatic-refund"
4852
5235
  ])));
4853
5236
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4854
5237
  }
@@ -4888,7 +5271,7 @@ async function readNamedSeatInvitation(db, input, now) {
4888
5271
  await loadMembershipAuthorityState(db, target);
4889
5272
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
4890
5273
  requireMembershipScope(member, target);
4891
- if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt) || !seat.acceptedAt && now >= Number(seat.acceptanceEndsAt)) membershipFail("named_seat_invitation_unavailable");
5274
+ if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
4892
5275
  return {
4893
5276
  seatId: input.seatId,
4894
5277
  primaryApplicationId: target.applicationId,
@@ -4918,7 +5301,7 @@ async function readMembershipChapterEffect(db, input, now) {
4918
5301
  requireMembershipScope(member, input);
4919
5302
  if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
4920
5303
  let applicable;
4921
- if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(seat.acceptanceEndsAt) && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
5304
+ if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
4922
5305
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
4923
5306
  else applicable = (await readNamedSeatAccess(db, {
4924
5307
  ...input,
@@ -4947,6 +5330,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
4947
5330
  "seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
4948
5331
  "seat-payments": ["seatId", "eventId"],
4949
5332
  "seat-refunds": ["seatId", "eventId"],
5333
+ "seat-operator-refund": ["seatId"],
4950
5334
  "seat-accept": recipient,
4951
5335
  "seat-approve": recipient,
4952
5336
  "seat-decline": recipient,
@@ -5015,6 +5399,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5015
5399
  if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
5016
5400
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
5017
5401
  }
5402
+ if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
5018
5403
  if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
5019
5404
  ...target,
5020
5405
  priceId: peer.seatPriceId,
@@ -5103,7 +5488,7 @@ function createMembershipAuthorityHandler(options) {
5103
5488
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
5104
5489
  const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
5105
5490
  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);
5491
+ if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
5107
5492
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
5108
5493
  if (!peer) return respond({ error: "unauthorized" }, 401);
5109
5494
  let body;
@@ -5125,13 +5510,23 @@ function createMembershipAuthorityHandler(options) {
5125
5510
  let input;
5126
5511
  try {
5127
5512
  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] ?? []];
5513
+ const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
5129
5514
  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
5515
  throw new TypeError("invalid request");
5131
5516
  }
5132
5517
  } catch {
5133
5518
  return respond({ error: "invalid_offer_request" }, 400);
5134
5519
  }
5520
+ if (action === "offer-preview") {
5521
+ return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
5522
+ appId: peer.appId,
5523
+ chapterId: peer.chapterId,
5524
+ environment: peer.environment,
5525
+ runtime: peer.runtime,
5526
+ skuId: String(input.skuId),
5527
+ now: options.now?.() ?? Date.now()
5528
+ }) });
5529
+ }
5135
5530
  if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
5136
5531
  return respond({ ok: true, seat: await handleMembershipSeatAction(
5137
5532
  action,
@@ -5150,6 +5545,15 @@ function createMembershipAuthorityHandler(options) {
5150
5545
  applicationId: String(input.applicationId),
5151
5546
  skuId: String(input.skuId)
5152
5547
  };
5548
+ if (action === "restarts" || action === "cancel-renewal") {
5549
+ const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
5550
+ if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
5551
+ if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
5552
+ return respond({ ok: true, restart: await prepareMembershipRestart(db, {
5553
+ ...target,
5554
+ previousSubscriptionId: String(input.previousSubscriptionId)
5555
+ }, options.now?.() ?? Date.now(), stripeSecret) });
5556
+ }
5153
5557
  if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
5154
5558
  if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
5155
5559
  db,
@@ -5273,6 +5677,12 @@ async function requestMembershipCommercialOffer(options) {
5273
5677
  lifetimeMs: 6e4
5274
5678
  });
5275
5679
  }
5680
+ async function requestMembershipOfferPreview(options) {
5681
+ const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
5682
+ const preview = result.preview;
5683
+ if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
5684
+ return preview;
5685
+ }
5276
5686
  async function requestMembershipCheckoutBinding(options) {
5277
5687
  const result = await requestAuthority(options, "checkouts", {
5278
5688
  applicationId: options.applicationId,
@@ -5311,6 +5721,12 @@ async function requestNamedSeatDecline(options) {
5311
5721
  if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
5312
5722
  return seat;
5313
5723
  }
5724
+ async function requestNamedSeatOperatorRefund(options) {
5725
+ const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
5726
+ const seat = result.seat;
5727
+ if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
5728
+ return seat;
5729
+ }
5314
5730
  async function requestNamedSeatReservation(options) {
5315
5731
  const result = await requestAuthority(options, "seat-reservations", {
5316
5732
  applicationId: options.applicationId,
@@ -5423,7 +5839,7 @@ async function requestMembershipRefund(options) {
5423
5839
  async function requestMembershipRenewal(options) {
5424
5840
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
5425
5841
  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)) {
5842
+ 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
5843
  throw new Error("invalid membership renewal receipt");
5428
5844
  }
5429
5845
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -5465,9 +5881,25 @@ async function requestMembershipAccess(options) {
5465
5881
  const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
5466
5882
  return verifyMembershipAccessReceipt(result.access, options, Date.now());
5467
5883
  }
5884
+ async function requestMembershipRestart(options) {
5885
+ const result = await requestAuthority(options, "restarts", {
5886
+ applicationId: options.applicationId,
5887
+ skuId: options.skuId,
5888
+ previousSubscriptionId: options.previousSubscriptionId
5889
+ });
5890
+ const row2 = result.restart;
5891
+ 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");
5892
+ return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
5893
+ }
5894
+ async function requestMembershipCancellation(options) {
5895
+ const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
5896
+ const row2 = result.renewal;
5897
+ 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");
5898
+ return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
5899
+ }
5468
5900
 
5469
5901
  // src/membership-seat-cancel.ts
5470
- var import_stripe7 = require("@odla-ai/stripe");
5902
+ var import_stripe9 = require("@odla-ai/stripe");
5471
5903
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5472
5904
  membershipClock(now);
5473
5905
  const state = await loadMembershipAuthorityState(db, target);
@@ -5495,7 +5927,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5495
5927
  }
5496
5928
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
5497
5929
  const request = async (method, path, params) => {
5498
- const result = await (0, import_stripe7.stripeRequest)(
5930
+ const result = await (0, import_stripe9.stripeRequest)(
5499
5931
  { secretKey },
5500
5932
  method,
5501
5933
  path,
@@ -5508,7 +5940,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
5508
5940
  let payment;
5509
5941
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
5510
5942
  else {
5511
- const snapshot = await listStripeObjects((0, import_stripe7.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
5943
+ const snapshot = await listStripeObjects((0, import_stripe9.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
5512
5944
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
5513
5945
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
5514
5946
  payment = matches2[0];
@@ -5539,7 +5971,7 @@ async function drainMembershipEffects(db, options, now) {
5539
5971
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
5540
5972
  await loadMembershipAuthorityState(db, scope2);
5541
5973
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
5542
- let expired = 0, pending = 0, complete = 0, more = false;
5974
+ let pending = 0, complete = 0, more = false;
5543
5975
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
5544
5976
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
5545
5977
  more ||= seats.length > limit;
@@ -5557,26 +5989,6 @@ async function drainMembershipEffects(db, options, now) {
5557
5989
  }
5558
5990
  }
5559
5991
  }
5560
- for (const status2 of ["renewal_pending", "pending_acceptance"]) {
5561
- const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
5562
- more ||= seats.length > limit;
5563
- for (const seat of seats.slice(0, limit)) {
5564
- if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
5565
- if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
5566
- try {
5567
- await requestNamedSeatRefund(db, {
5568
- ...scope2,
5569
- applicationId: String(seat.applicationId),
5570
- skuId: String(seat.skuId),
5571
- seatId: String(seat.id),
5572
- reason: "expired"
5573
- }, now);
5574
- expired++;
5575
- } catch {
5576
- pending++;
5577
- }
5578
- }
5579
- }
5580
5992
  const effects = (await db.query({ membershipEffect: { $: { where: {
5581
5993
  chapterId: scope2.chapterId,
5582
5994
  environment: scope2.environment,
@@ -5638,7 +6050,7 @@ async function drainMembershipEffects(db, options, now) {
5638
6050
  pending++;
5639
6051
  }
5640
6052
  }
5641
- return { complete, pending, expired, more };
6053
+ return { complete, pending, more };
5642
6054
  }
5643
6055
 
5644
6056
  // src/schema-membership-offers.ts
@@ -5732,7 +6144,27 @@ var membershipAuthorityEntities = {
5732
6144
  cancellationRequestedAt: attr9("number", true),
5733
6145
  graceEndsAt: attr9("number", true),
5734
6146
  ...times(),
5735
- refundRequestedAt: attr9("number", true)
6147
+ refundRequestedAt: attr9("number", true),
6148
+ restartId: key2(true)
6149
+ } },
6150
+ membershipRestart: { attrs: {
6151
+ id: id9(),
6152
+ ...scope(),
6153
+ membershipId: key2(),
6154
+ restartId: key2(),
6155
+ previousSubscriptionId: key2(),
6156
+ memberNumber: attr9("number"),
6157
+ status: key2(),
6158
+ createdAt: attr9("number")
6159
+ } },
6160
+ membershipPurchaseHistory: { attrs: {
6161
+ id: id9(),
6162
+ ...scope(),
6163
+ membershipId: key2(),
6164
+ providerSubscriptionId: key2(),
6165
+ memberSnapshot: attr9("json"),
6166
+ bindingSnapshot: attr9("json"),
6167
+ archivedAt: attr9("number")
5736
6168
  } },
5737
6169
  networkEntitlement: { attrs: {
5738
6170
  id: id9(),
@@ -6531,7 +6963,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
6531
6963
  }
6532
6964
 
6533
6965
  // src/payments.ts
6534
- var import_stripe8 = require("@odla-ai/stripe");
6966
+ var import_stripe10 = require("@odla-ai/stripe");
6535
6967
  function paymentsReady(group, hasSecretKey) {
6536
6968
  return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
6537
6969
  }
@@ -7158,9 +7590,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
7158
7590
  }
7159
7591
  const nameField = def.nameField ?? "name";
7160
7592
  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;
7593
+ for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
7594
+ const value = record4.fields?.[field2];
7595
+ if (value !== void 0) input[field2] = value;
7164
7596
  }
7165
7597
  if (input[nameField] === void 0) input[nameField] = record4.name;
7166
7598
  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 +7861,9 @@ function subAnnualCents(sub) {
7429
7861
  }
7430
7862
 
7431
7863
  // src/payments-stripe.ts
7432
- var import_stripe9 = require("@odla-ai/stripe");
7864
+ var import_stripe11 = require("@odla-ai/stripe");
7433
7865
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7434
- return (0, import_stripe9.stripeRequest)(
7866
+ return (0, import_stripe11.stripeRequest)(
7435
7867
  { secretKey: sk },
7436
7868
  method,
7437
7869
  path,
@@ -7741,7 +8173,8 @@ function applicationSummary(app) {
7741
8173
  status: app.status,
7742
8174
  createdAt: app.createdAt ?? null,
7743
8175
  meetingLink: app.meetingLink ?? null,
7744
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
8176
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
8177
+ ...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
7745
8178
  renewalAt: app.renewalAt ?? null,
7746
8179
  canceled: app.canceled === true,
7747
8180
  tierId: app.tierId ?? null
@@ -7923,7 +8356,7 @@ function isValidTimeZone(tz) {
7923
8356
  function resolveScheduling(config) {
7924
8357
  const result = validateScheduling(config);
7925
8358
  if (result.ok) return result.value;
7926
- const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
8359
+ const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
7927
8360
  throw new Error(`scheduling: ${detail}`);
7928
8361
  }
7929
8362
  function validateScheduling(config) {
@@ -9068,7 +9501,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
9068
9501
  MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
9069
9502
  );
9070
9503
  function projectMemberSelf(record4) {
9071
- return Object.fromEntries(Object.entries(record4).filter(([field]) => MEMBER_SELF_READABLE_FIELDS.has(field)));
9504
+ return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
9072
9505
  }
9073
9506
  function classifyMemberSystemWrite(intent) {
9074
9507
  if (intent.actor === "bnf") return "reject_bnf_surface";