@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.
@@ -36,6 +36,7 @@ __export(worker_exports, {
36
36
  deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
37
37
  deliverSignupControl: () => deliverSignupControl,
38
38
  drainMembershipEffects: () => drainMembershipEffects,
39
+ refundNamedSeatByOperator: () => refundNamedSeatByOperator,
39
40
  renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
40
41
  resolveNetworkTargetTransport: () => resolveNetworkTargetTransport,
41
42
  retryChapterApprovalEffects: () => retryChapterApprovalEffects,
@@ -375,6 +376,7 @@ function createWorkerContext(options) {
375
376
  crmBase,
376
377
  requirePaymentQuote: options.requirePaymentQuote === true,
377
378
  membershipAuthority: options.membershipAuthority,
379
+ membershipAuthorityOperator: options.membershipAuthorityOperator,
378
380
  getPublicConfig,
379
381
  verifyUser,
380
382
  makeDb,
@@ -1700,6 +1702,11 @@ function canApprove(status2, p) {
1700
1702
  }
1701
1703
 
1702
1704
  // src/membership-policy.ts
1705
+ function seatPurchaseDeadline(offering, member) {
1706
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) return Number(member.termEndsAt);
1707
+ if (Number.isSafeInteger(member.seatPurchaseEndsAt)) return Number(member.seatPurchaseEndsAt);
1708
+ return addMembershipCalendarMonths(Number(member.initialTermStartedAt ?? member.termStartedAt), offering.seatPurchaseWindowCalendarMonths);
1709
+ }
1703
1710
  var fail2 = (field) => {
1704
1711
  throw new TypeError(`invalid membership policy: ${field}`);
1705
1712
  };
@@ -1756,7 +1763,7 @@ function membershipOfferingVersion(value) {
1756
1763
  autoRenew: true,
1757
1764
  paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
1758
1765
  seatPriceCents: integer2(input.seatPriceCents, 1, Number.MAX_SAFE_INTEGER),
1759
- seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120),
1766
+ ...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
1760
1767
  requiredDisclosures: ids(input.requiredDisclosures)
1761
1768
  };
1762
1769
  }
@@ -1883,24 +1890,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
1883
1890
  if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
1884
1891
  return restore({ ...value, approvedFoundingCount: 0 }, expected);
1885
1892
  }
1886
- async function issueMembershipCommercialOffer(db, input) {
1887
- validate(input);
1888
- const stateId = `${input.chapterId}:${input.environment}`;
1889
- const state = await load(db, "membershipAuthorityState", stateId);
1893
+ async function previewMembershipCommercialOffer(db, input) {
1894
+ for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
1895
+ if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
1896
+ }
1897
+ if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
1898
+ const state = await authoritativeState(db, input);
1899
+ const { offering, policy } = await currentPolicy(db, input, false, state);
1900
+ const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
1901
+ if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
1902
+ const contract = createMembershipCommercialOffer({
1903
+ offering,
1904
+ discountPolicy: policy,
1905
+ approvedFoundingCount: Number(approvedFoundingCount),
1906
+ now: input.now,
1907
+ couponId: await couponId(input, policy?.id)
1908
+ });
1909
+ const discount = contract.billingTerms.discount;
1910
+ return {
1911
+ skuId: input.skuId,
1912
+ offeringVersionId: offering.id,
1913
+ basePriceCents: offering.basePriceCents,
1914
+ currency: offering.currency,
1915
+ interval: offering.interval,
1916
+ founding: contract.founding,
1917
+ ...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
1918
+ dueTodayCents: contract.dueTodayCents,
1919
+ renewalCents: contract.renewalCents,
1920
+ ...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
1921
+ ...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
1922
+ };
1923
+ }
1924
+ async function authoritativeState(db, input) {
1925
+ const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
1890
1926
  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");
1891
- const existing = await load(db, "membershipCheckoutOffer", input.offerId);
1892
- if (existing) return restore(existing, input);
1927
+ return state;
1928
+ }
1929
+ async function currentPolicy(db, input, restartAware, state) {
1893
1930
  const rows5 = await db.query({
1894
1931
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
1895
1932
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
1896
1933
  });
1897
1934
  if (!Array.isArray(rows5.membershipOfferingVersion) || !Array.isArray(rows5.discountPolicyVersion) || rows5.membershipOfferingVersion.length >= 200 || rows5.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
1898
- 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));
1899
- 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);
1935
+ 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));
1936
+ 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);
1900
1937
  const offering = offerings.sort((a, b) => b.version - a.version)[0];
1901
- const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
1938
+ const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
1902
1939
  const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
1903
1940
  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");
