@odla-ai/chapter 0.49.0 → 0.50.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,
@@ -1883,24 +1885,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
1883
1885
  if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
1884
1886
  return restore({ ...value, approvedFoundingCount: 0 }, expected);
1885
1887
  }
1886
- async function issueMembershipCommercialOffer(db, input) {
1887
- validate(input);
1888
- const stateId = `${input.chapterId}:${input.environment}`;
1889
- const state = await load(db, "membershipAuthorityState", stateId);
1888
+ async function previewMembershipCommercialOffer(db, input) {
1889
+ for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
1890
+ if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
1891
+ }
1892
+ if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
1893
+ const state = await authoritativeState(db, input);
1894
+ const { offering, policy } = await currentPolicy(db, input, false, state);
1895
+ const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
1896
+ if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
1897
+ const contract = createMembershipCommercialOffer({
1898
+ offering,
1899
+ discountPolicy: policy,
1900
+ approvedFoundingCount: Number(approvedFoundingCount),
1901
+ now: input.now,
1902
+ couponId: await couponId(input, policy?.id)
1903
+ });
1904
+ const discount = contract.billingTerms.discount;
1905
+ return {
1906
+ skuId: input.skuId,
1907
+ offeringVersionId: offering.id,
1908
+ basePriceCents: offering.basePriceCents,
1909
+ currency: offering.currency,
1910
+ interval: offering.interval,
1911
+ founding: contract.founding,
1912
+ ...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
1913
+ dueTodayCents: contract.dueTodayCents,
1914
+ renewalCents: contract.renewalCents,
1915
+ ...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
1916
+ ...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
1917
+ };
1918
+ }
1919
+ async function authoritativeState(db, input) {
1920
+ const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
1890
1921
  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);
1922
+ return state;
1923
+ }
1924
+ async function currentPolicy(db, input, restartAware, state) {
1893
1925
  const rows5 = await db.query({
1894
1926
  membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
1895
1927
  discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
1896
1928
  });
1897
1929
  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);
1930
+ 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));
1931
+ 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
1932
  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_"));
1933
+ const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
1902
1934
  const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
1903
1935
  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");
