@odla-ai/chapter 0.52.1 → 0.54.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.
@@ -36,9 +36,15 @@ __export(worker_exports, {
36
36
  deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
37
37
  deliverSignupControl: () => deliverSignupControl,
38
38
  drainMembershipEffects: () => drainMembershipEffects,
39
+ grantIncludedNamedSeat: () => grantIncludedNamedSeat,
40
+ lowerSeatPrice: () => lowerSeatPrice,
41
+ namedSeatIncluded: () => namedSeatIncluded,
42
+ namedSeatRecipientState: () => namedSeatRecipientState,
43
+ readNamedSeatOffer: () => readNamedSeatOffer,
39
44
  refundNamedSeatByOperator: () => refundNamedSeatByOperator,
40
45
  renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
41
46
  resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
47
+ retermMembershipDiscount: () => retermMembershipDiscount,
42
48
  retryChapterApprovalEffects: () => retryChapterApprovalEffects,
43
49
  retryLeaderAdmissionDeliveries: () => retryLeaderAdmissionDeliveries,
44
50
  withActiveSignupControlTheme: () => withActiveSignupControlTheme
@@ -544,30 +550,30 @@ async function checkBillingTerms(secretKey, input, resume, now, createMissing) {
544
550
  const terms2 = subscriptionBillingTerms(input.billingTerms);
545
551
  if (!resume && terms2.initialTermEndsAt !== void 0 && (terms2.initialTermEndsAt <= now || terms2.initialTermEndsAt - now > 730 * 864e5)) fail();
546
552
  if (!terms2.discount) return;
547
- const { couponId: couponId2, percentOffBps, duration, policyVersionId } = terms2.discount;
553
+ const { couponId, percentOffBps, duration, policyVersionId } = terms2.discount;
548
554
  const productId = input.discountProductId;
549
555
  if (productId !== void 0 && !/^prod_[A-Za-z0-9_]+$/.test(productId)) fail();
550
556
  const metadata = { ...stripePartitionMetadata(input), discountPolicyVersionId: policyVersionId };
551
557
  const expansion = { "expand[0]": "applies_to" };
552
- let result = await (0, import_stripe.stripeRequest)({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId2)}`, expansion);
558
+ let result = await (0, import_stripe.stripeRequest)({ secretKey }, "GET", `/v1/coupons/${encodeURIComponent(couponId)}`, expansion);
553
559
  if (createMissing && result.status === 404 && result.body?.error?.code === "resource_missing") {
554
560
  result = await (0, import_stripe.stripeRequest)(
555
561
  { secretKey },
556
562
  "POST",
557
563
  "/v1/coupons",
558
564
  {
559
- id: couponId2,
565
+ id: couponId,
560
566
  percent_off: percentOffBps / 100,
561
567
  duration,
562
568
  metadata,
563
569
  ...expansion,
564
570
  ...productId ? { "applies_to[products][0]": productId } : {}
565
571
  },
566
- { idempotencyKey: `chapter-policy-coupon:${couponId2}` }
572
+ { idempotencyKey: `chapter-policy-coupon:${couponId}` }
567
573
  );
568
574
  }
569
575
  const coupon = result.body;
570
- if (!result.ok || !coupon || coupon.id !== couponId2 || !resume && coupon.valid !== true || coupon.livemode !== (input.environment === "prod") || coupon.percent_off !== percentOffBps / 100 || coupon.duration !== duration || coupon.amount_off != null || (productId ? !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== productId : coupon.applies_to != null) || !Object.entries(metadata).every(([key3, value]) => coupon.metadata?.[key3] === value)) fail();
576
+ if (!result.ok || !coupon || coupon.id !== couponId || !resume && coupon.valid !== true || coupon.livemode !== (input.environment === "prod") || coupon.percent_off !== percentOffBps / 100 || coupon.duration !== duration || coupon.amount_off != null || (productId ? !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== productId : coupon.applies_to != null) || !Object.entries(metadata).every(([key3, value]) => coupon.metadata?.[key3] === value)) fail();
571
577
  }
572
578
  function subscriptionBillingParams(input, productId) {
573
579
  if (!input.billingTerms) return {};
@@ -1802,8 +1808,8 @@ var object = (value) => {
1802
1808
  var id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
1803
1809
  var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
1804
1810
  var time = (value) => {
1805
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
1806
- return integer2(at, 0, 864e13);
1811
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
1812
+ return integer2(at2, 0, 864e13);
1807
1813
  };
1808
1814
  var ids = (value, allowEmpty = false) => {
1809
1815
  if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
@@ -1847,7 +1853,7 @@ function membershipOfferingVersion(value) {
1847
1853
  term: normalizedTerm,
1848
1854
  autoRenew: true,
1849
1855
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1850
- seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1856
+ seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
1851
1857
  ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1852
1858
  requiredDisclosures: ids(input.requiredDisclosures)
1853
1859
  };
@@ -1932,7 +1938,7 @@ async function hash(value) {
1932
1938
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value)));
1933
1939
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1934
1940
  }
1935
- async function couponId(input, policyId) {
1941
+ async function membershipDiscountCouponId(input, policyId) {
1936
1942
  return policyId ? `odla_m_${await hash([input.appId, input.chapterId, input.environment, input.runtime, input.skuId, policyId])}` : void 0;
1937
1943
  }
1938
1944
  async function load(db, ns, id6) {
@@ -1958,7 +1964,7 @@ async function restore(row2, input) {
1958
1964
  discountPolicy: contract.discountPolicy,
1959
1965
  approvedFoundingCount: Number(row2.approvedFoundingCount),
1960
1966
  now: Number(row2.issuedAt),
1961
- couponId: await couponId(input, contract.discountPolicy?.id)
1967
+ couponId: await membershipDiscountCouponId(input, contract.discountPolicy?.id)
1962
1968
  });
1963
1969
  if (expected.offering.chapterId !== input.chapterId || expected.offering.baseSkuId !== input.skuId) reject("invalid_offer_snapshot");
1964
1970
  const result = {
@@ -1989,7 +1995,7 @@ async function previewMembershipCommercialOffer(db, input) {
1989
1995
  discountPolicy: policy,
1990
1996
  approvedFoundingCount: Number(approvedFoundingCount),
1991
1997
  now: input.now,
1992
- couponId: await couponId(input, policy?.id)
1998
+ couponId: await membershipDiscountCouponId(input, policy?.id)
1993
1999
  });
1994
2000
  const discount = contract.billingTerms.discount;
1995
2001
  return {
@@ -2039,7 +2045,7 @@ async function issueMembershipCommercialOffer(db, input) {
2039
2045
  discountPolicy: policy,
2040
2046
  approvedFoundingCount: Number(approvedFoundingCount),
2041
2047
  now: input.now,
2042
- couponId: await couponId(input, policy?.id)
2048
+ couponId: await membershipDiscountCouponId(input, policy?.id)
2043
2049
  });
2044
2050
  const value = {
2045
2051
  id: input.offerId,
@@ -2935,7 +2941,7 @@ async function checkoutVariables(db, group, app) {
2935
2941
  const frozen = await frozenCheckoutVariables(db, app);
2936
2942
  return frozen.tierName ? frozen : { ...frozen, tierName: await catalogTierName(db, group, app) };
2937
2943
  }
2938
- function formatInstant(at, timeZone) {
2944
+ function formatInstant(at2, timeZone) {
2939
2945
  return new Intl.DateTimeFormat("en-US", {
2940
2946
  weekday: "long",
2941
2947
  month: "long",
@@ -2945,7 +2951,7 @@ function formatInstant(at, timeZone) {
2945
2951
  minute: "2-digit",
2946
2952
  timeZoneName: "short",
2947
2953
  timeZone
2948
- }).format(new Date(at));
2954
+ }).format(new Date(at2));
2949
2955
  }
2950
2956
  var adminUrlFor = (origin) => `${origin}/admin/`;
2951
2957
  var membersUrlFor = (origin) => `${origin}/members/`;
@@ -3893,6 +3899,181 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
3893
3899
  return { refundedCents, refundIds };
3894
3900
  }
3895
3901
 
3902
+ // src/membership-seat-reservation.ts
3903
+ async function recipientKey(input, email) {
3904
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
3905
+ return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
3906
+ }
3907
+ async function reserveNamedMembershipSeat(db, input, now) {
3908
+ membershipClock(now);
3909
+ const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
3910
+ const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
3911
+ const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
3912
+ if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
3913
+ const state = await loadMembershipAuthorityState(db, input);
3914
+ const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
3915
+ requireMembershipScope(member, input);
3916
+ if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
3917
+ const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
3918
+ let seat = await membershipLoad(db, "networkSeat", id6);
3919
+ if (!seat) {
3920
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
3921
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
3922
+ 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) {
3923
+ membershipFail("named_seat_purchase_unavailable");
3924
+ }
3925
+ const attrs = {
3926
+ id: id6,
3927
+ ...membershipScope(input),
3928
+ primaryMembershipId: memberId,
3929
+ recipientName: name,
3930
+ recipientEmailNormalized: email,
3931
+ purchaserUserId: input.purchaserUserId,
3932
+ recipientClaimId: claimId,
3933
+ priceId: input.priceId,
3934
+ primaryPriceId: member.providerPriceId,
3935
+ amountCents: offering.seatPriceCents,
3936
+ currency: offering.currency,
3937
+ interval: offering.interval,
3938
+ providerCustomerId: member.providerCustomerId,
3939
+ providerSubscriptionId: member.providerSubscriptionId,
3940
+ status: "payment_pending",
3941
+ preparedAt: now,
3942
+ termEndsAt: member.termEndsAt,
3943
+ seatPurchaseEndsAt: purchaseEndsAt,
3944
+ offeringSnapshot: offering,
3945
+ revision: 1,
3946
+ createdAt: now,
3947
+ updatedAt: now
3948
+ };
3949
+ try {
3950
+ await db.transact([
3951
+ membershipUpdate("networkSeat", id6, attrs),
3952
+ membershipUpdate("networkSeatRecipient", claimId, {
3953
+ id: claimId,
3954
+ appId: input.appId,
3955
+ chapterId: input.chapterId,
3956
+ environment: input.environment,
3957
+ runtime: input.runtime,
3958
+ seatId: id6,
3959
+ recipientEmailNormalized: email,
3960
+ createdAt: now
3961
+ }),
3962
+ membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
3963
+ ], { mutationId: `named-seat-reserve:${id6}`, guards: [
3964
+ { ns: "networkSeat", id: id6, exists: false },
3965
+ { ns: "networkSeatRecipient", id: claimId, exists: false },
3966
+ { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
3967
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
3968
+ ] });
3969
+ } catch (error) {
3970
+ if (error.code !== "transact_guard_failed") throw error;
3971
+ }
3972
+ seat = await membershipLoad(db, "networkSeat", id6);
3973
+ if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
3974
+ }
3975
+ requireMembershipScope(seat, input);
3976
+ if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
3977
+ return seat;
3978
+ }
3979
+
3980
+ // src/membership-seat-included.ts
3981
+ var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
3982
+ var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
3983
+ function namedSeatRecipientState(seat) {
3984
+ switch (String(seat.status)) {
3985
+ case "renewal_pending":
3986
+ case "pending_acceptance":
3987
+ return "invited";
3988
+ case "pending_approval":
3989
+ case "pending_primary":
3990
+ return "accepted";
3991
+ case "active":
3992
+ return "joined";
3993
+ case "refund_pending":
3994
+ case "refunded":
3995
+ return seat.refundReason === "declined" ? "declined" : "refunded";
3996
+ case "canceled":
3997
+ return "canceled";
3998
+ default:
3999
+ return "payment_pending";
4000
+ }
4001
+ }
4002
+ var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
4003
+ async function grantIncludedNamedSeat(db, input, now) {
4004
+ membershipClock(now);
4005
+ const seat = await reserveNamedMembershipSeat(db, input, now);
4006
+ if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
4007
+ const state = await loadMembershipAuthorityState(db, input);
4008
+ const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
4009
+ requireMembershipScope(member, input);
4010
+ if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || member.namedSeatId !== seat.id || now >= Number(member.termEndsAt)) {
4011
+ membershipFail("named_seat_purchase_unavailable");
4012
+ }
4013
+ if (seat.status === "payment_pending") {
4014
+ const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
4015
+ try {
4016
+ await db.transact([
4017
+ membershipUpdate("networkSeat", seat.id, {
4018
+ status: "pending_acceptance",
4019
+ purchasedAt: now,
4020
+ termStartedAt: now,
4021
+ termEndsAt: member.termEndsAt,
4022
+ acceptanceEndsAt: Number(member.termEndsAt),
4023
+ entitlementId,
4024
+ revision: Number(seat.revision) + 1,
4025
+ updatedAt: now
4026
+ }),
4027
+ membershipUpdate("networkEntitlement", entitlementId, {
4028
+ id: entitlementId,
4029
+ ...membershipScope(input),
4030
+ membershipId: member.id,
4031
+ seatId: seat.id,
4032
+ kind: "named_seat",
4033
+ status: "pending_acceptance",
4034
+ termStartedAt: now,
4035
+ termEndsAt: member.termEndsAt,
4036
+ offeringSnapshot: member.offeringSnapshot,
4037
+ sourceEventId: source,
4038
+ revision: 1,
4039
+ createdAt: now,
4040
+ updatedAt: now
4041
+ }),
4042
+ membershipUpdate("membershipEffect", inviteId, {
4043
+ id: inviteId,
4044
+ chapterId: input.chapterId,
4045
+ environment: input.environment,
4046
+ runtime: input.runtime,
4047
+ membershipId: member.id,
4048
+ seatId: seat.id,
4049
+ kind: "invite_named_recipient",
4050
+ status: "pending",
4051
+ attempts: 0,
4052
+ createdAt: now,
4053
+ updatedAt: now
4054
+ })
4055
+ ], { mutationId: `named-seat-included:${seat.id}`, guards: [
4056
+ {
4057
+ ns: "networkSeat",
4058
+ id: seat.id,
4059
+ exists: true,
4060
+ equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
4061
+ absent: ["purchasedAt", "providerPaymentIntentId"]
4062
+ },
4063
+ { ns: "networkEntitlement", id: entitlementId, exists: false },
4064
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
4065
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
4066
+ ] });
4067
+ } catch (error) {
4068
+ if (error.code !== "transact_guard_failed") throw error;
4069
+ }
4070
+ }
4071
+ const current = await membershipLoad(db, "networkSeat", seat.id);
4072
+ requireMembershipScope(current, input);
4073
+ if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
4074
+ return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
4075
+ }
4076
+
3896
4077
  // src/membership-seat-refund.ts
3897
4078
  async function requestNamedSeatRefund(db, input, now) {
3898
4079
  membershipClock(now);
@@ -3902,7 +4083,7 @@ async function requestNamedSeatRefund(db, input, now) {
3902
4083
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
3903
4084
  requireMembershipScope(member, input);
3904
4085
  if (seat.status === "refunded" || seat.status === "refund_pending") return;
3905
- 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");
4086
+ if (!seat.purchasedAt || !seat.providerPaymentIntentId && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
3906
4087
  const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
3907
4088
  await db.transact([
3908
4089
  membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
@@ -3947,7 +4128,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
3947
4128
  await loadMembershipAuthorityState(db, seat);
3948
4129
  if (seat.status === "refunded") return;
3949
4130
  if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
3950
- const result = await refundNamedSeatStripePayment(secretKey, seat);
4131
+ const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
3951
4132
  await db.transact([
3952
4133
  membershipUpdate("networkSeat", seatId, {
3953
4134
  status: "refunded",
@@ -3969,7 +4150,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3969
4150
  await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
3970
4151
  await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
3971
4152
  const refunded = await membershipLoad(db, "networkSeat", input.seatId);
3972
- if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
4153
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
3973
4154
  throw new Error("named seat refund unconfirmed");
3974
4155
  }
3975
4156
  return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
@@ -4001,9 +4182,56 @@ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
4001
4182
  return { lifted, more: rows5.length > limit };
4002
4183
  }
4003
4184
 
4185
+ // src/membership-seat-reprice.ts
4186
+ async function lowerSeatPrice(db, scope2, now, limit = 100) {
4187
+ membershipClock(now);
4188
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reprice limit");
4189
+ const state = await loadMembershipAuthorityState(db, scope2);
4190
+ const versions = (await db.query({ membershipOfferingVersion: { $: { where: { chapterId: scope2.chapterId }, limit: 200 } } })).membershipOfferingVersion ?? [];
4191
+ if (versions.length >= 200) throw Object.assign(new Error("policy_history_incomplete"), { code: "policy_history_incomplete" });
4192
+ const current = versions.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.baseSkuId === scope2.skuId && offer.status === "published" && now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || now < offer.effectiveUntil)).sort((a, b) => b.version - a.version)[0];
4193
+ if (!current) throw Object.assign(new Error("ambiguous_membership_policy"), { code: "ambiguous_membership_policy" });
4194
+ const target = current.seatPriceCents;
4195
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4196
+ const receipt = { seatPriceCents: target, repriced: 0, skipped: [], more: rows5.length > limit };
4197
+ for (const member of rows5.slice(0, limit)) {
4198
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership reprice scope mismatch");
4199
+ const id6 = String(member.id), skip = (reason) => {
4200
+ receipt.skipped.push({ membershipId: id6, reason });
4201
+ };
4202
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
4203
+ if (offering.seatPriceCents <= target) {
4204
+ skip("seat price already at or below the current offering");
4205
+ continue;
4206
+ }
4207
+ if (!["paid_pending_approval", "active", "cancel_at_term_end", "payment_grace"].includes(String(member.status))) {
4208
+ skip(`membership is ${String(member.status)}`);
4209
+ continue;
4210
+ }
4211
+ const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
4212
+ if (seat && (seat.status !== "payment_pending" || seat.providerPaymentIntentId !== void 0)) {
4213
+ skip(`seat is ${String(seat.status)}`);
4214
+ continue;
4215
+ }
4216
+ const ops = [membershipUpdate("networkMembership", id6, { offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(member.revision) + 1, updatedAt: now })];
4217
+ const guards = [
4218
+ { ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision } },
4219
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
4220
+ ];
4221
+ if (seat) {
4222
+ ops.push(membershipUpdate("networkSeat", String(seat.id), { amountCents: target, offeringSnapshot: { ...offering, seatPriceCents: target }, revision: Number(seat.revision) + 1, updatedAt: now }));
4223
+ guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision, status: "payment_pending" } });
4224
+ }
4225
+ await db.transact(ops, { mutationId: `seat-reprice:${id6}:${member.revision}`, guards });
4226
+ receipt.repriced++;
4227
+ }
4228
+ return receipt;
4229
+ }
4230
+
4004
4231
  // src/worker-routes-admin-membership-seats.ts
4005
4232
  var LIST = "/api/admin/membership-seats";
4006
4233
  var LIFT = "/api/admin/membership-seats/lift-purchase-window";
4234
+ var REPRICE = "/api/admin/membership-seats/reprice";
4007
4235
  var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
4008
4236
  var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
4009
4237
  var PUBLIC = [
@@ -4030,11 +4258,11 @@ var PUBLIC = [
4030
4258
  ];
4031
4259
  var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
4032
4260
  var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4033
- const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
4034
- if (url.pathname !== LIST && !one && !lift) return null;
4261
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT, reprice = url.pathname === REPRICE;
4262
+ if (url.pathname !== LIST && !one && !lift && !reprice) return null;
4035
4263
  const operator = ctx.membershipAuthorityOperator;
4036
4264
  if (!operator) return json({ error: "not found" }, 404);
4037
- if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4265
+ if (req.method !== (one || lift || reprice ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
4038
4266
  const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4039
4267
  if (!user) return json({ error: "unauthorized" }, 401);
4040
4268
  if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
@@ -4044,7 +4272,7 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4044
4272
  } catch {
4045
4273
  return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4046
4274
  }
4047
- if (lift) {
4275
+ if (lift || reprice) {
4048
4276
  let body;
4049
4277
  try {
4050
4278
  body = await req.json();
@@ -4052,14 +4280,16 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4052
4280
  return json({ error: "chapterId and runtime are required" }, 400);
4053
4281
  }
4054
4282
  const appId = body.appId ?? body.chapterId;
4055
- 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)) {
4056
- return json({ error: "chapterId and runtime are required" }, 400);
4283
+ 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) || reprice && (typeof body.skuId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(body.skuId))) {
4284
+ return json({ error: reprice ? "chapterId, runtime and skuId are required" : "chapterId and runtime are required" }, 400);
4057
4285
  }
4286
+ const scope2 = { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime };
4058
4287
  try {
4059
- return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
4288
+ if (reprice) return json({ ok: true, ...await lowerSeatPrice(db, { ...scope2, skuId: String(body.skuId) }, Date.now()) });
4289
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, scope2, Date.now()) });
4060
4290
  } catch (error) {
4061
4291
  const code = error.code;
4062
- return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4292
+ return json({ error: `${reprice ? "seat reprice" : "purchase window lift"} is pending; retry to confirm`, ...code ? { code } : {} }, code ? 409 : 503);
4063
4293
  }
4064
4294
  }
4065
4295
  if (!one) {
@@ -4095,8 +4325,8 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4095
4325
  var LIMIT = 200;
4096
4326
  var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4097
4327
  var instant = (value) => {
4098
- const at = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4099
- return Number.isSafeInteger(at) && at >= 0 ? at : void 0;
4328
+ const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4329
+ return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4100
4330
  };
4101
4331
  function provenance(row2) {
4102
4332
  const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
@@ -4192,6 +4422,239 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
4192
4422
  return json(await listMembershipPolicyVersions(db, { chapterId, now: Date.now() }));
4193
4423
  };
4194
4424
 
4425
+ // src/membership-discount-reterm.ts
4426
+ var import_stripe8 = require("@odla-ai/stripe");
4427
+
4428
+ // src/subscription-provider-contract.ts
4429
+ var import_stripe7 = require("@odla-ai/stripe");
4430
+ function subscriptionContractMismatch() {
4431
+ throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
4432
+ }
4433
+ async function verifySubscriptionPrice(secretKey, input, resume = false) {
4434
+ 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();
4435
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
4436
+ const price = result.body;
4437
+ 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();
4438
+ return typeof price.product === "string" ? price.product : void 0;
4439
+ }
4440
+ function verifySubscriptionResult(sub, input, customerId, metadata) {
4441
+ const items = sub?.items;
4442
+ if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
4443
+ const invoice = sub.latest_invoice;
4444
+ if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
4445
+ verifySubscriptionBillingResult(sub, input);
4446
+ }
4447
+
4448
+ // src/membership-discount-reterm.ts
4449
+ var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4450
+ var RETERMABLE = /* @__PURE__ */ new Set(["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace"]);
4451
+ var PROVIDER_LIVE = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
4452
+ var couponOf = (discount) => {
4453
+ const ref = discount?.source?.coupon ?? discount?.coupon;
4454
+ return typeof ref === "string" ? ref : typeof ref?.id === "string" ? ref.id : void 0;
4455
+ };
4456
+ async function retermPolicy(db, scope2, id6) {
4457
+ const row2 = await membershipLoad(db, "discountPolicyVersion", id6);
4458
+ if (!row2) membershipFail("membership_reterm_policy_unknown");
4459
+ let policy;
4460
+ try {
4461
+ policy = membershipDiscountPolicyVersion(row2.snapshot);
4462
+ } catch {
4463
+ return membershipFail("membership_reterm_policy_ineligible");
4464
+ }
4465
+ if (policy.id !== id6 || policy.chapterId !== scope2.chapterId || policy.status !== "published" || policy.benefitDuration.kind !== "while_active" || policy.percentOffBps <= 0 || policy.discountEligibleSkuIds.length === 0) membershipFail("membership_reterm_policy_ineligible");
4466
+ return policy;
4467
+ }
4468
+ async function attachProviderDiscount(sk, scope2, member, policy, couponId, productId) {
4469
+ const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
4470
+ const read2 = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
4471
+ const sub = read2.body, items = sub?.items?.data;
4472
+ const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
4473
+ if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
4474
+ throw Object.assign(new Error("provider subscription does not match the frozen membership"), { code: "provider_mismatch" });
4475
+ }
4476
+ const discounts = sub.discounts ?? (sub.discount ? [sub.discount] : []);
4477
+ if (!Array.isArray(discounts)) throw Object.assign(new Error("provider discounts unreadable"), { code: "provider_mismatch" });
4478
+ const present = discounts.map(couponOf);
4479
+ if (present.length === 1 && present[0] === couponId) return false;
4480
+ if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
4481
+ const written = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", path, {
4482
+ "discounts[0][coupon]": couponId,
4483
+ "expand[0]": "discounts",
4484
+ "metadata[discountPolicyVersionId]": policy.id
4485
+ }, { idempotencyKey: `membership-reterm:${String(member.id)}:${policy.id}` });
4486
+ const after = written.body?.discounts;
4487
+ if (!written.ok || written.body?.id !== subId || !Array.isArray(after) || after.length !== 1 || couponOf(after[0]) !== couponId) {
4488
+ membershipFail("membership_reterm_provider_unconfirmed");
4489
+ }
4490
+ return true;
4491
+ }
4492
+ async function retermMembershipDiscount(db, stripeSecretKey, scope2, input, now) {
4493
+ membershipClock(now);
4494
+ const limit = input.limit ?? 100;
4495
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid reterm limit");
4496
+ if (typeof input.policyVersionId !== "string" || !SAFE.test(input.policyVersionId)) throw new TypeError("invalid policy version id");
4497
+ if (input.membershipIds !== void 0 && (!Array.isArray(input.membershipIds) || input.membershipIds.length === 0 || input.membershipIds.length > limit || input.membershipIds.some((id6) => typeof id6 !== "string" || !SAFE.test(id6)))) throw new TypeError("invalid membership ids");
4498
+ const state = await loadMembershipAuthorityState(db, scope2);
4499
+ const policy = await retermPolicy(db, scope2, input.policyVersionId);
4500
+ const receipt = { policyVersionId: policy.id, retermed: [], unchanged: [], skipped: [], more: false };
4501
+ const skip = (membershipId, reason) => {
4502
+ receipt.skipped.push({ membershipId, reason });
4503
+ };
4504
+ let rows5;
4505
+ if (input.membershipIds) {
4506
+ rows5 = [];
4507
+ for (const id6 of input.membershipIds) {
4508
+ const member = await membershipLoad(db, "networkMembership", id6);
4509
+ if (!member) skip(id6, "not_found");
4510
+ else if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) skip(id6, "scope_mismatch");
4511
+ else rows5.push(member);
4512
+ }
4513
+ } else {
4514
+ const found = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
4515
+ if (found.some((member) => Object.entries(scope2).some(([key3, value]) => member[key3] !== value))) throw new Error("membership reterm scope mismatch");
4516
+ receipt.more = found.length > limit;
4517
+ rows5 = found.slice(0, limit);
4518
+ }
4519
+ for (const member of rows5) {
4520
+ const id6 = String(member.id);
4521
+ if (member.discountPolicyVersionId === policy.id) {
4522
+ receipt.unchanged.push(id6);
4523
+ continue;
4524
+ }
4525
+ if (!RETERMABLE.has(String(member.status))) {
4526
+ skip(id6, "status");
4527
+ continue;
4528
+ }
4529
+ if (member.founding !== true) {
4530
+ skip(id6, "not_founding");
4531
+ continue;
4532
+ }
4533
+ if (member.cohort !== policy.cohortLabel) {
4534
+ skip(id6, "cohort_mismatch");
4535
+ continue;
4536
+ }
4537
+ const skuId = String(member.baseSkuId);
4538
+ if (!policy.discountEligibleSkuIds.includes(skuId)) {
4539
+ skip(id6, "sku_not_discounted");
4540
+ continue;
4541
+ }
4542
+ const base = Number(member.baseAmountCents), current = Number(member.renewalAmountCents);
4543
+ const renewal = Math.round(base * (1e4 - policy.percentOffBps) / 1e4);
4544
+ if (!Number.isSafeInteger(base) || base <= 0 || !Number.isSafeInteger(current) || renewal > current) {
4545
+ skip(id6, "would_not_widen");
4546
+ continue;
4547
+ }
4548
+ const couponId = await membershipDiscountCouponId({ ...scope2, skuId }, policy.id);
4549
+ let providerUpdated;
4550
+ try {
4551
+ const productId = await verifySubscriptionPrice(stripeSecretKey, {
4552
+ priceId: String(member.providerPriceId),
4553
+ priceCents: base,
4554
+ currency: String(member.currency),
4555
+ interval: member.interval === "month" ? "month" : "year",
4556
+ environment: scope2.environment
4557
+ }, true);
4558
+ if (!productId) throw Object.assign(new Error("price has no product"), { code: "provider_mismatch" });
4559
+ await prepareSubscriptionBillingTerms(stripeSecretKey, {
4560
+ ...scope2,
4561
+ priceCents: base,
4562
+ currency: String(member.currency),
4563
+ discountProductId: productId,
4564
+ billingTerms: {
4565
+ offeringVersionId: String(member.offeringVersionId),
4566
+ discount: { policyVersionId: policy.id, couponId, percentOffBps: policy.percentOffBps, duration: "forever" }
4567
+ }
4568
+ }, false);
4569
+ providerUpdated = await attachProviderDiscount(stripeSecretKey, scope2, member, policy, couponId, productId);
4570
+ } catch (error) {
4571
+ const code = error.code;
4572
+ if (code === "provider_mismatch" || code === "stripe_contract_mismatch") {
4573
+ skip(id6, "provider_mismatch");
4574
+ continue;
4575
+ }
4576
+ throw error;
4577
+ }
4578
+ try {
4579
+ await db.transact([membershipUpdate("networkMembership", id6, {
4580
+ discountPolicyVersionId: policy.id,
4581
+ discountSnapshot: policy,
4582
+ renewalAmountCents: renewal,
4583
+ revision: Number(member.revision) + 1,
4584
+ updatedAt: now
4585
+ })], {
4586
+ mutationId: `membership-reterm:${id6}:${policy.id}:${member.revision}`,
4587
+ guards: [
4588
+ { ns: "networkMembership", id: id6, exists: true, equals: { revision: member.revision, status: member.status } },
4589
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } },
4590
+ { ns: "discountPolicyVersion", id: policy.id, exists: true }
4591
+ ]
4592
+ });
4593
+ } catch (error) {
4594
+ if (error.code !== "transact_guard_failed") throw error;
4595
+ return membershipFail("membership_reterm_retry_required");
4596
+ }
4597
+ receipt.retermed.push({
4598
+ membershipId: id6,
4599
+ ...typeof member.discountPolicyVersionId === "string" ? { previousPolicyVersionId: member.discountPolicyVersionId } : {},
4600
+ previousRenewalAmountCents: current,
4601
+ renewalAmountCents: renewal,
4602
+ providerUpdated
4603
+ });
4604
+ }
4605
+ return receipt;
4606
+ }
4607
+
4608
+ // src/worker-routes-admin-membership-reterm.ts
4609
+ var PATH4 = "/api/admin/membership-discount/reterm";
4610
+ var SCOPE3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
4611
+ var ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
4612
+ var BAD = "chapterId and policyVersionId are required; runtime, appId, membershipIds and limit are optional";
4613
+ var handleAdminMembershipReterm = async (req, url, env, ctx) => {
4614
+ if (url.pathname !== PATH4) return null;
4615
+ const operator = ctx.membershipAuthorityOperator;
4616
+ if (!operator) return json({ error: "not found" }, 404);
4617
+ if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
4618
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
4619
+ if (!user) return json({ error: "unauthorized" }, 401);
4620
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
4621
+ let db;
4622
+ try {
4623
+ db = operator.makeDb(env);
4624
+ } catch {
4625
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
4626
+ }
4627
+ let body;
4628
+ try {
4629
+ body = await req.json();
4630
+ } catch {
4631
+ return json({ error: BAD }, 400);
4632
+ }
4633
+ const appId = body.appId ?? body.chapterId, runtime = body.runtime ?? env.ODLA_RUNTIME;
4634
+ if (typeof body.chapterId !== "string" || !SCOPE3.test(body.chapterId) || typeof appId !== "string" || !SCOPE3.test(appId) || typeof runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(runtime) || typeof body.policyVersionId !== "string" || !ID.test(body.policyVersionId) || body.membershipIds !== void 0 && (!Array.isArray(body.membershipIds) || body.membershipIds.length === 0 || body.membershipIds.length > 200 || body.membershipIds.some((id6) => typeof id6 !== "string" || !ID.test(id6))) || body.limit !== void 0 && (!Number.isSafeInteger(body.limit) || Number(body.limit) < 1 || Number(body.limit) > 200)) {
4635
+ return json({ error: BAD }, 400);
4636
+ }
4637
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
4638
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
4639
+ try {
4640
+ const receipt = await retermMembershipDiscount(
4641
+ db,
4642
+ secret,
4643
+ { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime },
4644
+ {
4645
+ policyVersionId: body.policyVersionId,
4646
+ ...body.membershipIds ? { membershipIds: body.membershipIds } : {},
4647
+ ...body.limit !== void 0 ? { limit: Number(body.limit) } : {}
4648
+ },
4649
+ Date.now()
4650
+ );
4651
+ return json({ ok: true, ...receipt });
4652
+ } catch (error) {
4653
+ const code = error.code;
4654
+ return json({ error: "re-term is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
4655
+ }
4656
+ };
4657
+
4195
4658
  // src/worker-routes-membership-cancel.ts
4196
4659
  var handleMembershipCancellation = async (req, url, env, ctx) => {
4197
4660
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4852,7 +5315,7 @@ async function readAdmissionRows(db, namespace, where, maximum) {
4852
5315
  }
4853
5316
 
4854
5317
  // src/admission-grants.ts
4855
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
5318
+ var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
4856
5319
  var MAX_GRANTS = 2e3;
4857
5320
  var MAX_NOTE = 2e3;
4858
5321
  var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -4874,10 +5337,10 @@ function validateAdmissionGrantDocument(input) {
4874
5337
  const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
4875
5338
  const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
4876
5339
  const environment = typeof target.environment === "string" ? target.environment : "";
4877
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
5340
+ if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
4878
5341
  if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
4879
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
4880
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
5342
+ if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
5343
+ if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
4881
5344
  if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
4882
5345
  const rawGrants = Array.isArray(root.grants) ? root.grants : [];
4883
5346
  if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
@@ -4892,7 +5355,7 @@ function validateAdmissionGrantDocument(input) {
4892
5355
  const decision = grant.decision;
4893
5356
  const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
4894
5357
  const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
4895
- if (!ID.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
5358
+ if (!ID2.test(id6)) errors.push(`document.grants[${index2}].id must be an identifier`);
4896
5359
  if (seenIds.has(id6)) errors.push(`document.grants repeats id "${id6}"`);
4897
5360
  if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index2}].email must be valid`);
4898
5361
  if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
@@ -5108,7 +5571,7 @@ function chapterFlowFieldContract(chapter, flow, application = chapter.applicati
5108
5571
  // src/gift-offers.ts
5109
5572
  var GIFT_OFFER_CATALOG_VERSION = 1;
5110
5573
  var MAX_GIFT_OFFERS = 20;
5111
- var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
5574
+ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
5112
5575
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
5113
5576
  var CURRENCY = /^[a-z]{3}$/;
5114
5577
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -5141,7 +5604,7 @@ function validateGiftOfferCatalog(input, tierIds) {
5141
5604
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
5142
5605
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
5143
5606
  const id6 = text3(item.id, `${path}.id`, errors, 80);
5144
- if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
5607
+ if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
5145
5608
  if (ids2.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
5146
5609
  ids2.add(id6);
5147
5610
  const targetRaw = record(item.target);
@@ -5152,7 +5615,7 @@ function validateGiftOfferCatalog(input, tierIds) {
5152
5615
  target = { type: "tier", tierId };
5153
5616
  } else if (targetRaw?.type === "entitlement") {
5154
5617
  const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
5155
- if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
5618
+ if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
5156
5619
  target = { type: "entitlement", entitlementKey };
5157
5620
  } else errors.push(`${path}.target.type must be tier or entitlement`);
5158
5621
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
@@ -6236,7 +6699,7 @@ var handleMe = async (req, url, env, ctx) => {
6236
6699
  } catch {
6237
6700
  }
6238
6701
  }
6239
- return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(memberAccess && ctx.membershipAuthority?.namedSeats && member?.source.stripeSubscriptionId && member.source.status !== "submitted") });
6702
+ return json({ ...base, memberAccess, membershipRestart, application: member?.application ?? null, namedSeatsEnabled: Boolean(ctx.membershipAuthority?.namedSeats && member?.application.paid && member.source.status !== "submitted") });
6240
6703
  };
6241
6704
  var handleCrm = async (req, url, env, ctx) => {
6242
6705
  const crmBase = ctx.crmBase;
@@ -6938,7 +7401,7 @@ async function memberContentDigest(document) {
6938
7401
  }
6939
7402
 
6940
7403
  // src/member-content-validation-helpers.ts
6941
- var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
7404
+ var ID4 = /^[a-z][a-z0-9-]{0,79}$/;
6942
7405
  var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6943
7406
  var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
6944
7407
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
@@ -6978,7 +7441,7 @@ function offerValue(value, path, errors, tiers, audience) {
6978
7441
  }
6979
7442
  unknownKeys2(raw2, OFFER_KEYS, path, errors);
6980
7443
  const tierId = textValue(raw2.tierId, `${path}.tierId`, errors, 80, true) ?? "";
6981
- if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
7444
+ if (tierId && !ID4.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
6982
7445
  if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
6983
7446
  if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
6984
7447
  errors.push(`${path}.tierId must grant access to this page`);
@@ -7011,7 +7474,7 @@ function validateMemberContentDocument(input, options = {}) {
7011
7474
  const type = sourceRaw.type;
7012
7475
  const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
7013
7476
  if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
7014
- if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7477
+ if (appId && !ID4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7015
7478
  if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
7016
7479
  if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
7017
7480
  if (type === "parent" || type === "chapter") source = { type, appId };
@@ -7048,7 +7511,7 @@ function validateMemberContentDocument(input, options = {}) {
7048
7511
  const id6 = textValue(raw2.id, `${path}.id`, errors, 80, true) ?? "";
7049
7512
  const slug = textValue(raw2.slug, `${path}.slug`, errors, 120, true) ?? "";
7050
7513
  const title = textValue(raw2.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
7051
- if (id6 && !ID3.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7514
+ if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7052
7515
  if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
7053
7516
  if (ids2.has(id6) || knownParentIds.has(id6)) errors.push(`${path}.id duplicates page "${id6}"`);
7054
7517
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
@@ -7975,7 +8438,7 @@ async function deliverAdmissionGrants(options) {
7975
8438
  }
7976
8439
 
7977
8440
  // src/worker-routes-network-admission.ts
7978
- var PATH4 = "/api/admin/network/admissions";
8441
+ var PATH5 = "/api/admin/network/admissions";
7979
8442
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7980
8443
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7981
8444
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -8055,11 +8518,11 @@ async function parseBody(req) {
8055
8518
  }
8056
8519
  }
8057
8520
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
8058
- if (![PATH4, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
8521
+ if (![PATH5, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
8059
8522
  const auth = await gate2(req, env, ctx);
8060
8523
  if ("response" in auth) return auth.response;
8061
8524
  const db = auth.db;
8062
- if (url.pathname === PATH4 && req.method === "GET") {
8525
+ if (url.pathname === PATH5 && req.method === "GET") {
8063
8526
  const targetId2 = url.searchParams.get("targetId") ?? "";
8064
8527
  const target2 = targetFor(ctx, targetId2);
8065
8528
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -8333,10 +8796,10 @@ var handleSchedule = async (req, url, env, ctx) => {
8333
8796
  };
8334
8797
 
8335
8798
  // src/payments-stripe.ts
8336
- var import_stripe10 = require("@odla-ai/stripe");
8799
+ var import_stripe11 = require("@odla-ai/stripe");
8337
8800
 
8338
8801
  // src/payments-refund.ts
8339
- var import_stripe7 = require("@odla-ai/stripe");
8802
+ var import_stripe9 = require("@odla-ai/stripe");
8340
8803
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
8341
8804
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
8342
8805
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
@@ -8346,7 +8809,7 @@ function fail6(code = "refund_target_mismatch") {
8346
8809
  async function refundSubscriptionPayment(secretKey, input) {
8347
8810
  if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail6();
8348
8811
  const call = async (method, path, params, key3) => {
8349
- const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
8812
+ const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
8350
8813
  if (!result.ok) fail6("stripe_error");
8351
8814
  if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail6("stripe_invalid_response");
8352
8815
  return result.body;
@@ -8420,35 +8883,13 @@ async function refundSubscriptionPayment(secretKey, input) {
8420
8883
  }
8421
8884
 
8422
8885
  // src/subscription-payment-identity.ts
8423
- var import_stripe9 = require("@odla-ai/stripe");
8424
-
8425
- // src/subscription-provider-contract.ts
8426
- var import_stripe8 = require("@odla-ai/stripe");
8427
- function subscriptionContractMismatch() {
8428
- throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
8429
- }
8430
- async function verifySubscriptionPrice(secretKey, input, resume = false) {
8431
- 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();
8432
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
8433
- const price = result.body;
8434
- 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();
8435
- return typeof price.product === "string" ? price.product : void 0;
8436
- }
8437
- function verifySubscriptionResult(sub, input, customerId, metadata) {
8438
- const items = sub?.items;
8439
- if (!sub || !/^sub_[A-Za-z0-9]+$/.test(sub.id) || sub.customer !== customerId || input.existingSubscriptionId && sub.id !== input.existingSubscriptionId || !(input.billingTerms?.initialTermEndsAt ? ["incomplete", "active", "trialing"] : ["incomplete", "active"]).includes(sub.status) || sub.livemode !== (input.environment === "prod") || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || !Array.isArray(items?.data) || items.has_more !== false || items.data.length !== 1 || items.data[0]?.price?.id !== input.priceId || items.data[0]?.quantity !== 1) subscriptionContractMismatch();
8440
- const invoice = sub.latest_invoice;
8441
- if (!invoice || typeof invoice !== "object" || invoice.customer !== customerId || invoice.currency !== input.currency || invoice.total !== subscriptionAmountDue(input) || invoice.billing_reason !== "subscription_create") subscriptionContractMismatch();
8442
- verifySubscriptionBillingResult(sub, input);
8443
- }
8444
-
8445
- // src/subscription-payment-identity.ts
8886
+ var import_stripe10 = require("@odla-ai/stripe");
8446
8887
  var id3 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
8447
8888
  async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
8448
8889
  let paymentId = id3(invoice.payment_intent);
8449
8890
  if (!paymentId) {
8450
8891
  if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
8451
- const result2 = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
8892
+ const result2 = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
8452
8893
  const list = result2.body;
8453
8894
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
8454
8895
  const link = list.data[0];
@@ -8457,11 +8898,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
8457
8898
  }
8458
8899
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
8459
8900
  const path = `/v1/payment_intents/${paymentId}`;
8460
- const result = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", path);
8901
+ const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", path);
8461
8902
  const payment = result.body;
8462
8903
  if (!result.ok || payment?.id !== paymentId || id3(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
8463
8904
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
8464
- const tagged = await (0, import_stripe9.stripeRequest)(
8905
+ const tagged = await (0, import_stripe10.stripeRequest)(
8465
8906
  { secretKey },
8466
8907
  "POST",
8467
8908
  path,
@@ -8473,7 +8914,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
8473
8914
 
8474
8915
  // src/payments-stripe.ts
8475
8916
  async function stripeCall(sk, method, path, params, idempotencyKey) {
8476
- return (0, import_stripe10.stripeRequest)(
8917
+ return (0, import_stripe11.stripeRequest)(
8477
8918
  { secretKey: sk },
8478
8919
  method,
8479
8920
  path,
@@ -8592,7 +9033,7 @@ function createStripeProvider(config) {
8592
9033
  return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
8593
9034
  },
8594
9035
  async ingestWebhook(rawBody, sigHeader) {
8595
- if (!webhookSecret || !await (0, import_stripe10.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
9036
+ if (!webhookSecret || !await (0, import_stripe11.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
8596
9037
  return { ok: false, reason: "bad_signature" };
8597
9038
  }
8598
9039
  const event = JSON.parse(rawBody);
@@ -9256,6 +9697,7 @@ async function namedSeatOfferDigest(offer) {
9256
9697
  ])));
9257
9698
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
9258
9699
  }
9700
+ var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
9259
9701
  async function readNamedSeatOffer(db, input, now) {
9260
9702
  membershipClock(now);
9261
9703
  await loadMembershipAuthorityState(db, input);
@@ -9278,10 +9720,19 @@ async function readNamedSeatOffer(db, input, now) {
9278
9720
  seatId: String(seat.id),
9279
9721
  status: String(seat.status),
9280
9722
  recipientName: String(seat.recipientName),
9281
- recipientEmail: String(seat.recipientEmailNormalized)
9723
+ recipientEmail: String(seat.recipientEmailNormalized),
9724
+ recipientState: namedSeatRecipientState(seat),
9725
+ ...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
9726
+ ...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
9727
+ ...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
9282
9728
  } } : {}
9283
9729
  };
9284
- return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
9730
+ return {
9731
+ ...offer,
9732
+ digest: await namedSeatOfferDigest(offer),
9733
+ included: offering.seatPriceCents === 0,
9734
+ purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
9735
+ };
9285
9736
  }
9286
9737
 
9287
9738
  // src/named-seat-request.ts
@@ -9357,8 +9808,9 @@ var handleNamedSeat = async (req, url, env, ctx) => {
9357
9808
  if (recipientName.length < 2 || /[\u0000-\u001f\u007f]/.test(body.recipientName) || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipientEmail) || recipientEmail === normalizeChapterEmail(user.email)) {
9358
9809
  return json({ error: "a valid name and email for someone other than the purchaser are required" }, 400);
9359
9810
  }
9360
- if (!offer.eligible || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
9361
- if (!group?.stripePublishableKey || !group.refundPolicyText || !group.merchantDisclosureText) return json({ error: "seat checkout disclosures unavailable" }, 503);
9811
+ const granted = offer.included && offer.seat !== void 0 && offer.seat.recipientEmail === recipientEmail && offer.seat.recipientName === recipientName && ["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(offer.seat.status);
9812
+ if (!offer.eligible && !granted || body.quoteDigest !== offer.digest) return json({ error: "seat offer changed or is unavailable; refresh before paying" }, 409);
9813
+ if (!group?.refundPolicyText || !group.merchantDisclosureText || !offer.included && !group.stripePublishableKey) return json({ error: "seat checkout disclosures unavailable" }, 503);
9362
9814
  const consentId = `${offer.membershipId}:named-seat-consent`;
9363
9815
  const prior = await membershipLoad(db, "namedSeatConsents", consentId);
9364
9816
  const attrs = {
@@ -9397,7 +9849,7 @@ var handleNamedSeat = async (req, url, env, ctx) => {
9397
9849
  }
9398
9850
  return json({
9399
9851
  ...receipt,
9400
- publishableKey: String(group.stripePublishableKey),
9852
+ ...group.stripePublishableKey ? { publishableKey: String(group.stripePublishableKey) } : {},
9401
9853
  lineItems: { standardCents: offer.amountCents, discountCents: 0, dueTodayCents: offer.amountCents }
9402
9854
  });
9403
9855
  } catch {
@@ -10260,6 +10712,7 @@ function dashboardMetricData(points, now) {
10260
10712
  }
10261
10713
 
10262
10714
  // src/chapter-billing-amount.ts
10715
+ var hasProviderDiscount = (sub, items) => Boolean(sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length));
10263
10716
  function chapterBillingAmount(sub, intent, env) {
10264
10717
  const unknown = { amountCents: null, listAmountCents: null, interval: "year", annualCents: null };
10265
10718
  const items = sub?.items?.data;
@@ -10276,10 +10729,11 @@ function chapterBillingAmount(sub, intent, env) {
10276
10729
  const primary = items.filter((it) => it.price?.id === intent.priceId);
10277
10730
  if (primary.length !== 1 || primary[0].quantity !== 1 || primary[0].price.unit_amount !== intent.priceCents || intent.interval !== interval || intent.currency !== "usd") return { ...unknown, listAmountCents, interval };
10278
10731
  if (terms2?.discount?.duration === "forever") amountCents = listAmountCents - intent.priceCents + Math.round(intent.priceCents * (1e4 - terms2.discount.percentOffBps) / 1e4);
10732
+ else if (hasProviderDiscount(sub, items)) amountCents = null;
10279
10733
  } catch {
10280
10734
  amountCents = null;
10281
10735
  }
10282
- } else if (sub?.discount || Array.isArray(sub?.discounts) && sub.discounts.length || items.some((it) => it.discounts?.length)) amountCents = null;
10736
+ } else if (hasProviderDiscount(sub, items)) amountCents = null;
10283
10737
  return { amountCents, listAmountCents, interval, annualCents: amountCents === null ? null : amountCents * (interval === "month" ? 12 : 1) };
10284
10738
  }
10285
10739
  function chapterBillingActive(sub, app) {
@@ -11192,7 +11646,7 @@ async function networkApplicationAction(opts) {
11192
11646
  if (app.status !== input.expectedStage || !applicationActionStages(app, env, ctx).includes(input.stage)) {
11193
11647
  return json({ error: "application action is no longer available; reload", code: "invalid_application_action" }, 409);
11194
11648
  }
11195
- const at = Date.now();
11649
+ const at2 = Date.now();
11196
11650
  const meta = { [ACTION_META]: {
11197
11651
  version: 1,
11198
11652
  state: "pending",
@@ -11212,7 +11666,7 @@ async function networkApplicationAction(opts) {
11212
11666
  recordId: record4.id,
11213
11667
  kind: "note",
11214
11668
  authorId: `network:${sourceId}`,
11215
- createdAt: at,
11669
+ createdAt: at2,
11216
11670
  body: `Requested ${input.expectedStage} \u2192 ${input.stage} from ${sourceId}`,
11217
11671
  meta
11218
11672
  } },
@@ -11221,7 +11675,7 @@ async function networkApplicationAction(opts) {
11221
11675
  recordId: record4.id,
11222
11676
  kind: "note",
11223
11677
  authorId: `network:${sourceId}`,
11224
- createdAt: at,
11678
+ createdAt: at2,
11225
11679
  body: "Application operation reconciliation",
11226
11680
  meta: { state: "pending", activityId }
11227
11681
  } }
@@ -12068,9 +12522,9 @@ function normalizeNetworkIdentity(kind, value) {
12068
12522
  return trimmed;
12069
12523
  }
12070
12524
  function emailLooksReal(value) {
12071
- const at = value.lastIndexOf("@");
12072
- if (at <= 0 || at === value.length - 1) return false;
12073
- const domain = value.slice(at + 1).toLowerCase();
12525
+ const at2 = value.lastIndexOf("@");
12526
+ if (at2 <= 0 || at2 === value.length - 1) return false;
12527
+ const domain = value.slice(at2 + 1).toLowerCase();
12074
12528
  return !domain.endsWith(".invalid") && domain !== "example.com" && domain !== "example.test";
12075
12529
  }
12076
12530
  function validateNetworkPersonImport(input, allowedChapterIds) {
@@ -12858,7 +13312,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
12858
13312
  }
12859
13313
 
12860
13314
  // src/worker-routes-network-person-registry.ts
12861
- var PATH5 = "/api/admin/network-person-registry";
13315
+ var PATH6 = "/api/admin/network-person-registry";
12862
13316
  var MAX_BODY2 = 8 * 1024 * 1024;
12863
13317
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
12864
13318
  var positiveRevision = (value) => {
@@ -12883,7 +13337,7 @@ function registryError(error) {
12883
13337
  return null;
12884
13338
  }
12885
13339
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12886
- if (url.pathname !== PATH5) return null;
13340
+ if (url.pathname !== PATH6) return null;
12887
13341
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
12888
13342
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
12889
13343
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -13058,7 +13512,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
13058
13512
  };
13059
13513
 
13060
13514
  // src/worker-routes-network-commercial.ts
13061
- var import_stripe11 = require("@odla-ai/stripe");
13515
+ var import_stripe12 = require("@odla-ai/stripe");
13062
13516
 
13063
13517
  // src/worker-routes-network-commercial-parity.ts
13064
13518
  var import_db13 = require("@odla-ai/db");
@@ -13434,7 +13888,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
13434
13888
  return json({ error: "commercial webhook is not configured" }, 503);
13435
13889
  }
13436
13890
  try {
13437
- const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
13891
+ const event = await (0, import_stripe12.constructStripeEvent)(body, signature, secret);
13438
13892
  const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
13439
13893
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
13440
13894
  const result = await ingestNetworkCommercialFact(
@@ -13455,7 +13909,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
13455
13909
  ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
13456
13910
  });
13457
13911
  } catch (error) {
13458
- if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
13912
+ if (error instanceof import_stripe12.StripeError) return json({ error: error.message, code: error.code }, 400);
13459
13913
  throw error;
13460
13914
  }
13461
13915
  };
@@ -13518,7 +13972,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
13518
13972
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
13519
13973
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
13520
13974
  try {
13521
- const stripe = (0, import_stripe11.initStripe)({ secretKey });
13975
+ const stripe = (0, import_stripe12.initStripe)({ secretKey });
13522
13976
  const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
13523
13977
  listStripeObjects(stripe, "/v1/customers"),
13524
13978
  listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
@@ -13532,7 +13986,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
13532
13986
  ...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
13533
13987
  });
13534
13988
  } catch (error) {
13535
- if (error instanceof import_stripe11.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
13989
+ if (error instanceof import_stripe12.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
13536
13990
  return json({ error: "Stripe reconciliation failed" }, 502);
13537
13991
  }
13538
13992
  }
@@ -13629,7 +14083,7 @@ var handleFormation = async (req, url, env, ctx) => {
13629
14083
  var import_db14 = require("@odla-ai/db");
13630
14084
 
13631
14085
  // src/builder-library-validation.ts
13632
- var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14086
+ var ID5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13633
14087
  var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
13634
14088
  var MAX_RESOURCES = 200;
13635
14089
  var MAX_MARKDOWN3 = 65536;
@@ -13690,7 +14144,7 @@ function resourceValue(value, path, errors) {
13690
14144
  }
13691
14145
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
13692
14146
  const id6 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
13693
- if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
14147
+ if (id6 && !ID5.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
13694
14148
  const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
13695
14149
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
13696
14150
  const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
@@ -14112,7 +14566,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
14112
14566
  };
14113
14567
 
14114
14568
  // src/membership-cancel-authority.ts
14115
- var import_stripe12 = require("@odla-ai/stripe");
14569
+ var import_stripe13 = require("@odla-ai/stripe");
14116
14570
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
14117
14571
  membershipClock(now);
14118
14572
  const id6 = await networkMembershipId(target);
@@ -14123,14 +14577,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
14123
14577
  membershipFail("membership_cancellation_unavailable");
14124
14578
  }
14125
14579
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
14126
- const read2 = await (0, import_stripe12.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
14580
+ const read2 = await (0, import_stripe13.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
14127
14581
  const sub = read2.body;
14128
14582
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
14129
14583
  ...stripePartitionMetadata(target),
14130
14584
  applicationId: target.applicationId
14131
14585
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
14132
14586
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
14133
- const write = await (0, import_stripe12.stripeRequest)(
14587
+ const write = await (0, import_stripe13.stripeRequest)(
14134
14588
  { secretKey: stripeSecret },
14135
14589
  "POST",
14136
14590
  path,
@@ -14352,7 +14806,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
14352
14806
  }
14353
14807
 
14354
14808
  // src/membership-provider-lifecycle.ts
14355
- var import_stripe13 = require("@odla-ai/stripe");
14809
+ var import_stripe14 = require("@odla-ai/stripe");
14356
14810
 
14357
14811
  // src/membership-renewal-authority.ts
14358
14812
  async function reconcileMembershipRenewal(db, input, now) {
@@ -14389,7 +14843,9 @@ async function reconcileMembershipRenewal(db, input, now) {
14389
14843
  if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
14390
14844
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
14391
14845
  if (seat) requireMembershipScope(seat, input);
14392
- const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
14846
+ const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
14847
+ const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
14848
+ const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
14393
14849
  const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
14394
14850
  const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
14395
14851
  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");
@@ -14429,19 +14885,19 @@ async function reconcileMembershipRenewal(db, input, now) {
14429
14885
  evidenceGuard
14430
14886
  ];
14431
14887
  if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
14432
- if (billedSeat?.entitlementId) {
14433
- const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(billedSeat.entitlementId));
14888
+ if (carriedSeat?.entitlementId) {
14889
+ const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
14434
14890
  requireMembershipScope(seatEntitlement, input);
14435
14891
  guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
14436
14892
  ops.push(
14437
- membershipUpdate("networkSeat", String(billedSeat.id), {
14893
+ membershipUpdate("networkSeat", String(carriedSeat.id), {
14438
14894
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
14439
- revision: Number(billedSeat.revision) + 1,
14895
+ revision: Number(carriedSeat.revision) + 1,
14440
14896
  updatedAt: now
14441
14897
  }),
14442
14898
  membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
14443
14899
  ...paid ? { termStartedAt: start, termEndsAt: end } : {},
14444
- status: billedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
14900
+ status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
14445
14901
  revision: Number(seatEntitlement.revision) + 1,
14446
14902
  updatedAt: now
14447
14903
  })
@@ -14514,7 +14970,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
14514
14970
  }
14515
14971
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
14516
14972
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
14517
- const result = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
14973
+ const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
14518
14974
  const sub = result.body;
14519
14975
  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)) {
14520
14976
  membershipFail("membership_billing_provider_mismatch");
@@ -14551,86 +15007,8 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
14551
15007
  return membershipFail("membership_billing_retry_required");
14552
15008
  }
14553
15009
 
14554
- // src/membership-seat-reservation.ts
14555
- async function recipientKey(input, email) {
14556
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
14557
- return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
14558
- }
14559
- async function reserveNamedMembershipSeat(db, input, now) {
14560
- membershipClock(now);
14561
- const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
14562
- const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
14563
- const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
14564
- if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
14565
- const state = await loadMembershipAuthorityState(db, input);
14566
- const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
14567
- requireMembershipScope(member, input);
14568
- if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
14569
- const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
14570
- let seat = await membershipLoad(db, "networkSeat", id6);
14571
- if (!seat) {
14572
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
14573
- const purchaseEndsAt = seatPurchaseDeadline(offering, member);
14574
- 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) {
14575
- membershipFail("named_seat_purchase_unavailable");
14576
- }
14577
- const attrs = {
14578
- id: id6,
14579
- ...membershipScope(input),
14580
- primaryMembershipId: memberId,
14581
- recipientName: name,
14582
- recipientEmailNormalized: email,
14583
- purchaserUserId: input.purchaserUserId,
14584
- recipientClaimId: claimId,
14585
- priceId: input.priceId,
14586
- primaryPriceId: member.providerPriceId,
14587
- amountCents: offering.seatPriceCents,
14588
- currency: offering.currency,
14589
- interval: offering.interval,
14590
- providerCustomerId: member.providerCustomerId,
14591
- providerSubscriptionId: member.providerSubscriptionId,
14592
- status: "payment_pending",
14593
- preparedAt: now,
14594
- termEndsAt: member.termEndsAt,
14595
- seatPurchaseEndsAt: purchaseEndsAt,
14596
- offeringSnapshot: offering,
14597
- revision: 1,
14598
- createdAt: now,
14599
- updatedAt: now
14600
- };
14601
- try {
14602
- await db.transact([
14603
- membershipUpdate("networkSeat", id6, attrs),
14604
- membershipUpdate("networkSeatRecipient", claimId, {
14605
- id: claimId,
14606
- appId: input.appId,
14607
- chapterId: input.chapterId,
14608
- environment: input.environment,
14609
- runtime: input.runtime,
14610
- seatId: id6,
14611
- recipientEmailNormalized: email,
14612
- createdAt: now
14613
- }),
14614
- membershipUpdate("networkMembership", memberId, { namedSeatId: id6, revision: Number(member.revision) + 1, updatedAt: now })
14615
- ], { mutationId: `named-seat-reserve:${id6}`, guards: [
14616
- { ns: "networkSeat", id: id6, exists: false },
14617
- { ns: "networkSeatRecipient", id: claimId, exists: false },
14618
- { ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
14619
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
14620
- ] });
14621
- } catch (error) {
14622
- if (error.code !== "transact_guard_failed") throw error;
14623
- }
14624
- seat = await membershipLoad(db, "networkSeat", id6);
14625
- if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
14626
- }
14627
- requireMembershipScope(seat, input);
14628
- if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
14629
- return seat;
14630
- }
14631
-
14632
15010
  // src/membership-seat-stripe.ts
14633
- var import_stripe14 = require("@odla-ai/stripe");
15011
+ var import_stripe15 = require("@odla-ai/stripe");
14634
15012
  var reject7 = () => {
14635
15013
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
14636
15014
  };
@@ -14642,7 +15020,7 @@ var metadataFor = (seat) => ({
14642
15020
  });
14643
15021
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
14644
15022
  async function read(sk, path, params) {
14645
- const response = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", path, params);
15023
+ const response = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", path, params);
14646
15024
  if (!response.ok) return reject7();
14647
15025
  return response.body;
14648
15026
  }
@@ -14665,9 +15043,9 @@ async function subscriptionForSeat(sk, seat) {
14665
15043
  if (!Array.isArray(discounts) || discounts.length > 5) reject7();
14666
15044
  for (const discount of discounts) {
14667
15045
  const couponRef = discount.source?.coupon ?? discount.coupon;
14668
- const couponId2 = typeof couponRef === "string" ? couponRef : couponRef?.id;
14669
- if (!couponId2 || typeof couponId2 !== "string") reject7();
14670
- const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId2)}`, { "expand[0]": "applies_to" });
15046
+ const couponId = typeof couponRef === "string" ? couponRef : couponRef?.id;
15047
+ if (!couponId || typeof couponId !== "string") reject7();
15048
+ const coupon = await read(sk, `/v1/coupons/${encodeURIComponent(couponId)}`, { "expand[0]": "applies_to" });
14671
15049
  if (coupon.livemode !== (seat.environment === "prod") || !Array.isArray(coupon.applies_to?.products) || coupon.applies_to.products.length !== 1 || coupon.applies_to.products[0] !== primary.price.product) reject7();
14672
15050
  }
14673
15051
  return sub;
@@ -14679,11 +15057,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
14679
15057
  }
14680
15058
  await subscriptionForSeat(sk, seat);
14681
15059
  const metadata = metadataFor(seat);
14682
- const result = seat.providerPaymentIntentId ? await (0, import_stripe14.stripeRequest)(
15060
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe15.stripeRequest)(
14683
15061
  { secretKey: sk },
14684
15062
  "GET",
14685
15063
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
14686
- ) : await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
15064
+ ) : await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
14687
15065
  amount: seat.amountCents,
14688
15066
  currency: seat.currency,
14689
15067
  customer: seat.providerCustomerId,
@@ -14701,7 +15079,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
14701
15079
  const sub = await subscriptionForSeat(sk, seat);
14702
15080
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
14703
15081
  if (existing) return String(existing.id);
14704
- const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
15082
+ const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
14705
15083
  subscription: seat.providerSubscriptionId,
14706
15084
  price: seat.priceId,
14707
15085
  quantity: 1,
@@ -14716,6 +15094,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
14716
15094
  // src/membership-seat-checkout.ts
14717
15095
  async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
14718
15096
  let seat = await reserveNamedMembershipSeat(db, input, now);
15097
+ if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
14719
15098
  const state = await loadMembershipAuthorityState(db, input);
14720
15099
  const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
14721
15100
  requireMembershipScope(member, input);
@@ -14922,7 +15301,7 @@ async function recordRecipient(db, input, now, approve) {
14922
15301
  requireMembershipScope(member, input);
14923
15302
  const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
14924
15303
  requireMembershipScope(entitlement, input);
14925
- 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");
15304
+ 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) || !namedSeatSettled(seat)) membershipFail("named_seat_acceptance_unavailable");
14926
15305
  if (seat.acceptedAt === void 0) {
14927
15306
  if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
14928
15307
  membershipFail("named_seat_recipient_verification_required");
@@ -15109,6 +15488,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
15109
15488
  };
15110
15489
  if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
15111
15490
  if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
15491
+ const checkout = {
15492
+ ...target,
15493
+ priceId: peer.seatPriceId,
15494
+ recipientName: String(input.recipientName),
15495
+ recipientEmail: String(input.recipientEmail),
15496
+ purchaserEmail: String(input.purchaserEmail),
15497
+ purchaserUserId: String(input.purchaserUserId)
15498
+ };
15499
+ if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
15112
15500
  const secret = await getVaultSecret(db, stripeSecretName);
15113
15501
  if (!secret) throw new Error("network Stripe provider is unavailable");
15114
15502
  if (action === "seat-decline") {
@@ -15124,14 +15512,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
15124
15512
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
15125
15513
  }
15126
15514
  if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
15127
- if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
15128
- ...target,
15129
- priceId: peer.seatPriceId,
15130
- recipientName: String(input.recipientName),
15131
- recipientEmail: String(input.recipientEmail),
15132
- purchaserEmail: String(input.purchaserEmail),
15133
- purchaserUserId: String(input.purchaserUserId)
15134
- }, secret, now);
15515
+ if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
15135
15516
  const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
15136
15517
  if (action === "seat-payments") {
15137
15518
  const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
@@ -15153,7 +15534,7 @@ var respond = (body, status2 = 200) => new Response(JSON.stringify(body), {
15153
15534
  status: status2,
15154
15535
  headers: { "content-type": "application/json", "cache-control": "no-store" }
15155
15536
  });
15156
- var SAFE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
15537
+ var SAFE2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/;
15157
15538
  var SAFE_ERROR = /* @__PURE__ */ new Set([
15158
15539
  "offer_identity_conflict",
15159
15540
  "membership_authority_unavailable",
@@ -15201,7 +15582,7 @@ function createMembershipAuthorityHandler(options) {
15201
15582
  if (options.stripeSecretName !== void 0 && !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(options.stripeSecretName)) throw new TypeError("invalid Stripe secret slot");
15202
15583
  const identities = /* @__PURE__ */ new Set();
15203
15584
  for (const peer of peers) {
15204
- if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
15585
+ if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(peer.appId) || !SAFE2.test(peer.chapterId) || !/^[a-z0-9][a-z0-9-]{1,31}$/.test(peer.environment) || !/^[a-z0-9]{2,12}$/.test(peer.runtime) || !/^[A-Za-z][A-Za-z0-9_-]{1,127}$/.test(peer.secretName)) throw new TypeError("invalid authority peer");
15205
15586
  const identity = `${peer.appId}:${peer.runtime}`;
15206
15587
  if (identities.has(identity)) throw new TypeError("ambiguous authority peer");
15207
15588
  identities.add(identity);
@@ -15235,7 +15616,7 @@ function createMembershipAuthorityHandler(options) {
15235
15616
  try {
15236
15617
  input = JSON.parse(verified.body);
15237
15618
  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] ?? []];
15238
- 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])))) {
15619
+ 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]) : SAFE2.test(input[key3])))) {
15239
15620
  throw new TypeError("invalid request");
15240
15621
  }
15241
15622
  } catch {
@@ -15572,7 +15953,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
15572
15953
  }
15573
15954
 
15574
15955
  // src/membership-seat-cancel.ts
15575
- var import_stripe15 = require("@odla-ai/stripe");
15956
+ var import_stripe16 = require("@odla-ai/stripe");
15576
15957
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
15577
15958
  membershipClock(now);
15578
15959
  const state = await loadMembershipAuthorityState(db, target);
@@ -15600,7 +15981,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
15600
15981
  }
15601
15982
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
15602
15983
  const request = async (method, path, params) => {
15603
- const result = await (0, import_stripe15.stripeRequest)(
15984
+ const result = await (0, import_stripe16.stripeRequest)(
15604
15985
  { secretKey },
15605
15986
  method,
15606
15987
  path,
@@ -15613,7 +15994,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
15613
15994
  let payment;
15614
15995
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
15615
15996
  else {
15616
- const snapshot = await listStripeObjects((0, import_stripe15.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
15997
+ const snapshot = await listStripeObjects((0, import_stripe16.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
15617
15998
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
15618
15999
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
15619
16000
  payment = matches2[0];
@@ -15987,13 +16368,13 @@ async function requestNamedSeatDecline(options) {
15987
16368
  verifiedRecipientEmail: options.verifiedRecipientEmail
15988
16369
  });
15989
16370
  const seat = result.seat;
15990
- 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");
16371
+ 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");
15991
16372
  return seat;
15992
16373
  }
15993
16374
  async function requestNamedSeatOperatorRefund(options) {
15994
16375
  const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
15995
16376
  const seat = result.seat;
15996
- 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");
16377
+ 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");
15997
16378
  return seat;
15998
16379
  }
15999
16380
  async function requestNamedSeatReservation(options) {
@@ -16007,13 +16388,13 @@ async function requestNamedSeatReservation(options) {
16007
16388
  });
16008
16389
  const seat = result.seat;
16009
16390
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
16010
- if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents <= 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
16391
+ if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
16011
16392
  return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
16012
16393
  }
16013
16394
  async function requestNamedSeatOffer(options) {
16014
16395
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
16015
16396
  const offer = result.seat;
16016
- 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");
16397
+ if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents < 0 || offer.included !== (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");
16017
16398
  return offer;
16018
16399
  }
16019
16400
  async function requestNamedSeatInvitation(options) {
@@ -16049,7 +16430,8 @@ async function requestNamedSeatCheckout(options) {
16049
16430
  });
16050
16431
  const seat = result.seat;
16051
16432
  const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
16052
- if (!seat || seat.seatId !== expected || !/^pi_[A-Za-z0-9_]+$/.test(seat.paymentIntentId) || typeof seat.clientSecret !== "string" || !Number.isSafeInteger(seat.amountCents) || seat.amountCents <= 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status)) {
16433
+ const included = seat?.amountCents === 0;
16434
+ if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || (included ? seat.included !== true || seat.paymentIntentId !== void 0 || seat.clientSecret !== void 0 || !["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(seat.status) : seat.included !== void 0 || !/^pi_[A-Za-z0-9_]+$/.test(String(seat.paymentIntentId)) || typeof seat.clientSecret !== "string" || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status))) {
16053
16435
  throw new Error("invalid named seat checkout receipt");
16054
16436
  }
16055
16437
  return seat;
@@ -16459,6 +16841,7 @@ var BUILTIN_ROUTES = [
16459
16841
  handleAdminNamedSeat,
16460
16842
  handleAdminMembershipSeats,
16461
16843
  handleAdminMembershipPolicyVersions,
16844
+ handleAdminMembershipReterm,
16462
16845
  handleAdminMeetings,
16463
16846
  handleAdminScheduling,
16464
16847
  // Roster + identity