1941
+ return { offering, policy };
1942
+ }
1943
+ async function issueMembershipCommercialOffer(db, input) {
1944
+ validate(input);
1945
+ const stateId = `${input.chapterId}:${input.environment}`;
1946
+ const state = await authoritativeState(db, input);
1947
+ const existing = await load(db, "membershipCheckoutOffer", input.offerId);
1948
+ if (existing) return restore(existing, input);
1949
+ const { offering, policy } = await currentPolicy(db, input, true, state);
1904
1950
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
1905
1951
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
1906
1952
  const contract = createMembershipCommercialOffer({
@@ -2108,7 +2154,7 @@ async function confirmMembershipPayment(db, input, now) {
2108
2154
  initialTermEndsAt: endsAt,
2109
2155
  termStartedAt: paidAt,
2110
2156
  termEndsAt: endsAt,
2111
- seatPurchaseEndsAt: addMembershipCalendarMonths(Number(paidAt), offering.seatPurchaseWindowCalendarMonths),
2157
+ seatPurchaseEndsAt: seatPurchaseDeadline(offering, { initialTermStartedAt: paidAt, termEndsAt: endsAt }),
2112
2158
  autoRenew: true,
2113
2159
  refundedCents: 0,
2114
2160
  revision: 1,
@@ -2208,7 +2254,7 @@ function findTier(tiers, tierId) {
2208
2254
  if (!tierId) return tiers.length === 1 ? tiers[0] : null;
2209
2255
  return tiers.find((tier) => tier.id === tierId) ?? null;
2210
2256
  }
2211
- function joinConfigTiers(tiers, defaults = {}) {
2257
+ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
2212
2258
  return tiers.map((tier) => ({
2213
2259
  id: tier.id,
2214
2260
  name: tier.name,
@@ -2216,7 +2262,8 @@ function joinConfigTiers(tiers, defaults = {}) {
2216
2262
  currency: tier.currency ?? defaults.currency ?? "usd",
2217
2263
  interval: tier.interval ?? defaults.interval ?? "year",
2218
2264
  blurb: tier.blurb ?? "",
2219
- free: tierIsFree(tier)
2265
+ free: tierIsFree(tier),
2266
+ ...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
2220
2267
  }));
2221
2268
  }
2222
2269
 
@@ -3451,6 +3498,313 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
3451
3498
  }
3452
3499
  };
3453
3500
 
3501
+ // src/worker-routes-admin-named-seat.ts
3502
+ var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3503
+ var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
3504
+ var handleAdminNamedSeat = async (req, url, env, ctx) => {
3505
+ const match = url.pathname.match(PATH);
3506
+ if (!match) return null;
3507
+ const seats = ctx.membershipAuthority?.namedSeats;
3508
+ if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
3509
+ const refund = Boolean(match[2]);
3510
+ if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3511
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3512
+ if (!user) return json({ error: "unauthorized" }, 401);
3513
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
3514
+ const db = raw2, app = await membershipLoad(db, "applications", match[1]);
3515
+ if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
3516
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
3517
+ if (!intent) return json({ error: "paid membership required" }, 409);
3518
+ try {
3519
+ const target = await membershipTargetForIntent(intent, app, env, ctx);
3520
+ const offer = await seats.offer(target, env);
3521
+ const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
3522
+ if (!refund) {
3523
+ return json({
3524
+ seat,
3525
+ amountCents: offer.amountCents,
3526
+ currency: offer.currency,
3527
+ termEndsAt: offer.termEndsAt,
3528
+ purchaseEndsAt: offer.purchaseEndsAt,
3529
+ refundable
3530
+ });
3531
+ }
3532
+ if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
3533
+ if (seat.status === "refunded") {
3534
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
3535
+ return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
3536
+ }
3537
+ if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
3538
+ const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
3539
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
3540
+ return json({ ok: true, ...receipt });
3541
+ } catch {
3542
+ return json({ error: "named seat service unavailable; retry to confirm" }, 503);
3543
+ }
3544
+ };
3545
+ async function closeRecipientApplication(db, env, ctx, seatId) {
3546
+ const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
3547
+ if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
3548
+ const attrs = { status: "refunded", renewalAt: 0, canceled: true };
3549
+ await db.transact(
3550
+ [{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
3551
+ { mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
3552
+ );
3553
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
3554
+ }
3555
+
3556
+ // src/membership-seat-refund-stripe.ts
3557
+ var import_stripe6 = require("@odla-ai/stripe");
3558
+ var fail3 = (code = "named_seat_refund_mismatch") => {
3559
+ throw Object.assign(new Error(code), { code });
3560
+ };
3561
+ async function refundNamedSeatStripePayment(secretKey, seat) {
3562
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
3563
+ const call = async (method, path, params, idempotencyKey) => {
3564
+ const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
3565
+ if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
3566
+ return result.body;
3567
+ };
3568
+ const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
3569
+ const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
3570
+ 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();
3571
+ const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
3572
+ 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();
3573
+ const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
3574
+ if (items.length > 1) fail3();
3575
+ if (items.length && sub.status !== "canceled") {
3576
+ const item = items[0];
3577
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
3578
+ const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
3579
+ if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
3580
+ }
3581
+ const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
3582
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
3583
+ let refundedCents = 0, pendingCents = 0;
3584
+ const refundIds = [];
3585
+ for (const refund of list.data) {
3586
+ 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();
3587
+ if (refund.status === "succeeded") {
3588
+ refundedCents += refund.amount;
3589
+ refundIds.push(refund.id);
3590
+ } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
3591
+ }
3592
+ if (refundedCents + pendingCents > seat.amountCents) fail3();
3593
+ if (pendingCents) fail3("named_seat_refund_pending");
3594
+ if (refundedCents < seat.amountCents) {
3595
+ const remaining = seat.amountCents - refundedCents;
3596
+ const refund = await call("POST", "/v1/refunds", {
3597
+ payment_intent: seat.providerPaymentIntentId,
3598
+ amount: remaining,
3599
+ reason: "requested_by_customer",
3600
+ metadata
3601
+ }, `named-seat-refund:${seat.id}:${remaining}`);
3602
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
3603
+ if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
3604
+ refundedCents += remaining;
3605
+ refundIds.push(refund.id);
3606
+ }
3607
+ return { refundedCents, refundIds };
3608
+ }
3609
+
3610
+ // src/membership-seat-refund.ts
3611
+ async function requestNamedSeatRefund(db, input, now) {
3612
+ membershipClock(now);
3613
+ const state = await loadMembershipAuthorityState(db, input);
3614
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
3615
+ requireMembershipScope(seat, input);
3616
+ const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
3617
+ requireMembershipScope(member, input);
3618
+ if (seat.status === "refunded" || seat.status === "refund_pending") return;
3619
+ 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");
3620
+ const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
3621
+ await db.transact([
3622
+ membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
3623
+ ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
3624
+ membershipUpdate("membershipEffect", `${seat.id}:refund`, {
3625
+ id: `${seat.id}:refund`,
3626
+ chapterId: input.chapterId,
3627
+ environment: input.environment,
3628
+ runtime: input.runtime,
3629
+ membershipId: member.id,
3630
+ seatId: seat.id,
3631
+ kind: "refund_seat",
3632
+ status: "pending",
3633
+ attempts: 0,
3634
+ createdAt: now,
3635
+ updatedAt: now
3636
+ }),
3637
+ ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
3638
+ id: `${seat.id}:revoke`,
3639
+ chapterId: input.chapterId,
3640
+ environment: input.environment,
3641
+ runtime: input.runtime,
3642
+ membershipId: member.id,
3643
+ seatId: seat.id,
3644
+ kind: "revoke_named_recipient_access",
3645
+ status: "pending",
3646
+ attempts: 0,
3647
+ createdAt: now,
3648
+ updatedAt: now
3649
+ })] : []
3650
+ ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
3651
+ { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
3652
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3653
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3654
+ ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
3655
+ ] });
3656
+ }
3657
+ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
3658
+ membershipClock(now);
3659
+ const seat = await membershipLoad(db, "networkSeat", seatId);
3660
+ if (!seat) return membershipFail("named_seat_not_found");
3661
+ await loadMembershipAuthorityState(db, seat);
3662
+ if (seat.status === "refunded") return;
3663
+ if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
3664
+ const result = await refundNamedSeatStripePayment(secretKey, seat);
3665
+ await db.transact([
3666
+ membershipUpdate("networkSeat", seatId, {
3667
+ status: "refunded",
3668
+ refundedCents: result.refundedCents,
3669
+ providerRefundIds: result.refundIds,
3670
+ revision: Number(seat.revision) + 1,
3671
+ updatedAt: now
3672
+ }),
3673
+ membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
3674
+ ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
3675
+ { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
3676
+ ] });
3677
+ }
3678
+
3679
+ // src/membership-seat-operator-refund.ts
3680
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3681
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
3682
+ requireMembershipScope(seat, input);
3683
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
3684
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
3685
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
3686
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
3687
+ throw new Error("named seat refund unconfirmed");
3688
+ }
3689
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
3690
+ }
3691
+
3692
+ // src/membership-seat-window.ts
3693
+ async function liftSeatPurchaseWindow(db, scope2, now, limit = 100) {
3694
+ membershipClock(now);
3695
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new TypeError("invalid lift limit");
3696
+ const state = await loadMembershipAuthorityState(db, scope2);
3697
+ const rows5 = (await db.query({ networkMembership: { $: { where: { ...scope2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkMembership ?? [];
3698
+ let lifted = 0;
3699
+ for (const member of rows5.slice(0, limit)) {
3700
+ if (Object.entries(scope2).some(([key3, value]) => member[key3] !== value)) throw new Error("membership lift scope mismatch");
3701
+ const offering = membershipOfferingVersion(member.offeringSnapshot);
3702
+ if (offering.seatPurchaseWindowCalendarMonths === void 0) continue;
3703
+ const { seatPurchaseWindowCalendarMonths: _window, ...windowless } = offering;
3704
+ await db.transact([membershipUpdate("networkMembership", String(member.id), {
3705
+ offeringSnapshot: windowless,
3706
+ seatPurchaseEndsAt: Number(member.termEndsAt),
3707
+ revision: Number(member.revision) + 1,
3708
+ updatedAt: now
3709
+ })], { mutationId: `seat-window-lift:${member.id}:${member.revision}`, guards: [
3710
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3711
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative" } }
3712
+ ] });
3713
+ lifted++;
3714
+ }
3715
+ return { lifted, more: rows5.length > limit };
3716
+ }
3717
+
3718
+ // src/worker-routes-admin-membership-seats.ts
3719
+ var LIST = "/api/admin/membership-seats";
3720
+ var LIFT = "/api/admin/membership-seats/lift-purchase-window";
3721
+ var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
3722
+ var SCOPE = /^[a-z0-9][a-z0-9-]{1,62}$/;
3723
+ var PUBLIC = [
3724
+ "id",
3725
+ "appId",
3726
+ "chapterId",
3727
+ "environment",
3728
+ "runtime",
3729
+ "applicationId",
3730
+ "skuId",
3731
+ "status",
3732
+ "amountCents",
3733
+ "currency",
3734
+ "recipientName",
3735
+ "recipientEmailNormalized",
3736
+ "purchaserUserId",
3737
+ "purchasedAt",
3738
+ "acceptedAt",
3739
+ "journeyApprovedAt",
3740
+ "refundedCents",
3741
+ "refundReason",
3742
+ "termEndsAt",
3743
+ "updatedAt"
3744
+ ];
3745
+ var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
3746
+ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3747
+ const one = url.pathname.match(ONE), lift = url.pathname === LIFT;
3748
+ if (url.pathname !== LIST && !one && !lift) return null;
3749
+ const operator = ctx.membershipAuthorityOperator;
3750
+ if (!operator) return json({ error: "not found" }, 404);
3751
+ if (req.method !== (one || lift ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3752
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3753
+ if (!user) return json({ error: "unauthorized" }, 401);
3754
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
3755
+ let db;
3756
+ try {
3757
+ db = operator.makeDb(env);
3758
+ } catch {
3759
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
3760
+ }
3761
+ if (lift) {
3762
+ let body;
3763
+ try {
3764
+ body = await req.json();
3765
+ } catch {
3766
+ return json({ error: "chapterId and runtime are required" }, 400);
3767
+ }
3768
+ const appId = body.appId ?? body.chapterId;
3769
+ 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)) {
3770
+ return json({ error: "chapterId and runtime are required" }, 400);
3771
+ }
3772
+ try {
3773
+ return json({ ok: true, ...await liftSeatPurchaseWindow(db, { appId, chapterId: body.chapterId, environment: env.ODLA_ENV, runtime: body.runtime }, Date.now()) });
3774
+ } catch (error) {
3775
+ const code = error.code;
3776
+ return json({ error: "purchase window lift is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3777
+ }
3778
+ }
3779
+ if (!one) {
3780
+ const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
3781
+ return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
3782
+ }
3783
+ const seat = await membershipLoad(db, "networkSeat", one[1]);
3784
+ if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
3785
+ if (seat.status === "refunded") {
3786
+ return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
3787
+ }
3788
+ if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
3789
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
3790
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
3791
+ try {
3792
+ const receipt = await refundNamedSeatByOperator(db, {
3793
+ appId: String(seat.appId),
3794
+ chapterId: String(seat.chapterId),
3795
+ environment: String(seat.environment),
3796
+ runtime: String(seat.runtime),
3797
+ applicationId: String(seat.applicationId),
3798
+ skuId: String(seat.skuId),
3799
+ seatId: String(seat.id)
3800
+ }, secret, Date.now());
3801
+ return json({ ok: true, ...receipt });
3802
+ } catch (error) {
3803
+ const code = error.code;
3804
+ return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3805
+ }
3806
+ };
3807
+
3454
3808
  // src/worker-routes-membership-cancel.ts
3455
3809
  var handleMembershipCancellation = async (req, url, env, ctx) => {
3456
3810
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4047,6 +4401,29 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
4047
4401
  }
4048
4402
  }
4049
4403
 
4404
+ // src/join-founding-preview.ts
4405
+ async function foundingPreviews(tiers, env, ctx) {
4406
+ const result = /* @__PURE__ */ new Map();
4407
+ const preview = ctx.membershipAuthority?.previewOffer;
4408
+ if (!preview) return result;
4409
+ const scope2 = { appId: env.ODLA_APP_ID, chapterId: ctx.chapter.id, environment: env.ODLA_ENV, runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME) };
4410
+ await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
4411
+ try {
4412
+ const offer = await preview({ ...scope2, skuId: tier.id }, env);
4413
+ if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
4414
+ result.set(tier.id, {
4415
+ cohortLabel: offer.cohortLabel,
4416
+ dueTodayCents: offer.dueTodayCents,
4417
+ discountCents: offer.basePriceCents - offer.dueTodayCents,
4418
+ renewalCents: offer.renewalCents,
4419
+ ...offer.discountDuration ? { discountDuration: offer.discountDuration } : {}
4420
+ });
4421
+ } catch {
4422
+ }
4423
+ }));
4424
+ return result;
4425
+ }
4426
+
4050
4427
  // src/admission-grants-contract.ts
4051
4428
  var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
4052
4429
  var ADMISSION_GRANTS_PATH = "/api/admission-grants";
@@ -4573,10 +4950,24 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
4573
4950
  }
4574
4951
 
4575
4952
  // src/payment-quote.ts
4953
+ function tierPresentationFor(document, tierId) {
4954
+ const compareAt = document?.membershipPage?.tiers?.find((tier) => tier.tierId === tierId)?.compareAtPriceCents;
4955
+ return Number.isSafeInteger(compareAt) && Number(compareAt) > 0 ? { compareAtPriceCents: Number(compareAt) } : void 0;
4956
+ }
4957
+ function foundingOf(offer) {
4958
+ return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
4959
+ }
4576
4960
  async function createPaymentQuote(input) {
4577
4961
  const { group, tier, policy } = input;
4578
4962
  const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
4963
+ const compareAt = input.presentation?.compareAtPriceCents;
4964
+ const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
4579
4965
  const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
4966
+ const lines = dueToday === void 0 ? paymentLineItems(group, tier) : {
4967
+ standardCents: tier.priceCents,
4968
+ discountCents: tier.priceCents - dueToday,
4969
+ dueTodayCents: dueToday
4970
+ };
4580
4971
  const quote = {
4581
4972
  applicationId: input.applicationId,
4582
4973
  chapterId: input.chapterId,
@@ -4584,11 +4975,12 @@ async function createPaymentQuote(input) {
4584
4975
  tierName: tier.name,
4585
4976
  currency: tier.currency ?? String(group.currency ?? "usd"),
4586
4977
  interval: tier.interval ?? (group.interval === "month" ? "month" : "year"),
4587
- lineItems: dueToday === void 0 ? paymentLineItems(group, tier) : {
4588
- standardCents: tier.priceCents,
4589
- discountCents: tier.priceCents - dueToday,
4590
- dueTodayCents: dueToday
4591
- },
4978
+ lineItems: presentation ? {
4979
+ standardCents: presentation.compareAtPriceCents,
4980
+ discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
4981
+ dueTodayCents: lines.dueTodayCents
4982
+ } : lines,
4983
+ ...presentation ? { presentation } : {},
4592
4984
  refundPolicyText: policy?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
4593
4985
  merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
4594
4986
  trustCopy: policy ? policy.trustCopy ?? "" : String(group.trustCopy ?? ""),
@@ -4613,37 +5005,39 @@ async function createPaymentQuote(input) {
4613
5005
  });
4614
5006
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(contract));
4615
5007
  const digest = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4616
- return { ...quote, digest };
5008
+ return { ...quote, digest, ...input.founding ? { founding: input.founding } : {} };
4617
5009
  }
4618
5010
 
4619
5011
  // src/membership-quote-projection.ts
4620
5012
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
4621
5013
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
4622
- var fail3 = (code) => {
5014
+ var fail4 = (code) => {
4623
5015
  throw Object.assign(new Error(code), { code });
4624
5016
  };
4625
5017
  var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
4626
5018
  async function restore2(row2, target, now, allowExpired = false) {
4627
- 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");
4628
- if (row2.status !== "ready") return fail3("membership_quote_pending");
5019
+ 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");
5020
+ if (row2.status !== "ready") return fail4("membership_quote_pending");
4629
5021
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
4630
5022
  ...target,
4631
5023
  requestId: row2.requestId
4632
5024
  }), now, lifetimeMs: 6e4 });
