@odla-ai/chapter 0.50.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.
@@ -842,7 +842,9 @@ interface MembershipOfferingVersion {
842
842
  autoRenew: true;
843
843
  paymentGraceDays: number;
844
844
  seatPriceCents: number;
845
- seatPurchaseWindowCalendarMonths: number;
845
+ /** Calendar months after the initial payment during which a seat may be bought. Absent: no window; a seat
846
+ * may be bought whenever the primary membership is paid and active, renewals included. */
847
+ seatPurchaseWindowCalendarMonths?: number;
846
848
  requiredDisclosures: string[];
847
849
  }
848
850
  /** Cohort qualification survives the expiration of its monetary benefit. */
@@ -1010,6 +1012,8 @@ interface NamedSeatOffer {
1010
1012
  autoRenew: boolean;
1011
1013
  eligible: boolean;
1012
1014
  digest: string;
1015
+ /** False when the frozen offering sets no purchase window: seats may be bought for as long as the membership is paid. */
1016
+ purchaseWindowed?: boolean;
1013
1017
  seat?: {
1014
1018
  seatId: string;
1015
1019
  status: string;
@@ -842,7 +842,9 @@ interface MembershipOfferingVersion {
842
842
  autoRenew: true;
843
843
  paymentGraceDays: number;
844
844
  seatPriceCents: number;
845
- seatPurchaseWindowCalendarMonths: number;
845
+ /** Calendar months after the initial payment during which a seat may be bought. Absent: no window; a seat
846
+ * may be bought whenever the primary membership is paid and active, renewals included. */
847
+ seatPurchaseWindowCalendarMonths?: number;
846
848
  requiredDisclosures: string[];
847
849
  }
848
850
  /** Cohort qualification survives the expiration of its monetary benefit. */
@@ -1010,6 +1012,8 @@ interface NamedSeatOffer {
1010
1012
  autoRenew: boolean;
1011
1013
  eligible: boolean;
1012
1014
  digest: string;
1015
+ /** False when the frozen offering sets no purchase window: seats may be bought for as long as the membership is paid. */
1016
+ purchaseWindowed?: boolean;
1013
1017
  seat?: {
1014
1018
  seatId: string;
1015
1019
  status: string;
@@ -1660,6 +1660,11 @@ function canApprove(status2, p) {
1660
1660
  }
1661
1661
 
1662
1662
  // src/membership-policy.ts
1663
+ function seatPurchaseDeadline(offering, member) {
1664
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
1665
+ if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
1666
+ return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
1667
+ }
1663
1668
  var fail2 = (field) => {
1664
1669
  throw new TypeError(`invalid membership policy: ${field}`);
1665
1670
  };
@@ -1716,7 +1721,7 @@ function membershipOfferingVersion(value) {
1716
1721
  autoRenew: true,
1717
1722
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1718
1723
  seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1719
- seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
1724
+ ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1720
1725
  requiredDisclosures: ids(input.requiredDisclosures)
1721
1726
  };
1722
1727
  }
@@ -2107,7 +2112,7 @@ async function confirmMembershipPayment(db, input, now) {
2107
2112
  initialTermEndsAt: endsAt,
2108
2113
  termStartedAt: paidAt,
2109
2114
  termEndsAt: endsAt,
2110
- seatPurchaseEndsAt: addMembershipCalendarMonths(Number(paidAt), offering.seatPurchaseWindowCalendarMonths),
2115
+ seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
2111
2116
  autoRenew: true,
2112
2117
  refundedCents: 0,
2113
2118
  revision: 1,
@@ -3642,9 +3647,37 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3642
3647
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
3643
3648
  }
3644
3649
 
3650
+ // src/membership-seat-window.ts
3651
+ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
3652
+ membershipClock(now);
3653
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
3654
+ const state = await loadMembershipAuthorityState(db, scope2);
3655
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
3656
+ let lifted = 0;
3657
+ for (const member of rows5.slice(0, limit)) {
3658
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
3659
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
3660
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
3661
+ const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
3662
+ await db.transact([membershipUpdate("networkMembership", String(member.id), {
3663
+ offeringSnapshot: windowless,
3664
+ seatPurchaseEndsAt: Number(member.termEndsAt),
3665
+ revision: Number(member.revision) + 1,
3666
+ updatedAt: now
3667
+ })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
3668
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3669
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
3670
+ ] });
3671
+ lifted++;
3672
+ }
3673
+ return { lifted, more: rows5.length > limit };
3674
+ }
3675
+
3645
3676
  // src/worker-routes-admin-membership-seats.ts
3646
3677
  var LIST = "/api/admin/membership-seats";
3678
+ var LIFT = "/api/admin/membership-seats/lift-purchase-window";
3647
3679
  var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
3680
+ var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
3648
3681
  var PUBLIC = [
3649
3682
  "id",
3650
3683
  "appId",
@@ -3669,11 +3702,11 @@ var PUBLIC = [
3669
3702
  ];
3670
3703
  var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
3671
3704
  var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3672
- const one = url.pathname.match(ONE);
3673
- if (url.pathname !== LIST && !one) return null;
3705
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
3706
+ if (url.pathname !== LIST && !one && !lift) return null;
3674
3707
  const operator = ctx.membershipAuthorityOperator;
3675
3708
  if (!operator) return json({ error: "not found" }, 404);
3676
- if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3709
+ if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3677
3710
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3678
3711
  if (!user) return json({ error: "unauthorized" }, 401);
3679
3712
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
@@ -3683,6 +3716,24 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3683
3716
  } catch {
3684
3717
  return json({ error: "the membership authority is not hosted by this deployment" }, 404);
3685
3718
  }
3719
+ if (lift) {
3720
+ let body;
3721
+ try {
3722
+ body = await req.json();
3723
+ } catch {
3724
+ return json({ error: "chapterId and runtime are required" }, 400);
3725
+ }
3726
+ const appId = body.appId ?? body.chapterId;
3727
+ if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime)) {
3728
+ return json({ error: "chapterId and runtime are required" }, 400);
3729
+ }
3730
+ try {
3731
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
3732
+ } catch (error) {
3733
+ const code = error.code;
3734
+ return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3735
+ }
3736
+ }
3686
3737
  if (!one) {
3687
3738
  const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
3688
3739
  return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
@@ -8730,15 +8781,16 @@ async function readNamedSeatOffer(db, input, now) {
8730
8781
  const offering = membershipOfferingVersion(member.offeringSnapshot);
8731
8782
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
8732
8783
  if (seat) requireMembershipScope(seat, input);
8784
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
8733
8785
  const offer = {
8734
8786
  membershipId: id6,
8735
8787
  amountCents: offering.seatPriceCents,
8736
8788
  currency: offering.currency,
8737
8789
  interval: offering.interval,
8738
8790
  termEndsAt: Number(member.termEndsAt),
8739
- purchaseEndsAt: Number(member.seatPurchaseEndsAt),
8791
+ purchaseEndsAt,
8740
8792
  autoRenew: member.autoRenew === true,
8741
- 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))),
8793
+ 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))),
8742
8794
  ...seat ? { seat: {
8743
8795
  seatId: String(seat.id),
8744
8796
  status: String(seat.status),
@@ -8746,7 +8798,7 @@ async function readNamedSeatOffer(db, input, now) {
8746
8798
  recipientEmail: String(seat.recipientEmailNormalized)
8747
8799
  } } : {}
8748
8800
  };