1936
+ return { offering, policy };
1937
+ }
1938
+ async function issueMembershipCommercialOffer(db, input) {
1939
+ validate(input);
1940
+ const stateId = `${input.chapterId}:${input.environment}`;
1941
+ const state = await authoritativeState(db, input);
1942
+ const existing = await load(db, "membershipCheckoutOffer", input.offerId);
1943
+ if (existing) return restore(existing, input);
1944
+ const { offering, policy } = await currentPolicy(db, input, true, state);
1904
1945
  const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
1905
1946
  if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
1906
1947
  const contract = createMembershipCommercialOffer({
@@ -2208,7 +2249,7 @@ function findTier(tiers, tierId) {
2208
2249
  if (!tierId) return tiers.length === 1 ? tiers[0] : null;
2209
2250
  return tiers.find((tier) => tier.id === tierId) ?? null;
2210
2251
  }
2211
- function joinConfigTiers(tiers, defaults = {}) {
2252
+ function joinConfigTiers(tiers, defaults = {}, founding = /* @__PURE__ */ new Map()) {
2212
2253
  return tiers.map((tier) => ({
2213
2254
  id: tier.id,
2214
2255
  name: tier.name,
@@ -2216,7 +2257,8 @@ function joinConfigTiers(tiers, defaults = {}) {
2216
2257
  currency: tier.currency ?? defaults.currency ?? "usd",
2217
2258
  interval: tier.interval ?? defaults.interval ?? "year",
2218
2259
  blurb: tier.blurb ?? "",
2219
- free: tierIsFree(tier)
2260
+ free: tierIsFree(tier),
2261
+ ...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
2220
2262
  }));
2221
2263
  }
2222
2264
 
@@ -3451,6 +3493,267 @@ var handlePaymentRecovery = async (req, url, env, ctx) => {
3451
3493
  }
3452
3494
  };
3453
3495
 
3496
+ // src/worker-routes-admin-named-seat.ts
3497
+ var PATH = /^\/api\/admin\/applications\/([0-9a-f-]+)\/named-seat(\/refund)?$/;
3498
+ var REFUNDABLE = /* @__PURE__ */ new Set(["renewal_pending", "pending_acceptance", "pending_approval", "pending_primary", "active"]);
3499
+ var handleAdminNamedSeat = async (req, url, env, ctx) => {
3500
+ const match = url.pathname.match(PATH);
3501
+ if (!match) return null;
3502
+ const seats = ctx.membershipAuthority?.namedSeats;
3503
+ if (ctx.chapter.mode !== "chapter" || !seats) return json({ error: "named seats are not configured" }, 404);
3504
+ const refund = Boolean(match[2]);
3505
+ if (req.method !== (refund ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3506
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3507
+ if (!user) return json({ error: "unauthorized" }, 401);
3508
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
3509
+ const db = raw2, app = await membershipLoad(db, "applications", match[1]);
3510
+ if (!app || !chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME) || app.namedSeatId) return json({ error: "not found" }, 404);
3511
+ const intent = await membershipLoad(db, "subscriptionCheckoutIntents", String(app.id));
3512
+ if (!intent) return json({ error: "paid membership required" }, 409);
3513
+ try {
3514
+ const target = await membershipTargetForIntent(intent, app, env, ctx);
3515
+ const offer = await seats.offer(target, env);
3516
+ const seat = offer.seat ?? null, refundable = Boolean(seat && REFUNDABLE.has(seat.status));
3517
+ if (!refund) {
3518
+ return json({
3519
+ seat,
3520
+ amountCents: offer.amountCents,
3521
+ currency: offer.currency,
3522
+ termEndsAt: offer.termEndsAt,
3523
+ purchaseEndsAt: offer.purchaseEndsAt,
3524
+ refundable
3525
+ });
3526
+ }
3527
+ if (!seat) return json({ error: "no purchased seat", code: "seat_not_found" }, 409);
3528
+ if (seat.status === "refunded") {
3529
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
3530
+ return json({ ok: true, seatId: seat.seatId, status: "refunded", replayed: true });
3531
+ }
3532
+ if (!refundable) return json({ error: `seat is ${seat.status.replaceAll("_", " ")}`, code: "seat_not_refundable" }, 409);
3533
+ const receipt = await seats.operatorRefund({ ...target, seatId: seat.seatId }, env);
3534
+ await closeRecipientApplication(db, env, ctx, seat.seatId);
3535
+ return json({ ok: true, ...receipt });
3536
+ } catch {
3537
+ return json({ error: "named seat service unavailable; retry to confirm" }, 503);
3538
+ }
3539
+ };
3540
+ async function closeRecipientApplication(db, env, ctx, seatId) {
3541
+ const recipient2 = (await db.query({ applications: { $: { where: { namedSeatId: seatId }, limit: 1 } } })).applications?.[0];
3542
+ if (!recipient2 || !chapterBillingApplication(recipient2, ctx.chapter.id, env.ODLA_RUNTIME) || recipient2.status === "refunded") return;
3543
+ const attrs = { status: "refunded", renewalAt: 0, canceled: true };
3544
+ await db.transact(
3545
+ [{ t: "update", ns: "applications", id: String(recipient2.id), attrs }],
3546
+ { mutationId: `verified-refund:${String(recipient2.id)}:${seatId}` }
3547
+ );
3548
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...recipient2, ...attrs }, stage: "refunded" }).catch(() => void 0);
3549
+ }
3550
+
3551
+ // src/membership-seat-refund-stripe.ts
3552
+ var import_stripe6 = require("@odla-ai/stripe");
3553
+ var fail3 = (code = "named_seat_refund_mismatch") => {
3554
+ throw Object.assign(new Error(code), { code });
3555
+ };
3556
+ async function refundNamedSeatStripePayment(secretKey, seat) {
3557
+ if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail3();
3558
+ const call = async (method, path, params, idempotencyKey) => {
3559
+ const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
3560
+ if (!result.ok || !result.body) return fail3("named_seat_refund_provider_unavailable");
3561
+ return result.body;
3562
+ };
3563
+ const metadata = { ...stripePartitionMetadata(seat), applicationId: seat.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
3564
+ const payment = await call("GET", `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`);
3565
+ 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();
3566
+ const sub = await call("GET", `/v1/subscriptions/${encodeURIComponent(seat.providerSubscriptionId)}`);
3567
+ 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();
3568
+ const items = sub.items.data.filter((item) => item.metadata?.seatId === seat.id);
3569
+ if (items.length > 1) fail3();
3570
+ if (items.length && sub.status !== "canceled") {
3571
+ const item = items[0];
3572
+ if (!/^si_[A-Za-z0-9_]+$/.test(item.id) || item.price?.id !== seat.priceId || item.quantity !== 1) fail3();
3573
+ const removed = await call("DELETE", `/v1/subscription_items/${encodeURIComponent(item.id)}`, { proration_behavior: "none" });
3574
+ if (removed.id !== item.id || removed.deleted !== true) fail3("named_seat_renewal_removal_unconfirmed");
3575
+ }
3576
+ const list = await call("GET", "/v1/refunds", { payment_intent: seat.providerPaymentIntentId, limit: 100 });
3577
+ if (!Array.isArray(list.data) || list.has_more !== false || list.data.length >= 100) fail3("named_seat_refund_history_incomplete");
3578
+ let refundedCents = 0, pendingCents = 0;
3579
+ const refundIds = [];
3580
+ for (const refund of list.data) {
3581
+ 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();
3582
+ if (refund.status === "succeeded") {
3583
+ refundedCents += refund.amount;
3584
+ refundIds.push(refund.id);
3585
+ } else if (refund.status === "pending" || refund.status === "requires_action") pendingCents += refund.amount;
3586
+ }
3587
+ if (refundedCents + pendingCents > seat.amountCents) fail3();
3588
+ if (pendingCents) fail3("named_seat_refund_pending");
3589
+ if (refundedCents < seat.amountCents) {
3590
+ const remaining = seat.amountCents - refundedCents;
3591
+ const refund = await call("POST", "/v1/refunds", {
3592
+ payment_intent: seat.providerPaymentIntentId,
3593
+ amount: remaining,
3594
+ reason: "requested_by_customer",
3595
+ metadata
3596
+ }, `named-seat-refund:${seat.id}:${remaining}`);
3597
+ if (refund.payment_intent !== seat.providerPaymentIntentId || refund.amount !== remaining || refund.currency !== seat.currency || !/^re_[A-Za-z0-9_]+$/.test(refund.id)) fail3();
3598
+ if (refund.status !== "succeeded") fail3("named_seat_refund_pending");
3599
+ refundedCents += remaining;
3600
+ refundIds.push(refund.id);
3601
+ }
3602
+ return { refundedCents, refundIds };
3603
+ }
3604
+
3605
+ // src/membership-seat-refund.ts
3606
+ async function requestNamedSeatRefund(db, input, now) {
3607
+ membershipClock(now);
3608
+ const state = await loadMembershipAuthorityState(db, input);
3609
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
3610
+ requireMembershipScope(seat, input);
3611
+ const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
3612
+ requireMembershipScope(member, input);
3613
+ if (seat.status === "refunded" || seat.status === "refund_pending") return;
3614
+ 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");
3615
+ const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
3616
+ await db.transact([
3617
+ membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
3618
+ ...entitlement ? [membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now })] : [],
3619
+ membershipUpdate("membershipEffect", `${seat.id}:refund`, {
3620
+ id: `${seat.id}:refund`,
3621
+ chapterId: input.chapterId,
3622
+ environment: input.environment,
3623
+ runtime: input.runtime,
3624
+ membershipId: member.id,
3625
+ seatId: seat.id,
3626
+ kind: "refund_seat",
3627
+ status: "pending",
3628
+ attempts: 0,
3629
+ createdAt: now,
3630
+ updatedAt: now
3631
+ }),
3632
+ ...seat.recipientUserId ? [membershipUpdate("membershipEffect", `${seat.id}:revoke`, {
3633
+ id: `${seat.id}:revoke`,
3634
+ chapterId: input.chapterId,
3635
+ environment: input.environment,
3636
+ runtime: input.runtime,
3637
+ membershipId: member.id,
3638
+ seatId: seat.id,
3639
+ kind: "revoke_named_recipient_access",
3640
+ status: "pending",
3641
+ attempts: 0,
3642
+ createdAt: now,
3643
+ updatedAt: now
3644
+ })] : []
3645
+ ], { mutationId: `named-seat-refund-request:${seat.id}`, guards: [
3646
+ { ns: "networkSeat", id: input.seatId, exists: true, equals: { revision: seat.revision } },
3647
+ { ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
3648
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3649
+ ...entitlement ? [{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } }] : []
3650
+ ] });
3651
+ }
3652
+ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
3653
+ membershipClock(now);
3654
+ const seat = await membershipLoad(db, "networkSeat", seatId);
3655
+ if (!seat) return membershipFail("named_seat_not_found");
3656
+ await loadMembershipAuthorityState(db, seat);
3657
+ if (seat.status === "refunded") return;
3658
+ if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
3659
+ const result = await refundNamedSeatStripePayment(secretKey, seat);
3660
+ await db.transact([
3661
+ membershipUpdate("networkSeat", seatId, {
3662
+ status: "refunded",
3663
+ refundedCents: result.refundedCents,
3664
+ providerRefundIds: result.refundIds,
3665
+ revision: Number(seat.revision) + 1,
3666
+ updatedAt: now
3667
+ }),
3668
+ membershipUpdate("membershipEffect", `${seatId}:refund`, { status: "complete", updatedAt: now })
3669
+ ], { mutationId: `named-seat-refunded:${seatId}`, guards: [
3670
+ { ns: "networkSeat", id: seatId, exists: true, equals: { status: "refund_pending", revision: seat.revision } }
3671
+ ] });
3672
+ }
3673
+
3674
+ // src/membership-seat-operator-refund.ts
3675
+ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
3676
+ const seat = await membershipLoad(db, "networkSeat", input.seatId);
3677
+ requireMembershipScope(seat, input);
3678
+ await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
3679
+ await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
3680
+ const refunded = await membershipLoad(db, "networkSeat", input.seatId);
3681
+ if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
3682
+ throw new Error("named seat refund unconfirmed");
3683
+ }
3684
+ return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
3685
+ }
3686
+
3687
+ // src/worker-routes-admin-membership-seats.ts
3688
+ var LIST = "/api/admin/membership-seats";
3689
+ var ONE = /^\/api\/admin\/membership-seats\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,159})\/refund$/;
3690
+ var PUBLIC = [
3691
+ "id",
3692
+ "appId",
3693
+ "chapterId",
3694
+ "environment",
3695
+ "runtime",
3696
+ "applicationId",
3697
+ "skuId",
3698
+ "status",
3699
+ "amountCents",
3700
+ "currency",
3701
+ "recipientName",
3702
+ "recipientEmailNormalized",
3703
+ "purchaserUserId",
3704
+ "purchasedAt",
3705
+ "acceptedAt",
3706
+ "journeyApprovedAt",
3707
+ "refundedCents",
3708
+ "refundReason",
3709
+ "termEndsAt",
3710
+ "updatedAt"
3711
+ ];
3712
+ var project = (row2) => Object.fromEntries(PUBLIC.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]]));
3713
+ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
3714
+ const one = url.pathname.match(ONE);
3715
+ if (url.pathname !== LIST && !one) return null;
3716
+ const operator = ctx.membershipAuthorityOperator;
3717
+ if (!operator) return json({ error: "not found" }, 404);
3718
+ if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
3719
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
3720
+ if (!user) return json({ error: "unauthorized" }, 401);
3721
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
3722
+ let db;
3723
+ try {
3724
+ db = operator.makeDb(env);
3725
+ } catch {
3726
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
3727
+ }
3728
+ if (!one) {
3729
+ const rows5 = (await db.query({ networkSeat: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 201 } } })).networkSeat ?? [];
3730
+ return json({ seats: rows5.slice(0, 200).map(project), truncated: rows5.length > 200 });
3731
+ }
3732
+ const seat = await membershipLoad(db, "networkSeat", one[1]);
3733
+ if (!seat || seat.environment !== env.ODLA_ENV) return json({ error: "not found" }, 404);
3734
+ if (seat.status === "refunded") {
3735
+ return json({ ok: true, seatId: seat.id, status: "refunded", refundedCents: seat.refundedCents, subscriptionCanceled: false, replayed: true });
3736
+ }
3737
+ if (!seat.purchasedAt) return json({ error: "seat has no completed payment to refund", code: "seat_not_refundable" }, 409);
3738
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
3739
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
3740
+ try {
3741
+ const receipt = await refundNamedSeatByOperator(db, {
3742
+ appId: String(seat.appId),
3743
+ chapterId: String(seat.chapterId),
3744
+ environment: String(seat.environment),
3745
+ runtime: String(seat.runtime),
3746
+ applicationId: String(seat.applicationId),
3747
+ skuId: String(seat.skuId),
3748
+ seatId: String(seat.id)
3749
+ }, secret, Date.now());
3750
+ return json({ ok: true, ...receipt });
3751
+ } catch (error) {
3752
+ const code = error.code;
3753
+ return json({ error: "seat refund is pending; retry to confirm", ...code ? { code } : {} }, code ? 409 : 503);
3754
+ }
3755
+ };
3756
+
3454
3757
  // src/worker-routes-membership-cancel.ts