4633
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail3("membership_quote_expired");
5025
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
4634
5026
  const tier = row2.tier, quote = row2.quote;
4635
5027
  const offering = offer.contract.offering;
4636
5028
  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) {
4637
- return fail3("membership_catalog_mismatch");
5029
+ return fail4("membership_catalog_mismatch");
4638
5030
  }
4639
5031
  const expected = await createPaymentQuote({
4640
5032
  ...target,
4641
5033
  tier,
4642
5034
  group: { merchantDisclosureText: quote.merchantDisclosureText },
4643
5035
  policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
4644
- billingTerms: offer.contract.billingTerms
5036
+ billingTerms: offer.contract.billingTerms,
5037
+ presentation: quote.presentation,
5038
+ founding: foundingOf(offer)
4645
5039
  });
4646
- if (quote.digest !== expected.digest) return fail3("membership_quote_conflict");
5040
+ if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
4647
5041
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
4648
5042
  }
4649
5043
  async function readMembershipQuote(db, target, now) {
@@ -4651,9 +5045,9 @@ async function readMembershipQuote(db, target, now) {
4651
5045
  }
4652
5046
  async function prepareMembershipQuote(db, input, issue) {
4653
5047
  const { applicationId: id6, now, app } = input;
4654
- 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");
5048
+ 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");
4655
5049
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
4656
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail3("membership_quote_conflict");
5050
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
4657
5051
  if (row2?.status === "ready") {
4658
5052
  try {
4659
5053
  return await restore2(row2, input, now);
@@ -4698,7 +5092,7 @@ async function prepareMembershipQuote(db, input, issue) {
4698
5092
  }
4699
5093
  if (row2?.status === "ready") return restore2(row2, input, now);
4700
5094
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
4701
- return fail3("membership_quote_pending");
5095
+ return fail4("membership_quote_pending");
4702
5096
  }
4703
5097
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
4704
5098
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -4712,7 +5106,15 @@ async function prepareMembershipQuote(db, input, issue) {
4712
5106
  currency: input.tier.currency ?? String(input.group.currency ?? "usd"),
4713
5107
  interval: input.tier.interval ?? (input.group.interval === "month" ? "month" : "year")
4714
5108
  };
4715
- const quote = await createPaymentQuote({ ...target, tier, group: input.group, policy: input.policy, billingTerms: offer.contract.billingTerms });
5109
+ const quote = await createPaymentQuote({
5110
+ ...target,
5111
+ tier,
5112
+ group: input.group,
5113
+ policy: input.policy,
5114
+ billingTerms: offer.contract.billingTerms,
5115
+ presentation: input.presentation,
5116
+ founding: foundingOf(offer)
5117
+ });
4716
5118
  const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
4717
5119
  await restore2(ready, target, now, true);
4718
5120
  try {
@@ -4767,7 +5169,9 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4767
5169
  group: { merchantDisclosureText: quote2.merchantDisclosureText },
4768
5170
  tier: tier2,
4769
5171
  policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
4770
- billingTerms
5172
+ billingTerms,
5173
+ presentation: quote2.presentation,
5174
+ founding: foundingOf(intent.membershipOffer)
4771
5175
  });
4772
5176
  if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
4773
5177
  return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
@@ -4794,9 +5198,10 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4794
5198
  if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
4795
5199
  const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4796
5200
  const policy = control3?.document.policy?.tierPolicies?.[tier.id];
5201
+ const presentation = tierPresentationFor(control3?.document, tier.id);
4797
5202
  if (ctx.membershipAuthority) return prepareMembershipQuote(
4798
5203
  db,
4799
- { ...target, app, group, tier, policy, now: Date.now() },
5204
+ { ...target, app, group, tier, policy, presentation, now: Date.now() },
4800
5205
  (input) => ctx.membershipAuthority.issueOffer(input, env)
4801
5206
  );
4802
5207
  const quote = await createPaymentQuote({
@@ -4807,7 +5212,8 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4807
5212
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4808
5213
  group,
4809
5214
  tier,
4810
- policy
5215
+ policy,
5216
+ presentation
4811
5217
  });
4812
5218
  return { tier, quote, billingTerms: void 0 };
4813
5219
  }
@@ -5461,10 +5867,11 @@ var handleMember = async (req, url, env, ctx) => {
5461
5867
  group,
5462
5868
  Boolean(stripeKey)
5463
5869
  );
5870
+ const founding = await foundingPreviews(offered, env, ctx);
5464
5871
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
5465
5872
  const application = controlledApplication(chapter.application, activeControl?.document);
5466
5873
  return json({
5467
- ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }), application.conditions),
5874
+ ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
5468
5875
  fieldContract: chapterFlowFieldContract(chapter, "join", application),