8749
- return { ...offer, digest: await namedSeatOfferDigest(offer) };
8801
+ return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
8750
8802
  }
8751
8803
 
8752
8804
  // src/named-seat-request.ts
@@ -13763,7 +13815,11 @@ async function reconcileMembershipRenewal(db, input, now) {
13763
13815
  membershipUpdate("networkMembership", id6, {
13764
13816
  status: status2,
13765
13817
  graceEndsAt,
13766
- ...paid ? { termStartedAt: start, termEndsAt: end } : {},
13818
+ ...paid ? {
13819
+ termStartedAt: start,
13820
+ termEndsAt: end,
13821
+ ...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
13822
+ } : {},
13767
13823
  revision: Number(member.revision) + 1,
13768
13824
  updatedAt: now
13769
13825
  }),
@@ -13923,10 +13979,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
13923
13979
  const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
13924
13980
  let seat = await membershipLoad(db, "networkSeat", id6);
13925
13981
  if (!seat) {
13926
- 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) {
13982
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
13983
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
13984
+ 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) {
13927
13985
  membershipFail("named_seat_purchase_unavailable");
13928
13986
  }
13929
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
13930
13987
  const attrs = {
13931
13988
  id: id6,
13932
13989
  ...membershipScope(input),
@@ -13945,7 +14002,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13945
14002
  status: "payment_pending",
13946
14003
  preparedAt: now,
13947
14004
  termEndsAt: member.termEndsAt,
13948
- seatPurchaseEndsAt: member.seatPurchaseEndsAt,
14005
+ seatPurchaseEndsAt: purchaseEndsAt,
13949
14006
  offeringSnapshot: offering,
13950
14007
  revision: 1,
13951
14008
  createdAt: now,
@@ -14147,7 +14204,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
14147
14204
  }
14148
14205
  if (seat.purchasedAt === void 0) {
14149
14206
  if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
14150
- 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);
14207
+ 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);
14151
14208
  const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
14152
14209
  try {
14153
14210
  await db.transact([
@@ -15366,7 +15423,7 @@ async function requestNamedSeatReservation(options) {
15366
15423
  async function requestNamedSeatOffer(options) {
15367
15424
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
15368
15425
  const offer = result.seat;
15369
- 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");
15426
+ 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");
15370
15427
  return offer;
15371
15428
  }
15372
15429
  async function requestNamedSeatInvitation(options) {