3455
3758
  var handleMembershipCancellation = async (req, url, env, ctx) => {
3456
3759
  const match = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/cancel-renewal$/);
@@ -4047,6 +4350,29 @@ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
4047
4350
  }
4048
4351
  }
4049
4352
 
4353
+ // src/join-founding-preview.ts
4354
+ async function foundingPreviews(tiers, env, ctx) {
4355
+ const result = /* @__PURE__ */ new Map();
4356
+ const preview = ctx.membershipAuthority?.previewOffer;
4357
+ if (!preview) return result;
4358
+ const scope2 = { appId: env.ODLA_APP_ID, chapterId: ctx.chapter.id, environment: env.ODLA_ENV, runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME) };
4359
+ await Promise.all(tiers.filter((tier) => !tierIsFree(tier) && tier.stripePriceId).map(async (tier) => {
4360
+ try {
4361
+ const offer = await preview({ ...scope2, skuId: tier.id }, env);
4362
+ if (!offer.founding || !offer.cohortLabel || offer.basePriceCents !== tier.priceCents || offer.dueTodayCents >= offer.basePriceCents) return;
4363
+ result.set(tier.id, {
4364
+ cohortLabel: offer.cohortLabel,
4365
+ dueTodayCents: offer.dueTodayCents,
4366
+ discountCents: offer.basePriceCents - offer.dueTodayCents,
4367
+ renewalCents: offer.renewalCents,
4368
+ ...offer.discountDuration ? { discountDuration: offer.discountDuration } : {}
4369
+ });
4370
+ } catch {
4371
+ }
4372
+ }));
4373
+ return result;
4374
+ }
4375
+
4050
4376
  // src/admission-grants-contract.ts
4051
4377
  var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
4052
4378
  var ADMISSION_GRANTS_PATH = "/api/admission-grants";
@@ -4573,10 +4899,24 @@ async function materializeSignupControl(db, envelope, previous, receivedAt) {
4573
4899
  }
4574
4900
 
4575
4901
  // src/payment-quote.ts
4902
+ function tierPresentationFor(document, tierId) {
4903
+ const compareAt = document?.membershipPage?.tiers?.find((tier) => tier.tierId === tierId)?.compareAtPriceCents;
4904
+ return Number.isSafeInteger(compareAt) && Number(compareAt) > 0 ? { compareAtPriceCents: Number(compareAt) } : void 0;
4905
+ }
4906
+ function foundingOf(offer) {
4907
+ return offer?.contract?.founding === true && offer.contract.discountPolicy ? { cohortLabel: offer.contract.discountPolicy.cohortLabel } : void 0;
4908
+ }
4576
4909
  async function createPaymentQuote(input) {
4577
4910
  const { group, tier, policy } = input;
4578
4911
  const terms2 = input.billingTerms ? subscriptionBillingTerms(input.billingTerms) : void 0;
4912
+ const compareAt = input.presentation?.compareAtPriceCents;
4913
+ const presentation = Number.isSafeInteger(compareAt) && Number(compareAt) > tier.priceCents ? { compareAtPriceCents: Number(compareAt) } : void 0;
4579
4914
  const dueToday = terms2 ? subscriptionAmountDue({ priceCents: tier.priceCents, billingTerms: terms2 }) : void 0;
4915
+ const lines = dueToday === void 0 ? paymentLineItems(group, tier) : {
4916
+ standardCents: tier.priceCents,
4917
+ discountCents: tier.priceCents - dueToday,
4918
+ dueTodayCents: dueToday
4919
+ };
4580
4920
  const quote = {
4581
4921
  applicationId: input.applicationId,
4582
4922
  chapterId: input.chapterId,
@@ -4584,11 +4924,12 @@ async function createPaymentQuote(input) {
4584
4924
  tierName: tier.name,
4585
4925
  currency: tier.currency ?? String(group.currency ?? "usd"),
4586
4926
  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
- },
4927
+ lineItems: presentation ? {
4928
+ standardCents: presentation.compareAtPriceCents,
4929
+ discountCents: presentation.compareAtPriceCents - lines.dueTodayCents,
4930
+ dueTodayCents: lines.dueTodayCents
4931
+ } : lines,
4932
+ ...presentation ? { presentation } : {},
4592
4933
  refundPolicyText: policy?.refundPolicyText ?? String(group.refundPolicyText ?? ""),
4593
4934
  merchantDisclosureText: String(group.merchantDisclosureText ?? ""),
4594
4935
  trustCopy: policy ? policy.trustCopy ?? "" : String(group.trustCopy ?? ""),
@@ -4613,37 +4954,39 @@ async function createPaymentQuote(input) {
4613
4954
  });
4614
4955
  const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(contract));
4615
4956
  const digest = [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
4616
- return { ...quote, digest };
4957
+ return { ...quote, digest, ...input.founding ? { founding: input.founding } : {} };
4617
4958
  }
4618
4959
 
4619
4960
  // src/membership-quote-projection.ts
4620
4961
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
4621
4962
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
4622
- var fail3 = (code) => {
4963
+ var fail4 = (code) => {
4623
4964
  throw Object.assign(new Error(code), { code });
4624
4965
  };
4625
4966
  var load3 = async (db, ns, id6) => (await db.query({ [ns]: { $: { where: { id: id6 }, limit: 1 } } }))[ns]?.[0];
4626
4967
  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");
4968
+ 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");
4969
+ if (row2.status !== "ready") return fail4("membership_quote_pending");
4629
4970
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
4630
4971
  ...target,
4631
4972
  requestId: row2.requestId
4632
4973
  }), now, lifetimeMs: 6e4 });
