@odla-ai/chapter 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -329,6 +329,7 @@ function createWorkerContext(options) {
329
329
  crmBase,
330
330
  requirePaymentQuote: options.requirePaymentQuote === true,
331
331
  membershipAuthority: options.membershipAuthority,
332
+ membershipAuthorityOperator: options.membershipAuthorityOperator,
332
333
  getPublicConfig,
333
334
  verifyUser,
334
335
  makeDb,
@@ -1659,6 +1660,11 @@ function canApprove(status2, p) {
1659
1660
  }
1660
1661
 
1661
1662
  // src/membership-policy.ts
1663
+ function seatPurchaseDeadline(offering, member) {
1664
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
1665
+ if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
1666
+ return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
1667
+ }
1662
1668
  var fail2 = (field) => {
1663
1669
  throw new TypeError(`invalid membership policy: ${field}`);
1664
1670
  };
@@ -1715,7 +1721,7 @@ function membershipOfferingVersion(value) {
1715
1721
  autoRenew: true,
1716
1722
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1717
1723
  seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1718
- seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
1724
+ ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1719
1725
  requiredDisclosures: ids(input.requiredDisclosures)
1720
1726
  };
1721
1727
  }
@@ -1842,24 +1848,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
1842
1848
  if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
1843
1849
  return restore({ ...value, approvedFoundingCount: 0 }, expected);
1844
1850
  }