5469
5876
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
5470
5877
  ...activeControl ? { signupControl: {
@@ -7157,7 +7564,7 @@ async function deliverAdmissionGrants(options) {
7157
7564
  }
7158
7565
 
7159
7566
  // src/worker-routes-network-admission.ts
7160
- var PATH = "/api/admin/network/admissions";
7567
+ var PATH2 = "/api/admin/network/admissions";
7161
7568
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7162
7569
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7163
7570
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -7237,11 +7644,11 @@ async function parseBody(req) {
7237
7644
  }
7238
7645
  }
7239
7646
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
7240
- if (![PATH, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7647
+ if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7241
7648
  const auth = await gate2(req, env, ctx);
7242
7649
  if ("response" in auth) return auth.response;
7243
7650
  const db = auth.db;
7244
- if (url.pathname === PATH && req.method === "GET") {
7651
+ if (url.pathname === PATH2 && req.method === "GET") {
7245
7652
  const targetId2 = url.searchParams.get("targetId") ?? "";
7246
7653
  const target2 = targetFor(ctx, targetId2);
7247
7654
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -7485,28 +7892,28 @@ var handleSchedule = async (req, url, env, ctx) => {
7485
7892
  };
7486
7893
 
7487
7894
  // src/payments-stripe.ts
7488
- var import_stripe9 = require("@odla-ai/stripe");
7895
+ var import_stripe10 = require("@odla-ai/stripe");
7489
7896
 
7490
7897
  // src/payments-refund.ts
7491
- var import_stripe6 = require("@odla-ai/stripe");
7898
+ var import_stripe7 = require("@odla-ai/stripe");
7492
7899
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
7493
7900
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
7494
7901
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
7495
- function fail4(code = "refund_target_mismatch") {
7902
+ function fail5(code = "refund_target_mismatch") {
7496
7903
  throw Object.assign(new Error(code), { code });
7497
7904
  }
7498
7905
  async function refundSubscriptionPayment(secretKey, input) {
7499
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail4();
7906
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
7500
7907
  const call = async (method, path, params, key3) => {
7501
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7502
- if (!result.ok) fail4("stripe_error");
7503
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail4("stripe_invalid_response");
7908
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7909
+ if (!result.ok) fail5("stripe_error");
7910
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
7504
7911
  return result.body;
7505
7912
  };
7506
7913
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
7507
- if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail4();
7914
+ if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
7508
7915
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
7509
- if (row(sub.metadata)[key3] !== value) fail4();
7916
+ if (row(sub.metadata)[key3] !== value) fail5();
7510
7917
  }
7511
7918
  let cursor;
7512
7919
  let earliest;
@@ -7519,69 +7926,69 @@ async function refundSubscriptionPayment(secretKey, input) {
7519
7926
  limit: 100,
7520
7927
  ...cursor ? { starting_after: cursor } : {}
7521
7928
  });
7522
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail4("stripe_invalid_response");
7929
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
7523
7930
  for (const value of list.data) {
7524
7931
  const invoice2 = row(value);
7525
7932
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
7526
- 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();
7933
+ 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();
7527
7934
  seen.add(id2(invoice2));
7528
7935
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
7529
7936
  }
7530
7937
  if (!list.has_more) break;
7531
- if (!list.data.length || page === 9) fail4("refund_history_incomplete");
7938
+ if (!list.data.length || page === 9) fail5("refund_history_incomplete");
7532
7939
  cursor = id2(list.data[list.data.length - 1]);
7533
7940
  }
7534
- if (!earliest) fail4("no_charge");
7941
+ if (!earliest) fail5("no_charge");
7535
7942
  const invoice = earliest;
7536
- if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail4();
7943
+ if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
7537
7944
  let chargeId = id2(invoice.charge);
7538
7945
  let paymentIntentId = id2(invoice.payment_intent);
7539
7946
  if (!chargeId && !paymentIntentId) {
7540
7947
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id2(invoice), status: "paid", limit: 100 });
7541
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail4("refund_payment_ambiguous");
7948
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
7542
7949
  const payment = row(payments.data[0]);
7543
- if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail4();
7950
+ if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
7544
7951
  paymentIntentId = id2(row(payment.payment).payment_intent);
7545
7952
  }
7546
7953
  if (!chargeId) {
7547
- if (!providerId2(paymentIntentId, "pi")) fail4();
7954
+ if (!providerId2(paymentIntentId, "pi")) fail5();
7548
7955
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
7549
- if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail4();
7956
+ if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
7550
7957
  chargeId = id2(payment.latest_charge);
7551
7958
  }
7552
- if (!providerId2(chargeId, "ch")) fail4();
7959
+ if (!providerId2(chargeId, "ch")) fail5();
7553
7960
  const charge = await call("GET", `/v1/charges/${chargeId}`);
7554
- 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();
7555
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail4();
7961
+ 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();
7962
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
7556
7963
  if (charge.amount_refunded !== charge.amount) {
7557
7964
  const refund = await call("POST", "/v1/refunds", {
7558
7965
  charge: chargeId,
7559
7966
  ...input.metadata ? { metadata: input.metadata } : {}
7560
7967
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
7561
- 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");
7968
+ 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");
7562
7969
  }
7563
7970
  let subscriptionCanceled = sub.status === "canceled";
7564
7971
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
7565
7972
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
7566
- if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail4("refund_cancel_incomplete");
7973
+ if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
7567
7974
  subscriptionCanceled = true;
7568
7975
  }
7569
7976
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
7570
- 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");
7977
+ 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");
7571
7978
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
7572
7979
  }
7573
7980
 
7574
7981
  // src/subscription-payment-identity.ts
7575
- var import_stripe8 = require("@odla-ai/stripe");
7982
+ var import_stripe9 = require("@odla-ai/stripe");
7576
7983
 
7577
7984
  // src/subscription-provider-contract.ts
7578
- var import_stripe7 = require("@odla-ai/stripe");
7985
+ var import_stripe8 = require("@odla-ai/stripe");
7579
7986
  function subscriptionContractMismatch() {
7580
7987
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
7581
7988
  }
7582
7989
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
7583
7990
  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();
7584
- const result = await (0, import_stripe7.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
7991
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
7585
7992
  const price = result.body;
7586
7993
  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();
7587
7994
  return typeof price.product === "string" ? price.product : void 0;
@@ -7600,7 +8007,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7600
8007
  let paymentId = id3(invoice.payment_intent);
7601
8008
  if (!paymentId) {
7602
8009
  if (!/^in_[A-Za-z0-9]+$/.test(id3(invoice))) subscriptionContractMismatch();
7603
- const result2 = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
8010
+ const result2 = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
7604
8011
  const list = result2.body;
7605
8012
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
7606
8013
  const link = list.data[0];
@@ -7609,11 +8016,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7609
8016
  }
7610
8017
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
7611
8018
  const path = `/v1/payment_intents/${paymentId}`;
7612
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", path);
8019
+ const result = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", path);
7613
8020
  const payment = result.body;
7614
8021
  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();
7615
8022
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
7616
- const tagged = await (0, import_stripe8.stripeRequest)(
8023
+ const tagged = await (0, import_stripe9.stripeRequest)(
7617
8024
  { secretKey },
7618
8025
  "POST",
7619
8026
  path,
@@ -7625,7 +8032,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7625
8032
 
7626
8033
  // src/payments-stripe.ts
7627
8034
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7628
- return (0, import_stripe9.stripeRequest)(
8035
+ return (0, import_stripe10.stripeRequest)(
7629
8036
  { secretKey: sk },
7630
8037
  method,
7631
8038
  path,
@@ -7633,7 +8040,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
7633
8040
  idempotencyKey ? { idempotencyKey } : {}
7634
8041
  );
7635
8042
  }
7636
- function fail5(op, r) {
8043
+ function fail6(op, r) {
7637
8044
  const err = new Error(`stripe ${op} failed: ${r.status}`);
7638
8045
  err.code = "stripe_error";
7639
8046
  throw err;
@@ -7683,7 +8090,7 @@ function createStripeProvider(config) {
7683
8090
  { email: input.email, name: input.name, metadata: meta },
7684
8091
  `cus:${input.applicationId}`
7685
8092
  );
7686
- if (!cust.ok) fail5("customer create", cust);
8093
+ if (!cust.ok) fail6("customer create", cust);
7687
8094
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
7688
8095
  customerId = cust.body.id;
7689
8096
  }
@@ -7715,7 +8122,7 @@ function createStripeProvider(config) {
7715
8122
  // retry between create and the db write can't orphan a second one.
7716
8123
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
7717
8124
  );
7718
- if (!sub.ok) fail5("subscription create", sub);
8125
+ if (!sub.ok) fail6("subscription create", sub);
7719
8126
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
7720
8127
  const subscriptionId = sub.body.id;
7721
8128
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -7725,14 +8132,14 @@ function createStripeProvider(config) {
7725
8132
  "expand[2]": "discounts",
7726
8133
  "expand[3]": "latest_invoice.discounts"
7727
8134
  });
7728
- if (!expanded.ok) fail5("subscription invoice evidence", expanded);
8135
+ if (!expanded.ok) fail6("subscription invoice evidence", expanded);
7729
8136
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
7730
8137
  sub = expanded;
7731
8138
  }
7732
8139
  verifySubscriptionResult(sub.body, input, customerId, meta);
7733
8140
  const invoice = sub.body.latest_invoice;
7734
8141
  const clientSecret2 = clientSecretOf(sub.body);
7735
- if (!clientSecret2) fail5("subscription confirmation-secret missing", sub);
8142
+ if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
7736
8143
  await tagSubscriptionPayment(
7737
8144
  sk,
7738
8145
  invoice,
@@ -7744,7 +8151,7 @@ function createStripeProvider(config) {
7744
8151
  return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
7745
8152
  },
7746
8153
  async ingestWebhook(rawBody, sigHeader) {
7747
- if (!webhookSecret || !await (0, import_stripe9.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
8154
+ if (!webhookSecret || !await (0, import_stripe10.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
7748
8155
  return { ok: false, reason: "bad_signature" };
7749
8156
  }
7750
8157
  const event = JSON.parse(rawBody);
@@ -8404,7 +8811,7 @@ async function namedSeatOfferDigest(offer) {
8404
8811
  "named-only",
8405
8812
  "full-upfront",
8406
8813
  "auto-renew-coterm",
8407
- "30-day-unaccepted-full-refund"
8814
+ "no-automatic-refund"
8408
8815
  ])));
8409
8816
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
8410
8817
  }
@@ -8416,15 +8823,16 @@ async function readNamedSeatOffer(db, input, now) {
8416
8823
  const offering = membershipOfferingVersion(member.offeringSnapshot);
8417
8824
  const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
8418
8825
  if (seat) requireMembershipScope(seat, input);
8826
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
8419
8827
  const offer = {
8420
8828
  membershipId: id6,
8421
8829
  amountCents: offering.seatPriceCents,
8422
8830
  currency: offering.currency,
8423
8831
  interval: offering.interval,
8424
8832
  termEndsAt: Number(member.termEndsAt),
8425
- purchaseEndsAt: Number(member.seatPurchaseEndsAt),
8833
+ purchaseEndsAt,
8426
8834
  autoRenew: member.autoRenew === true,
8427
- 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))),
8835
+ 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))),
8428
8836
  ...seat ? { seat: {
8429
8837
  seatId: String(seat.id),
8430
8838
  status: String(seat.status),
@@ -8432,7 +8840,7 @@ async function readNamedSeatOffer(db, input, now) {
8432
8840
  recipientEmail: String(seat.recipientEmailNormalized)
8433
8841
  } } : {}
8434
8842
  };
8435
- return { ...offer, digest: await namedSeatOfferDigest(offer) };
8843
+ return { ...offer, digest: await namedSeatOfferDigest(offer), purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0 };
8436
8844
  }