4633
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail3("membership_quote_expired");
4974
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail4("membership_quote_expired");
4634
4975
  const tier = row2.tier, quote = row2.quote;
4635
4976
  const offering = offer.contract.offering;
4636
4977
  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");
4978
+ return fail4("membership_catalog_mismatch");
4638
4979
  }
4639
4980
  const expected = await createPaymentQuote({
4640
4981
  ...target,
4641
4982
  tier,
4642
4983
  group: { merchantDisclosureText: quote.merchantDisclosureText },
4643
4984
  policy: { refundPolicyText: quote.refundPolicyText, trustCopy: quote.trustCopy },
4644
- billingTerms: offer.contract.billingTerms
4985
+ billingTerms: offer.contract.billingTerms,
4986
+ presentation: quote.presentation,
4987
+ founding: foundingOf(offer)
4645
4988
  });
4646
- if (quote.digest !== expected.digest) return fail3("membership_quote_conflict");
4989
+ if (quote.digest !== expected.digest) return fail4("membership_quote_conflict");
4647
4990
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
4648
4991
  }
4649
4992
  async function readMembershipQuote(db, target, now) {
@@ -4651,9 +4994,9 @@ async function readMembershipQuote(db, target, now) {
4651
4994
  }
4652
4995
  async function prepareMembershipQuote(db, input, issue) {
4653
4996
  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");
4997
+ 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
4998
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id6);
4656
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail3("membership_quote_conflict");
4999
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail4("membership_quote_conflict");
4657
5000
  if (row2?.status === "ready") {
4658
5001
  try {
4659
5002
  return await restore2(row2, input, now);
@@ -4698,7 +5041,7 @@ async function prepareMembershipQuote(db, input, issue) {
4698
5041
  }
4699
5042
  if (row2?.status === "ready") return restore2(row2, input, now);
4700
5043
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
4701
- return fail3("membership_quote_pending");
5044
+ return fail4("membership_quote_pending");
4702
5045
  }
4703
5046
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
4704
5047
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -4712,7 +5055,15 @@ async function prepareMembershipQuote(db, input, issue) {
4712
5055
  currency: input.tier.currency ?? String(input.group.currency ?? "usd"),
4713
5056
  interval: input.tier.interval ?? (input.group.interval === "month" ? "month" : "year")
4714
5057
  };
4715
- const quote = await createPaymentQuote({ ...target, tier, group: input.group, policy: input.policy, billingTerms: offer.contract.billingTerms });
5058
+ const quote = await createPaymentQuote({
5059
+ ...target,
5060
+ tier,
5061
+ group: input.group,
5062
+ policy: input.policy,
5063
+ billingTerms: offer.contract.billingTerms,
5064
+ presentation: input.presentation,
5065
+ founding: foundingOf(offer)
5066
+ });
4716
5067
  const ready = { ...row2, offer, tier, quote, status: "ready", updatedAt: now };
4717
5068
  await restore2(ready, target, now, true);
4718
5069
  try {
@@ -4767,7 +5118,9 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4767
5118
  group: { merchantDisclosureText: quote2.merchantDisclosureText },
4768
5119
  tier: tier2,
4769
5120
  policy: { refundPolicyText: quote2.refundPolicyText, trustCopy: quote2.trustCopy },
4770
- billingTerms
5121
+ billingTerms,
5122
+ presentation: quote2.presentation,
5123
+ founding: foundingOf(intent.membershipOffer)
4771
5124
  });
4772
5125
  if (expected.digest !== quote2.digest) throw new Error("invalid commercial snapshot");
4773
5126
  return { tier: tier2, quote: expected, billingTerms, membershipOffer: intent.membershipOffer };
@@ -4794,9 +5147,10 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4794
5147
  if (!tier || tierIsFree(tier) || !tier.stripePriceId) throw Object.assign(new Error("tier unavailable"), { code: "tier_unresolved" });
4795
5148
  const control3 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4796
5149
  const policy = control3?.document.policy?.tierPolicies?.[tier.id];
5150
+ const presentation = tierPresentationFor(control3?.document, tier.id);
4797
5151
  if (ctx.membershipAuthority) return prepareMembershipQuote(
4798
5152
  db,
4799
- { ...target, app, group, tier, policy, now: Date.now() },
5153
+ { ...target, app, group, tier, policy, presentation, now: Date.now() },
4800
5154
  (input) => ctx.membershipAuthority.issueOffer(input, env)
4801
5155
  );
4802
5156
  const quote = await createPaymentQuote({
@@ -4807,7 +5161,8 @@ async function checkoutQuoteForApplication(db, app, group, env, ctx, prepare = f
4807
5161
  runtime: stripeRuntimeIdentity(env.ODLA_RUNTIME),
4808
5162
  group,
4809
5163
  tier,
4810
- policy
5164
+ policy,
5165
+ presentation
4811
5166
  });
4812
5167
  return { tier, quote, billingTerms: void 0 };
4813
5168
  }
@@ -5461,10 +5816,11 @@ var handleMember = async (req, url, env, ctx) => {
5461
5816
  group,
5462
5817
  Boolean(stripeKey)
5463
5818
  );
5819
+ const founding = await foundingPreviews(offered, env, ctx);
5464
5820
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
5465
5821
  const application = controlledApplication(chapter.application, activeControl?.document);
5466
5822
  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),
5823
+ ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding), application.conditions),
5468
5824
  fieldContract: chapterFlowFieldContract(chapter, "join", application),