1845
- async function issueMembershipCommercialOffer(db, input) {
1846
- validate(input);
1847
- const stateId = `${input.chapterId}:${input.environment}`;
1848
- const state = await load(db, "membershipAuthorityState", stateId);
1851
+ async function previewMembershipCommercialOffer(db, input) {
1852
+ for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
1853
+ if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
1854
+ }
1855
+ if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
1856
+ const state = await authoritativeState(db, input);
1857
+ const { offering, policy } = await currentPolicy(db, input, false, state);
1858
+ const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
1859
+ if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
1860
+ const contract = createMembershipCommercialOffer({
1861
+ offering,
1862
+ discountPolicy: policy,
1863
+ approvedFoundingCount: Number(approvedFoundingCount),
1864
+ now: input.now,
1865
+ couponId: await couponId(input, policy?.id)
1866
+ });
1867
+ const discount = contract.billingTerms.discount;
1868
+ return {
1869
+ skuId: input.skuId,
1870
+ offeringVersionId: offering.id,
1871
+ basePriceCents: offering.basePriceCents,
1872
+ currency: offering.currency,
1873
+ interval: offering.interval,
1874
+ founding: contract.founding,
1875
+ ...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
1876
+ dueTodayCents: contract.dueTodayCents,
1877
+ renewalCents: contract.renewalCents,
1878
+ ...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
1879
+ ...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
1880
+ };
1881
+ }
1882
+ async function authoritativeState(db, input) {
1883
+ const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
1849
1884
  if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
1850
- const existing = await load(db, "membershipCheckoutOffer", input.offerId);
1851
- if (existing) return restore(existing, input);
1885
+ return state;
1886
+ }
1887
+ async function currentPolicy(db, input, restartAware, state) {
1852
1888
  const rows5 = await db.query({
1853
1889
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
1854
1890
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
1855
1891
  });
1856
1892
  if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
1857
- const offerings = rows5.membershipOfferingVersion.map((row3) => membershipOfferingVersion(row3.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
1858
- const policies = rows5.discountPolicyVersion.map((row3) => membershipDiscountPolicyVersion(row3.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
1893
+ const offerings = rows5.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
1894
+ const policies = rows5.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
1859
1895
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
1860
- const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
1896
+ const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
1861
1897
  const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
1862
1898
  if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy && policies.filter((item) => item.version === policy.version).length !== 1) reject("ambiguous_membership_policy");
1899
+ return { offering, policy };
1900
+ }
1901
+ async function issueMembershipCommercialOffer(db, input) {
1902
+ validate(input);
1903
+ const stateId = `${input.chapterId}:${input.environment}`;
1904
+ const state = await authoritativeState(db, input);
1905
+ const existing = await load(db, "membershipCheckoutOffer", input.offerId);
1906
+ if (existing) return restore(existing, input);
1907
+ const { offering, policy } = await currentPolicy(db, input, true, state);
1863
1908
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
1864
1909
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
1865
1910
  const contract = createMembershipCommercialOffer({
@@ -2067,7 +2112,7 @@ async function confirmMembershipPayment(db, input, now) {
2067
2112
  initialTermEndsAt: endsAt,
2068
2113
  termStartedAt: paidAt,
2069
2114
  termEndsAt: endsAt,
2070
- seatPurchaseEndsAt: addMembershipCalendarMonths(Number(paidAt), offering.seatPurchaseWindowCalendarMonths),
2115
+ seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
2071
2116
  autoRenew: true,
2072
2117
  refundedCents: 0,
2073
2118
  revision: 1,
@@ -2167,7 +2212,7 @@ function findTier(tiers, tierId) {
2167
2212
  if (!tierId) return tiers.length === 1 ? tiers[0] : null;
2168
2213
  return tiers.find((tier) => tier.id === tierId) ?? null;
2169
2214
  }
2170
- function joinConfigTiers(tiers, defaults = {}) {
2215
+ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
2171
2216
  return tiers.map((tier) => ({
2172
2217
  id: tier.id,
2173
2218
  name: tier.name,
@@ -2175,7 +2220,8 @@ function joinConfigTiers(tiers, defaults = {}) {
2175
2220
  currency: tier.currency ?? defaults.currency ?? "usd",
2176
2221
  interval: tier.interval ?? defaults.interval ?? "year",
2177
2222
  blurb: tier.blurb ?? "",
2178
- free: tierIsFree(tier)
2223
+ free: tierIsFree(tier),
2224
+ ...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
2179
2225
  }));
2180
2226
  }
2181
2227
 
@@ -3410,6 +3456,313 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
3410
3456
  }
3411
3457
  };
3412
3458
 
3459
+ // src/worker-routes-admin-named-seat.ts
3460
+ var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3461
+ var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
3462
+ var handleAdminNamedSeat = async (req, url, env, ctx) => {
3463
+ const match = url.pathname.match(PATH);
3464
+ if (!match) return null;
3465
+ const seats = ctx.membershipAuthority?.namedSeats;
3466
+ if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
3467
+ const refund = Boolean(match[2]);
3468
+ if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3469
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3470
+ if (!user) return json({ error: "unauthorized" }, 401);
3471
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
3472
+ const db = raw2, app = await membershipLoad(db, "applications", match[1]);
3473
+ if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
3474
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
3475
+ if (!intent) return json({ error: "paid membership required" }, 409);
3476
+ try {
3477
+ const target = await membershipTargetForIntent(intent, app, env, ctx);
3478
+ const offer = await seats.offer(target, env);
3479
+ const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
3480
+ if (!refund) {
3481
+ return json({
3482
+ seat,
3483
+ amountCents: offer.amountCents,
3484
+ currency: offer.currency,
3485
+ termEndsAt: offer.termEndsAt,
3486
+ purchaseEndsAt: offer.purchaseEndsAt,
3487
+ refundable
3488
+ });
3489
+ }
3490
+ if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
3491
+ if (seat.status === "refunded") {
3492
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
3493
+ return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
3494
+ }
3495
+ if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
3496
+ const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
3497
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
3498
+ return json({ ok: true, ...receipt });
3499
+ } catch {
3500
+ return json({ error: "named seat service unavailable; retry to confirm" }, 503);
3501
+ }
3502
+ };
3503
+ async function closeRecipientApplication(db, env, ctx, seatId) {
3504
+ const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
3505
+ if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
3506
+ const attrs = { status: "refunded", renewalAt: 0, canceled: true };
3507
+ await db.transact(
3508
+ [{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
3509
+ { mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
3510
+ );
3511
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
3512
+ }
3513
+
3514
+ // src/membership-seat-refund-stripe.ts
3515
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
3516
+ var fail3 = (code = "named_seat_refund_mismatch") => {
3517
+ throw Object.assign(new Error(code), { code });
3518
+ };
3519
+ async function refundNamedSeatStripePayment(secretKey, seat) {
3520
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
3521
+ const call = async (method, path, params, idempotencyKey) => {
3522
+ const result = await stripeRequest4({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
3523
+ if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
3524
+ return result.body;
3525
+ };
3526
+ const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
3527
+ const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
3528
+ if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail3();
3529
+ const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
3530
+ if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail3();
3531
+ const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
3532
+ if (items.length > 1) fail3();
3533
+ if (items.length && sub.status !== "canceled") {
3534
+ const item = items[0];
3535
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
3536
+ const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
3537
+ if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
3538
+ }
3539
+ const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
3540
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
3541
+ let refundedCents = 0, pendingCents = 0;
3542
+ const refundIds = [];
3543
+ for (const refund of list.data) {
3544
+ if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail3();
3545
+ if (refund.status === "succeeded") {
3546
+ refundedCents += refund.amount;
3547
+ refundIds.push(refund.id);
3548
+ } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
3549
+ }
3550
+ if (refundedCents + pendingCents > seat.amountCents) fail3();
3551
+ if (pendingCents) fail3("named_seat_refund_pending");
3552
+ if (refundedCents < seat.amountCents) {
3553
+ const remaining = seat.amountCents - refundedCents;
3554
+ const refund = await call("POST", "/v1/refunds", {
3555
+ payment_intent: seat.providerPaymentIntentId,
3556
+ amount: remaining,
3557
+ reason: "requested_by_customer",
3558
+ metadata
3559
+ }, `named-seat-refund:${seat.id}:${remaining}`);
3560
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
3561
+ if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
3562
+ refundedCents += remaining;
3563
+ refundIds.push(refund.id);
3564
+ }
3565
+ return { refundedCents, refundIds };
3566
+ }
3567
+
3568
+ // src/membership-seat-refund.ts
3569
+ async function requestNamedSeatRefund(db, input, now) {
3570
+ membershipClock(now);
3571
+ const state = await loadMembershipAuthorityState(db, input);
3572
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
3573
+ requireMembershipScope(seat, input);
3574
+ const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
3575
+ requireMembershipScope(member, input);
3576
+ if (seat.status === "refunded" || seat.status === "refund_pending") return;
3577
+ 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");
3578
+ const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
3579
+ await db.transact([
3580
+ membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
3581
+ ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
3582
+ membershipUpdate("membershipEffect", `${seat.id}:refund`, {
3583
+ id: `${seat.id}:refund`,
3584
+ chapterId: input.chapterId,
3585
+ environment: input.environment,
3586
+ runtime: input.runtime,
3587
+ membershipId: member.id,
3588
+ seatId: seat.id,
3589
+ kind: "refund_seat",
3590
+ status: "pending",
3591
+ attempts: 0,
3592
+ createdAt: now,
3593
+ updatedAt: now
3594
+ }),
3595
+ ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
3596
+ id: `${seat.id}:revoke`,
3597
+ chapterId: input.chapterId,
3598
+ environment: input.environment,
3599
+ runtime: input.runtime,
3600
+ membershipId: member.id,
3601
+ seatId: seat.id,
3602
+ kind: "revoke_named_recipient_access",
3603
+ status: "pending",
3604
+ attempts: 0,
3605
+ createdAt: now,
3606
+ updatedAt: now
3607
+ })] : []
3608
+ ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
3609
+ { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
3610
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3611
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3612
+ ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
3613
+ ] });
3614
+ }
3615
+ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
3616
+ membershipClock(now);
3617
+ const seat = await membershipLoad(db, "networkSeat", seatId);
3618
+ if (!seat) return membershipFail("named_seat_not_found");
3619
+ await loadMembershipAuthorityState(db, seat);
3620
+ if (seat.status === "refunded") return;
3621
+ if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
3622
+ const result = await refundNamedSeatStripePayment(secretKey, seat);
3623
+ await db.transact([
3624
+ membershipUpdate("networkSeat", seatId, {
3625
+ status: "refunded",
3626
+ refundedCents: result.refundedCents,
3627
+ providerRefundIds: result.refundIds,
3628
+ revision: Number(seat.revision) + 1,
3629
+ updatedAt: now
3630
+ }),
3631
+ membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
3632
+ ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
3633
+ { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
3634
+ ] });
3635
+ }
3636
+
3637
+ // src/membership-seat-operator-refund.ts
3638
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3639
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
3640
+ requireMembershipScope(seat, input);
3641
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
3642
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
3643
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
3644
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
3645
+ throw new Error("named seat refund unconfirmed");
3646
+ }
3647
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
3648
+ }
3649
+
3650
+ // src/membership-seat-window.ts
3651
+ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
3652
+ membershipClock(now);
3653
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
3654
+ const state = await loadMembershipAuthorityState(db, scope2);
3655
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
3656
+ let lifted = 0;
3657
+ for (const member of rows5.slice(0, limit)) {
3658
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
3659
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
3660
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
3661
+ const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
3662
+ await db.transact([membershipUpdate("networkMembership", String(member.id), {
3663
+ offeringSnapshot: windowless,
3664
+ seatPurchaseEndsAt: Number(member.termEndsAt),
3665
+ revision: Number(member.revision) + 1,
3666
+ updatedAt: now
3667
+ })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
3668
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3669
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
3670
+ ] });
3671
+ lifted++;
3672
+ }
3673
+ return { lifted, more: rows5.length > limit };
3674
+ }
3675
+
3676
+ // src/worker-routes-admin-membership-seats.ts
3677
+ var LIST = "/api/admin/membership-seats";
3678
+ var LIFT = "/api/admin/membership-seats/lift-purchase-window";
3679
+ var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
3680
+ var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
3681
+ var PUBLIC = [
3682
+ "id",
3683
+ "appId",
3684
+ "chapterId",
3685
+ "environment",
3686
+ "runtime",
3687
+ "applicationId",
3688
+ "skuId",
3689
+ "status",
3690
+ "amountCents",
3691
+ "currency",
3692
+ "recipientName",
3693
+ "recipientEmailNormalized",
3694
+ "purchaserUserId",
3695
+ "purchasedAt",
3696
+ "acceptedAt",
3697
+ "journeyApprovedAt",
3698
+ "refundedCents",
3699
+ "refundReason",
3700
+ "termEndsAt",
3701
+ "updatedAt"
3702
+ ];
3703
+ var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
3704
+ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3705
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
3706
+ if (url.pathname !== LIST && !one && !lift) return null;
3707
+ const operator = ctx.membershipAuthorityOperator;
3708
+ if (!operator) return json({ error: "not found" }, 404);
3709
+ if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3710
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3711
+ if (!user) return json({ error: "unauthorized" }, 401);
3712
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
3713
+ let db;
3714
+ try {
3715
+ db = operator.makeDb(env);
3716
+ } catch {
3717
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
3718
+ }
3719
+ if (lift) {
3720
+ let body;
3721
+ try {
3722
+ body = await req.json();
3723
+ } catch {
3724
+ return json({ error: "chapterId and runtime are required" }, 400);
3725
+ }
3726
+ const appId = body.appId ?? body.chapterId;
3727
+ if (typeof body.chapterId !== "string" || !SCOPE.test(body.chapterId) || typeof appId !== "string" || !SCOPE.test(appId) || typeof body.runtime !== "string" || !/^[a-z0-9]{2,12}$/.test(body.runtime)) {
3728
+ return json({ error: "chapterId and runtime are required" }, 400);
3729
+ }
3730
+ try {
3731
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
3732
+ } catch (error) {
3733
+ const code = error.code;
3734
+ return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3735
+ }
3736
+ }
3737
+ if (!one) {
3738
+ const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
3739
+ return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
3740
+ }
3741
+ const seat = await membershipLoad(db, "networkSeat", one[1]);
3742
+ if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
3743
+ if (seat.status === "refunded") {
3744
+ return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
3745
+ }
3746
+ if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
3747
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
3748
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
3749
+ try {
3750
+ const receipt = await refundNamedSeatByOperator(db, {
3751
+ appId: String(seat.appId),
3752
+ chapterId: String(seat.chapterId),
3753
+ environment: String(seat.environment),
3754
+ runtime: String(seat.runtime),
3755
+ applicationId: String(seat.applicationId),
3756
+ skuId: String(seat.skuId),
3757
+ seatId: String(seat.id)
3758
+ }, secret, Date.now());
3759
+ return json({ ok: true, ...receipt });
3760
+ } catch (error) {
3761
+ const code = error.code;
3762
+ return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3763
+ }
3764
+ };
3765
+
3413
3766
  // src/worker-routes-membership-cancel.ts
3414
3767
  var handleMembershipCancellation = async (req, url, env, ctx) => {
3415
3768
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4006,6 +4359,29 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
4006
4359
  }
4007
4360
  }
4008
4361
 
4362
+ // src/join-founding-preview.ts
4363
+ async function foundingPreviews(tiers, env, ctx) {
4364
+ const result = /* @__PURE__ */ new Map();
4365
+ const preview = ctx.membershipAuthority?.previewOffer;
4366
+ if (!preview) return result;
4367
+ const scope2 = { appId: env.ODLA_APP_ID, chapterId: ctx.chapter.id, environment: env.ODLA_ENV, runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME) };
4368
+ await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
4369
+ try {
4370
+ const offer = await preview({ ...scope2, skuId: tier.id }, env);
4371
+ if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
4372
+ result.set(tier.id, {
4373
+ cohortLabel: offer.cohortLabel,
4374
+ dueTodayCents: offer.dueTodayCents,
4375
+ discountCents: offer.basePriceCents - offer.dueTodayCents,
4376
+ renewalCents: offer.renewalCents,
4377
+ ...offer.discountDuration ? { discountDuration: offer.discountDuration } : {}
4378
+ });
4379
+ } catch {
4380
+ }
4381
+ }));
4382
+ return result;
4383
+ }
4384
+
4009
4385
  // src/admission-grants-contract.ts
