@odla-ai/chapter 0.58.1 → 0.59.1

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.
@@ -246,12 +246,37 @@ function networkCommercialAlerts(ctx, route) {
246
246
  };
247
247
  }
248
248
 
249
+ // src/worker-defer.ts
250
+ function deferWork(ctx, req, task) {
251
+ return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
252
+ }
253
+ function createDeferral() {
254
+ const executions = /* @__PURE__ */ new WeakMap();
255
+ const settle = (task) => task().then(() => void 0, (error) => {
256
+ console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
257
+ });
258
+ return {
259
+ /** Remember the runtime's ExecutionContext for this request. */
260
+ bind(req, execution) {
261
+ if (execution) executions.set(req, execution);
262
+ },
263
+ /** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
264
+ defer(req, task) {
265
+ const execution = executions.get(req);
266
+ if (!execution) return settle(task);
267
+ execution.waitUntil(settle(task));
268
+ return Promise.resolve();
269
+ }
270
+ };
271
+ }
272
+
249
273
  // src/worker-context.ts
250
274
  var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
251
275
  function createWorkerContext(options) {
252
276
  const { chapter } = options;
253
277
  const auth = chapter.auth;
254
278
  const crmBase = options.crmBasePath ?? "/api/crm";
279
+ const deferral = createDeferral();
255
280
  let publicConfigCache = null;
256
281
  const jwksByIssuer = /* @__PURE__ */ new Map();
257
282
  async function getPublicConfig(env) {
@@ -356,6 +381,8 @@ function createWorkerContext(options) {
356
381
  membershipAuthority: options.membershipAuthority,
357
382
  membershipAuthorityOperator: options.membershipAuthorityOperator,
358
383
  recordError: options.recordError ?? consoleErrorRecorder,
384
+ bindExecution: deferral.bind,
385
+ defer: deferral.defer,
359
386
  getPublicConfig,
360
387
  verifyUser,
361
388
  makeDb,
@@ -683,6 +710,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
683
710
  else if (eventType.startsWith("refund.")) objectKind = "refund";
684
711
  else if (eventType === "charge.refunded") objectKind = "refund";
685
712
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
713
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
686
714
  const customerId = refId(object2.customer, "cus_");
687
715
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
688
716
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -692,6 +720,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
692
720
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
693
721
  const currency = text(object2.currency)?.toLowerCase();
694
722
  const status2 = text(object2.status);
723
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
695
724
  return {
696
725
  eventId,
697
726
  digest,
@@ -710,6 +739,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
710
739
  ...invoiceId ? { invoiceId } : {},
711
740
  ...chargeId ? { chargeId } : {},
712
741
  ...paymentIntentId ? { paymentIntentId } : {},
742
+ ...disputeReason ? { disputeReason } : {},
713
743
  ...price,
714
744
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
715
745
  ...status2 ? { status: status2 } : {},
@@ -752,6 +782,7 @@ function ownRef(fact) {
752
782
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
753
783
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
754
784
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
785
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
755
786
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
756
787
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
757
788
  }
@@ -807,6 +838,7 @@ function projectNetworkCommercialEvents(input, sources) {
807
838
  continue;
808
839
  }
809
840
  const source = assignments.get(fact.eventId);
841
+ if (!source && fact.objectKind === "dispute") continue;
810
842
  if (!source) {
811
843
  const direct = directSource(fact, sources);
812
844
  const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -823,6 +855,7 @@ function projectNetworkCommercialEvents(input, sources) {
823
855
  const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
824
856
  const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
825
857
  const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
858
+ const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
826
859
  const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
827
860
  for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
828
861
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -843,6 +876,7 @@ function projectNetworkCommercialEvents(input, sources) {
843
876
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
844
877
  return {
845
878
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
879
+ ...disputes.length ? { disputes } : {},
846
880
  customers,
847
881
  subscriptions,
848
882
  invoices,
@@ -1179,6 +1213,17 @@ function findGiftRef(obj) {
1179
1213
  }
1180
1214
  function normalizeWebhookEvent(event2, options = {}) {
1181
1215
  const obj = event2.data?.object ?? {};
1216
+ if (event2.type.startsWith("charge.dispute.")) {
1217
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
1218
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
1219
+ return {
1220
+ kind: "disputed",
1221
+ chargeId,
1222
+ disputeId: obj.id,
1223
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
1224
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
1225
+ };
1226
+ }
1182
1227
  if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
1183
1228
  const gift = findGiftRef(obj);
1184
1229
  if (gift.giftIntentId) {
@@ -1588,9 +1633,9 @@ async function backfillCrm(deps) {
1588
1633
  }
1589
1634
 
1590
1635
  // src/email-retirement.ts
1591
- function isRetiredEmailCopy(subject, text8) {
1636
+ function isRetiredEmailCopy(subject, text10) {
1592
1637
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1593
- const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1638
+ const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1594
1639
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1595
1640
  }
1596
1641
 
@@ -1668,10 +1713,10 @@ function planDelivery(input) {
1668
1713
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1669
1714
  if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1670
1715
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1671
- const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
1716
+ const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
1672
1717
 
1673
1718
  ` + renderedText : renderedText;
1674
- return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
1719
+ return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
1675
1720
  }
1676
1721
 
1677
1722
  // src/notify.ts
@@ -1819,8 +1864,8 @@ function formatInstant(at2, timeZone) {
1819
1864
  var adminUrlFor = (origin) => `${origin}/admin/`;
1820
1865
  var membersUrlFor = (origin) => `${origin}/members/`;
1821
1866
  function applicantVariables(app) {
1822
- const text8 = (v) => typeof v === "string" ? v : "";
1823
- return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
1867
+ const text10 = (v) => typeof v === "string" ? v : "";
1868
+ return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
1824
1869
  }
1825
1870
  function sampleVariables(origin, timeZone, now = Date.now()) {
1826
1871
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -2927,7 +2972,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
2927
2972
  if (!intent) throw new Error("membership checkout reconciliation pending");
2928
2973
  const target = await membershipTargetForIntent(intent, app, env, ctx);
2929
2974
  const row2 = await ctx.membershipAuthority.reconcileRenewal({ ...target, eventId }, env);
2930
- if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
2975
+ if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
2931
2976
  throw new Error("invalid network renewal receipt");
2932
2977
  }
2933
2978
  return row2;
@@ -3552,6 +3597,163 @@ function paymentLineItems(group, tier) {
3552
3597
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
3553
3598
  }
3554
3599
 
3600
+ // src/membership-dispute-authority.ts
3601
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
3602
+
3603
+ // src/membership-dispute-transition.ts
3604
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
3605
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
3606
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
3607
+ function disputeTransition(row2, fact, eventId, now, terminal) {
3608
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
3609
+ const sameDispute = row2.disputeId === fact.objectId;
3610
+ if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
3611
+ const won = DISPUTE_WON_STATUSES.includes(status2);
3612
+ const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
3613
+ const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
3614
+ const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
3615
+ return {
3616
+ status: next,
3617
+ disputeId: fact.objectId,
3618
+ disputeStatus: status2,
3619
+ disputeEventId: eventId,
3620
+ disputeClosedAt: closed ? now : 0,
3621
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
3622
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
3623
+ ...sameDispute ? {} : { disputedAt: now },
3624
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
3625
+ revision: Number(row2.revision) + 1,
3626
+ updatedAt: now
3627
+ };
3628
+ }
3629
+
3630
+ // src/membership-seat-dispute.ts
3631
+ var text2 = (value) => typeof value === "string" && value ? value : void 0;
3632
+ var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
3633
+ var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text2(seat.disputeStatus) ? { disputeStatus: text2(seat.disputeStatus) } : {}, applied });
3634
+ function disputedChargeCoversSeat(charge, seat) {
3635
+ return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
3636
+ }
3637
+ async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
3638
+ if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
3639
+ const membership = async (applied, seat) => {
3640
+ const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
3641
+ requireMembershipScope(member, input);
3642
+ return {
3643
+ membershipId: id7,
3644
+ status: String(member.status),
3645
+ termEndsAt: Number(member.termEndsAt),
3646
+ graceEndsAt: Number(member.graceEndsAt ?? 0),
3647
+ autoRenew: member.autoRenew === true,
3648
+ applied,
3649
+ ...text2(member.disputeStatus) ? { disputeStatus: text2(member.disputeStatus) } : {},
3650
+ ...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
3651
+ };
3652
+ };
3653
+ for (let attempt = 0; attempt < 8; attempt++) {
3654
+ const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
3655
+ if (!seat) return membership(false);
3656
+ requireMembershipScope(seat, input);
3657
+ if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
3658
+ if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
3659
+ const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
3660
+ if (!attrs) return membership(false, seat);
3661
+ try {
3662
+ await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
3663
+ { ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
3664
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3665
+ { ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
3666
+ evidence.guard
3667
+ ] });
3668
+ } catch (error) {
3669
+ if (error.code !== "transact_guard_failed") throw error;
3670
+ continue;
3671
+ }
3672
+ const saved = await membershipLoad(db, "networkSeat", String(seat.id));
3673
+ requireMembershipScope(saved, input);
3674
+ return membership(true, saved);
3675
+ }
3676
+ return membershipFail("membership_dispute_retry_required");
3677
+ }
3678
+
3679
+ // src/membership-dispute-authority.ts
3680
+ var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
3681
+ var text3 = (value) => typeof value === "string" && value ? value : void 0;
3682
+ var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
3683
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
3684
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
3685
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
3686
+ const charge = await stripeRequest4(config, "GET", `/v1/charges/${chargeId}`);
3687
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
3688
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
3689
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
3690
+ let subscriptionId;
3691
+ if (invoiceId) {
3692
+ const invoice = await stripeRequest4(config, "GET", `/v1/invoices/${invoiceId}`);
3693
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
3694
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
3695
+ }
3696
+ const seatId = text3(charge.body.metadata?.seatId);
3697
+ return {
3698
+ chargeId,
3699
+ customerId,
3700
+ ...invoiceId ? { invoiceId } : {},
3701
+ ...subscriptionId ? { subscriptionId } : {},
3702
+ ...paymentIntentId ? { paymentIntentId } : {},
3703
+ ...seatId ? { seatId } : {}
3704
+ };
3705
+ }
3706
+ function disputedChargeCoversMembership(charge, member) {
3707
+ return charge.customerId === member.providerCustomerId && !charge.seatId && (charge.invoiceId !== void 0 && charge.invoiceId === member.providerInitialInvoiceId || charge.subscriptionId !== void 0 && charge.subscriptionId === member.providerSubscriptionId || charge.paymentIntentId !== void 0 && charge.paymentIntentId === member.rechargePaymentIntentId);
3708
+ }
3709
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3710
+ membershipClock(now);
3711
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3712
+ const fact = row2?.fact;
3713
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3714
+ if (!fact || fact.objectKind !== "dispute" || fact.eventId !== input.eventId || fact.digest !== row2.digest || !fact.chargeId || !fact.objectId || fact.livemode !== (input.environment === "prod")) membershipFail("membership_billing_evidence_pending");
3715
+ const id7 = await networkMembershipId(input);
3716
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
3717
+ membershipId: id7,
3718
+ status: String(value.status),
3719
+ termEndsAt: Number(value.termEndsAt),
3720
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
3721
+ autoRenew: value.autoRenew === true,
3722
+ ...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
3723
+ applied
3724
+ });
3725
+ const known = await membershipLoad(db, "networkMembership", id7);
3726
+ requireMembershipScope(known, input);
3727
+ if (known.disputeEventId === input.eventId) return receipt(known);
3728
+ const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
3729
+ if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
3730
+ const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
3731
+ if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
3732
+ if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
3733
+ for (let attempt = 0; attempt < 8; attempt++) {
3734
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
3735
+ requireMembershipScope(member, input);
3736
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
3737
+ const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
3738
+ if (!attrs) return receipt(member, false);
3739
+ try {
3740
+ await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
3741
+ { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
3742
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3743
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
3744
+ evidenceGuard
3745
+ ] });
3746
+ } catch (error) {
3747
+ if (error.code !== "transact_guard_failed") throw error;
3748
+ continue;
3749
+ }
3750
+ const saved = await membershipLoad(db, "networkMembership", id7);
3751
+ requireMembershipScope(saved, input);
3752
+ return receipt(saved, true);
3753
+ }
3754
+ return membershipFail("membership_dispute_retry_required");
3755
+ }
3756
+
3555
3757
  // src/member-system-contract.ts
3556
3758
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3557
3759
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3676,7 +3878,8 @@ async function firstRow2(db, ns, q) {
3676
3878
  }
3677
3879
  async function reconcileChapterStripeEvent(input, env, ctx) {
3678
3880
  const db = ctx.makeDb(env);
3679
- const { rawBody, eventId, event: event2 } = input;
3881
+ const { rawBody, eventId } = input;
3882
+ let { event: event2 } = input;
3680
3883
  if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
3681
3884
  const digest = await stripeEventDigest(rawBody);
3682
3885
  const legacyDigest = await stripePayloadDigest(rawBody);
@@ -3732,6 +3935,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3732
3935
  }
3733
3936
  return json({ ok: true, matched: Boolean(claim) });
3734
3937
  }
3938
+ if (event2.kind === "disputed" && !event2.customerId) {
3939
+ const secret = await getVaultSecret(db, "stripe_secret_key");
3940
+ const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
3941
+ if (charge) event2 = { ...event2, customerId: charge.customerId };
3942
+ }
3735
3943
  const app = await findPaymentApplication(db, event2);
3736
3944
  if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
3737
3945
  reportStripePartition("warn", eventId, event2, "ignore_sibling");
@@ -3769,11 +3977,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3769
3977
  }
3770
3978
  let authoritativeEvent = event2;
3771
3979
  let membershipProjection = {};
3772
- if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
3980
+ if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
3773
3981
  try {
3774
3982
  const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
3775
3983
  membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
3776
- authoritativeEvent = receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
3984
+ authoritativeEvent = event2.kind === "disputed" ? event2 : receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
3777
3985
  } catch {
3778
3986
  return json({ error: "network membership renewal reconciliation pending" }, 503);
3779
3987
  }
@@ -4422,10 +4630,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
4422
4630
  }
4423
4631
 
4424
4632
  // src/membership-seat-refund-stripe.ts
4425
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4633
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4426
4634
 
4427
4635
  // src/membership-checkout-seat-refund.ts
4428
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4636
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4429
4637
  var fail4 = (code = "named_seat_refund_mismatch") => {
4430
4638
  throw Object.assign(new Error(code), { code });
4431
4639
  };
@@ -4434,7 +4642,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
4434
4642
  const invoiceRef = seat.combinedInvoice;
4435
4643
  if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
4436
4644
  const call = async (method, path, params, key3) => {
4437
- const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4645
+ const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4438
4646
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4439
4647
  return result.body;
4440
4648
  };
@@ -4507,7 +4715,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
4507
4715
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
4508
4716
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
4509
4717
  const call = async (method, path, params, idempotencyKey) => {
4510
- const result = await stripeRequest5({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4718
+ const result = await stripeRequest6({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4511
4719
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
4512
4720
  return result.body;
4513
4721
  };
@@ -4649,6 +4857,9 @@ function namedSeatRecipientState(seat) {
4649
4857
  return seat.refundReason === "declined" ? "declined" : "refunded";
4650
4858
  case "canceled":
4651
4859
  return "canceled";
4860
+ // A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
4861
+ case "payment_disputed":
4862
+ return "disputed";
4652
4863
  default:
4653
4864
  return "payment_pending";
4654
4865
  }
@@ -4975,15 +5186,232 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4975
5186
  }
4976
5187
  };
4977
5188
 
5189
+ // src/membership-dispute-recharge.ts
5190
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5191
+ var NS = { membership: "networkMembership", seat: "networkSeat" };
5192
+ var fail6 = (code, extra = {}) => {
5193
+ throw Object.assign(new Error(code), { code, ...extra });
5194
+ };
5195
+ var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
5196
+ async function defaultPaymentMethod(config, subscriptionId, customerId) {
5197
+ const subscription = await stripeRequest7(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
5198
+ if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
5199
+ const onSubscription = refOf2(subscription.body.default_payment_method);
5200
+ if (onSubscription) return onSubscription;
5201
+ const customer = await stripeRequest7(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
5202
+ if (!customer.ok) fail6("membership_dispute_provider_unavailable");
5203
+ return refOf2(customer.body.invoice_settings?.default_payment_method);
5204
+ }
5205
+ async function rechargeDisputed(db, kind, id7, secretKey, now) {
5206
+ membershipClock(now);
5207
+ const ns = NS[kind];
5208
+ let row2 = await membershipLoad(db, ns, id7);
5209
+ if (!row2) fail6("membership_not_found");
5210
+ const scope2 = { chapterId: String(row2.chapterId), environment: String(row2.environment) };
5211
+ const membershipId = kind === "membership" ? id7 : String(row2.primaryMembershipId);
5212
+ const amountCents = Number(row2.disputeAmountCents ?? row2.amountCents), currency = String(row2.currency);
5213
+ const done = (value, replayed) => ({
5214
+ membershipId,
5215
+ ...kind === "seat" ? { seatId: id7 } : {},
5216
+ status: String(value.status),
5217
+ paymentIntentId: String(value.rechargePaymentIntentId),
5218
+ amountCents,
5219
+ currency,
5220
+ ...replayed ? { replayed } : {}
5221
+ });
5222
+ if (row2.status !== MEMBERSHIP_DISPUTED_STATUS) {
5223
+ if (row2.disputeStatus === "recharged" && row2.rechargePaymentIntentId) return done(row2, true);
5224
+ fail6("membership_not_disputed");
5225
+ }
5226
+ if (row2.disputeStatus !== "lost") fail6("membership_dispute_open");
5227
+ if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
5228
+ let attempt = row2.rechargeAttempt;
5229
+ if (!attempt || attempt.finishedAt !== void 0) {
5230
+ attempt = { key: `${kind}-recharge:${id7}:${String(row2.disputeId)}:${now}`, startedAt: now };
5231
+ await db.transact(
5232
+ [membershipUpdate(ns, id7, { rechargeAttempt: attempt, revision: Number(row2.revision) + 1, updatedAt: now })],
5233
+ { mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{ ns, id: id7, exists: true, equals: { revision: row2.revision, status: MEMBERSHIP_DISPUTED_STATUS } }] }
5234
+ );
5235
+ row2 = await membershipLoad(db, ns, id7);
5236
+ if (!row2 || row2.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
5237
+ }
5238
+ const config = { secretKey };
5239
+ const paymentMethod = await defaultPaymentMethod(config, String(row2.providerSubscriptionId), String(row2.providerCustomerId));
5240
+ if (!paymentMethod) fail6("recharge_payment_method_unavailable");
5241
+ const intent = await stripeRequest7(config, "POST", "/v1/payment_intents", {
5242
+ amount: amountCents,
5243
+ currency,
5244
+ customer: String(row2.providerCustomerId),
5245
+ payment_method: paymentMethod,
5246
+ off_session: "true",
5247
+ confirm: "true",
5248
+ description: `${kind === "seat" ? "Named seat" : "Membership"} payment reissued after dispute ${String(row2.disputeId)}`,
5249
+ metadata: {
5250
+ ...stripePartitionMetadata({ appId: String(row2.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(row2.runtime) }),
5251
+ applicationId: String(row2.applicationId),
5252
+ membershipId,
5253
+ ...kind === "seat" ? { seatId: id7 } : {},
5254
+ disputeId: String(row2.disputeId),
5255
+ membershipRecharge: "true"
5256
+ }
5257
+ }, { idempotencyKey: attempt.key });
5258
+ const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
5259
+ const finish = async (outcome) => {
5260
+ const current = await membershipLoad(db, ns, id7);
5261
+ if (!current) return;
5262
+ await db.transact([membershipUpdate(ns, id7, {
5263
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome },
5264
+ revision: Number(current.revision) + 1,
5265
+ updatedAt: now
5266
+ })], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
5267
+ };
5268
+ if (!intent.ok) {
5269
+ if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
5270
+ await finish("declined");
5271
+ fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
5272
+ }
5273
+ fail6("membership_dispute_provider_unavailable");
5274
+ }
5275
+ if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
5276
+ await finish("declined");
5277
+ fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
5278
+ }
5279
+ if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
5280
+ if (refOf2(body.customer) !== row2.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
5281
+ for (let retry = 0; retry < 8; retry++) {
5282
+ const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, ns, id7);
5283
+ if (!current) fail6("membership_not_found");
5284
+ if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
5285
+ const status2 = String(current.disputePriorStatus ?? (kind === "seat" ? "active" : current.journeyApprovedAt ? "active" : "paid_pending_approval"));
5286
+ try {
5287
+ await db.transact(
5288
+ [membershipUpdate(ns, id7, {
5289
+ status: status2,
5290
+ disputeStatus: "recharged",
5291
+ rechargePaymentIntentId: String(body.id),
5292
+ rechargedAt: now,
5293
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
5294
+ revision: Number(current.revision) + 1,
5295
+ updatedAt: now
5296
+ })],
5297
+ { mutationId: `membership-recharged:${attempt.key}`, guards: [
5298
+ { ns, id: id7, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
5299
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
5300
+ ] }
5301
+ );
5302
+ } catch (cause) {
5303
+ if (cause.code !== "transact_guard_failed") throw cause;
5304
+ continue;
5305
+ }
5306
+ return done(await membershipLoad(db, ns, id7));
5307
+ }
5308
+ return fail6("membership_dispute_retry_required");
5309
+ }
5310
+ var rechargeDisputedMembership = (db, membershipId, secretKey, now) => rechargeDisputed(db, "membership", membershipId, secretKey, now);
5311
+ var rechargeDisputedSeat = (db, seatId, secretKey, now) => rechargeDisputed(db, "seat", seatId, secretKey, now);
5312
+
5313
+ // src/worker-routes-admin-membership-disputes.ts
5314
+ var LIST2 = "/api/admin/membership-disputes";
5315
+ var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
5316
+ var PUBLIC2 = [
5317
+ "id",
5318
+ "appId",
5319
+ "chapterId",
5320
+ "environment",
5321
+ "runtime",
5322
+ "applicationId",
5323
+ "skuId",
5324
+ "status",
5325
+ "memberNumber",
5326
+ "amountCents",
5327
+ "currency",
5328
+ "recipientName",
5329
+ "primaryMembershipId",
5330
+ "purchasedAt",
5331
+ "disputeId",
5332
+ "disputeStatus",
5333
+ "disputeReason",
5334
+ "disputeAmountCents",
5335
+ "disputedAt",
5336
+ "disputeClosedAt",
5337
+ "disputePriorStatus",
5338
+ "rechargePaymentIntentId",
5339
+ "rechargedAt",
5340
+ "termEndsAt",
5341
+ "updatedAt"
5342
+ ];
5343
+ var project2 = (row2) => ({
5344
+ ...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
5345
+ ...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
5346
+ startedAt: row2.rechargeAttempt.startedAt ?? null,
5347
+ finishedAt: row2.rechargeAttempt.finishedAt ?? null,
5348
+ outcome: row2.rechargeAttempt.outcome ?? null
5349
+ } } : {}
5350
+ });
5351
+ var FINAL = /* @__PURE__ */ new Set([
5352
+ "membership_not_found",
5353
+ "membership_not_disputed",
5354
+ "membership_dispute_open",
5355
+ "membership_dispute_amount_invalid",
5356
+ "recharge_payment_method_unavailable",
5357
+ "recharge_declined",
5358
+ "recharge_pending",
5359
+ "recharge_mismatch",
5360
+ "membership_dispute_changed"
5361
+ ]);
5362
+ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5363
+ const one = url.pathname.match(ONE2);
5364
+ if (url.pathname !== LIST2 && !one) return null;
5365
+ const operator = ctx.membershipAuthorityOperator;
5366
+ if (!operator) return json({ error: "not found" }, 404);
5367
+ if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
5368
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
5369
+ if (!user) return json({ error: "unauthorized" }, 401);
5370
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
5371
+ let db;
5372
+ try {
5373
+ db = operator.makeDb(env);
5374
+ } catch {
5375
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
5376
+ }
5377
+ if (!one) {
5378
+ const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
5379
+ const found = await db.query({ networkMembership: page, networkSeat: page });
5380
+ const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
5381
+ const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5382
+ const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5383
+ return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
5384
+ }
5385
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
5386
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
5387
+ try {
5388
+ const receipt = await (one[1].startsWith("seat_") ? rechargeDisputedSeat : rechargeDisputedMembership)(db, one[1], secret, Date.now());
5389
+ return json({ ok: true, ...receipt });
5390
+ } catch (error) {
5391
+ const failure = error;
5392
+ const code = typeof failure.code === "string" ? failure.code : void 0;
5393
+ if (code === "membership_not_found") return json({ error: "not found", code }, 404);
5394
+ if (code && FINAL.has(code)) {
5395
+ return json({
5396
+ error: "the charge could not be re-issued",
5397
+ code,
5398
+ ...failure.declineCode ? { declineCode: failure.declineCode } : {},
5399
+ ...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
5400
+ }, 409);
5401
+ }
5402
+ return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
5403
+ }
5404
+ };
5405
+
4978
5406
  // src/membership-policy-versions.ts
4979
5407
  var LIMIT = 200;
4980
- var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
5408
+ var text4 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4981
5409
  var instant = (value) => {
4982
5410
  const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4983
5411
  return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4984
5412
  };
4985
5413
  function provenance(row2) {
4986
- const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
5414
+ const source = text4(row2.source), actor = text4(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4987
5415
  return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
4988
5416
  }
4989
5417
  var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
@@ -5077,16 +5505,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
5077
5505
  };
5078
5506
 
5079
5507
  // src/membership-discount-reterm.ts
5080
- import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5508
+ import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
5081
5509
 
5082
5510
  // src/subscription-provider-contract.ts
5083
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
5511
+ import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
5084
5512
  function subscriptionContractMismatch() {
5085
5513
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5086
5514
  }
5087
5515
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
5088
5516
  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();
5089
- const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5517
+ const result = await stripeRequest8({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5090
5518
  const price = result.body;
5091
5519
  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();
5092
5520
  return typeof price.product === "string" ? price.product : void 0;
@@ -5124,7 +5552,7 @@ async function retermPolicy(db, scope2, id7) {
5124
5552
  }
5125
5553
  async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
5126
5554
  const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
5127
- const read2 = await stripeRequest7({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5555
+ const read2 = await stripeRequest9({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5128
5556
  const sub = read2.body, items = sub?.items?.data;
5129
5557
  const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
5130
5558
  if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
@@ -5135,7 +5563,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
5135
5563
  const present = discounts.map(couponOf);
5136
5564
  if (present.length === 1 && present[0] === couponId) return false;
5137
5565
  if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
5138
- const written = await stripeRequest7({ secretKey: sk }, "POST", path, {
5566
+ const written = await stripeRequest9({ secretKey: sk }, "POST", path, {
5139
5567
  "discounts[0][coupon]": couponId,
5140
5568
  "expand[0]": "discounts",
5141
5569
  "metadata[discountPolicyVersionId]": policy2.id
@@ -5984,8 +6412,8 @@ var APPLICATION_LABELS = {
5984
6412
  state: "State"
5985
6413
  };
5986
6414
  function humanize(id7) {
5987
- const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
5988
- return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
6415
+ const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6416
+ return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
5989
6417
  }
5990
6418
  function applicationDefs(chapter) {
5991
6419
  try {
@@ -6052,7 +6480,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
6052
6480
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
6053
6481
  var CURRENCY = /^[a-z]{3}$/;
6054
6482
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
6055
- var text3 = (value, path, errors, max) => {
6483
+ var text5 = (value, path, errors, max) => {
6056
6484
  if (typeof value !== "string" || !value.trim()) {
6057
6485
  errors.push(`${path} is required`);
6058
6486
  return "";
@@ -6080,23 +6508,23 @@ function validateGiftOfferCatalog(input, tierIds) {
6080
6508
  }
6081
6509
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6082
6510
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6083
- const id7 = text3(item.id, `${path}.id`, errors, 80);
6511
+ const id7 = text5(item.id, `${path}.id`, errors, 80);
6084
6512
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6085
6513
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
6086
6514
  ids2.add(id7);
6087
6515
  const targetRaw = record(item.target);
6088
6516
  let target = { type: "entitlement", entitlementKey: "invalid" };
6089
6517
  if (targetRaw?.type === "tier") {
6090
- const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6518
+ const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6091
6519
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
6092
6520
  target = { type: "tier", tierId };
6093
6521
  } else if (targetRaw?.type === "entitlement") {
6094
- const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6522
+ const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6095
6523
  if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
6096
6524
  target = { type: "entitlement", entitlementKey };
6097
6525
  } else errors.push(`${path}.target.type must be tier or entitlement`);
6098
6526
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
6099
- const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6527
+ const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6100
6528
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
6101
6529
  if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
6102
6530
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -6118,9 +6546,9 @@ function validateGiftOfferCatalog(input, tierIds) {
6118
6546
  redemptionLifetimeHours: lifetime,
6119
6547
  ...activeFrom !== void 0 ? { activeFrom } : {},
6120
6548
  ...activeUntil !== void 0 ? { activeUntil } : {},
6121
- title: text3(item.title, `${path}.title`, errors, 160),
6122
- description: text3(item.description, `${path}.description`, errors, 2e3),
6123
- ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6549
+ title: text5(item.title, `${path}.title`, errors, 160),
6550
+ description: text5(item.description, `${path}.description`, errors, 2e3),
6551
+ ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6124
6552
  });
6125
6553
  }
6126
6554
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -6347,23 +6775,23 @@ async function createPaymentQuote(input) {
6347
6775
  // src/membership-quote-projection.ts
6348
6776
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
6349
6777
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
6350
- var fail6 = (code) => {
6778
+ var fail7 = (code) => {
6351
6779
  throw Object.assign(new Error(code), { code });
6352
6780
  };
6353
6781
  var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
6354
6782
  async function restore2(row2, target, now, allowExpired = false) {
6355
- if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail6("membership_quote_unavailable");
6356
- if (row2.status !== "ready") return fail6("membership_quote_pending");
6783
+ if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail7("membership_quote_unavailable");
6784
+ if (row2.status !== "ready") return fail7("membership_quote_pending");
6357
6785
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
6358
6786
  ...target,
6359
6787
  requestId: row2.requestId
6360
6788
  }), now, lifetimeMs: 6e4 });
6361
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
6789
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
6362
6790
  const tier = row2.tier, quote = row2.quote;
6363
6791
  const offering = offer.contract.offering;
6364
- if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
6792
+ if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
6365
6793
  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) {
6366
- return fail6("membership_catalog_mismatch");
6794
+ return fail7("membership_catalog_mismatch");
6367
6795
  }
6368
6796
  const expected = await createPaymentQuote({
6369
6797
  ...target,
@@ -6376,7 +6804,7 @@ async function restore2(row2, target, now, allowExpired = false) {
6376
6804
  seatOffer: quote.seatOffer,
6377
6805
  namedSeat: quote.namedSeat
6378
6806
  });
6379
- if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
6807
+ if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
6380
6808
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
6381
6809
  }
6382
6810
  async function readMembershipQuote(db, target, now) {
@@ -6384,9 +6812,9 @@ async function readMembershipQuote(db, target, now) {
6384
6812
  }
6385
6813
  async function prepareMembershipQuote(db, input, issue) {
6386
6814
  const { applicationId: id7, now, app } = input;
6387
- if (app.id !== id7 || 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 fail6("membership_quote_unavailable");
6815
+ if (app.id !== id7 || 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 fail7("membership_quote_unavailable");
6388
6816
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
6389
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
6817
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
6390
6818
  if (row2?.status === "ready") {
6391
6819
  try {
6392
6820
  return await restore2(row2, input, now);
@@ -6431,7 +6859,7 @@ async function prepareMembershipQuote(db, input, issue) {
6431
6859
  }
6432
6860
  if (row2?.status === "ready") return restore2(row2, input, now);
6433
6861
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
6434
- return fail6("membership_quote_pending");
6862
+ return fail7("membership_quote_pending");
6435
6863
  }
6436
6864
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
6437
6865
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -6480,7 +6908,7 @@ async function prepareMembershipQuote(db, input, issue) {
6480
6908
  return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
6481
6909
  }
6482
6910
  async function selectCheckoutNamedSeat(db, target, input) {
6483
- if (!input.enabled) return fail6("named_seat_purchase_unavailable");
6911
+ if (!input.enabled) return fail7("named_seat_purchase_unavailable");
6484
6912
  const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
6485
6913
  const current = await restore2(row2, target, input.now);
6486
6914
  const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
@@ -6536,17 +6964,17 @@ async function readBoundedRequestText(req, maxBytes) {
6536
6964
  if (!reader) return "";
6537
6965
  const decoder = new TextDecoder();
6538
6966
  let bytes = 0;
6539
- let text8 = "";
6967
+ let text10 = "";
6540
6968
  for (; ; ) {
6541
6969
  const { done, value } = await reader.read();
6542
6970
  if (req.signal.aborted) return invalid2();
6543
6971
  if (done) {
6544
6972
  complete = true;
6545
- return text8 + decoder.decode();
6973
+ return text10 + decoder.decode();
6546
6974
  }
6547
6975
  bytes += value.byteLength;
6548
6976
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
6549
- text8 += decoder.decode(value, { stream: true });
6977
+ text10 += decoder.decode(value, { stream: true });
6550
6978
  }
6551
6979
  } catch {
6552
6980
  return invalid2();
@@ -6650,9 +7078,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
6650
7078
  let selection;
6651
7079
  if (request?.body) {
6652
7080
  try {
6653
- const text8 = await readBoundedRequestText(request, 2048);
6654
- if (text8 instanceof Response) return text8;
6655
- const body = JSON.parse(text8);
7081
+ const text10 = await readBoundedRequestText(request, 2048);
7082
+ if (text10 instanceof Response) return text10;
7083
+ const body = JSON.parse(text10);
6656
7084
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
6657
7085
  selection = body.namedSeat;
6658
7086
  } catch {
@@ -7307,10 +7735,13 @@ var handleMember = async (req, url, env, ctx) => {
7307
7735
  const db = ctx.makeDb(env);
7308
7736
  const groupId = url.searchParams.get("group") ?? chapter.id;
7309
7737
  if (groupId !== chapter.id) return json({ error: "not found" }, 404);
7310
- const { groups } = await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } });
7738
+ const [{ groups }, stripeKey, activeControl] = await Promise.all([
7739
+ db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
7740
+ getVaultSecret(db, "stripe_secret_key"),
7741
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
7742
+ ]);
7311
7743
  const group = Array.isArray(groups) ? groups[0] : void 0;
7312
7744
  if (!group) return json({ error: "not found" }, 404);
7313
- const stripeKey = await getVaultSecret(db, "stripe_secret_key");
7314
7745
  const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
7315
7746
  const offered = await loadOfferableTiers(
7316
7747
  db,
@@ -7319,7 +7750,6 @@ var handleMember = async (req, url, env, ctx) => {
7319
7750
  );
7320
7751
  const seats = /* @__PURE__ */ new Map();
7321
7752
  const founding = await foundingPreviews(offered, env, ctx, seats);
7322
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7323
7753
  const application = controlledApplication(chapter.application, activeControl?.document);
7324
7754
  return json({
7325
7755
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
@@ -7357,11 +7787,14 @@ var handleMember = async (req, url, env, ctx) => {
7357
7787
  parsed.tierId = named.invitation.skuId;
7358
7788
  submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
7359
7789
  }
7360
- const eligibility = named ? null : await resolveSignupEligibility(db, chapter, parsed, submissionId);
7790
+ const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
7791
+ named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
7792
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
7793
+ typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
7794
+ db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
7795
+ ]);
7361
7796
  if (eligibility?.conflict) return eligibility.conflict;
7362
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7363
7797
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
7364
- const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
7365
7798
  const now = Date.now();
7366
7799
  const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
7367
7800
  const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
@@ -7400,18 +7833,16 @@ var handleMember = async (req, url, env, ctx) => {
7400
7833
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
7401
7834
  return json(provisioningFailure.body, provisioningFailure.status);
7402
7835
  }
7403
- if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
7404
- const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
7405
- const group = Array.isArray(groups) ? groups[0] : void 0;
7836
+ if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
7406
7837
  const notice = group ? { group, application: canonical3, origin: url.origin } : null;
7407
- if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
7838
+ if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
7408
7839
  const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
7409
7840
  if (!state) return json({
7410
7841
  error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
7411
7842
  code: "application_continuation_unavailable",
7412
7843
  id: result.id
7413
7844
  }, 503);
7414
- if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
7845
+ if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
7415
7846
  const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
7416
7847
  if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
7417
7848
  return json({ error: "application changed; reload its current state" }, 409);
@@ -7531,10 +7962,10 @@ function boundedString(value, path, errors, max, required = false) {
7531
7962
  errors.push(`${path} must be a string`);
7532
7963
  return void 0;
7533
7964
  }
7534
- const text8 = value.trim();
7535
- if (required && !text8) errors.push(`${path} is required`);
7536
- if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7537
- return text8;
7965
+ const text10 = value.trim();
7966
+ if (required && !text10) errors.push(`${path} is required`);
7967
+ if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7968
+ return text10;
7538
7969
  }
7539
7970
  function validateMembershipPageDocument(input, options = {}) {
7540
7971
  const errors = [];
@@ -7583,8 +8014,8 @@ function validateMembershipPageDocument(input, options = {}) {
7583
8014
  else {
7584
8015
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
7585
8016
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
7586
- const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
7587
- return text8 ? [text8] : [];
8017
+ const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8018
+ return text10 ? [text10] : [];
7588
8019
  });
7589
8020
  }
7590
8021
  }
@@ -7678,7 +8109,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
7678
8109
  function unknownKeys(value, allowed2, path, errors) {
7679
8110
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7680
8111
  }
7681
- function text4(value, path, errors, max = 65536) {
8112
+ function text6(value, path, errors, max = 65536) {
7682
8113
  if (typeof value !== "string" || !value.trim()) {
7683
8114
  errors.push(`${path} is required`);
7684
8115
  return "";
@@ -7701,11 +8132,11 @@ function tiersOf(value, errors) {
7701
8132
  return [];
7702
8133
  }
7703
8134
  unknownKeys(item, TIER_KEYS2, path, errors);
7704
- const id7 = text4(item.id, `${path}.id`, errors, 80);
8135
+ const id7 = text6(item.id, `${path}.id`, errors, 80);
7705
8136
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7706
8137
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
7707
8138
  seen.add(id7);
7708
- const name = text4(item.name, `${path}.name`, errors, 160);
8139
+ const name = text6(item.name, `${path}.name`, errors, 160);
7709
8140
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
7710
8141
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
7711
8142
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -7723,7 +8154,7 @@ function tiersOf(value, errors) {
7723
8154
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
7724
8155
  errors.push(`${path}.interval must be year or month`);
7725
8156
  }
7726
- const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
8157
+ const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
7727
8158
  return [{
7728
8159
  id: id7,
7729
8160
  name,
@@ -7781,7 +8212,7 @@ function policyOf(value, errors, tiers) {
7781
8212
  const result = {};
7782
8213
  for (const key3 of allowed2) {
7783
8214
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
7784
- result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
8215
+ result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
7785
8216
  }
7786
8217
  const tierPolicies = {};
7787
8218
  if (input.tierPolicies !== void 0) {
@@ -7796,8 +8227,8 @@ function policyOf(value, errors, tiers) {
7796
8227
  }
7797
8228
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
7798
8229
  tierPolicies[id7] = {
7799
- refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
7800
- ...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8230
+ refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8231
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
7801
8232
  };
7802
8233
  }
7803
8234
  }
@@ -7837,9 +8268,9 @@ function validateSignupControlDocument(input) {
7837
8268
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
7838
8269
  const source = recordOf(root.source);
7839
8270
  const target = recordOf(root.target);
7840
- const appId = text4(source?.appId, "document.source.appId", errors, 63);
7841
- const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
7842
- const environment = text4(target?.environment, "document.target.environment", errors, 63);
8271
+ const appId = text6(source?.appId, "document.source.appId", errors, 63);
8272
+ const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
8273
+ const environment = text6(target?.environment, "document.target.environment", errors, 63);
7843
8274
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7844
8275
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
7845
8276
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9351,8 +9782,8 @@ async function bookSlot(req, env, ctx) {
9351
9782
  const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
9352
9783
  await db.transact([meetingOp, appOp]);
9353
9784
  const booked = { ...app, ...bookingAttrs };
9354
- await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
9355
- await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
9785
+ await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
9786
+ await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
9356
9787
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
9357
9788
  }
9358
9789
  var handleSchedule = async (req, url, env, ctx) => {
@@ -9376,28 +9807,28 @@ var handleSchedule = async (req, url, env, ctx) => {
9376
9807
  };
9377
9808
 
9378
9809
  // src/payments-stripe.ts
9379
- import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
9810
+ import { stripeRequest as stripeRequest12, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
9380
9811
 
9381
9812
  // src/payments-refund.ts
9382
- import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
9813
+ import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
9383
9814
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9384
9815
  var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
9385
9816
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
9386
- function fail7(code = "refund_target_mismatch") {
9817
+ function fail8(code = "refund_target_mismatch") {
9387
9818
  throw Object.assign(new Error(code), { code });
9388
9819
  }
9389
9820
  async function refundSubscriptionPayment(secretKey, input) {
9390
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
9821
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
9391
9822
  const call = async (method, path, params, key3) => {
9392
- const result = await stripeRequest8({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9393
- if (!result.ok) fail7("stripe_error");
9394
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
9823
+ const result = await stripeRequest10({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9824
+ if (!result.ok) fail8("stripe_error");
9825
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
9395
9826
  return result.body;
9396
9827
  };
9397
9828
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
9398
- if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
9829
+ if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
9399
9830
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
9400
- if (row(sub.metadata)[key3] !== value) fail7();
9831
+ if (row(sub.metadata)[key3] !== value) fail8();
9401
9832
  }
9402
9833
  let cursor;
9403
9834
  let earliest;
@@ -9410,66 +9841,66 @@ async function refundSubscriptionPayment(secretKey, input) {
9410
9841
  limit: 100,
9411
9842
  ...cursor ? { starting_after: cursor } : {}
9412
9843
  });
9413
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
9844
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
9414
9845
  for (const value of list.data) {
9415
9846
  const invoice2 = row(value);
9416
9847
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
9417
- if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail7();
9848
+ if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail8();
9418
9849
  seen.add(id3(invoice2));
9419
9850
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
9420
9851
  }
9421
9852
  if (!list.has_more) break;
9422
- if (!list.data.length || page === 9) fail7("refund_history_incomplete");
9853
+ if (!list.data.length || page === 9) fail8("refund_history_incomplete");
9423
9854
  cursor = id3(list.data[list.data.length - 1]);
9424
9855
  }
9425
- if (!earliest) fail7("no_charge");
9856
+ if (!earliest) fail8("no_charge");
9426
9857
  const invoice = earliest;
9427
- if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
9858
+ if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
9428
9859
  let chargeId = id3(invoice.charge);
9429
9860
  let paymentIntentId = id3(invoice.payment_intent);
9430
9861
  if (!chargeId && !paymentIntentId) {
9431
9862
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
9432
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
9863
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
9433
9864
  const payment = row(payments.data[0]);
9434
- if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
9865
+ if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
9435
9866
  paymentIntentId = id3(row(payment.payment).payment_intent);
9436
9867
  }
9437
9868
  if (!chargeId) {
9438
- if (!providerId2(paymentIntentId, "pi")) fail7();
9869
+ if (!providerId2(paymentIntentId, "pi")) fail8();
9439
9870
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
9440
- if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
9871
+ if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
9441
9872
  chargeId = id3(payment.latest_charge);
9442
9873
  }
9443
- if (!providerId2(chargeId, "ch")) fail7();
9874
+ if (!providerId2(chargeId, "ch")) fail8();
9444
9875
  const charge = await call("GET", `/v1/charges/${chargeId}`);
9445
- if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(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)) fail7();
9446
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
9876
+ if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(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)) fail8();
9877
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
9447
9878
  if (charge.amount_refunded !== charge.amount) {
9448
9879
  const refund = await call("POST", "/v1/refunds", {
9449
9880
  charge: chargeId,
9450
9881
  ...input.metadata ? { metadata: input.metadata } : {}
9451
9882
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
9452
- if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail7("refund_not_confirmed");
9883
+ if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail8("refund_not_confirmed");
9453
9884
  }
9454
9885
  let subscriptionCanceled = sub.status === "canceled";
9455
9886
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
9456
9887
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
9457
- if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
9888
+ if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
9458
9889
  subscriptionCanceled = true;
9459
9890
  }
9460
9891
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
9461
- if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail7("refund_not_confirmed");
9892
+ if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail8("refund_not_confirmed");
9462
9893
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
9463
9894
  }
9464
9895
 
9465
9896
  // src/subscription-payment-identity.ts
9466
- import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
9897
+ import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
9467
9898
  var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
9468
9899
  async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
9469
9900
  let paymentId = id4(invoice.payment_intent);
9470
9901
  if (!paymentId) {
9471
9902
  if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
9472
- const result2 = await stripeRequest9({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9903
+ const result2 = await stripeRequest11({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9473
9904
  const list = result2.body;
9474
9905
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
9475
9906
  const link = list.data[0];
@@ -9478,11 +9909,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9478
9909
  }
9479
9910
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
9480
9911
  const path = `/v1/payment_intents/${paymentId}`;
9481
- const result = await stripeRequest9({ secretKey }, "GET", path);
9912
+ const result = await stripeRequest11({ secretKey }, "GET", path);
9482
9913
  const payment = result.body;
9483
9914
  if (!result.ok || payment?.id !== paymentId || id4(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();
9484
9915
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
9485
- const tagged = await stripeRequest9(
9916
+ const tagged = await stripeRequest11(
9486
9917
  { secretKey },
9487
9918
  "POST",
9488
9919
  path,
@@ -9494,7 +9925,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9494
9925
 
9495
9926
  // src/payments-stripe.ts
9496
9927
  async function stripeCall(sk, method, path, params, idempotencyKey) {
9497
- return stripeRequest10(
9928
+ return stripeRequest12(
9498
9929
  { secretKey: sk },
9499
9930
  method,
9500
9931
  path,
@@ -9502,7 +9933,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
9502
9933
  idempotencyKey ? { idempotencyKey } : {}
9503
9934
  );
9504
9935
  }
9505
- function fail8(op, r) {
9936
+ function fail9(op, r) {
9506
9937
  const err = new Error(`stripe ${op} failed: ${r.status}`);
9507
9938
  err.code = "stripe_error";
9508
9939
  throw err;
@@ -9561,7 +9992,7 @@ function createStripeProvider(config) {
9561
9992
  { email: input.email, name: input.name, metadata: meta },
9562
9993
  `cus:${input.applicationId}`
9563
9994
  );
9564
- if (!cust.ok) fail8("customer create", cust);
9995
+ if (!cust.ok) fail9("customer create", cust);
9565
9996
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
9566
9997
  customerId = cust.body.id;
9567
9998
  }
@@ -9606,7 +10037,7 @@ function createStripeProvider(config) {
9606
10037
  // retry between create and the db write can't orphan a second one.
9607
10038
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
9608
10039
  );
9609
- if (!sub.ok) fail8("subscription create", sub);
10040
+ if (!sub.ok) fail9("subscription create", sub);
9610
10041
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
9611
10042
  const subscriptionId = sub.body.id;
9612
10043
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -9616,14 +10047,14 @@ function createStripeProvider(config) {
9616
10047
  "expand[2]": "discounts",
9617
10048
  "expand[3]": "latest_invoice.discounts"
9618
10049
  });
9619
- if (!expanded.ok) fail8("subscription invoice evidence", expanded);
10050
+ if (!expanded.ok) fail9("subscription invoice evidence", expanded);
9620
10051
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
9621
10052
  sub = expanded;
9622
10053
  }
9623
10054
  verifySubscriptionResult(sub.body, input, customerId, meta);
9624
10055
  const invoice = sub.body.latest_invoice;
9625
10056
  const clientSecret2 = clientSecretOf(sub.body);
9626
- if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
10057
+ if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
9627
10058
  await tagSubscriptionPayment(
9628
10059
  sk,
9629
10060
  invoice,
@@ -10110,7 +10541,7 @@ async function startSubscription(req, env, ctx) {
10110
10541
 
10111
10542
  // src/worker-payment-receipt.ts
10112
10543
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10113
- var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10544
+ var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10114
10545
  async function paymentReceiptRoute(req, url, env, ctx) {
10115
10546
  const id7 = url.searchParams.get("application");
10116
10547
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
@@ -10121,7 +10552,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10121
10552
  if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
10122
10553
  const receipt = intent.paymentReceipt;
10123
10554
  if (!receipt) return json2({ pending: true }, 202);
10124
- const text8 = paymentReceiptText(receipt, ctx.chapter.name);
10555
+ const text10 = paymentReceiptText(receipt, ctx.chapter.name);
10125
10556
  if (req.method === "POST") {
10126
10557
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10127
10558
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10139,7 +10570,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10139
10570
  template: "paymentReceipt",
10140
10571
  to: String(intent.purchaserEmailNormalized),
10141
10572
  applicationId: id7,
10142
- vars: { chapterName: ctx.chapter.name, receiptText: text8 },
10573
+ vars: { chapterName: ctx.chapter.name, receiptText: text10 },
10143
10574
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10144
10575
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10145
10576
  });
@@ -10149,7 +10580,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10149
10580
  const nonce = crypto.randomUUID();
10150
10581
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10151
10582
  <style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
10152
- <h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
10583
+ <h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
10153
10584
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10154
10585
  headers: {
10155
10586
  "content-type": "text/html; charset=utf-8",
@@ -10619,16 +11050,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
10619
11050
  };
10620
11051
 
10621
11052
  // src/named-seat-accepted-notice.ts
10622
- var text5 = (value) => typeof value === "string" ? value : "";
11053
+ var text7 = (value) => typeof value === "string" ? value : "";
10623
11054
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
10624
11055
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
10625
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
11056
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
10626
11057
  return { sent: false, reason: "purchaser-missing" };
10627
11058
  }
10628
11059
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
10629
11060
  if (!group) return { sent: false, reason: "group-missing" };
10630
11061
  const config = emailGroupFrom(group);
10631
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
11062
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
10632
11063
  return sendTemplated({
10633
11064
  db,
10634
11065
  envName: env.ODLA_ENV,
@@ -10640,12 +11071,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
10640
11071
  }, {
10641
11072
  group: config,
10642
11073
  template: "seatAccepted",
10643
- to: text5(purchaser.email),
11074
+ to: text7(purchaser.email),
10644
11075
  dedupeKey: `named-seat-accepted:${input.seatId}`,
10645
11076
  applicationId: input.primaryApplicationId,
10646
11077
  tierId: input.skuId,
10647
11078
  vars: {
10648
- firstName: text5(purchaser.firstName),
11079
+ firstName: text7(purchaser.firstName),
10649
11080
  purchaserName,
10650
11081
  recipientName: input.recipientName,
10651
11082
  tierName: await tierName(db, group, input.skuId),
@@ -12112,12 +12543,12 @@ async function updateEmailSettings(db, ctx, env, body) {
12112
12543
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12113
12544
  const t = value;
12114
12545
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12115
- const subject = str2(t.subject).trim(), text8 = str2(t.text);
12116
- const issues = validateEmailCopy4({ subject, text: text8 }, { allowedVariables: events[key3].variables });
12546
+ const subject = str2(t.subject).trim(), text10 = str2(t.text);
12547
+ const issues = validateEmailCopy4({ subject, text: text10 }, { allowedVariables: events[key3].variables });
12117
12548
  if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
12118
12549
  return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
12119
12550
  }
12120
- clean[key3] = { subject, text: text8, enabled: t.enabled };
12551
+ clean[key3] = { subject, text: text10, enabled: t.enabled };
12121
12552
  }
12122
12553
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12123
12554
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -14330,9 +14761,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14330
14761
  if (req.method === "POST") {
14331
14762
  let body;
14332
14763
  try {
14333
- const text8 = await req.text();
14334
- if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14335
- const parsed = JSON.parse(text8);
14764
+ const text10 = await req.text();
14765
+ if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14766
+ const parsed = JSON.parse(text10);
14336
14767
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14337
14768
  body = parsed;
14338
14769
  } catch {
@@ -14501,7 +14932,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
14501
14932
  // src/network-commercial-parity.ts
14502
14933
  var DIGEST = /^[0-9a-f]{64}$/;
14503
14934
  var MAX_RECORDS = 500;
14504
- var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14935
+ var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14505
14936
  function parityMessage(partition, kind, value) {
14506
14937
  return [
14507
14938
  "chapter-commercial-parity",
@@ -14533,18 +14964,18 @@ async function digestWithKey(key3, partition, kind, value) {
14533
14964
  async function buildChapterCommercialParitySnapshot(input) {
14534
14965
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
14535
14966
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
14536
- const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
14967
+ const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
14537
14968
  const selected2 = eligible.slice(0, maxRecords);
14538
14969
  const key3 = await parityKey(input.secret);
14539
14970
  const records = await Promise.all(selected2.map(async (row2) => {
14540
- const applicationId = text6(row2.id);
14541
- const customerId = text6(row2.stripeCustomerId);
14542
- const subscriptionId = text6(row2.stripeSubscriptionId);
14971
+ const applicationId = text8(row2.id);
14972
+ const customerId = text8(row2.stripeCustomerId);
14973
+ const subscriptionId = text8(row2.stripeSubscriptionId);
14543
14974
  return {
14544
14975
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
14545
14976
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
14546
14977
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
14547
- status: text6(row2.status, 64),
14978
+ status: text8(row2.status, 64),
14548
14979
  canceled: row2.canceled === true
14549
14980
  };
14550
14981
  }));
@@ -14570,7 +15001,7 @@ function validSnapshot(snapshot, source) {
14570
15001
  if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
14571
15002
  const seen = /* @__PURE__ */ new Set();
14572
15003
  for (const record4 of records) {
14573
- if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text6(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
15004
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text8(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
14574
15005
  const key3 = recordKey(record4);
14575
15006
  if (seen.has(key3)) return false;
14576
15007
  seen.add(key3);
@@ -14581,16 +15012,16 @@ async function reconcileCommercialParitySnapshot(input) {
14581
15012
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
14582
15013
  const sourceKey = networkCommercialSourceKey(input.source);
14583
15014
  const key3 = await parityKey(input.secret);
14584
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
14585
- const customerId = text6(row2.customerId);
14586
- const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
15015
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
15016
+ const customerId = text8(row2.customerId);
15017
+ const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
14587
15018
  return {
14588
15019
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
14589
15020
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
14590
15021
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
14591
15022
  status: null,
14592
15023
  canceled: false,
14593
- stripeStatus: text6(row2.status, 64),
15024
+ stripeStatus: text8(row2.status, 64),
14594
15025
  stripeCanceled: row2.status === "canceled"
14595
15026
  };
14596
15027
  }));
@@ -14787,6 +15218,40 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
14787
15218
  };
14788
15219
  }
14789
15220
 
15221
+ // src/worker-network-commercial-dispute.ts
15222
+ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15223
+ const operator = ctx.membershipAuthorityOperator;
15224
+ if (!operator || !fact.chargeId) return { disposition: "unhosted" };
15225
+ let db;
15226
+ try {
15227
+ db = operator.makeDb(env);
15228
+ } catch {
15229
+ return { disposition: "unhosted" };
15230
+ }
15231
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
15232
+ if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
15233
+ const charge = await readDisputedCharge(secret, fact.chargeId);
15234
+ const scopeOf = (row2) => ({
15235
+ appId: String(row2.appId),
15236
+ chapterId: String(row2.chapterId),
15237
+ environment: String(row2.environment),
15238
+ runtime: String(row2.runtime),
15239
+ applicationId: String(row2.applicationId),
15240
+ skuId: String(row2.skuId)
15241
+ });
15242
+ if (charge.seatId) {
15243
+ const seat = await membershipLoad(db, "networkSeat", charge.seatId);
15244
+ if (!seat || seat.environment !== env.ODLA_ENV || !disputedChargeCoversSeat(charge, seat)) return { disposition: "not_applicable" };
15245
+ const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
15246
+ return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
15247
+ }
15248
+ const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15249
+ const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
15250
+ if (!member) return { disposition: "not_applicable" };
15251
+ const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
15252
+ return { disposition: "applied", membershipId: receipt.membershipId };
15253
+ }
15254
+
14790
15255
  // src/worker-routes-network-commercial.ts
14791
15256
  var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
14792
15257
  var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
@@ -14851,6 +15316,12 @@ function safeProjection(projection) {
14851
15316
  amountReceivedCents: seat.amountReceivedCents,
14852
15317
  refundedCents: seat.refundedCents ?? 0
14853
15318
  })),
15319
+ disputes: (projection.disputes ?? []).map((dispute2) => ({
15320
+ ...row2(dispute2),
15321
+ chargeId: dispute2.chargeId ?? null,
15322
+ paymentIntentId: dispute2.paymentIntentId ?? null,
15323
+ reason: dispute2.disputeReason ?? null
15324
+ })),
14854
15325
  exceptions: projection.exceptions
14855
15326
  };
14856
15327
  }
@@ -14883,10 +15354,20 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
14883
15354
  if (result.status === "collision") {
14884
15355
  console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
14885
15356
  }
15357
+ let dispute2;
15358
+ if (fact.objectKind === "dispute" && result.status !== "collision") {
15359
+ try {
15360
+ dispute2 = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
15361
+ } catch (error) {
15362
+ console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
15363
+ return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
15364
+ }
15365
+ }
14886
15366
  return json({
14887
15367
  ok: true,
14888
15368
  status: result.status,
14889
15369
  eventId: fact.eventId,
15370
+ ...dispute2 ? { dispute: dispute2.disposition } : {},
14890
15371
  ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
14891
15372
  });
14892
15373
  } catch (error) {
@@ -15084,7 +15565,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
15084
15565
  function unknownKeys3(value, allowed2, path, errors) {
15085
15566
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
15086
15567
  }
15087
- function text7(value, path, errors, max = MAX_TEXT2, required = true) {
15568
+ function text9(value, path, errors, max = MAX_TEXT2, required = true) {
15088
15569
  if (value === void 0 && !required) return void 0;
15089
15570
  if (typeof value !== "string" || required && !value.trim()) {
15090
15571
  errors.push(`${path} must be a non-empty string`);
@@ -15110,7 +15591,7 @@ function scopeValue(value, path, errors) {
15110
15591
  return { kind: raw2.kind };
15111
15592
  }
15112
15593
  if (raw2.kind === "chapter") {
15113
- const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15594
+ const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15114
15595
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15115
15596
  return { kind: "chapter", chapterId };
15116
15597
  }
@@ -15124,22 +15605,22 @@ function resourceValue(value, path, errors) {
15124
15605
  return null;
15125
15606
  }
15126
15607
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15127
- const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
15608
+ const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
15128
15609
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15129
- const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
15610
+ const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
15130
15611
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15131
- const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
15132
- const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
15133
- const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
15612
+ const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
15613
+ const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
15614
+ const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
15134
15615
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15135
15616
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15136
15617
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15137
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15618
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15138
15619
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15139
15620
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15140
15621
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15141
- const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15142
- const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
15622
+ const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15623
+ const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
15143
15624
  if (url) {
15144
15625
  try {
15145
15626
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15151,9 +15632,9 @@ function resourceValue(value, path, errors) {
15151
15632
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15152
15633
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15153
15634
  const brandAsset = asset ? {
15154
- bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15155
- assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15156
- ...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15635
+ bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15636
+ assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15637
+ ...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15157
15638
  } : void 0;
15158
15639
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15159
15640
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15168,17 +15649,17 @@ function resourceValue(value, path, errors) {
15168
15649
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15169
15650
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15170
15651
  const promotedFrom = promotedRaw ? {
15171
- chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15172
- resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15652
+ chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15653
+ resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15173
15654
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15174
15655
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15175
- promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15656
+ promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15176
15657
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15177
15658
  } : void 0;
15178
15659
  const provenance2 = {
15179
- createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15660
+ createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15180
15661
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15181
- updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15662
+ updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15182
15663
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15183
15664
  ...promotedFrom ? { promotedFrom } : {}
15184
15665
  };
@@ -15197,7 +15678,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15197
15678
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15198
15679
  const sourceType = sourceRaw?.type;
15199
15680
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15200
- const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15681
+ const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15201
15682
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15202
15683
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15203
15684
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15205,8 +15686,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15205
15686
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15206
15687
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15207
15688
  const target = targetRaw ? {
15208
- appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15209
- environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15689
+ appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15690
+ environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15210
15691
  } : void 0;
15211
15692
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15212
15693
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15547,7 +16028,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
15547
16028
  };
15548
16029
 
15549
16030
  // src/membership-cancel-authority.ts
15550
- import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
16031
+ import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
15551
16032
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15552
16033
  membershipClock(now);
15553
16034
  const id7 = await networkMembershipId(target);
@@ -15558,14 +16039,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15558
16039
  membershipFail("membership_cancellation_unavailable");
15559
16040
  }
15560
16041
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
15561
- const read2 = await stripeRequest11({ secretKey: stripeSecret }, "GET", path);
16042
+ const read2 = await stripeRequest13({ secretKey: stripeSecret }, "GET", path);
15562
16043
  const sub = read2.body;
15563
16044
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
15564
16045
  ...stripePartitionMetadata(target),
15565
16046
  applicationId: target.applicationId
15566
16047
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
15567
16048
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
15568
- const write = await stripeRequest11(
16049
+ const write = await stripeRequest13(
15569
16050
  { secretKey: stripeSecret },
15570
16051
  "POST",
15571
16052
  path,
@@ -15787,7 +16268,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
15787
16268
  }
15788
16269
 
15789
16270
  // src/membership-provider-lifecycle.ts
15790
- import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
16271
+ import { stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
15791
16272
 
15792
16273
  // src/membership-renewal-authority.ts
15793
16274
  async function reconcileMembershipRenewal(db, input, now) {
@@ -15909,6 +16390,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15909
16390
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
15910
16391
  const fact = row2?.fact;
15911
16392
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
16393
+ if (fact?.objectKind === "dispute") {
16394
+ const secret = await getVaultSecret(db, stripeSecretName);
16395
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
16396
+ return reconcileMembershipDispute(db, input, now, secret);
16397
+ }
15912
16398
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
15913
16399
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
15914
16400
  appId: input.appId,
@@ -15951,7 +16437,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15951
16437
  }
15952
16438
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
15953
16439
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
15954
- const result = await stripeRequest12({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
16440
+ const result = await stripeRequest14({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
15955
16441
  const sub = result.body;
15956
16442
  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)) {
15957
16443
  membershipFail("membership_billing_provider_mismatch");
@@ -15989,7 +16475,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15989
16475
  }
15990
16476
 
15991
16477
  // src/membership-seat-stripe.ts
15992
- import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
16478
+ import { stripeRequest as stripeRequest15 } from "@odla-ai/stripe";
15993
16479
  var reject7 = () => {
15994
16480
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
15995
16481
  };
@@ -16001,7 +16487,7 @@ var metadataFor = (seat) => ({
16001
16487
  });
16002
16488
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
16003
16489
  async function read(sk, path, params) {
16004
- const response = await stripeRequest13({ secretKey: sk }, "GET", path, params);
16490
+ const response = await stripeRequest15({ secretKey: sk }, "GET", path, params);
16005
16491
  if (!response.ok) return reject7();
16006
16492
  return response.body;
16007
16493
  }
@@ -16038,11 +16524,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
16038
16524
  }
16039
16525
  await subscriptionForSeat(sk, seat);
16040
16526
  const metadata = metadataFor(seat);
16041
- const result = seat.providerPaymentIntentId ? await stripeRequest13(
16527
+ const result = seat.providerPaymentIntentId ? await stripeRequest15(
16042
16528
  { secretKey: sk },
16043
16529
  "GET",
16044
16530
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
16045
- ) : await stripeRequest13({ secretKey: sk }, "POST", "/v1/payment_intents", {
16531
+ ) : await stripeRequest15({ secretKey: sk }, "POST", "/v1/payment_intents", {
16046
16532
  amount: seat.amountCents,
16047
16533
  currency: seat.currency,
16048
16534
  customer: seat.providerCustomerId,
@@ -16064,7 +16550,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
16064
16550
  return String(existing.id);
16065
16551
  }
16066
16552
  if (seat.combinedInvoice) reject7();
16067
- const result = await stripeRequest13({ secretKey: sk }, "POST", "/v1/subscription_items", {
16553
+ const result = await stripeRequest15({ secretKey: sk }, "POST", "/v1/subscription_items", {
16068
16554
  subscription: seat.providerSubscriptionId,
16069
16555
  price: seat.priceId,
16070
16556
  quantity: 1,
@@ -16726,6 +17212,19 @@ var id5 = () => attr("string", false, true, true);
16726
17212
  var key = (optional = false) => attr("string", optional, false, true);
16727
17213
  var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
16728
17214
  var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
17215
+ var dispute = () => ({
17216
+ disputeId: key(true),
17217
+ disputeStatus: attr("string", true),
17218
+ disputeReason: attr("string", true),
17219
+ disputeAmountCents: attr("number", true),
17220
+ disputedAt: attr("number", true),
17221
+ disputeClosedAt: attr("number", true),
17222
+ disputePriorStatus: attr("string", true),
17223
+ disputeEventId: key(true),
17224
+ rechargePaymentIntentId: key(true),
17225
+ rechargedAt: attr("number", true),
17226
+ rechargeAttempt: attr("json", true)
17227
+ });
16729
17228
  var membershipAuthorityEntities = {
16730
17229
  networkMembership: { attrs: {
16731
17230
  id: id5(),
@@ -16765,7 +17264,8 @@ var membershipAuthorityEntities = {
16765
17264
  graceEndsAt: attr("number", true),
16766
17265
  ...times(),
16767
17266
  refundRequestedAt: attr("number", true),
16768
- restartId: key(true)
17267
+ restartId: key(true),
17268
+ ...dispute()
16769
17269
  } },
16770
17270
  membershipRestart: { attrs: {
16771
17271
  id: id5(),
@@ -16841,7 +17341,8 @@ var membershipAuthorityEntities = {
16841
17341
  refundedCents: attr("number", true),
16842
17342
  providerRefundIds: attr("json", true),
16843
17343
  regiftedAt: attr("number", true),
16844
- formerRecipients: attr("json", true)
17344
+ formerRecipients: attr("json", true),
17345
+ ...dispute()
16845
17346
  } },
16846
17347
  networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
16847
17348
  membershipEffect: { attrs: {
@@ -16962,7 +17463,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
16962
17463
  }
16963
17464
 
16964
17465
  // src/membership-seat-cancel.ts
16965
- import { initStripe as initStripe3, stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
17466
+ import { initStripe as initStripe3, stripeRequest as stripeRequest16 } from "@odla-ai/stripe";
16966
17467
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
16967
17468
  membershipClock(now);
16968
17469
  const state = await loadMembershipAuthorityState(db, target);
@@ -16990,7 +17491,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
16990
17491
  }
16991
17492
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
16992
17493
  const request = async (method, path, params) => {
16993
- const result = await stripeRequest14(
17494
+ const result = await stripeRequest16(
16994
17495
  { secretKey },
16995
17496
  method,
16996
17497
  path,
@@ -17178,7 +17679,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17178
17679
  };
17179
17680
  signal.addEventListener("abort", cancel, { once: true });
17180
17681
  let size = 0;
17181
- let text8 = "";
17682
+ let text10 = "";
17182
17683
  const decoder = new TextDecoder();
17183
17684
  try {
17184
17685
  while (true) {
@@ -17188,10 +17689,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17188
17689
  if (done) break;
17189
17690
  size += value.byteLength;
17190
17691
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17191
- text8 += decoder.decode(value, { stream: true });
17692
+ text10 += decoder.decode(value, { stream: true });
17192
17693
  }
17193
17694
  try {
17194
- return JSON.parse(text8 + decoder.decode());
17695
+ return JSON.parse(text10 + decoder.decode());
17195
17696
  } catch {
17196
17697
  throw new TypeError(`invalid ${label}`);
17197
17698
  }
@@ -17526,7 +18027,7 @@ async function requestMembershipRefund(options) {
17526
18027
  async function requestMembershipRenewal(options) {
17527
18028
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
17528
18029
  const row2 = result.renewal;
17529
- if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
18030
+ if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
17530
18031
  throw new Error("invalid membership renewal receipt");
17531
18032
  }
17532
18033
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -17878,6 +18379,7 @@ var BUILTIN_ROUTES = [
17878
18379
  handlePaymentRecovery,
17879
18380
  handleAdminNamedSeat,
17880
18381
  handleAdminMembershipSeats,
18382
+ handleAdminMembershipDisputes,
17881
18383
  handleAdminMembershipPolicyVersions,
17882
18384
  handleAdminMembershipReterm,
17883
18385
  handleAdminMeetings,
@@ -17926,8 +18428,9 @@ function chapterWorker(options) {
17926
18428
  const ctx = createWorkerContext(options);
17927
18429
  const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
17928
18430
  return {
17929
- async fetch(req, env) {
18431
+ async fetch(req, env, execution) {
17930
18432
  const url = new URL(req.url);
18433
+ ctx.bindExecution(req, execution);
17931
18434
  for (const route of routes) {
17932
18435
  let res;
17933
18436
  try {
@@ -17949,6 +18452,7 @@ export {
17949
18452
  DECLINABLE_FROM,
17950
18453
  MEMBERSHIP_AUTHORITY_PATH,
17951
18454
  MEMBERSHIP_CHAPTER_EFFECT_KINDS,
18455
+ MEMBERSHIP_DISPUTED_STATUS,
17952
18456
  NetworkTargetTransportError,
17953
18457
  REGIFTABLE_SEAT_STATUSES,
17954
18458
  chapterMembershipAccess,
@@ -17966,6 +18470,7 @@ export {
17966
18470
  deliverMemberContent,
17967
18471
  deliverMembershipChapterEffect,
17968
18472
  deliverSignupControl,
18473
+ disputedChargeCoversSeat,
17969
18474
  drainMembershipEffects,
17970
18475
  grantIncludedNamedSeat,
17971
18476
  liftSeatPurchaseWindow,
@@ -17974,7 +18479,12 @@ export {
17974
18479
  namedSeatInvitationOpen,
17975
18480
  namedSeatRecipientState,
17976
18481
  namedSeatRegiftable,
18482
+ readDisputedCharge,
17977
18483
  readNamedSeatOffer,
18484
+ rechargeDisputedMembership,
18485
+ rechargeDisputedSeat,
18486
+ reconcileMembershipDispute,
18487
+ reconcileNamedSeatDispute,
17978
18488
  refundNamedSeatByOperator,
17979
18489
  regiftNamedSeat,
17980
18490
  releaseNamedSeatRecipientApplications,