5469
5825
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
5470
5826
  ...activeControl ? { signupControl: {
@@ -7157,7 +7513,7 @@ async function deliverAdmissionGrants(options) {
7157
7513
  }
7158
7514
 
7159
7515
  // src/worker-routes-network-admission.ts
7160
- var PATH = "/api/admin/network/admissions";
7516
+ var PATH2 = "/api/admin/network/admissions";
7161
7517
  var RETRY_PATH = "/api/admin/network/admissions/retry";
7162
7518
  var REVOKE_PATH = "/api/admin/network/admissions/revoke";
7163
7519
  var MUTATION_ID2 = /^[A-Za-z0-9_-]{16,128}$/;
@@ -7237,11 +7593,11 @@ async function parseBody(req) {
7237
7593
  }
7238
7594
  }
7239
7595
  var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
7240
- if (![PATH, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7596
+ if (![PATH2, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
7241
7597
  const auth = await gate2(req, env, ctx);
7242
7598
  if ("response" in auth) return auth.response;
7243
7599
  const db = auth.db;
7244
- if (url.pathname === PATH && req.method === "GET") {
7600
+ if (url.pathname === PATH2 && req.method === "GET") {
7245
7601
  const targetId2 = url.searchParams.get("targetId") ?? "";
7246
7602
  const target2 = targetFor(ctx, targetId2);
7247
7603
  if (!target2) return reject6(404, "unknown_target", "admission target is not configured");
@@ -7485,28 +7841,28 @@ var handleSchedule = async (req, url, env, ctx) => {
7485
7841
  };
7486
7842
 
7487
7843
  // src/payments-stripe.ts
7488
- var import_stripe9 = require("@odla-ai/stripe");
7844
+ var import_stripe10 = require("@odla-ai/stripe");
7489
7845
 
7490
7846
  // src/payments-refund.ts
7491
- var import_stripe6 = require("@odla-ai/stripe");
7847
+ var import_stripe7 = require("@odla-ai/stripe");
7492
7848
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
7493
7849
  var id2 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
7494
7850
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
7495
- function fail4(code = "refund_target_mismatch") {
7851
+ function fail5(code = "refund_target_mismatch") {
7496
7852
  throw Object.assign(new Error(code), { code });
7497
7853
  }
7498
7854
  async function refundSubscriptionPayment(secretKey, input) {
7499
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail4();
7855
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail5();
7500
7856
  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");
7857
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
7858
+ if (!result.ok) fail5("stripe_error");
7859
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail5("stripe_invalid_response");
7504
7860
  return result.body;
7505
7861
  };
7506
7862
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
7507
- if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail4();
7863
+ if (id2(sub) !== input.subscriptionId || id2(sub.customer) !== input.customerId) fail5();
7508
7864
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
7509
- if (row(sub.metadata)[key3] !== value) fail4();
7865
+ if (row(sub.metadata)[key3] !== value) fail5();
7510
7866
  }
7511
7867
  let cursor;
7512
7868
  let earliest;
@@ -7519,69 +7875,69 @@ async function refundSubscriptionPayment(secretKey, input) {
7519
7875
  limit: 100,
7520
7876
  ...cursor ? { starting_after: cursor } : {}
7521
7877
  });
7522
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail4("stripe_invalid_response");
7878
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail5("stripe_invalid_response");
7523
7879
  for (const value of list.data) {
7524
7880
  const invoice2 = row(value);
7525
7881
  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();
7882
+ 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
7883
  seen.add(id2(invoice2));
7528
7884
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
7529
7885
  }
7530
7886
  if (!list.has_more) break;
7531
- if (!list.data.length || page === 9) fail4("refund_history_incomplete");
7887
+ if (!list.data.length || page === 9) fail5("refund_history_incomplete");
7532
7888
  cursor = id2(list.data[list.data.length - 1]);
7533
7889
  }
7534
- if (!earliest) fail4("no_charge");
7890
+ if (!earliest) fail5("no_charge");
7535
7891
  const invoice = earliest;
7536
- if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail4();
7892
+ if (input.expectedInvoiceId !== void 0 && id2(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail5();
7537
7893
  let chargeId = id2(invoice.charge);
7538
7894
  let paymentIntentId = id2(invoice.payment_intent);
7539
7895
  if (!chargeId && !paymentIntentId) {
7540
7896
  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");
7897
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail5("refund_payment_ambiguous");
7542
7898
  const payment = row(payments.data[0]);
7543
- if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail4();
7899
+ if (id2(payment.invoice) !== id2(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail5();
7544
7900
  paymentIntentId = id2(row(payment.payment).payment_intent);
7545
7901
  }
7546
7902
  if (!chargeId) {
7547
- if (!providerId2(paymentIntentId, "pi")) fail4();
7903
+ if (!providerId2(paymentIntentId, "pi")) fail5();
7548
7904
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
7549
- if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail4();
7905
+ if (id2(payment) !== paymentIntentId || id2(payment.customer) !== input.customerId || payment.status !== "succeeded") fail5();
7550
7906
  chargeId = id2(payment.latest_charge);
7551
7907
  }
7552
- if (!providerId2(chargeId, "ch")) fail4();
7908
+ if (!providerId2(chargeId, "ch")) fail5();
7553
7909
  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();
7910
+ 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();
7911
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail5();
7556
7912
  if (charge.amount_refunded !== charge.amount) {
7557
7913
  const refund = await call("POST", "/v1/refunds", {
7558
7914
  charge: chargeId,
7559
7915
  ...input.metadata ? { metadata: input.metadata } : {}
7560
7916
  }, `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");
7917
+ 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
7918
  }
7563
7919
  let subscriptionCanceled = sub.status === "canceled";
7564
7920
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
7565
7921
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
7566
- if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail4("refund_cancel_incomplete");
7922
+ if (id2(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail5("refund_cancel_incomplete");
7567
7923
  subscriptionCanceled = true;
7568
7924
  }
7569
7925
  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");
7926
+ 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
7927
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
7572
7928
  }
7573
7929
 
7574
7930
  // src/subscription-payment-identity.ts
7575
- var import_stripe8 = require("@odla-ai/stripe");
7931
+ var import_stripe9 = require("@odla-ai/stripe");
7576
7932
 
7577
7933
  // src/subscription-provider-contract.ts
7578
- var import_stripe7 = require("@odla-ai/stripe");
7934
+ var import_stripe8 = require("@odla-ai/stripe");
7579
7935
  function subscriptionContractMismatch() {
7580
7936
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
7581
7937
  }
7582
7938
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
7583
7939
  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)}`);
7940
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
7585
7941
  const price = result.body;
7586
7942
  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
7943
  return typeof price.product === "string" ? price.product : void 0;
@@ -7600,7 +7956,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7600
7956
  let paymentId = id3(invoice.payment_intent);
7601
7957
  if (!paymentId) {
7602
7958
  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 });
7959
+ const result2 = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id3(invoice), limit: 100 });
7604
7960
  const list = result2.body;
7605
7961
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
7606
7962
  const link = list.data[0];
@@ -7609,11 +7965,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7609
7965
  }
7610
7966
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
7611
7967
  const path = `/v1/payment_intents/${paymentId}`;
7612
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", path);
7968
+ const result = await (0, import_stripe9.stripeRequest)({ secretKey }, "GET", path);
7613
7969
  const payment = result.body;
7614
7970
  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
7971
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
7616
- const tagged = await (0, import_stripe8.stripeRequest)(
7972
+ const tagged = await (0, import_stripe9.stripeRequest)(
7617
7973
  { secretKey },
7618
7974
  "POST",
7619
7975
  path,
@@ -7625,7 +7981,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
7625
7981
 
7626
7982
  // src/payments-stripe.ts
7627
7983
  async function stripeCall(sk, method, path, params, idempotencyKey) {
7628
- return (0, import_stripe9.stripeRequest)(
7984
+ return (0, import_stripe10.stripeRequest)(
7629
7985
  { secretKey: sk },
7630
7986
  method,
7631
7987
  path,
@@ -7633,7 +7989,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
7633
7989
  idempotencyKey ? { idempotencyKey } : {}
7634
7990
  );
7635
7991
  }
7636
- function fail5(op, r) {
7992
+ function fail6(op, r) {
7637
7993
  const err = new Error(`stripe ${op} failed: ${r.status}`);
7638
7994
  err.code = "stripe_error";
7639
7995
  throw err;
@@ -7683,7 +8039,7 @@ function createStripeProvider(config) {
7683
8039
  { email: input.email, name: input.name, metadata: meta },
7684
8040
  `cus:${input.applicationId}`
7685
8041
  );
7686
- if (!cust.ok) fail5("customer create", cust);
8042
+ if (!cust.ok) fail6("customer create", cust);
7687
8043
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
7688
8044
  customerId = cust.body.id;
7689
8045
  }
@@ -7715,7 +8071,7 @@ function createStripeProvider(config) {
7715
8071
  // retry between create and the db write can't orphan a second one.
7716
8072
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
7717
8073
  );
7718
- if (!sub.ok) fail5("subscription create", sub);
8074
+ if (!sub.ok) fail6("subscription create", sub);
7719
8075
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
7720
8076
  const subscriptionId = sub.body.id;
7721
8077
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -7725,14 +8081,14 @@ function createStripeProvider(config) {
7725
8081
  "expand[2]": "discounts",
7726
8082
  "expand[3]": "latest_invoice.discounts"
7727
8083
  });
7728
- if (!expanded.ok) fail5("subscription invoice evidence", expanded);
8084
+ if (!expanded.ok) fail6("subscription invoice evidence", expanded);
7729
8085
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
7730
8086
  sub = expanded;
7731
8087
  }
7732
8088
  verifySubscriptionResult(sub.body, input, customerId, meta);
7733
8089
  const invoice = sub.body.latest_invoice;
7734
8090
  const clientSecret2 = clientSecretOf(sub.body);
7735
- if (!clientSecret2) fail5("subscription confirmation-secret missing", sub);
8091
+ if (!clientSecret2) fail6("subscription confirmation-secret missing", sub);
7736
8092
  await tagSubscriptionPayment(
7737
8093
  sk,
7738
8094
  invoice,
@@ -7744,7 +8100,7 @@ function createStripeProvider(config) {
7744
8100
  return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
7745
8101
  },
7746
8102
  async ingestWebhook(rawBody, sigHeader) {
7747
- if (!webhookSecret || !await (0, import_stripe9.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
8103
+ if (!webhookSecret || !await (0, import_stripe10.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
7748
8104
  return { ok: false, reason: "bad_signature" };
7749
8105
  }
7750
8106
  const event = JSON.parse(rawBody);
@@ -8404,7 +8760,7 @@ async function namedSeatOfferDigest(offer) {
8404
8760
  "named-only",
8405
8761
  "full-upfront",
8406
8762
  "auto-renew-coterm",
8407
- "30-day-unaccepted-full-refund"
8763
+ "no-automatic-refund"
8408
8764
  ])));
8409
8765
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
8410
8766
  }
@@ -8809,6 +9165,23 @@ async function updateChapterPerson(opts) {
8809
9165
  });
8810
9166
  }
8811
9167
 
9168
+ // src/application-actions.ts
9169
+ function isApplicationStage(ctx, stage) {
9170
+ return ctx.chapter.pipeline.stages.includes(stage);
9171
+ }
9172
+ function applicationActionStages(app, env, ctx) {
9173
+ if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return [];
9174
+ const from = String(app.status ?? "");
9175
+ if (app.canceled === true || ["refunded", "canceled"].includes(from)) return [];
9176
+ return ctx.chapter.pipeline.stages.filter((to) => {
9177
+ if (to === from) return false;
9178
+ if (to === "approved") return canApprove(from, ctx.chapter.pipeline) && from !== "declined";
9179
+ if (to === "refunded") return Boolean(app.namedSeatId || app.stripeCustomerId && app.stripeSubscriptionId) && (!ctx.chapter.operations.refund.allowedFrom || ctx.chapter.operations.refund.allowedFrom.includes(from));
9180
+ if (ctx.membershipAuthority && ["submitted", "paid_pending_vetting", "declined", "canceled"].includes(to)) return false;
9181
+ return canTransition(from, to, ctx.chapter.pipeline);
9182
+ });
9183
+ }
9184
+
8812
9185
  // src/worker-routes-people-profile.ts
8813
9186
  var handlePeopleProfile = async (req, url, env, ctx) => {
8814
9187
  if (ctx.chapter.mode !== "chapter") return null;
@@ -8830,7 +9203,16 @@ var handlePeopleProfile = async (req, url, env, ctx) => {
8830
9203
  const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, id6);
8831
9204
  if (!record4) return json({ error: "not found" }, 404);
8832
9205
  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);
9206
+ if (match?.[2]) {
9207
+ const linked = typeof record4.fields.applicationId === "string" && record4.fields.applicationId !== "";
9208
+ let to = "";
9209
+ try {
9210
+ to = String((await req.clone().json()).to ?? "");
9211
+ } catch {
9212
+ }
9213
+ if (!linked && to && !isApplicationStage(ctx, to)) return null;
9214
+ return json({ error: "use the application lifecycle workflow", code: "application_workflow_required" }, 409);
9215
+ }
8834
9216
  if (req.method === "GET" && dedicated) {
8835
9217
  const excluded = profileReadOnlyFields(record4, ctx);
8836
9218
  const fields = Object.entries(ctx.chapter.crm.type(record4.type).fields).filter(([id7]) => !excluded.includes(id7)).map(([id7, field]) => ({
@@ -9521,20 +9903,6 @@ var handleAdminBilling = async (req, url, env, ctx) => {
9521
9903
  // src/worker-routes-admin-lifecycle.ts
9522
9904
  var import_calendar3 = require("@odla-ai/calendar");
9523
9905
 
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
9906
  // src/refund-application.ts
9539
9907
  async function refundApplication(db, env, ctx, app) {
9540
9908
  if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
@@ -9725,6 +10093,12 @@ var handleAdminRefund = async (req, url, env, ctx) => {
9725
10093
  const app = await loadApp(db, m[1]);
9726
10094
  if (!app) return json({ error: "not found" }, 404);
9727
10095
  if (!chapterBillingApplication(app, ctx.chapter.id, env.ODLA_RUNTIME)) return json({ error: "not found" }, 404);
10096
+ if (app.namedSeatId) {
10097
+ const actor = await ctx.verifyUser(req, env);
10098
+ if (!actor || !await ctx.isSuperAdminEmail(db, actor.email, actor.userId)) {
10099
+ return json({ error: "super-admin required", code: "super_admin_required" }, 403);
10100
+ }
10101
+ }
9728
10102
  return refundApplication(db, env, ctx, app);
9729
10103
  };
9730
10104
  var handleAdminApplicationPatch = async (req, url, env, ctx) => {
@@ -10334,7 +10708,7 @@ async function operationalReaderRecord(record4, reader, db, env, ctx) {
10334
10708
  const appId = record4.fields.applicationId;
10335
10709
  const app = typeof appId === "string" ? (await db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })).applications?.[0] : void 0;
10336
10710
  if (reader.stageTransitions.includes(record4.type)) {
10337
- result.edit.allowedStages = app ? applicationActionStages(app, env, ctx).map((id6) => ({ id: id6, label: id6.replaceAll("_", " ") })) : [];
10711
+ 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
10712
  const lock = (await db.query({ crm_activity: { $: { where: { id: `network-action-lock:${record4.id}` }, limit: 1 } } })).crm_activity?.[0];
10339
10713
  const meta = lock?.meta;
10340
10714
  if (meta?.state === "pending" && typeof meta.activityId === "string") {
@@ -10552,7 +10926,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
10552
10926
  }
10553
10927
  };
10554
10928
  try {
10555
- if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person") {
10929
+ if (hasStage && ctx.chapter.mode === "chapter" && record4.type === "person" && (record4.fields.applicationId || isApplicationStage(ctx, input.stage))) {
10556
10930
  const updated2 = await networkApplicationAction({
10557
10931
  db,
10558
10932
  env,
@@ -11908,7 +12282,7 @@ async function revealNetworkPersonRegistryRecord(db, input, mutationId, actor, n
11908
12282
  }
11909
12283
 
11910
12284
  // src/worker-routes-network-person-registry.ts
11911
- var PATH2 = "/api/admin/network-person-registry";
12285
+ var PATH3 = "/api/admin/network-person-registry";
11912
12286
  var MAX_BODY2 = 8 * 1024 * 1024;
11913
12287
  var MUTATION_ID4 = /^[A-Za-z0-9_-]{16,128}$/;
11914
12288
  var positiveRevision = (value) => {
@@ -11933,7 +12307,7 @@ function registryError(error) {
11933
12307
  return null;
11934
12308
  }
11935
12309
  var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
11936
- if (url.pathname !== PATH2) return null;
12310
+ if (url.pathname !== PATH3) return null;
11937
12311
  if (!ctx.chapter.networkPersonRegistry) return json({ error: "not found" }, 404);
11938
12312
  if (req.method !== "GET" && req.method !== "POST") return json({ error: "method not allowed" }, 405);
11939
12313
  if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY2) {
@@ -12108,7 +12482,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
12108
12482
  };
12109
12483
 
12110
12484
  // src/worker-routes-network-commercial.ts
12111
- var import_stripe10 = require("@odla-ai/stripe");
12485
+ var import_stripe11 = require("@odla-ai/stripe");
12112
12486
 
12113
12487
  // src/worker-routes-network-commercial-parity.ts
12114
12488
  var import_db13 = require("@odla-ai/db");
@@ -12484,7 +12858,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
12484
12858
  return json({ error: "commercial webhook is not configured" }, 503);
12485
12859
  }
12486
12860
  try {
12487
- const event = await (0, import_stripe10.constructStripeEvent)(body, signature, secret);
12861
+ const event = await (0, import_stripe11.constructStripeEvent)(body, signature, secret);
12488
12862
  const fact = normalizeNetworkCommercialStripeEvent(event, await stripeEventDigest(body));
12489
12863
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
12490
12864
  const result = await ingestNetworkCommercialFact(db, ctx.chapter.networkCommercialReadModel.sources, fact, Date.now(), await stripePayloadDigest(body));
@@ -12493,7 +12867,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
12493
12867
  }
12494
12868
  return json({ ok: true, status: result.status, eventId: fact.eventId });
12495
12869
  } catch (error) {
12496
- if (error instanceof import_stripe10.StripeError) return json({ error: error.message, code: error.code }, 400);
12870
+ if (error instanceof import_stripe11.StripeError) return json({ error: error.message, code: error.code }, 400);
12497
12871
  throw error;
12498
12872
  }
12499
12873
  };
@@ -12535,7 +12909,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12535
12909
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
12536
12910
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
12537
12911
  try {
12538
- const stripe = (0, import_stripe10.initStripe)({ secretKey });
12912
+ const stripe = (0, import_stripe11.initStripe)({ secretKey });
12539
12913
  const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
12540
12914
  listStripeObjects(stripe, "/v1/customers"),
12541
12915
  listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
@@ -12549,7 +12923,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
12549
12923
  ...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
12550
12924
  });
12551
12925
  } catch (error) {
12552
- if (error instanceof import_stripe10.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
12926
+ if (error instanceof import_stripe11.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
12553
12927
  return json({ error: "Stripe reconciliation failed" }, 502);
12554
12928
  }
12555
12929
  }
@@ -13129,7 +13503,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
13129
13503
  };
13130
13504
 
13131
13505
  // src/membership-cancel-authority.ts
13132
- var import_stripe11 = require("@odla-ai/stripe");
13506
+ var import_stripe12 = require("@odla-ai/stripe");
13133
13507
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
13134
13508
  membershipClock(now);
13135
13509
  const id6 = await networkMembershipId(target);
@@ -13140,14 +13514,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
13140
13514
  membershipFail("membership_cancellation_unavailable");
13141
13515
  }
13142
13516
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
13143
- const read2 = await (0, import_stripe11.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
13517
+ const read2 = await (0, import_stripe12.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
13144
13518
  const sub = read2.body;
13145
13519
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
13146
13520
  ...stripePartitionMetadata(target),
13147
13521
  applicationId: target.applicationId
13148
13522
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
13149
13523
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
13150
- const write = await (0, import_stripe11.stripeRequest)(
13524
+ const write = await (0, import_stripe12.stripeRequest)(
13151
13525
  { secretKey: stripeSecret },
13152
13526
  "POST",
13153
13527
  path,
@@ -13369,7 +13743,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
13369
13743
  }
13370
13744
 
13371
13745
  // src/membership-provider-lifecycle.ts
13372
- var import_stripe12 = require("@odla-ai/stripe");
13746
+ var import_stripe13 = require("@odla-ai/stripe");
13373
13747
 
13374
13748
  // src/membership-renewal-authority.ts
13375
13749
  async function reconcileMembershipRenewal(db, input, now) {
@@ -13527,7 +13901,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
13527
13901
  }
13528
13902
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
13529
13903
  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))}`);
13904
+ const result = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
13531
13905
  const sub = result.body;
13532
13906
  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
13907
  membershipFail("membership_billing_provider_mismatch");
@@ -13642,7 +14016,7 @@ async function reserveNamedMembershipSeat(db, input, now) {
13642
14016
  }
13643
14017
 
13644
14018
  // src/membership-seat-stripe.ts
13645
- var import_stripe13 = require("@odla-ai/stripe");
14019
+ var import_stripe14 = require("@odla-ai/stripe");
13646
14020
  var reject7 = () => {
13647
14021
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
13648
14022
  };
@@ -13654,7 +14028,7 @@ var metadataFor = (seat) => ({
13654
14028
  });
13655
14029
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
13656
14030
  async function read(sk, path, params) {
13657
- const response = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "GET", path, params);
14031
+ const response = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "GET", path, params);
13658
14032
  if (!response.ok) return reject7();
13659
14033
  return response.body;
13660
14034
  }
@@ -13691,11 +14065,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
13691
14065
  }
13692
14066
  await subscriptionForSeat(sk, seat);
13693
14067
  const metadata = metadataFor(seat);
13694
- const result = seat.providerPaymentIntentId ? await (0, import_stripe13.stripeRequest)(
14068
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe14.stripeRequest)(
13695
14069
  { secretKey: sk },
13696
14070
  "GET",
13697
14071
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
13698
- ) : await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
14072
+ ) : await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
13699
14073
  amount: seat.amountCents,
13700
14074
  currency: seat.currency,
13701
14075
  customer: seat.providerCustomerId,
@@ -13713,7 +14087,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
13713
14087
  const sub = await subscriptionForSeat(sk, seat);
13714
14088
  const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
13715
14089
  if (existing) return String(existing.id);
13716
- const result = await (0, import_stripe13.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
14090
+ const result = await (0, import_stripe14.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
13717
14091
  subscription: seat.providerSubscriptionId,
13718
14092
  price: seat.priceId,
13719
14093
  quantity: 1,
@@ -13813,7 +14187,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
13813
14187
  membershipUpdate("networkSeat", input.seatId, {
13814
14188
  status: status2,
13815
14189
  purchasedAt: paidAt,
13816
- acceptanceEndsAt: Math.min(paidAt + 30 * 864e5, Number(member.termEndsAt)),
14190
+ acceptanceEndsAt: Number(member.termEndsAt),
13817
14191
  termStartedAt: paidAt,
13818
14192
  termEndsAt: member.termEndsAt,
13819
14193
  paymentEventId: input.eventId,
@@ -13861,7 +14235,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
13861
14235
  if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
13862
14236
  const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
13863
14237
  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");
14238
+ 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
14239
  const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
13866
14240
  const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
13867
14241
  try {
@@ -13936,7 +14310,7 @@ async function recordRecipient(db, input, now, approve) {
13936
14310
  requireMembershipScope(entitlement, input);
13937
14311
  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
14312
  if (seat.acceptedAt === void 0) {
13939
- if (approve || now >= Number(seat.acceptanceEndsAt) || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
14313
+ if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
13940
14314
  membershipFail("named_seat_recipient_verification_required");
13941
14315
  }
13942
14316
  } else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
@@ -13986,129 +14360,6 @@ async function recordRecipient(db, input, now, approve) {
13986
14360
  return membershipFail("named_seat_recipient_retry_required");
13987
14361
  }
13988
14362
 
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
14363
  // src/membership-seat-provider-refund.ts
14113
14364
  async function confirmNamedSeatProviderRefund(db, input, now) {
14114
14365
  const seat = await membershipLoad(db, "networkSeat", input.seatId);
@@ -14130,7 +14381,7 @@ async function readNamedSeatInvitation(db, input, now) {
14130
14381
  await loadMembershipAuthorityState(db, target);
14131
14382
  const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
14132
14383
  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");
14384
+ 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
14385
  return {
14135
14386
  seatId: input.seatId,
14136
14387
  primaryApplicationId: target.applicationId,
@@ -14160,7 +14411,7 @@ async function readMembershipChapterEffect(db, input, now) {
14160
14411
  requireMembershipScope(member, input);
14161
14412
  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
14413
  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));
14414
+ 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
14415
  else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
14165
14416
  else applicable = (await readNamedSeatAccess(db, {
14166
14417
  ...input,
@@ -14189,6 +14440,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
14189
14440
  "seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
14190
14441
  "seat-payments": ["seatId", "eventId"],
14191
14442
  "seat-refunds": ["seatId", "eventId"],
14443
+ "seat-operator-refund": ["seatId"],
14192
14444
  "seat-accept": recipient,
14193
14445
  "seat-approve": recipient,
14194
14446
  "seat-decline": recipient,
@@ -14257,6 +14509,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
14257
14509
  if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
14258
14510
  return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
14259
14511
  }
14512
+ if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
14260
14513
  if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
14261
14514
  ...target,
14262
14515
  priceId: peer.seatPriceId,
@@ -14345,7 +14598,7 @@ function createMembershipAuthorityHandler(options) {
14345
14598
  if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
14346
14599
  const sender = req.headers.get(import_db15.FEDERATION_HEADERS.sender);
14347
14600
  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);
14601
+ 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
14602
  const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
14350
14603
  if (!peer) return respond({ error: "unauthorized" }, 401);
14351
14604
  let body;
@@ -14367,13 +14620,23 @@ function createMembershipAuthorityHandler(options) {
14367
14620
  let input;
14368
14621
  try {
14369
14622
  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] ?? []];
14623
+ 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
14624
  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
14625
  throw new TypeError("invalid request");
14373
14626
  }
14374
14627
  } catch {
14375
14628
  return respond({ error: "invalid_offer_request" }, 400);
14376
14629
  }
14630
+ if (action === "offer-preview") {
14631
+ return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
14632
+ appId: peer.appId,
14633
+ chapterId: peer.chapterId,
14634
+ environment: peer.environment,
14635
+ runtime: peer.runtime,
14636
+ skuId: String(input.skuId),
14637
+ now: options.now?.() ?? Date.now()
14638
+ }) });
14639
+ }
14377
14640
  if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
14378
14641
  return respond({ ok: true, seat: await handleMembershipSeatAction(
14379
14642
  action,
@@ -14767,7 +15030,7 @@ async function drainMembershipEffects(db, options, now) {
14767
15030
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
14768
15031
  await loadMembershipAuthorityState(db, scope2);
14769
15032
  const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
14770
- let expired = 0, pending = 0, complete = 0, more = false;
15033
+ let pending = 0, complete = 0, more = false;
14771
15034
  for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
14772
15035
  const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
14773
15036
  more ||= seats.length > limit;
@@ -14785,26 +15048,6 @@ async function drainMembershipEffects(db, options, now) {
14785
15048
  }
14786
15049
  }
14787
15050
  }
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
15051
  const effects = (await db.query({ membershipEffect: { $: { where: {
14809
15052
  chapterId: scope2.chapterId,
14810
15053
  environment: scope2.environment,
@@ -14866,7 +15109,7 @@ async function drainMembershipEffects(db, options, now) {
14866
15109
  pending++;
14867
15110
  }
14868
15111
  }
14869
- return { complete, pending, expired, more };
15112
+ return { complete, pending, more };
14870
15113
  }
14871
15114
 
14872
15115
  // src/approval-effects-retry.ts
@@ -15016,7 +15259,7 @@ function createMembershipChapterEffectRoute(options) {
15016
15259
  config.emailTemplates = { ...config.emailTemplates, namedSeatInvitation: config.emailTemplates?.namedSeatInvitation ?? {
15017
15260
  enabled: true,
15018
15261
  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."
15262
+ 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
15263
  } };
15021
15264
  const template = config.emailTemplates.namedSeatInvitation;
15022
15265
  if (!template.text.includes("{{purchaserName}}")) config.emailTemplates.namedSeatInvitation = {
@@ -15089,6 +15332,12 @@ async function requestMembershipCommercialOffer(options) {
15089
15332
  lifetimeMs: 6e4
15090
15333
  });
15091
15334
  }
15335
+ async function requestMembershipOfferPreview(options) {
15336
+ const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
15337
+ const preview = result.preview;
15338
+ 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");
15339
+ return preview;
15340
+ }
15092
15341
  async function requestMembershipCheckoutBinding(options) {
15093
15342
  const result = await requestAuthority(options, "checkouts", {
15094
15343
  applicationId: options.applicationId,
@@ -15127,6 +15376,12 @@ async function requestNamedSeatDecline(options) {
15127
15376
  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
15377
  return seat;
15129
15378
  }
15379
+ async function requestNamedSeatOperatorRefund(options) {
15380
+ const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
15381
+ const seat = result.seat;
15382
+ 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");
15383
+ return seat;
15384
+ }
15130
15385
  async function requestNamedSeatReservation(options) {
15131
15386
  const result = await requestAuthority(options, "seat-reservations", {
15132
15387
  applicationId: options.applicationId,
@@ -15312,6 +15567,7 @@ function createMembershipWorkerAdapter(options) {
15312
15567
  return {
15313
15568
  authority: {
15314
15569
  issueOffer: async (input, env) => requestMembershipCommercialOffer({ ...input, ...await transport(input, env) }),
15570
+ previewOffer: async (input, env) => requestMembershipOfferPreview({ ...input, ...await transport(input, env) }),
15315
15571
  bindCheckout: async (input, env) => requestMembershipCheckoutBinding({ ...input, ...await transport(input, env) }),
15316
15572
  confirmPayment: async (input, env) => requestMembershipPayment({ ...input, ...await transport(input, env) }),
15317
15573
  approveMembership: async (input, env) => requestMembershipApproval({ ...input, ...await transport(input, env) }),
@@ -15330,7 +15586,8 @@ function createMembershipWorkerAdapter(options) {
15330
15586
  confirmRefund: async (input, env) => requestNamedSeatRefund2({ ...input, ...await transport(input, env) }),
15331
15587
  accept: async (input, env) => requestNamedSeatAcceptance({ ...input, ...await transport(input, env) }),
15332
15588
  approve: async (input, env) => requestNamedSeatApproval({ ...input, ...await transport(input, env) }),
15333
- decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) })
15589
+ decline: async (input, env) => requestNamedSeatDecline({ ...input, ...await transport(input, env) }),
15590
+ operatorRefund: async (input, env) => requestNamedSeatOperatorRefund({ ...input, ...await transport(input, env) })
15334
15591
  } } : {}
15335
15592
  },
15336
15593
  readEffect: async (input, env) => requestMembershipChapterEffect({ ...input, ...await transport(input, env) })
@@ -15585,6 +15842,8 @@ var BUILTIN_ROUTES = [
15585
15842
  handleMembershipRestart,
15586
15843
  handleMembershipCancellation,
15587
15844
  handlePaymentRecovery,
15845
+ handleAdminNamedSeat,
15846
+ handleAdminMembershipSeats,
15588
15847
  handleAdminMeetings,
15589
15848
  handleAdminScheduling,
15590
15849
  // Roster + identity