4010
4386
  var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
4011
4387
  var ADMISSION_GRANTS_PATH = "/api/admission-grants";
@@ -4532,10 +4908,24 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
4532
4908
  }
4533
4909
 
4534
4910
  // src/payment-quote.ts
4911
+ function tierPresentationFor(document, tierId) {
4912
+ const compareAt = document?.membershipPage?.tiers?.find((tier) => tier.tierId === tierId)?.compareAtPriceCents;
4913
+ return Number.isSafeInteger(compareAt) && Number(compareAt) > 0 ? { compareAtPriceCents: Number(compareAt) } : void 0;
4914
+ }
4915
+ function foundingOf(offer) {
4916
+ return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
4917
+ }
4535
4918
  async function createPaymentQuote(input) {
4536
4919
  const { group, tier, policy } = input;
4537
4920
  const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
4921
+ const compareAt = input.presentation?.compareAtPriceCents;
4922
+ const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
4538
4923
  const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
4924
+ const lines = dueToday === void 0 ? paymentLineItems(group, tier) : {
4925
+ standardCents: tier.priceCents,
4926
+ discountCents: tier.priceCents - dueToday,
4927
+ dueTodayCents: dueToday
4928
+ };
4539
4929
  const quote = {
4540
4930
  applicationId: input.applicationId,
4541
4931
  chapterId: input.chapterId,
@@ -4543,11 +4933,12 @@ async function createPaymentQuote(input) {
4543
4933
  tierName: tier.name,
4544
4934
  currency: tier.currency ?? String(group.currency ?? "usd"),
4545
4935
  interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
4546
- lineItems: dueToday === void 0 ? paymentLineItems(group, tier) : {
4547
- standardCents: tier.priceCents,
4548
- discountCents: tier.priceCents - dueToday,
4549
- dueTodayCents: dueToday
4550
- },
4936
+ lineItems: presentation ? {
4937
+ standardCents: presentation.compareAtPriceCents,
4938
+ discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
4939
+ dueTodayCents: lines.dueTodayCents
4940
+ } : lines,
4941
+ ...presentation ? { presentation } : {},
4551
4942
  refundPolicyText: policy?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
4552
4943
  merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
4553
4944
  trustCopy: policy ? policy.trustCopy ?? "" : String(group.trustCopy ?? ""),
@@ -4572,37 +4963,39 @@ async function createPaymentQuote(input) {
4572
4963
  });
4573
4964
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(contract));
4574
4965
  const digest = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4575
- return { ...quote, digest };
4966
+ return { ...quote, digest, ...input.founding ? { founding: input.founding } : {} };
4576
4967
  }
4577
4968
 
4578
4969
  // src/membership-quote-projection.ts
4579
4970
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
4580
4971
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
4581
- var fail3 = (code) => {
4972
+ var fail4 = (code) => {
4582
4973
  throw Object.assign(new Error(code), { code });
4583
4974
  };
4584
4975
  var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
4585
4976
  async function restore2(row2, target, now, allowExpired = false) {
4586
- if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail3("membership_quote_unavailable");
4587
- if (row2.status !== "ready") return fail3("membership_quote_pending");
4977
+ if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail4("membership_quote_unavailable");
4978
+ if (row2.status !== "ready") return fail4("membership_quote_pending");
4588
4979
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
4589
4980
  ...target,
4590
4981
  requestId: row2.requestId
4591
4982
  }), now, lifetimeMs: 6e4 });
4592
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail3("membership_quote_expired");
4983
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
4593
4984
  const tier = row2.tier, quote = row2.quote;
4594
4985
  const offering = offer.contract.offering;
4595
4986
  if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
4596
- return fail3("membership_catalog_mismatch");
4987
+ return fail4("membership_catalog_mismatch");
4597
4988
  }
4598
4989
  const expected = await createPaymentQuote({
4599
4990
  ...target,
4600
4991
  tier,
4601
4992
  group: { merchantDisclosureText: quote.merchantDisclosureText },
4602
4993
  policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
4603
- billingTerms: offer.contract.billingTerms
4994
+ billingTerms: offer.contract.billingTerms,
4995
+ presentation: quote.presentation,
4996
+ founding: foundingOf(offer)
4604
4997
  });
4605
- if (quote.digest !== expected.digest) return fail3("membership_quote_conflict");
4998
+ if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
4606
4999
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
4607
5000
  }