8437
8845
 
8438
8846
  // src/named-seat-request.ts
@@ -8809,6 +9217,23 @@ async function updateChapterPerson(opts) {
8809
9217
  });
8810
9218
  }
8811
9219
 
9220
+ // src/application-actions.ts
9221
+ function isApplicationStage(ctx, stage) {
9222
+ return ctx.chapter.pipeline.stages.includes(stage);
9223
+ }
9224
+ function applicationActionStages(app, env, ctx) {
9225
+ if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
9226
+ const from = String(app.status ?? "");
9227
+ if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
9228
+ return ctx.chapter.pipeline.stages.filter((to) => {
9229
+ if (to === from) return false;
9230
+ if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
9231
+ if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
9232
+ if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
9233
+ return canTransition(from, to, ctx.chapter.pipeline);
9234
+ });
9235
+ }
9236
+
8812
9237
  // src/worker-routes-people-profile.ts
8813
9238
  var handlePeopleProfile = async (req, url, env, ctx) => {
8814
9239
  if (ctx.chapter.mode !== "chapter") return null;
@@ -8830,7 +9255,16 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
8830
9255
  const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, id6);
8831
9256
  if (!record4) return json({ error: "not found" }, 404);
8832
9257
  if (record4.type !== "person") return dedicated ? json({ error: "person required" }, 422) : null;
