@odla-ai/chapter 0.49.0 → 0.51.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,
@@ -208,6 +209,7 @@ __export(src_exports, {
208
209
  requestMembershipChapterEffect: () => requestMembershipChapterEffect,
209
210
  requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
210
211
  requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
212
+ requestMembershipOfferPreview: () => requestMembershipOfferPreview,
211
213
  requestMembershipPayment: () => requestMembershipPayment,
212
214
  requestMembershipRefund: () => requestMembershipRefund,
213
215
  requestMembershipRenewal: () => requestMembershipRenewal,
@@ -219,6 +221,7 @@ __export(src_exports, {
219
221
  requestNamedSeatDecline: () => requestNamedSeatDecline,
220
222
  requestNamedSeatInvitation: () => requestNamedSeatInvitation,
221
223
  requestNamedSeatOffer: () => requestNamedSeatOffer,
224
+ requestNamedSeatOperatorRefund: () => requestNamedSeatOperatorRefund,
222
225
  requestNamedSeatPayment: () => requestNamedSeatPayment,
223
226
  requestNamedSeatRefund: () => requestNamedSeatRefund2,
224
227
  requestNamedSeatReservation: () => requestNamedSeatReservation,
@@ -236,6 +239,7 @@ __export(src_exports, {
236
239
  resolveSignupControl: () => resolveSignupControl,
237
240
  resolveTiers: () => resolveTiers,
238
241
  roleFromClaim: () => roleFromClaim,
242
+ seatPurchaseDeadline: () => seatPurchaseDeadline,
239
243
  sendTemplated: () => sendTemplated,
240
244
  sharedPersonInput: () => sharedPersonInput,
241
245
  sharedRecordFromCrm: () => sharedRecordFromCrm,
@@ -1713,6 +1717,7 @@ var DEFAULT_ADMIN_COPY = {
1713
1717
  },
1714
1718
  records: {
1715
1719
  workflowMissing: "This record is not linked to an application workflow.",
1720
+ invitation: "Invitation",
1716
1721
  comms: "Comms",
1717
1722
  commsHistory: "Comms history",
1718
1723
  scheduling: "Scheduling",
@@ -1797,7 +1802,9 @@ var DEFAULT_JOIN_COPY = {
1797
1802
  processing: "Processing\u2026",
1798
1803
  payAndContinue: "Pay and continue",
1799
1804
  incomplete: "The payment did not complete. Please try again.",
1800
- consent: "I have read and agree to the pricing and refund policy above."
1805
+ consent: "I have read and agree to the pricing and refund policy above.",
1806
+ foundingMember: "Founding member rate",
1807
+ foundingDiscount: "Founding discount"
1801
1808
  },
1802
1809
  done: {
1803
1810
  label: "You're booked",
@@ -3354,7 +3361,7 @@ function findTier(tiers2, tierId) {
3354
3361
  if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
3355
3362
  return tiers2.find((tier) => tier.id === tierId) ?? null;
3356
3363
  }
3357
- function joinConfigTiers(tiers2, defaults = {}) {
3364
+ function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
3358
3365
  return tiers2.map((tier) => ({
3359
3366
  id: tier.id,
3360
3367
  name: tier.name,
@@ -3362,7 +3369,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
3362
3369
  currency: tier.currency ?? defaults.currency ?? "usd",
3363
3370
  interval: tier.interval ?? defaults.interval ?? "year",
3364
3371
  blurb: tier.blurb ?? "",
3365
- free: tierIsFree(tier)
3372
+ free: tierIsFree(tier),
3373
+ ...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
3366
3374
  }));
3367
3375
  }
3368
3376
 
@@ -3465,6 +3473,11 @@ function createChapterIntegration(chapter, options = {}) {
3465
3473
  }
3466
3474
 
3467
3475
  // src/membership-policy.ts
3476
+ function seatPurchaseDeadline(offering, member) {
3477
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
3478
+ if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
3479
+ return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
3480
+ }
3468
3481
  var fail2 = (field2) => {
3469
3482
  throw new TypeError(`invalid membership policy: ${field2}`);
3470
3483
  };
@@ -3521,7 +3534,7 @@ function membershipOfferingVersion(value) {
3521
3534
  autoRenew: true,
3522
3535
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
3523
3536
  seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
3524
- seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
3537
+ ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
3525
3538
  requiredDisclosures: ids(input.requiredDisclosures)
3526
3539
  };
3527
3540
  }
@@ -3648,24 +3661,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
3648
3661
  if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
3649
3662
  return restore({ ...value, approvedFoundingCount: 0 }, expected);
3650
3663
  }
3651
- async function issueMembershipCommercialOffer(db, input) {
3652
- validate(input);
3653
- const stateId = `${input.chapterId}:${input.environment}`;
3654
- const state = await load(db, "membershipAuthorityState", stateId);
3664
+ async function previewMembershipCommercialOffer(db, input) {
3665
+ for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
3666
+ if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
3667
+ }
3668
+ if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
3669
+ const state = await authoritativeState(db, input);
3670
+ const { offering, policy } = await currentPolicy(db, input, false, state);
3671
+ const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3672
+ if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3673
+ const contract = createMembershipCommercialOffer({
3674
+ offering,
3675
+ discountPolicy: policy,
3676
+ approvedFoundingCount: Number(approvedFoundingCount),
3677
+ now: input.now,
3678
+ couponId: await couponId(input, policy?.id)
3679
+ });
3680
+ const discount = contract.billingTerms.discount;
3681
+ return {
3682
+ skuId: input.skuId,
3683
+ offeringVersionId: offering.id,
3684
+ basePriceCents: offering.basePriceCents,
3685
+ currency: offering.currency,
3686
+ interval: offering.interval,
3687
+ founding: contract.founding,
3688
+ ...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
3689
+ dueTodayCents: contract.dueTodayCents,
3690
+ renewalCents: contract.renewalCents,
3691
+ ...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
3692
+ ...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
3693
+ };
3694
+ }
3695
+ async function authoritativeState(db, input) {
3696
+ const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
3655
3697
  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");
3656
- const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3657
- if (existing) return restore(existing, input);
3698
+ return state;
3699
+ }
3700
+ async function currentPolicy(db, input, restartAware, state) {
3658
3701
  const rows3 = await db.query({
3659
3702
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
3660
3703
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
3661
3704
  });
3662
3705
  if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
3663
- const offerings = rows3.membershipOfferingVersion.map((row3) => membershipOfferingVersion(row3.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
3664
- const policies = rows3.discountPolicyVersion.map((row3) => membershipDiscountPolicyVersion(row3.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
3706
+ 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));
3707
+ 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);
3665
3708
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
3666
- const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
3709
+ const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
3667
3710
  const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
3668
3711
  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");
3712
+ return { offering, policy };
3713
+ }
3714
+ async function issueMembershipCommercialOffer(db, input) {
3715
+ validate(input);
3716
+ const stateId = `${input.chapterId}:${input.environment}`;
3717
+ const state = await authoritativeState(db, input);
3718
+ const existing = await load(db, "membershipCheckoutOffer", input.offerId);
3719
+ if (existing) return restore(existing, input);
3720
+ const { offering, policy } = await currentPolicy(db, input, true, state);
3669
3721
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
3670
3722
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
3671
3723
  const contract = createMembershipCommercialOffer({
@@ -3876,7 +3928,7 @@ async function confirmMembershipPayment(db, input, now) {
3876
3928
  initialTermEndsAt: endsAt,
3877
3929
  termStartedAt: paidAt,
3878
3930
  termEndsAt: endsAt,
3879
- seatPurchaseEndsAt: addMembershipCalendarMonths(Number(paidAt), offering.seatPurchaseWindowCalendarMonths),
3931
+ seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
3880
3932
  autoRenew: true,
3881
3933
  refundedCents: 0,
3882
3934
  revision: 1,
@@ -4304,7 +4356,11 @@ async function reconcileMembershipRenewal(db, input, now) {
4304
4356
  membershipUpdate("networkMembership", id10, {
4305
4357
  status: status2,
4306
4358
  graceEndsAt,
4307
- ...paid ? { termStartedAt: start, termEndsAt: end } : {},
4359
+ ...paid ? {
4360
+ termStartedAt: start,
4361
+ termEndsAt: end,
4362
+ ...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
4363
+ } : {},
4308
4364
  revision: Number(member.revision) + 1,
4309
4365
  updatedAt: now
4310
4366
  }),
@@ -4606,10 +4662,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
4606
4662
  const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
4607
4663
  let seat = await membershipLoad(db, "networkSeat", id10);
4608
4664
  if (!seat) {
4609
- if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > Number(member.seatPurchaseEndsAt) || !Number.isSafeInteger(member.seatPurchaseEndsAt) || member.namedSeatId !== void 0) {
4665
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4666
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
4667
+ if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
4610
4668
  membershipFail("named_seat_purchase_unavailable");
4611
4669
  }
4612
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
4613
4670
  const attrs = {
4614
4671
  id: id10,
4615
4672
  ...membershipScope(input),
@@ -4628,7 +4685,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
4628
4685
  status: "payment_pending",
4629
4686
  preparedAt: now,
4630
4687
  termEndsAt: member.termEndsAt,
4631
- seatPurchaseEndsAt: member.seatPurchaseEndsAt,
4688
+ seatPurchaseEndsAt: purchaseEndsAt,
4632
4689
  offeringSnapshot: offering,
4633
4690
  revision: 1,
4634
4691
  createdAt: now,
@@ -4845,14 +4902,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
4845
4902
  }
4846
4903
  if (seat.purchasedAt === void 0) {
4847
4904
  if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
4848
- const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= Number(member.seatPurchaseEndsAt) && now < Number(member.termEndsAt);
4905
+ const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
4849
4906
  const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
4850
4907
  try {
4851
4908
  await db.transact([
4852
4909
  membershipUpdate("networkSeat", input.seatId, {
4853
4910
  status: status2,
4854
4911
  purchasedAt: paidAt,
4855
- acceptanceEndsAt: Math.min(paidAt + 30 * 864e5, Number(member.termEndsAt)),
4912
+ acceptanceEndsAt: Number(member.termEndsAt),
4856
4913
  termStartedAt: paidAt,
4857
4914
  termEndsAt: member.termEndsAt,
4858
4915
  paymentEventId: input.eventId,
@@ -4900,7 +4957,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
4900
4957
  if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
4901
4958
  const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
4902
4959
  requireMembershipScope(member, target);
4903
- 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");
4960
+ 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");
4904
4961
  const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
4905
4962
  const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
4906
4963
  try {
@@ -4975,7 +5032,7 @@ async function recordRecipient(db, input, now, approve) {
4975
5032
  requireMembershipScope(entitlement, input);
4976
5033
  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");
4977
5034
  if (seat.acceptedAt === void 0) {
4978
- if (approve || now >= Number(seat.acceptanceEndsAt) || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
5035
+ if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
4979
5036
  membershipFail("named_seat_recipient_verification_required");
4980
5037
  }
4981
5038
  } else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
@@ -5088,7 +5145,7 @@ async function requestNamedSeatRefund(db, input, now) {
5088
5145
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5089
5146
  requireMembershipScope(member, input);
5090
5147
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
5091
- 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");
5148
+ 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");
5092
5149
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
5093
5150
  await db.transact([
5094
5151
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -5160,6 +5217,19 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
5160
5217
  await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
5161
5218
  }
5162
5219
 
5220
+ // src/membership-seat-operator-refund.ts
5221
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
5222
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
5223
+ requireMembershipScope(seat, input);
5224
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
5225
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
5226
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
5227
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
5228
+ throw new Error("named seat refund unconfirmed");
5229
+ }
5230
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
5231
+ }
5232
+
5163
5233
  // src/membership-seat-offer.ts
5164
5234
  async function namedSeatOfferDigest(offer) {
5165
5235
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
@@ -5172,7 +5242,7 @@ async function namedSeatOfferDigest(offer) {
5172
5242
  "named-only",
5173
5243
  "full-upfront",
5174
5244
  "auto-renew-coterm",
5175
- "30-day-unaccepted-full-refund"
5245
+ "no-automatic-refund"
5176
5246
  ])));
5177
5247
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5178
5248
  }
@@ -5184,15 +5254,16 @@ async function readNamedSeatOffer(db, input, now) {
5184
5254
  const offering = membershipOfferingVersion(member.offeringSnapshot);
5185
5255
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
5186
5256
  if (seat) requireMembershipScope(seat, input);
5257
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
5187
5258
  const offer = {
5188
5259
  membershipId: id10,
5189
5260
  amountCents: offering.seatPriceCents,
5190
5261
  currency: offering.currency,
5191
5262
  interval: offering.interval,
5192
5263
  termEndsAt: Number(member.termEndsAt),
5193
- purchaseEndsAt: Number(member.seatPurchaseEndsAt),
5264
+ purchaseEndsAt,
5194
5265
  autoRenew: member.autoRenew === true,
5195
- eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= Number(member.seatPurchaseEndsAt) && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
5266
+ eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
5196
5267
  ...seat ? { seat: {
5197
5268
  seatId: String(seat.id),
5198
5269
  status: String(seat.status),
@@ -5200,7 +5271,7 @@ async function readNamedSeatOffer(db, input, now) {
5200
5271
  recipientEmail: String(seat.recipientEmailNormalized)
5201
5272
  } } : {}
5202
5273
  };
5203
- return { ...offer, digest: await namedSeatOfferDigest(offer) };
5274
+ return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
5204
5275
  }
5205
5276
 
5206
5277
  // src/membership-seat-invitation.ts
@@ -5212,7 +5283,7 @@ async function readNamedSeatInvitation(db, input, now) {
5212
5283
  await loadMembershipAuthorityState(db, target);
5213
5284
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
5214
5285
  requireMembershipScope(member, target);
5215
- 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");
5286
+ 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");
5216
5287
  return {
5217
5288
  seatId: input.seatId,
5218
5289
  primaryApplicationId: target.applicationId,
@@ -5242,7 +5313,7 @@ async function readMembershipChapterEffect(db, input, now) {
5242
5313
  requireMembershipScope(member, input);
5243
5314
  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");
5244
5315
  let applicable;
5245
- 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));
5316
+ 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));
5246
5317
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
5247
5318
  else applicable = (await readNamedSeatAccess(db, {
5248
5319
  ...input,
@@ -5271,6 +5342,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
5271
5342
  "seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
5272
5343
  "seat-payments": ["seatId", "eventId"],
5273
5344
  "seat-refunds": ["seatId", "eventId"],
5345
+ "seat-operator-refund": ["seatId"],
5274
5346
  "seat-accept": recipient,
5275
5347
  "seat-approve": recipient,
5276
5348
  "seat-decline": recipient,
@@ -5339,6 +5411,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
5339
5411
  if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
5340
5412
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
5341
5413
  }
5414
+ if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
5342
5415
  if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
5343
5416
  ...target,
5344
5417
  priceId: peer.seatPriceId,
@@ -5427,7 +5500,7 @@ function createMembershipAuthorityHandler(options) {
5427
5500
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
5428
5501
  const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
5429
5502
  const action = url.pathname.split("/").at(-1);
5430
- if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
5503
+ 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);
5431
5504
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
5432
5505
  if (!peer) return respond({ error: "unauthorized" }, 401);
5433
5506
  let body;
@@ -5449,13 +5522,23 @@ function createMembershipAuthorityHandler(options) {
5449
5522
  let input;
5450
5523
  try {
5451
5524
  input = JSON.parse(verified.body);
5452
- const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
5525
+ 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] ?? []];
5453
5526
  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])))) {
5454
5527
  throw new TypeError("invalid request");
5455
5528
  }
5456
5529
  } catch {
5457
5530
  return respond({ error: "invalid_offer_request" }, 400);
5458
5531
  }
5532
+ if (action === "offer-preview") {
5533
+ return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
5534
+ appId: peer.appId,
5535
+ chapterId: peer.chapterId,
5536
+ environment: peer.environment,
5537
+ runtime: peer.runtime,
5538
+ skuId: String(input.skuId),
5539
+ now: options.now?.() ?? Date.now()
5540
+ }) });
5541
+ }
5459
5542
  if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
5460
5543
  return respond({ ok: true, seat: await handleMembershipSeatAction(
5461
5544
  action,
@@ -5606,6 +5689,12 @@ async function requestMembershipCommercialOffer(options) {
5606
5689
  lifetimeMs: 6e4
5607
5690
  });
5608
5691
  }
5692
+ async function requestMembershipOfferPreview(options) {
5693
+ const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
5694
+ const preview = result.preview;
5695
+ 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");
5696
+ return preview;
5697
+ }
5609
5698
  async function requestMembershipCheckoutBinding(options) {
5610
5699
  const result = await requestAuthority(options, "checkouts", {
5611
5700
  applicationId: options.applicationId,
@@ -5644,6 +5733,12 @@ async function requestNamedSeatDecline(options) {
5644
5733
  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");
5645
5734
  return seat;
5646
5735
  }
5736
+ async function requestNamedSeatOperatorRefund(options) {
5737
+ const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
5738
+ const seat = result.seat;
5739
+ 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");
5740
+ return seat;
5741
+ }
5647
5742
  async function requestNamedSeatReservation(options) {
5648
5743
  const result = await requestAuthority(options, "seat-reservations", {
5649
5744
  applicationId: options.applicationId,
@@ -5661,7 +5756,7 @@ async function requestNamedSeatReservation(options) {
5661
5756
  async function requestNamedSeatOffer(options) {
5662
5757
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
5663
5758
  const offer = result.seat;
5664
- if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
5759
+ if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
5665
5760
  return offer;
5666
5761
  }
5667
5762
  async function requestNamedSeatInvitation(options) {
@@ -5888,7 +5983,7 @@ async function drainMembershipEffects(db, options, now) {
5888
5983
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
5889
5984
  await loadMembershipAuthorityState(db, scope2);
5890
5985
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
5891
- let expired = 0, pending = 0, complete = 0, more = false;
5986
+ let pending = 0, complete = 0, more = false;
5892
5987
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
5893
5988
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
5894
5989
  more ||= seats.length > limit;
@@ -5906,26 +6001,6 @@ async function drainMembershipEffects(db, options, now) {
5906
6001
  }
5907
6002
  }
5908
6003
  }
5909
- for (const status2 of ["renewal_pending", "pending_acceptance"]) {
5910
- const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
5911
- more ||= seats.length > limit;
5912
- for (const seat of seats.slice(0, limit)) {
5913
- if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
5914
- if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
5915
- try {
5916
- await requestNamedSeatRefund(db, {
5917
- ...scope2,
5918
- applicationId: String(seat.applicationId),
5919
- skuId: String(seat.skuId),
5920
- seatId: String(seat.id),
5921
- reason: "expired"
5922
- }, now);
5923
- expired++;
5924
- } catch {
5925
- pending++;
5926
- }
5927
- }
5928
- }
5929
6004
  const effects = (await db.query({ membershipEffect: { $: { where: {
5930
6005
  chapterId: scope2.chapterId,
5931
6006
  environment: scope2.environment,
@@ -5987,7 +6062,7 @@ async function drainMembershipEffects(db, options, now) {
5987
6062
  pending++;
5988
6063
  }
5989
6064
  }
5990
- return { complete, pending, expired, more };
6065
+ return { complete, pending, more };
5991
6066
  }
5992
6067
 
5993
6068
  // src/schema-membership-offers.ts