4608
5001
  async function readMembershipQuote(db, target, now) {
@@ -4610,9 +5003,9 @@ async function readMembershipQuote(db, target, now) {
4610
5003
  }
4611
5004
  async function prepareMembershipQuote(db, input, issue) {
4612
5005
  const { applicationId: id6, now, app } = input;
4613
- if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail3("membership_quote_unavailable");
5006
+ if (app.id !== id6 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail4("membership_quote_unavailable");
4614
5007
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
4615
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail3("membership_quote_conflict");
5008
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
4616
5009
  if (row2?.status === "ready") {
4617
5010
  try {
4618
5011
  return await restore2(row2, input, now);
@@ -4657,7 +5050,7 @@ async function prepareMembershipQuote(db, input, issue) {
4657
5050
  }
4658
5051
  if (row2?.status === "ready") return restore2(row2, input, now);
4659
5052
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
4660
- return fail3("membership_quote_pending");
5053
+ return fail4("membership_quote_pending");
4661
5054
  }
4662
5055
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
4663
5056
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -4671,7 +5064,15 @@ async function prepareMembershipQuote(db, input, issue) {
4671
5064
  currency: input.tier.currency ?? String(input.group.currency ?? "usd"),
4672
5065
  interval: input.tier.interval ?? (input.group.interval === "month" ? "month" : "year")
4673
5066
  };
4674
- const quote = await createPaymentQuote({ ...target, tier, group: input.group, policy: input.policy, billingTerms: offer.contract.billingTerms });
5067
+ const quote = await createPaymentQuote({
5068
+ ...target,
5069
+ tier,
5070
+ group: input.group,
5071
+ policy: input.policy,
5072
+ billingTerms: offer.contract.billingTerms,
5073
+ presentation: input.presentation,
5074
+ founding: foundingOf(offer)
5075
+ });
4675
5076
  const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
4676
5077
  await restore2(ready, target, now, true);
4677
5078
  try {
@@ -4726,7 +5127,9 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4726
5127
  group: { merchantDisclosureText: quote2.merchantDisclosureText },
4727
5128
  tier: tier2,
4728
5129
  policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
4729
- billingTerms
5130
+ billingTerms,
5131
+ presentation: quote2.presentation,
5132
+ founding: foundingOf(intent.membershipOffer)
4730
5133
  });
4731
5134
  if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
4732
5135
  return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
@@ -4753,9 +5156,10 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4753
5156
  if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
4754
5157
  const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4755
5158
  const policy = control3?.document.policy?.tierPolicies?.[tier.id];
5159
+ const presentation = tierPresentationFor(control3?.document, tier.id);
4756
5160
  if (ctx.membershipAuthority) return prepareMembershipQuote(
4757
5161
  db,
4758
- { ...target, app, group, tier, policy, now: Date.now() },
5162
+ { ...target, app, group, tier, policy, presentation, now: Date.now() },
4759
5163
  (input) => ctx.membershipAuthority.issueOffer(input, env)
4760
5164
  );
4761
5165
  const quote = await createPaymentQuote({
@@ -4766,7 +5170,8 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4766
5170
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4767
5171
  group,
4768
5172
  tier,
4769
- policy
5173
+ policy,
5174
+ presentation
4770
5175
  });
4771
5176
  return { tier, quote, billingTerms: void 0 };
4772
5177
  }
@@ -5420,10 +5825,11 @@ var handleMember = async (req, url, env, ctx) => {
5420
5825
  group,
5421
5826
  Boolean(stripeKey)
5422
5827
  );
5828
+ const founding = await foundingPreviews(offered, env, ctx);
5423
5829
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
5424
5830
  const application = controlledApplication(chapter.application, activeControl?.document);
5425
5831
  return json({
5426
- ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
5832
+ ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
5427
5833
  fieldContract: chapterFlowFieldContract(chapter, "join", application),
5428
5834
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
5429
5835
  ...activeControl ? { signupControl: {
@@ -7116,7 +7522,7 @@ async function deliverAdmissionGrants(options) {
7116
7522
  }
7117
7523
 
7118
7524
  // src/worker-routes-network-admission.ts
7119
- var PATH = "/api/admin/network/admissions";
7525
+ var PATH2 = "/api/admin/network/admissions";
7120
7526
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7121
7527
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7122
7528
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -7196,11 +7602,11 @@ async function parseBody(req) {
7196
7602
  }
7197
7603
  }
7198
7604
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
7199
- if (![PATH, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7605
+ if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7200
7606
  const auth = await gate2(req, env, ctx);
7201
7607
  if ("response" in auth) return auth.response;
7202
7608
  const db = auth.db;
7203
- if (url.pathname === PATH && req.method === "GET") {
7609
+ if (url.pathname === PATH2 && req.method === "GET") {
7204
7610
  const targetId2 = url.searchParams.get("targetId") ?? "";
7205
7611
  const target2 = targetFor(ctx, targetId2);
7206
7612
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -7444,28 +7850,28 @@ var handleSchedule = async (req, url, env, ctx) => {
7444
7850
  };
7445
7851
 
7446
7852
  // src/payments-stripe.ts
7447
- import { stripeRequest as stripeRequest7, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7853
+ import { stripeRequest as stripeRequest8, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
7448
7854
 
7449
7855
  // src/payments-refund.ts
7450
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
7856
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
7451
7857
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
7452
7858
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
7453
7859
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
7454
- function fail4(code = "refund_target_mismatch") {
7860
+ function fail5(code = "refund_target_mismatch") {
7455
7861
  throw Object.assign(new Error(code), { code });
7456
7862
  }
7457
7863
  async function refundSubscriptionPayment(secretKey, input) {
7458
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail4();
7864
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
7459
7865
  const call = async (method, path, params, key3) => {
7460
- const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7461
- if (!result.ok) fail4("stripe_error");
7462
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail4("stripe_invalid_response");
7866
+ const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7867
+ if (!result.ok) fail5("stripe_error");
7868
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
7463
7869
  return result.body;
7464
7870
  };
7465
7871
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
7466
- if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail4();
7872
+ if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
7467
7873
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
7468
- if (row(sub.metadata)[key3] !== value) fail4();
7874
+ if (row(sub.metadata)[key3] !== value) fail5();
7469
7875
  }
7470
7876
  let cursor;
7471
7877
  let earliest;
@@ -7478,69 +7884,69 @@ async function refundSubscriptionPayment(secretKey, input) {
7478
7884
  limit: 100,
7479
7885
  ...cursor ? { starting_after: cursor } : {}
7480
7886
  });
7481
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail4("stripe_invalid_response");
7887
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
7482
7888
  for (const value of list.data) {
7483
7889
  const invoice2 = row(value);
7484
7890
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
7485
- if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail4();
7891
+ if (!providerId2(id2(invoice2), "in") || seen.has(id2(invoice2)) || id2(subscription) !== input.subscriptionId || id2(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail5();
7486
7892
  seen.add(id2(invoice2));
7487
7893
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
7488
7894
  }
7489
7895
  if (!list.has_more) break;
7490
- if (!list.data.length || page === 9) fail4("refund_history_incomplete");
7896
+ if (!list.data.length || page === 9) fail5("refund_history_incomplete");
7491
7897
  cursor = id2(list.data[list.data.length - 1]);
7492
7898
  }
7493
- if (!earliest) fail4("no_charge");
7899
+ if (!earliest) fail5("no_charge");
7494
7900
  const invoice = earliest;
7495
- if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail4();
7901
+ if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
7496
7902
  let chargeId = id2(invoice.charge);
7497
7903
  let paymentIntentId = id2(invoice.payment_intent);
7498
7904
  if (!chargeId && !paymentIntentId) {
7499
7905
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
7500
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail4("refund_payment_ambiguous");
7906
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
7501
7907
  const payment = row(payments.data[0]);
7502
- if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail4();
7908
+ if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
7503
7909
  paymentIntentId = id2(row(payment.payment).payment_intent);
7504
7910
  }
7505
7911
  if (!chargeId) {
7506
- if (!providerId2(paymentIntentId, "pi")) fail4();
7912
+ if (!providerId2(paymentIntentId, "pi")) fail5();
7507
7913
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
7508
- if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail4();
7914
+ if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
7509
7915
  chargeId = id2(payment.latest_charge);
7510
7916
  }
7511
- if (!providerId2(chargeId, "ch")) fail4();
7917
+ if (!providerId2(chargeId, "ch")) fail5();
7512
7918
  const charge = await call("GET", `/v1/charges/${chargeId}`);
7513
- if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail4();
7514
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail4();
7919
+ if (id2(charge) !== chargeId || id2(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id2(charge.invoice) !== id2(invoice) || paymentIntentId && id2(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail5();
7920
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
7515
7921
  if (charge.amount_refunded !== charge.amount) {
7516
7922
  const refund = await call("POST", "/v1/refunds", {
7517
7923
  charge: chargeId,
7518
7924
  ...input.metadata ? { metadata: input.metadata } : {}
7519
7925
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
7520
- if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail4("refund_not_confirmed");
7926
+ if (id2(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail5("refund_not_confirmed");
7521
7927
  }
7522
7928
  let subscriptionCanceled = sub.status === "canceled";
7523
7929
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
7524
7930
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
7525
- if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail4("refund_cancel_incomplete");
7931
+ if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
7526
7932
  subscriptionCanceled = true;
7527
7933
  }
7528
7934
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
7529
- if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail4("refund_not_confirmed");
7935
+ if (id2(confirmed) !== chargeId || id2(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail5("refund_not_confirmed");
7530
7936
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
7531
7937
  }
7532
7938
 
7533
7939
  // src/subscription-payment-identity.ts
7534
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
7940
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
7535
7941
 
7536
7942
  // src/subscription-provider-contract.ts
7537
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
7943
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
7538
7944
  function subscriptionContractMismatch() {
7539
7945
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
7540
7946
  }
7541
7947
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
7542
7948
  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();
7543
- const result = await stripeRequest5({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
7949
+ const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
7544
7950
  const price = result.body;
7545
7951
  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();
7546
7952
  return typeof price.product === "string" ? price.product : void 0;
@@ -7559,7 +7965,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7559
7965
  let paymentId = id3(invoice.payment_intent);
7560
7966
  if (!paymentId) {
7561
7967
  if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
7562
- const result2 = await stripeRequest6({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
7968
+ const result2 = await stripeRequest7({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
7563
7969
  const list = result2.body;
7564
7970
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
7565
7971
  const link = list.data[0];
@@ -7568,11 +7974,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7568
7974
  }
7569
7975
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
7570
7976
  const path = `/v1/payment_intents/${paymentId}`;
7571
- const result = await stripeRequest6({ secretKey }, "GET", path);
7977
+ const result = await stripeRequest7({ secretKey }, "GET", path);
7572
7978
  const payment = result.body;
7573
7979
  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();
7574
7980
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
7575
- const tagged = await stripeRequest6(
7981
+ const tagged = await stripeRequest7(
7576
7982
  { secretKey },
7577
7983
  "POST",
7578
7984
  path,
@@ -7584,7 +7990,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7584
7990
 
7585
7991
  // src/payments-stripe.ts
7586
7992
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7587
- return stripeRequest7(
7993
+ return stripeRequest8(
7588
7994
  { secretKey: sk },
7589
7995
  method,
7590
7996
  path,
@@ -7592,7 +7998,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
7592
7998
  idempotencyKey ? { idempotencyKey } : {}
7593
7999
  );
7594
8000
  }
7595
- function fail5(op, r) {
8001
+ function fail6(op, r) {
7596
8002
  const err = new Error(`stripe ${op} failed: ${r.status}`);
7597
8003
  err.code = "stripe_error";
7598
8004
  throw err;
@@ -7642,7 +8048,7 @@ function createStripeProvider(config) {
7642
8048
  { email: input.email, name: input.name, metadata: meta },
7643
8049
  `cus:${input.applicationId}`
7644
8050
  );
7645
- if (!cust.ok) fail5("customer create", cust);
8051
+ if (!cust.ok) fail6("customer create", cust);
7646
8052
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
7647
8053
  customerId = cust.body.id;
7648
8054
  }
@@ -7674,7 +8080,7 @@ function createStripeProvider(config) {
7674
8080
  // retry between create and the db write can't orphan a second one.
7675
8081
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
7676
8082
  );
7677
- if (!sub.ok) fail5("subscription create", sub);
8083
+ if (!sub.ok) fail6("subscription create", sub);
7678
8084
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
7679
8085
  const subscriptionId = sub.body.id;
7680
8086
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -7684,14 +8090,14 @@ function createStripeProvider(config) {
7684
8090
  "expand[2]": "discounts",
7685
8091
  "expand[3]": "latest_invoice.discounts"
7686
8092
  });
7687
- if (!expanded.ok) fail5("subscription invoice evidence", expanded);
8093
+ if (!expanded.ok) fail6("subscription invoice evidence", expanded);
7688
8094
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
7689
8095
  sub = expanded;
7690
8096
  }
7691
8097
  verifySubscriptionResult(sub.body, input, customerId, meta);
7692
8098
  const invoice = sub.body.latest_invoice;
7693
8099
  const clientSecret2 = clientSecretOf(sub.body);
7694
- if (!clientSecret2) fail5("subscription confirmation-secret missing", sub);
8100
+ if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
7695
8101
  await tagSubscriptionPayment(
7696
8102
  sk,
7697
8103
  invoice,
@@ -8363,7 +8769,7 @@ async function namedSeatOfferDigest(offer) {
8363
8769
  "named-only",
8364
8770
  "full-upfront",
8365
8771
  "auto-renew-coterm",
8366
- "30-day-unaccepted-full-refund"
8772
+ "no-automatic-refund"
8367
8773
  ])));
8368
8774
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
8369
8775
  }
@@ -8375,15 +8781,16 @@ async function readNamedSeatOffer(db, input, now) {
8375
8781
  const offering = membershipOfferingVersion(member.offeringSnapshot);
8376
8782
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
8377
8783
  if (seat) requireMembershipScope(seat, input);
8784
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
8378
8785
  const offer = {
8379
8786
  membershipId: id6,
8380
8787
  amountCents: offering.seatPriceCents,
8381
8788
  currency: offering.currency,
8382
8789
  interval: offering.interval,
8383
8790
  termEndsAt: Number(member.termEndsAt),
8384
- purchaseEndsAt: Number(member.seatPurchaseEndsAt),
8791
+ purchaseEndsAt,
8385
8792
  autoRenew: member.autoRenew === true,
8386
- eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= Number(member.seatPurchaseEndsAt) && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
8793
+ eligible: ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && member.refundedCents === 0 && now >= Number(member.initialTermStartedAt) && now < Number(member.termEndsAt) && now <= purchaseEndsAt && (!seat || ["payment_pending", "payment_processing"].includes(String(seat.status))),
8387
8794
  ...seat ? { seat: {
8388
8795
  seatId: String(seat.id),
8389
8796
  status: String(seat.status),
@@ -8391,7 +8798,7 @@ async function readNamedSeatOffer(db, input, now) {
8391
8798
  recipientEmail: String(seat.recipientEmailNormalized)
8392
8799
  } } : {}
8393
8800
  };
8394
- return { ...offer, digest: await namedSeatOfferDigest(offer) };
8801
+ return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
8395
8802
  }
8396
8803
 
8397
8804
  // src/named-seat-request.ts
@@ -8768,6 +9175,23 @@ async function updateChapterPerson(opts) {
8768
9175
  });
8769
9176
  }
8770
9177
 
9178
+ // src/application-actions.ts
9179
+ function isApplicationStage(ctx, stage) {
9180
+ return ctx.chapter.pipeline.stages.includes(stage);
9181
+ }
9182
+ function applicationActionStages(app, env, ctx) {
9183
+ if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
9184
+ const from = String(app.status ?? "");
9185
+ if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
9186
+ return ctx.chapter.pipeline.stages.filter((to) => {
9187
+ if (to === from) return false;
9188
+ if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
9189
+ if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
9190
+ if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
9191
+ return canTransition(from, to, ctx.chapter.pipeline);
9192
+ });
9193
+ }
9194
+
8771
9195
  // src/worker-routes-people-profile.ts
8772
9196
  var handlePeopleProfile = async (req, url, env, ctx) => {
8773
9197
  if (ctx.chapter.mode !== "chapter") return null;
@@ -8789,7 +9213,16 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
8789
9213
  const record4 = await getRecord3({ crm: ctx.chapter.crm, db }, id6);
8790
9214
  if (!record4) return json({ error: "not found" }, 404);
8791
9215
  if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
8792
- if (match?.[2]) return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
9216
+ if (match?.[2]) {
9217
+ const linked = typeof record4.fields.applicationId === "string" && record4.fields.applicationId !== "";
9218
+ let to = "";
9219
+ try {
9220
+ to = String((await req.clone().json()).to ?? "");
9221
+ } catch {
9222
+ }
9223
+ if (!linked && to && !isApplicationStage(ctx, to)) return null;
9224
+ return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
9225
+ }
8793
9226
  if (req.method === "GET" && dedicated) {
8794
9227
  const excluded = profileReadOnlyFields(record4, ctx);
8795
9228
  const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id7]) => !excluded.includes(id7)).map(([id7, field]) => ({
@@ -9480,20 +9913,6 @@ var handleAdminBilling = async (req, url, env, ctx) => {
9480
9913
  // src/worker-routes-admin-lifecycle.ts
9481
9914
  import { computeBookableSlots as computeBookableSlots2, initCalendar as initCalendar3 } from "@odla-ai/calendar";
9482
9915
 
9483
- // src/application-actions.ts
9484
- function applicationActionStages(app, env, ctx) {
9485
- if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
9486
- const from = String(app.status ?? "");
9487
- if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
9488
- return ctx.chapter.pipeline.stages.filter((to) => {
9489
- if (to === from) return false;
9490
- if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
9491
- if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
9492
- if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
9493
- return canTransition(from, to, ctx.chapter.pipeline);
9494
- });
9495
- }
9496
-
9497
9916
  // src/refund-application.ts
9498
9917
  async function refundApplication(db, env, ctx, app) {
9499
9918
  if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
@@ -9684,6 +10103,12 @@ var handleAdminRefund = async (req, url, env, ctx) => {
9684
10103
  const app = await loadApp(db, m[1]);
9685
10104
  if (!app) return json({ error: "not found" }, 404);
9686
10105
  if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
10106
+ if (app.namedSeatId) {
10107
+ const actor = await ctx.verifyUser(req, env);
10108
+ if (!actor || !await ctx.isSuperAdminEmail(db, actor.email, actor.userId)) {
10109
+ return json({ error: "super-admin required", code: "super_admin_required" }, 403);
10110
+ }
10111
+ }
9687
10112
  return refundApplication(db, env, ctx, app);
9688
10113
  };
9689
10114
  var handleAdminApplicationPatch = async (req, url, env, ctx) => {
@@ -10298,7 +10723,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
10298
10723
  const appId = record4.fields.applicationId;
10299
10724
  const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
10300
10725
  if (reader.stageTransitions.includes(record4.type)) {
10301
- result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : [];
10726
+ result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : result.edit.allowedStages.filter((stage) => !isApplicationStage(ctx, stage.id));
10302
10727
  const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
10303
10728
  const meta = lock?.meta;
10304
10729
  if (meta?.state === "pending" && typeof meta.activityId === "string") {
@@ -10516,7 +10941,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
10516
10941
  }
10517
10942
  };
10518
10943
  try {
10519
- if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person") {
10944
+ if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person" && (record4.fields.applicationId || isApplicationStage(ctx, input.stage))) {
10520
10945
  const updated2 = await networkApplicationAction({
10521
10946
  db,
10522
10947
  env,
@@ -11876,7 +12301,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
11876
12301
  }
11877
12302
 
11878
12303
  // src/worker-routes-network-person-registry.ts
11879
- var PATH2 = "/api/admin/network-person-registry";
12304
+ var PATH3 = "/api/admin/network-person-registry";
11880
12305
  var MAX_BODY2 = 8 * 1024 * 1024;
11881
12306
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
11882
12307
  var positiveRevision = (value) => {
@@ -11901,7 +12326,7 @@ function registryError(error) {
11901
12326
  return null;
11902
12327
  }
11903
12328
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
11904
- if (url.pathname !== PATH2) return null;
12329
+ if (url.pathname !== PATH3) return null;
11905
12330
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
11906
12331
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
11907
12332
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -13097,7 +13522,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
13097
13522
  };
13098
13523
 
13099
13524
  // src/membership-cancel-authority.ts
13100
- import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
13525
+ import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
13101
13526
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
13102
13527
  membershipClock(now);
13103
13528
  const id6 = await networkMembershipId(target);
@@ -13108,14 +13533,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
13108
13533
  membershipFail("membership_cancellation_unavailable");
13109
13534
  }
13110
13535
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
13111
- const read2 = await stripeRequest8({ secretKey: stripeSecret }, "GET", path);
13536
+ const read2 = await stripeRequest9({ secretKey: stripeSecret }, "GET", path);
13112
13537
  const sub = read2.body;
13113
13538
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
13114
13539
  ...stripePartitionMetadata(target),
13115
13540
  applicationId: target.applicationId
13116
13541
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
13117
13542
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
13118
- const write = await stripeRequest8(
13543
+ const write = await stripeRequest9(
13119
13544
  { secretKey: stripeSecret },
13120
13545
  "POST",
13121
13546
  path,
@@ -13337,7 +13762,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
13337
13762
  }
13338
13763
 
13339
13764
  // src/membership-provider-lifecycle.ts
13340
- import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
13765
+ import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
13341
13766
 
13342
13767
  // src/membership-renewal-authority.ts
13343
13768
  async function reconcileMembershipRenewal(db, input, now) {
@@ -13390,7 +13815,11 @@ async function reconcileMembershipRenewal(db, input, now) {
13390
13815
  membershipUpdate("networkMembership", id6, {
13391
13816
  status: status2,
13392
13817
  graceEndsAt,
13393
- ...paid ? { termStartedAt: start, termEndsAt: end } : {},
13818
+ ...paid ? {
13819
+ termStartedAt: start,
13820
+ termEndsAt: end,
13821
+ ...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
13822
+ } : {},
13394
13823
  revision: Number(member.revision) + 1,
13395
13824
  updatedAt: now
13396
13825
  }),
@@ -13495,7 +13924,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
13495
13924
  }
13496
13925
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
13497
13926
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
13498
- const result = await stripeRequest9({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
13927
+ const result = await stripeRequest10({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
13499
13928
  const sub = result.body;
13500
13929
  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)) {
13501
13930
  membershipFail("membership_billing_provider_mismatch");
@@ -13550,10 +13979,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
13550
13979
  const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
13551
13980
  let seat = await membershipLoad(db, "networkSeat", id6);
13552
13981
  if (!seat) {
13553
- if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > Number(member.seatPurchaseEndsAt) || !Number.isSafeInteger(member.seatPurchaseEndsAt) || member.namedSeatId !== void 0) {
13982
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
13983
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
13984
+ if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
13554
13985
  membershipFail("named_seat_purchase_unavailable");
13555
13986
  }
13556
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
13557
13987
  const attrs = {
13558
13988
  id: id6,
13559
13989
  ...membershipScope(input),
@@ -13572,7 +14002,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13572
14002
  status: "payment_pending",
13573
14003
  preparedAt: now,
13574
14004
  termEndsAt: member.termEndsAt,
13575
- seatPurchaseEndsAt: member.seatPurchaseEndsAt,
14005
+ seatPurchaseEndsAt: purchaseEndsAt,
13576
14006
  offeringSnapshot: offering,
13577
14007
  revision: 1,
13578
14008
  createdAt: now,
@@ -13610,7 +14040,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13610
14040
  }
13611
14041
 
13612
14042
  // src/membership-seat-stripe.ts
13613
- import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
14043
+ import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
13614
14044
  var reject7 = () => {
13615
14045
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
13616
14046
  };
@@ -13622,7 +14052,7 @@ var metadataFor = (seat) => ({
13622
14052
  });
13623
14053
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
13624
14054
  async function read(sk, path, params) {
13625
- const response = await stripeRequest10({ secretKey: sk }, "GET", path, params);
14055
+ const response = await stripeRequest11({ secretKey: sk }, "GET", path, params);
13626
14056
  if (!response.ok) return reject7();
13627
14057
  return response.body;
13628
14058
  }
@@ -13659,11 +14089,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
13659
14089
  }
13660
14090
  await subscriptionForSeat(sk, seat);
13661
14091
  const metadata = metadataFor(seat);
13662
- const result = seat.providerPaymentIntentId ? await stripeRequest10(
14092
+ const result = seat.providerPaymentIntentId ? await stripeRequest11(
13663
14093
  { secretKey: sk },
13664
14094
  "GET",
13665
14095
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
13666
- ) : await stripeRequest10({ secretKey: sk }, "POST", "/v1/payment_intents", {
14096
+ ) : await stripeRequest11({ secretKey: sk }, "POST", "/v1/payment_intents", {
13667
14097
  amount: seat.amountCents,
13668
14098
  currency: seat.currency,
13669
14099
  customer: seat.providerCustomerId,
@@ -13681,7 +14111,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
13681
14111
  const sub = await subscriptionForSeat(sk, seat);
13682
14112
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
13683
14113
  if (existing) return String(existing.id);
13684
- const result = await stripeRequest10({ secretKey: sk }, "POST", "/v1/subscription_items", {
14114
+ const result = await stripeRequest11({ secretKey: sk }, "POST", "/v1/subscription_items", {
13685
14115
  subscription: seat.providerSubscriptionId,
13686
14116
  price: seat.priceId,
13687
14117
  quantity: 1,
@@ -13774,14 +14204,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
13774
14204
  }
13775
14205
  if (seat.purchasedAt === void 0) {
13776
14206
  if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
13777
- const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= Number(member.seatPurchaseEndsAt) && now < Number(member.termEndsAt);
14207
+ const eligible = seat.status !== "payment_canceling" && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) && Number(member.refundedCents) === 0 && paidAt <= seatPurchaseDeadline(membershipOfferingVersion(member.offeringSnapshot), member) && now < Number(member.termEndsAt);
13778
14208
  const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
13779
14209
  try {
13780
14210
  await db.transact([
13781
14211
  membershipUpdate("networkSeat", input.seatId, {
13782
14212
  status: status2,
13783
14213
  purchasedAt: paidAt,
13784
- acceptanceEndsAt: Math.min(paidAt + 30 * 864e5, Number(member.termEndsAt)),
14214
+ acceptanceEndsAt: Number(member.termEndsAt),
13785
14215
  termStartedAt: paidAt,
13786
14216
  termEndsAt: member.termEndsAt,
13787
14217
  paymentEventId: input.eventId,
@@ -13829,7 +14259,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
13829
14259
  if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
13830
14260
  const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
13831
14261
  requireMembershipScope(member, target);
13832
- if (now >= Number(seat.acceptanceEndsAt) || now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
14262
+ if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
13833
14263
  const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
13834
14264
  const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
13835
14265
  try {
@@ -13904,7 +14334,7 @@ async function recordRecipient(db, input, now, approve) {
13904
14334
  requireMembershipScope(entitlement, input);
13905
14335
  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");
13906
14336
  if (seat.acceptedAt === void 0) {
13907
- if (approve || now >= Number(seat.acceptanceEndsAt) || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
14337
+ if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
13908
14338
  membershipFail("named_seat_recipient_verification_required");
13909
14339
  }
13910
14340
  } else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
@@ -13954,129 +14384,6 @@ async function recordRecipient(db, input, now, approve) {
13954
14384
  return membershipFail("named_seat_recipient_retry_required");
13955
14385
  }
13956
14386
 
13957
- // src/membership-seat-refund-stripe.ts
13958
- import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
13959
- var fail6 = (code = "named_seat_refund_mismatch") => {
13960
- throw Object.assign(new Error(code), { code });
13961
- };
13962
- async function refundNamedSeatStripePayment(secretKey, seat) {
13963
- if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail6();
13964
- const call = async (method, path, params, idempotencyKey) => {
13965
- const result = await stripeRequest11({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
13966
- if (!result.ok || !result.body) return fail6("named_seat_refund_provider_unavailable");
13967
- return result.body;
13968
- };
13969
- const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
13970
- const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
13971
- if (payment.id !== seat.providerPaymentIntentId || payment.customer !== seat.providerCustomerId || payment.amount !== seat.amountCents || payment.amount_received !== seat.amountCents || payment.currency !== seat.currency || payment.livemode !== (seat.environment === "prod") || payment.status !== "succeeded" || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) fail6();
13972
- const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
13973
- if (sub.id !== seat.providerSubscriptionId || sub.customer !== seat.providerCustomerId || sub.livemode !== (seat.environment === "prod") || !Object.entries({ ...stripePartitionMetadata(seat), applicationId: seat.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(sub.items?.data)) fail6();
13974
- const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
13975
- if (items.length > 1) fail6();
13976
- if (items.length && sub.status !== "canceled") {
13977
- const item = items[0];
13978
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail6();
13979
- const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
13980
- if (removed.id !== item.id || removed.deleted !== true) fail6("named_seat_renewal_removal_unconfirmed");
13981
- }
13982
- const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
13983
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail6("named_seat_refund_history_incomplete");
13984
- let refundedCents = 0, pendingCents = 0;
13985
- const refundIds = [];
13986
- for (const refund of list.data) {
13987
- if (!/^re_[A-Za-z0-9_]+$/.test(refund.id) || refund.payment_intent !== seat.providerPaymentIntentId || refund.currency !== seat.currency || !Number.isSafeInteger(refund.amount) || refund.amount <= 0 || !["succeeded", "pending", "requires_action", "failed", "canceled"].includes(refund.status)) fail6();
13988
- if (refund.status === "succeeded") {
13989
- refundedCents += refund.amount;
13990
- refundIds.push(refund.id);
13991
- } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
13992
- }
13993
- if (refundedCents + pendingCents > seat.amountCents) fail6();
13994
- if (pendingCents) fail6("named_seat_refund_pending");
13995
- if (refundedCents < seat.amountCents) {
13996
- const remaining = seat.amountCents - refundedCents;
13997
- const refund = await call("POST", "/v1/refunds", {
13998
- payment_intent: seat.providerPaymentIntentId,
13999
- amount: remaining,
14000
- reason: "requested_by_customer",
14001
- metadata
14002
- }, `named-seat-refund:${seat.id}:${remaining}`);
14003
- if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail6();
14004
- if (refund.status !== "succeeded") fail6("named_seat_refund_pending");
14005
- refundedCents += remaining;
14006
- refundIds.push(refund.id);
14007
- }
14008
- return { refundedCents, refundIds };
14009
- }
14010
-
14011
- // src/membership-seat-refund.ts
14012
- async function requestNamedSeatRefund(db, input, now) {
14013
- membershipClock(now);
14014
- const state = await loadMembershipAuthorityState(db, input);
14015
- const seat = await membershipLoad(db, "networkSeat", input.seatId);
14016
- requireMembershipScope(seat, input);
14017
- const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
14018
- requireMembershipScope(member, input);
14019
- if (seat.status === "refunded" || seat.status === "refund_pending") return;
14020
- if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "expired" && (seat.acceptedAt !== void 0 || !Number.isSafeInteger(seat.acceptanceEndsAt) || now < Number(seat.acceptanceEndsAt)) || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
14021
- const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
14022
- await db.transact([
14023
- membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
14024
- ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
14025
- membershipUpdate("membershipEffect", `${seat.id}:refund`, {
14026
- id: `${seat.id}:refund`,
14027
- chapterId: input.chapterId,
14028
- environment: input.environment,
14029
- runtime: input.runtime,
14030
- membershipId: member.id,
14031
- seatId: seat.id,
14032
- kind: "refund_seat",
14033
- status: "pending",
14034
- attempts: 0,
14035
- createdAt: now,
14036
- updatedAt: now
14037
- }),
14038
- ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
14039
- id: `${seat.id}:revoke`,
14040
- chapterId: input.chapterId,
14041
- environment: input.environment,
14042
- runtime: input.runtime,
14043
- membershipId: member.id,
14044
- seatId: seat.id,
14045
- kind: "revoke_named_recipient_access",
14046
- status: "pending",
14047
- attempts: 0,
14048
- createdAt: now,
14049
- updatedAt: now
14050
- })] : []
14051
- ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
14052
- { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
14053
- { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
14054
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
14055
- ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
14056
- ] });
14057
- }
14058
- async function completeNamedSeatRefund(db, seatId, secretKey, now) {
14059
- membershipClock(now);
14060
- const seat = await membershipLoad(db, "networkSeat", seatId);
14061
- if (!seat) return membershipFail("named_seat_not_found");
14062
- await loadMembershipAuthorityState(db, seat);
14063
- if (seat.status === "refunded") return;
14064
- if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
14065
- const result = await refundNamedSeatStripePayment(secretKey, seat);
14066
- await db.transact([
14067
- membershipUpdate("networkSeat", seatId, {
14068
- status: "refunded",
14069
- refundedCents: result.refundedCents,
14070
- providerRefundIds: result.refundIds,
14071
- revision: Number(seat.revision) + 1,
14072
- updatedAt: now
14073
- }),
14074
- membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
14075
- ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
14076
- { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
14077
- ] });
14078
- }
14079
-
14080
14387
  // src/membership-seat-provider-refund.ts
14081
14388
  async function confirmNamedSeatProviderRefund(db, input, now) {
14082
14389
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
@@ -14098,7 +14405,7 @@ async function readNamedSeatInvitation(db, input, now) {
14098
14405
  await loadMembershipAuthorityState(db, target);
14099
14406
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
14100
14407
  requireMembershipScope(member, target);
14101
- if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt) || !seat.acceptedAt && now >= Number(seat.acceptanceEndsAt)) membershipFail("named_seat_invitation_unavailable");
14408
+ if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
14102
14409
  return {
14103
14410
  seatId: input.seatId,
14104
14411
  primaryApplicationId: target.applicationId,
@@ -14128,7 +14435,7 @@ async function readMembershipChapterEffect(db, input, now) {
14128
14435
  requireMembershipScope(member, input);
14129
14436
  if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
14130
14437
  let applicable;
14131
- if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(seat.acceptanceEndsAt) && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
14438
+ if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
14132
14439
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
14133
14440
  else applicable = (await readNamedSeatAccess(db, {
14134
14441
  ...input,
@@ -14157,6 +14464,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
14157
14464
  "seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
14158
14465
  "seat-payments": ["seatId", "eventId"],
14159
14466
  "seat-refunds": ["seatId", "eventId"],
14467
+ "seat-operator-refund": ["seatId"],
14160
14468
  "seat-accept": recipient,
14161
14469
  "seat-approve": recipient,
14162
14470
  "seat-decline": recipient,
@@ -14225,6 +14533,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
14225
14533
  if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
14226
14534
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
14227
14535
  }
14536
+ if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
14228
14537
  if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
14229
14538
  ...target,
14230
14539
  priceId: peer.seatPriceId,
@@ -14313,7 +14622,7 @@ function createMembershipAuthorityHandler(options) {
14313
14622
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
14314
14623
  const sender = req.headers.get(FEDERATION_HEADERS2.sender);
14315
14624
  const action = url.pathname.split("/").at(-1);
14316
- if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
14625
+ if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
14317
14626
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
14318
14627
  if (!peer) return respond({ error: "unauthorized" }, 401);
14319
14628
  let body;
@@ -14335,13 +14644,23 @@ function createMembershipAuthorityHandler(options) {
14335
14644
  let input;
14336
14645
  try {
14337
14646
  input = JSON.parse(verified.body);
14338
- const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
14647
+ 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] ?? []];
14339
14648
  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])))) {
14340
14649
  throw new TypeError("invalid request");
14341
14650
  }
14342
14651
  } catch {
14343
14652
  return respond({ error: "invalid_offer_request" }, 400);
14344
14653
  }
14654
+ if (action === "offer-preview") {
14655
+ return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
14656
+ appId: peer.appId,
14657
+ chapterId: peer.chapterId,
14658
+ environment: peer.environment,
14659
+ runtime: peer.runtime,
14660
+ skuId: String(input.skuId),
14661
+ now: options.now?.() ?? Date.now()
14662
+ }) });
14663
+ }
14345
14664
  if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
14346
14665
  return respond({ ok: true, seat: await handleMembershipSeatAction(
14347
14666
  action,
@@ -14735,7 +15054,7 @@ async function drainMembershipEffects(db, options, now) {
14735
15054
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
14736
15055
  await loadMembershipAuthorityState(db, scope2);
14737
15056
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
14738
- let expired = 0, pending = 0, complete = 0, more = false;
15057
+ let pending = 0, complete = 0, more = false;
14739
15058
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
14740
15059
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
14741
15060
  more ||= seats.length > limit;
@@ -14753,26 +15072,6 @@ async function drainMembershipEffects(db, options, now) {
14753
15072
  }
14754
15073
  }
14755
15074
  }
14756
- for (const status2 of ["renewal_pending", "pending_acceptance"]) {
14757
- const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
14758
- more ||= seats.length > limit;
14759
- for (const seat of seats.slice(0, limit)) {
14760
- if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
14761
- if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
14762
- try {
14763
- await requestNamedSeatRefund(db, {
14764
- ...scope2,
14765
- applicationId: String(seat.applicationId),
14766
- skuId: String(seat.skuId),
14767
- seatId: String(seat.id),
14768
- reason: "expired"
14769
- }, now);
14770
- expired++;
14771
- } catch {
14772
- pending++;
14773
- }
14774
- }
14775
- }
14776
15075
  const effects = (await db.query({ membershipEffect: { $: { where: {
14777
15076
  chapterId: scope2.chapterId,
14778
15077
  environment: scope2.environment,
@@ -14834,7 +15133,7 @@ async function drainMembershipEffects(db, options, now) {
14834
15133
  pending++;
14835
15134
  }
14836
15135
  }
14837
- return { complete, pending, expired, more };
15136
+ return { complete, pending, more };
14838
15137
  }
14839
15138
 
14840
15139
  // src/approval-effects-retry.ts
@@ -14984,7 +15283,7 @@ function createMembershipChapterEffectRoute(options) {
14984
15283
  config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
14985
15284
  enabled: true,
14986
15285
  subject: "{{purchaserName}} invited you to {{chapterName}}",
14987
- text: "Hello {{firstName}},\n\n{{purchaserName}} has purchased a membership for you at {{chapterName}}. Your seat is already paid for.\n\nAccept your invitation by {{acceptBy}}:\n{{joinUrl}}\n\nYou will complete your own application and membership requirements. You will not be asked to pay for this seat."
15286
+ text: "Hello {{firstName}},\n\n{{purchaserName}} has purchased a membership for you at {{chapterName}}. Your seat is already paid for.\n\nAccept your invitation here:\n{{joinUrl}}\n\nYou will complete your own application and membership requirements. You will not be asked to pay for this seat."
14988
15287
  } };
14989
15288
  const template = config.emailTemplates.namedSeatInvitation;
14990
15289
  if (!template.text.includes("{{purchaserName}}")) config.emailTemplates.namedSeatInvitation = {
@@ -15057,6 +15356,12 @@ async function requestMembershipCommercialOffer(options) {
15057
15356
  lifetimeMs: 6e4
15058
15357
  });
15059
15358
  }
15359
+ async function requestMembershipOfferPreview(options) {
15360
+ const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
15361
+ const preview = result.preview;
15362
+ if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
15363
+ return preview;
15364
+ }
15060
15365
  async function requestMembershipCheckoutBinding(options) {
15061
15366
  const result = await requestAuthority(options, "checkouts", {
15062
15367
  applicationId: options.applicationId,
@@ -15095,6 +15400,12 @@ async function requestNamedSeatDecline(options) {
15095
15400
  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");
15096
15401
  return seat;
15097
15402
  }
15403
+ async function requestNamedSeatOperatorRefund(options) {
15404
+ const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
15405
+ const seat = result.seat;
15406
+ 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");
15407
+ return seat;
15408
+ }
15098
15409
  async function requestNamedSeatReservation(options) {
15099
15410
  const result = await requestAuthority(options, "seat-reservations", {
15100
15411
  applicationId: options.applicationId,
@@ -15112,7 +15423,7 @@ async function requestNamedSeatReservation(options) {
15112
15423
  async function requestNamedSeatOffer(options) {
15113
15424
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
15114
15425
  const offer = result.seat;
15115
- if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
15426
+ if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents <= 0 || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
15116
15427
  return offer;
15117
15428
  }
15118
15429
  async function requestNamedSeatInvitation(options) {
@@ -15280,6 +15591,7 @@ function createMembershipWorkerAdapter(options) {
15280
15591
  return {
15281
15592
  authority: {
15282
15593
  issueOffer: async (input, env) => requestMembershipCommercialOffer({ ...input, ...await transport(input, env) }),
15594
+ previewOffer: async (input, env) => requestMembershipOfferPreview({ ...input, ...await transport(input, env) }),
15283
15595
  bindCheckout: async (input, env) => requestMembershipCheckoutBinding({ ...input, ...await transport(input, env) }),
15284
15596
  confirmPayment: async (input, env) => requestMembershipPayment({ ...input, ...await transport(input, env) }),
15285
15597
  approveMembership: async (input, env) => requestMembershipApproval({ ...input, ...await transport(input, env) }),
@@ -15298,7 +15610,8 @@ function createMembershipWorkerAdapter(options) {
15298
15610
  confirmRefund: async (input, env) => requestNamedSeatRefund2({ ...input, ...await transport(input, env) }),
15299
15611
  accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
15300
15612
  approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
15301
- decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) })
15613
+ decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
15614
+ operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
15302
15615
  } } : {}
15303
15616
  },
15304
15617
  readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
@@ -15553,6 +15866,8 @@ var BUILTIN_ROUTES = [
15553
15866
  handleMembershipRestart,
15554
15867
  handleMembershipCancellation,
15555
15868
  handlePaymentRecovery,
15869
+ handleAdminNamedSeat,
15870
+ handleAdminMembershipSeats,
15556
15871
  handleAdminMeetings,
15557
15872
  handleAdminScheduling,
15558
15873
  // Roster + identity
@@ -15633,6 +15948,7 @@ export {
15633
15948
  deliverMembershipChapterEffect,
15634
15949
  deliverSignupControl,
15635
15950
  drainMembershipEffects,
15951
+ refundNamedSeatByOperator,
15636
15952
  renderActiveSignupControlHtml,
15637
15953
  resolveNetworkTargetTransport,
15638
15954
  retryChapterApprovalEffects,