8833
- if (match?.[2]) return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
9258
+ if (match?.[2]) {
9259
+ const linked = typeof record4.fields.applicationId === "string" && record4.fields.applicationId !== "";
9260
+ let to = "";
9261
+ try {
9262
+ to = String((await req.clone().json()).to ?? "");
9263
+ } catch {
9264
+ }
9265
+ if (!linked && to && !isApplicationStage(ctx, to)) return null;
9266
+ return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
9267
+ }
8834
9268
  if (req.method === "GET" && dedicated) {
8835
9269
  const excluded = profileReadOnlyFields(record4, ctx);
8836
9270
  const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id7]) => !excluded.includes(id7)).map(([id7, field]) => ({
@@ -9521,20 +9955,6 @@ var handleAdminBilling = async (req, url, env, ctx) => {
9521
9955
  // src/worker-routes-admin-lifecycle.ts
9522
9956
  var import_calendar3 = require("@odla-ai/calendar");
9523
9957
 
9524
- // src/application-actions.ts
9525
- function applicationActionStages(app, env, ctx) {
9526
- if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
9527
- const from = String(app.status ?? "");
9528
- if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
9529
- return ctx.chapter.pipeline.stages.filter((to) => {
9530
- if (to === from) return false;
9531
- if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
9532
- if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
9533
- if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
9534
- return canTransition(from, to, ctx.chapter.pipeline);
9535
- });
9536
- }
9537
-
9538
9958
  // src/refund-application.ts
9539
9959
  async function refundApplication(db, env, ctx, app) {
9540
9960
  if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
@@ -9725,6 +10145,12 @@ var handleAdminRefund = async (req, url, env, ctx) => {
9725
10145
  const app = await loadApp(db, m[1]);
9726
10146
  if (!app) return json({ error: "not found" }, 404);
9727
10147
  if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
10148
+ if (app.namedSeatId) {
10149
+ const actor = await ctx.verifyUser(req, env);
10150
+ if (!actor || !await ctx.isSuperAdminEmail(db, actor.email, actor.userId)) {
10151
+ return json({ error: "super-admin required", code: "super_admin_required" }, 403);
10152
+ }
10153
+ }
9728
10154
  return refundApplication(db, env, ctx, app);
9729
10155
  };
9730
10156
  var handleAdminApplicationPatch = async (req, url, env, ctx) => {
@@ -10334,7 +10760,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
10334
10760
  const appId = record4.fields.applicationId;
10335
10761
  const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
10336
10762
  if (reader.stageTransitions.includes(record4.type)) {
10337
- result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : [];
10763
+ result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : result.edit.allowedStages.filter((stage) => !isApplicationStage(ctx, stage.id));
10338
10764
  const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
10339
10765
  const meta = lock?.meta;
10340
10766
  if (meta?.state === "pending" && typeof meta.activityId === "string") {
@@ -10552,7 +10978,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
10552
10978
  }
10553
10979
  };
10554
10980
  try {
10555
- if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person") {
10981
+ if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person" && (record4.fields.applicationId || isApplicationStage(ctx, input.stage))) {
10556
10982
  const updated2 = await networkApplicationAction({
10557
10983
  db,
10558
10984
  env,
@@ -11908,7 +12334,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
11908
12334
  }
11909
12335
 
11910
12336
  // src/worker-routes-network-person-registry.ts
11911
- var PATH2 = "/api/admin/network-person-registry";
12337
+ var PATH3 = "/api/admin/network-person-registry";
11912
12338
  var MAX_BODY2 = 8 * 1024 * 1024;
11913
12339
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
11914
12340
  var positiveRevision = (value) => {
@@ -11933,7 +12359,7 @@ function registryError(error) {
11933
12359
  return null;
11934
12360
  }
11935
12361
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
11936
- if (url.pathname !== PATH2) return null;
12362
+ if (url.pathname !== PATH3) return null;
11937
12363
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
11938
12364
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
11939
12365
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -12108,7 +12534,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12108
12534
  };
12109
12535
 
12110
12536
  // src/worker-routes-network-commercial.ts
12111
- var import_stripe10 = require("@odla-ai/stripe");
12537
+ var import_stripe11 = require("@odla-ai/stripe");
12112
12538
 
12113
12539
  // src/worker-routes-network-commercial-parity.ts
12114
12540
  var import_db13 = require("@odla-ai/db");
@@ -12484,7 +12910,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
12484
12910
  return json({ error: "commercial webhook is not configured" }, 503);
12485
12911
  }
12486
12912
  try {
12487
- const event = await (0, import_stripe10.constructStripeEvent)(body, signature, secret);
12913
+ const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
12488
12914
  const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
12489
12915
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
12490
12916
  const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact, Date.now(), await stripePayloadDigest(body));
@@ -12493,7 +12919,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
12493
12919
  }
12494
12920
  return json({ ok: true, status: result.status, eventId: fact.eventId });
12495
12921
  } catch (error) {
12496
- if (error instanceof import_stripe10.StripeError) return json({ error: error.message, code: error.code }, 400);
12922
+ if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
12497
12923
  throw error;
12498
12924
  }
12499
12925
  };
@@ -12535,7 +12961,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12535
12961
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
12536
12962
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
12537
12963
  try {
12538
- const stripe = (0, import_stripe10.initStripe)({ secretKey });
12964
+ const stripe = (0, import_stripe11.initStripe)({ secretKey });
12539
12965
  const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
12540
12966
  listStripeObjects(stripe, "/v1/customers"),
12541
12967
  listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
@@ -12549,7 +12975,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12549
12975
  ...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
12550
12976
  });
12551
12977
  } catch (error) {
12552
- if (error instanceof import_stripe10.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
12978
+ if (error instanceof import_stripe11.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
12553
12979
  return json({ error: "Stripe reconciliation failed" }, 502);
12554
12980
  }
12555
12981
  }
@@ -13129,7 +13555,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
13129
13555
  };
13130
13556
 
13131
13557
  // src/membership-cancel-authority.ts
13132
- var import_stripe11 = require("@odla-ai/stripe");
13558
+ var import_stripe12 = require("@odla-ai/stripe");
13133
13559
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
13134
13560
  membershipClock(now);
13135
13561
  const id6 = await networkMembershipId(target);
@@ -13140,14 +13566,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
13140
13566
  membershipFail("membership_cancellation_unavailable");
13141
13567
  }
13142
13568
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
13143
- const read2 = await (0, import_stripe11.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
13569
+ const read2 = await (0, import_stripe12.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
13144
13570
  const sub = read2.body;
13145
13571
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
13146
13572
  ...stripePartitionMetadata(target),
13147
13573
  applicationId: target.applicationId
13148
13574
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
13149
13575
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
13150
- const write = await (0, import_stripe11.stripeRequest)(
13576
+ const write = await (0, import_stripe12.stripeRequest)(
13151
13577
  { secretKey: stripeSecret },
13152
13578
  "POST",
13153
13579
  path,
@@ -13369,7 +13795,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
13369
13795
  }
13370
13796
 
13371
13797
  // src/membership-provider-lifecycle.ts
13372
- var import_stripe12 = require("@odla-ai/stripe");
13798
+ var import_stripe13 = require("@odla-ai/stripe");
13373
13799
 
13374
13800
  // src/membership-renewal-authority.ts
13375
13801
  async function reconcileMembershipRenewal(db, input, now) {
@@ -13422,7 +13848,11 @@ async function reconcileMembershipRenewal(db, input, now) {
13422
13848
  membershipUpdate("networkMembership", id6, {
13423
13849
  status: status2,
13424
13850
  graceEndsAt,
13425
- ...paid ? { termStartedAt: start, termEndsAt: end } : {},
13851
+ ...paid ? {
13852
+ termStartedAt: start,
13853
+ termEndsAt: end,
13854
+ ...membershipOfferingVersion(member.offeringSnapshot).seatPurchaseWindowCalendarMonths === void 0 ? { seatPurchaseEndsAt: end } : {}
13855
+ } : {},
13426
13856
  revision: Number(member.revision) + 1,
13427
13857
  updatedAt: now
13428
13858
  }),
@@ -13527,7 +13957,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
13527
13957
  }
13528
13958
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
13529
13959
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
13530
- const result = await (0, import_stripe12.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
13960
+ const result = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
13531
13961
  const sub = result.body;
13532
13962
  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)) {
13533
13963
  membershipFail("membership_billing_provider_mismatch");
@@ -13582,10 +14012,11 @@ async function reserveNamedMembershipSeat(db, input, now) {
13582
14012
  const id6 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
13583
14013
  let seat = await membershipLoad(db, "networkSeat", id6);
13584
14014
  if (!seat) {
13585
- 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) {
14015
+ const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
14016
+ const purchaseEndsAt = seatPurchaseDeadline(offering, member);
14017
+ 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) {
13586
14018
  membershipFail("named_seat_purchase_unavailable");
13587
14019
  }
13588
- const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
13589
14020
  const attrs = {
13590
14021
  id: id6,
13591
14022
  ...membershipScope(input),
@@ -13604,7 +14035,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13604
14035
  status: "payment_pending",
13605
14036
  preparedAt: now,
13606
14037
  termEndsAt: member.termEndsAt,
13607
- seatPurchaseEndsAt: member.seatPurchaseEndsAt,
14038
+ seatPurchaseEndsAt: purchaseEndsAt,
13608
14039
  offeringSnapshot: offering,
13609
14040
  revision: 1,
13610
14041
  createdAt: now,
@@ -13642,7 +14073,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13642
14073
  }
13643
14074
 
13644
14075
  // src/membership-seat-stripe.ts
13645
- var import_stripe13 = require("@odla-ai/stripe");
14076
+ var import_stripe14 = require("@odla-ai/stripe");
13646
14077
  var reject7 = () => {
13647
14078
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
13648
14079
  };
@@ -13654,7 +14085,7 @@ var metadataFor = (seat) => ({
13654
14085
  });
13655
14086
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
13656
14087
  async function read(sk, path, params) {
13657
- const response = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", path, params);
14088
+ const response = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", path, params);
13658
14089
  if (!response.ok) return reject7();
13659
14090
  return response.body;
13660
14091
  }
@@ -13691,11 +14122,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
13691
14122
  }
13692
14123
  await subscriptionForSeat(sk, seat);
13693
14124
  const metadata = metadataFor(seat);
13694
- const result = seat.providerPaymentIntentId ? await (0, import_stripe13.stripeRequest)(
14125
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe14.stripeRequest)(
13695
14126
  { secretKey: sk },
13696
14127
  "GET",
13697
14128
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
13698
- ) : await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
14129
+ ) : await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
13699
14130
  amount: seat.amountCents,
13700
14131
  currency: seat.currency,
13701
14132
  customer: seat.providerCustomerId,
@@ -13713,7 +14144,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
13713
14144
  const sub = await subscriptionForSeat(sk, seat);
13714
14145
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
13715
14146
  if (existing) return String(existing.id);
13716
- const result = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
14147
+ const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
13717
14148
  subscription: seat.providerSubscriptionId,
13718
14149
  price: seat.priceId,
13719
14150
  quantity: 1,
@@ -13806,14 +14237,14 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
13806
14237
  }
13807
14238
  if (seat.purchasedAt === void 0) {
13808
14239
  if (!["payment_processing", "payment_canceling"].includes(String(seat.status))) membershipFail("named_seat_payment_not_expected");
13809
- 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);
14240
+ 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);
13810
14241
  const status2 = eligible ? "renewal_pending" : "refund_pending", effectId = `${seat.id}:${eligible ? "attach-renewal" : "refund"}`;
13811
14242
  try {
13812
14243
  await db.transact([
13813
14244
  membershipUpdate("networkSeat", input.seatId, {
13814
14245
  status: status2,
13815
14246
  purchasedAt: paidAt,
13816
- acceptanceEndsAt: Math.min(paidAt + 30 * 864e5, Number(member.termEndsAt)),
14247
+ acceptanceEndsAt: Number(member.termEndsAt),
13817
14248
  termStartedAt: paidAt,
13818
14249
  termEndsAt: member.termEndsAt,
13819
14250
  paymentEventId: input.eventId,
@@ -13861,7 +14292,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
13861
14292
  if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
13862
14293
  const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
13863
14294
  requireMembershipScope(member, target);
13864
- 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");
14295
+ 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");
13865
14296
  const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
13866
14297
  const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
13867
14298
  try {
@@ -13936,7 +14367,7 @@ async function recordRecipient(db, input, now, approve) {
13936
14367
  requireMembershipScope(entitlement, input);
13937
14368
  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");
13938
14369
  if (seat.acceptedAt === void 0) {
13939
- if (approve || now >= Number(seat.acceptanceEndsAt) || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
14370
+ if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
13940
14371
  membershipFail("named_seat_recipient_verification_required");
13941
14372
  }
13942
14373
  } else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
@@ -13986,129 +14417,6 @@ async function recordRecipient(db, input, now, approve) {
13986
14417
  return membershipFail("named_seat_recipient_retry_required");
13987
14418
  }
13988
14419
 
13989
- // src/membership-seat-refund-stripe.ts
13990
- var import_stripe14 = require("@odla-ai/stripe");
13991
- var fail6 = (code = "named_seat_refund_mismatch") => {
13992
- throw Object.assign(new Error(code), { code });
13993
- };
13994
- async function refundNamedSeatStripePayment(secretKey, seat) {
13995
- if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail6();
13996
- const call = async (method, path, params, idempotencyKey) => {
13997
- const result = await (0, import_stripe14.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
13998
- if (!result.ok || !result.body) return fail6("named_seat_refund_provider_unavailable");
13999
- return result.body;
14000
- };
14001
- const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
14002
- const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
14003
- 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();
14004
- const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
14005
- 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();
14006
- const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
14007
- if (items.length > 1) fail6();
14008
- if (items.length && sub.status !== "canceled") {
14009
- const item = items[0];
14010
- if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail6();
14011
- const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
14012
- if (removed.id !== item.id || removed.deleted !== true) fail6("named_seat_renewal_removal_unconfirmed");
14013
- }
14014
- const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
14015
- if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail6("named_seat_refund_history_incomplete");
14016
- let refundedCents = 0, pendingCents = 0;
14017
- const refundIds = [];
14018
- for (const refund of list.data) {
14019
- 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();
14020
- if (refund.status === "succeeded") {
14021
- refundedCents += refund.amount;
14022
- refundIds.push(refund.id);
14023
- } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
14024
- }
14025
- if (refundedCents + pendingCents > seat.amountCents) fail6();
14026
- if (pendingCents) fail6("named_seat_refund_pending");
14027
- if (refundedCents < seat.amountCents) {
14028
- const remaining = seat.amountCents - refundedCents;
14029
- const refund = await call("POST", "/v1/refunds", {
14030
- payment_intent: seat.providerPaymentIntentId,
14031
- amount: remaining,
14032
- reason: "requested_by_customer",
14033
- metadata
14034
- }, `named-seat-refund:${seat.id}:${remaining}`);
14035
- if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail6();
14036
- if (refund.status !== "succeeded") fail6("named_seat_refund_pending");
14037
- refundedCents += remaining;
14038
- refundIds.push(refund.id);
14039
- }
14040
- return { refundedCents, refundIds };
14041
- }
14042
-
14043
- // src/membership-seat-refund.ts
14044
- async function requestNamedSeatRefund(db, input, now) {
14045
- membershipClock(now);
14046
- const state = await loadMembershipAuthorityState(db, input);
14047
- const seat = await membershipLoad(db, "networkSeat", input.seatId);
14048
- requireMembershipScope(seat, input);
14049
- const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
14050
- requireMembershipScope(member, input);
14051
- if (seat.status === "refunded" || seat.status === "refund_pending") return;
14052
- 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");
14053
- const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
14054
- await db.transact([
14055
- membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
14056
- ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
14057
- membershipUpdate("membershipEffect", `${seat.id}:refund`, {
14058
- id: `${seat.id}:refund`,
14059
- chapterId: input.chapterId,
14060
- environment: input.environment,
14061
- runtime: input.runtime,
14062
- membershipId: member.id,
14063
- seatId: seat.id,
14064
- kind: "refund_seat",
14065
- status: "pending",
14066
- attempts: 0,
14067
- createdAt: now,
14068
- updatedAt: now
14069
- }),
14070
- ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
14071
- id: `${seat.id}:revoke`,
14072
- chapterId: input.chapterId,
14073
- environment: input.environment,
14074
- runtime: input.runtime,
14075
- membershipId: member.id,
14076
- seatId: seat.id,
14077
- kind: "revoke_named_recipient_access",
14078
- status: "pending",
14079
- attempts: 0,
14080
- createdAt: now,
14081
- updatedAt: now
14082
- })] : []
14083
- ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
14084
- { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
14085
- { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
14086
- { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
14087
- ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
14088
- ] });
14089
- }
14090
- async function completeNamedSeatRefund(db, seatId, secretKey, now) {
14091
- membershipClock(now);
14092
- const seat = await membershipLoad(db, "networkSeat", seatId);
14093
- if (!seat) return membershipFail("named_seat_not_found");
14094
- await loadMembershipAuthorityState(db, seat);
14095
- if (seat.status === "refunded") return;
14096
- if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
14097
- const result = await refundNamedSeatStripePayment(secretKey, seat);
14098
- await db.transact([
14099
- membershipUpdate("networkSeat", seatId, {
14100
- status: "refunded",
14101
- refundedCents: result.refundedCents,
14102
- providerRefundIds: result.refundIds,
14103
- revision: Number(seat.revision) + 1,
14104
- updatedAt: now
14105
- }),
14106
- membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
14107
- ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
14108
- { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
14109
- ] });
14110
- }
14111
-
14112
14420
  // src/membership-seat-provider-refund.ts
14113
14421
  async function confirmNamedSeatProviderRefund(db, input, now) {
14114
14422
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
@@ -14130,7 +14438,7 @@ async function readNamedSeatInvitation(db, input, now) {
14130
14438
  await loadMembershipAuthorityState(db, target);
14131
14439
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
14132
14440
  requireMembershipScope(member, target);
14133
- 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");
14441
+ 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");
14134
14442
  return {
14135
14443
  seatId: input.seatId,
14136
14444
  primaryApplicationId: target.applicationId,
@@ -14160,7 +14468,7 @@ async function readMembershipChapterEffect(db, input, now) {
14160
14468
  requireMembershipScope(member, input);
14161
14469
  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");
14162
14470
  let applicable;
14163
- 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));
14471
+ 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));
14164
14472
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
14165
14473
  else applicable = (await readNamedSeatAccess(db, {
14166
14474
  ...input,
@@ -14189,6 +14497,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
14189
14497
  "seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
14190
14498
  "seat-payments": ["seatId", "eventId"],
14191
14499
  "seat-refunds": ["seatId", "eventId"],
14500
+ "seat-operator-refund": ["seatId"],
14192
14501
  "seat-accept": recipient,
14193
14502
  "seat-approve": recipient,
14194
14503
  "seat-decline": recipient,
@@ -14257,6 +14566,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
14257
14566
  if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
14258
14567
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
14259
14568
  }
14569
+ if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
14260
14570
  if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
14261
14571
  ...target,
14262
14572
  priceId: peer.seatPriceId,
@@ -14345,7 +14655,7 @@ function createMembershipAuthorityHandler(options) {
14345
14655
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
14346
14656
  const sender = req.headers.get(import_db15.FEDERATION_HEADERS.sender);
14347
14657
  const action = url.pathname.split("/").at(-1);
14348
- if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
14658
+ 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);
14349
14659
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
14350
14660
  if (!peer) return respond({ error: "unauthorized" }, 401);
14351
14661
  let body;
@@ -14367,13 +14677,23 @@ function createMembershipAuthorityHandler(options) {
14367
14677
  let input;
14368
14678
  try {
14369
14679
  input = JSON.parse(verified.body);
14370
- 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] ?? []];
14680
+ 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] ?? []];
14371
14681
  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])))) {
14372
14682
  throw new TypeError("invalid request");
14373
14683
  }
14374
14684
  } catch {
14375
14685
  return respond({ error: "invalid_offer_request" }, 400);
14376
14686
  }
14687
+ if (action === "offer-preview") {
14688
+ return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
14689
+ appId: peer.appId,
14690
+ chapterId: peer.chapterId,
14691
+ environment: peer.environment,
14692
+ runtime: peer.runtime,
14693
+ skuId: String(input.skuId),
14694
+ now: options.now?.() ?? Date.now()
14695
+ }) });
14696
+ }
14377
14697
  if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
14378
14698
  return respond({ ok: true, seat: await handleMembershipSeatAction(
14379
14699
  action,
@@ -14767,7 +15087,7 @@ async function drainMembershipEffects(db, options, now) {
14767
15087
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
14768
15088
  await loadMembershipAuthorityState(db, scope2);
14769
15089
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
14770
- let expired = 0, pending = 0, complete = 0, more = false;
15090
+ let pending = 0, complete = 0, more = false;
14771
15091
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
14772
15092
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
14773
15093
  more ||= seats.length > limit;
@@ -14785,26 +15105,6 @@ async function drainMembershipEffects(db, options, now) {
14785
15105
  }
14786
15106
  }
14787
15107
  }
14788
- for (const status2 of ["renewal_pending", "pending_acceptance"]) {
14789
- const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
14790
- more ||= seats.length > limit;
14791
- for (const seat of seats.slice(0, limit)) {
14792
- if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
14793
- if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
14794
- try {
14795
- await requestNamedSeatRefund(db, {
14796
- ...scope2,
14797
- applicationId: String(seat.applicationId),
14798
- skuId: String(seat.skuId),
14799
- seatId: String(seat.id),
14800
- reason: "expired"
14801
- }, now);
14802
- expired++;
14803
- } catch {
14804
- pending++;
14805
- }
14806
- }
14807
- }
14808
15108
  const effects = (await db.query({ membershipEffect: { $: { where: {
14809
15109
  chapterId: scope2.chapterId,
14810
15110
  environment: scope2.environment,
@@ -14866,7 +15166,7 @@ async function drainMembershipEffects(db, options, now) {
14866
15166
  pending++;
14867
15167
  }
14868
15168
  }
14869
- return { complete, pending, expired, more };
15169
+ return { complete, pending, more };
14870
15170
  }
14871
15171
 
14872
15172
  // src/approval-effects-retry.ts
@@ -15016,7 +15316,7 @@ function createMembershipChapterEffectRoute(options) {
15016
15316
  config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
15017
15317
  enabled: true,
15018
15318
  subject: "{{purchaserName}} invited you to {{chapterName}}",
15019
- 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."
15319
+ 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."
15020
15320
  } };
15021
15321
  const template = config.emailTemplates.namedSeatInvitation;
15022
15322
  if (!template.text.includes("{{purchaserName}}")) config.emailTemplates.namedSeatInvitation = {
@@ -15089,6 +15389,12 @@ async function requestMembershipCommercialOffer(options) {
15089
15389
  lifetimeMs: 6e4
15090
15390
  });
15091
15391
  }
15392
+ async function requestMembershipOfferPreview(options) {
15393
+ const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
15394
+ const preview = result.preview;
15395
+ 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");
15396
+ return preview;
15397
+ }
15092
15398
  async function requestMembershipCheckoutBinding(options) {
15093
15399
  const result = await requestAuthority(options, "checkouts", {
15094
15400
  applicationId: options.applicationId,
@@ -15127,6 +15433,12 @@ async function requestNamedSeatDecline(options) {
15127
15433
  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");
15128
15434
  return seat;
15129
15435
  }
15436
+ async function requestNamedSeatOperatorRefund(options) {
15437
+ const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
15438
+ const seat = result.seat;
15439
+ 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");
15440
+ return seat;
15441
+ }
15130
15442
  async function requestNamedSeatReservation(options) {
15131
15443
  const result = await requestAuthority(options, "seat-reservations", {
15132
15444
  applicationId: options.applicationId,
@@ -15144,7 +15456,7 @@ async function requestNamedSeatReservation(options) {
15144
15456
  async function requestNamedSeatOffer(options) {
15145
15457
  const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
15146
15458
  const offer = result.seat;
15147
- 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");
15459
+ 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");
15148
15460
  return offer;
15149
15461
  }
15150
15462
  async function requestNamedSeatInvitation(options) {
@@ -15312,6 +15624,7 @@ function createMembershipWorkerAdapter(options) {
15312
15624
  return {
15313
15625
  authority: {
15314
15626
  issueOffer: async (input, env) => requestMembershipCommercialOffer({ ...input, ...await transport(input, env) }),
15627
+ previewOffer: async (input, env) => requestMembershipOfferPreview({ ...input, ...await transport(input, env) }),
15315
15628
  bindCheckout: async (input, env) => requestMembershipCheckoutBinding({ ...input, ...await transport(input, env) }),
15316
15629
  confirmPayment: async (input, env) => requestMembershipPayment({ ...input, ...await transport(input, env) }),
15317
15630
  approveMembership: async (input, env) => requestMembershipApproval({ ...input, ...await transport(input, env) }),
@@ -15330,7 +15643,8 @@ function createMembershipWorkerAdapter(options) {
15330
15643
  confirmRefund: async (input, env) => requestNamedSeatRefund2({ ...input, ...await transport(input, env) }),
15331
15644
  accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
15332
15645
  approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
15333
- decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) })
15646
+ decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
15647
+ operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
15334
15648
  } } : {}
15335
15649
  },
15336
15650
  readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
@@ -15585,6 +15899,8 @@ var BUILTIN_ROUTES = [
15585
15899
  handleMembershipRestart,
15586
15900
  handleMembershipCancellation,
15587
15901
  handlePaymentRecovery,
15902
+ handleAdminNamedSeat,
15903
+ handleAdminMembershipSeats,
15588
15904
  handleAdminMeetings,
15589
15905
  handleAdminScheduling,
15590
15906
  // Roster + identity