@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.
@@ -24,6 +24,7 @@ __export(worker_exports, {
24
24
  DECLINABLE_FROM: () => DECLINABLE_FROM,
25
25
  MEMBERSHIP_AUTHORITY_PATH: () => MEMBERSHIP_AUTHORITY_PATH,
26
26
  MEMBERSHIP_CHAPTER_EFFECT_KINDS: () => MEMBERSHIP_CHAPTER_EFFECT_KINDS,
27
+ MEMBERSHIP_DISPUTED_STATUS: () => MEMBERSHIP_DISPUTED_STATUS,
27
28
  NetworkTargetTransportError: () => NetworkTargetTransportError,
28
29
  REGIFTABLE_SEAT_STATUSES: () => REGIFTABLE_SEAT_STATUSES,
29
30
  chapterMembershipAccess: () => chapterMembershipAccess,
@@ -41,6 +42,7 @@ __export(worker_exports, {
41
42
  deliverMemberContent: () => deliverMemberContent,
42
43
  deliverMembershipChapterEffect: () => deliverMembershipChapterEffect,
43
44
  deliverSignupControl: () => deliverSignupControl,
45
+ disputedChargeCoversSeat: () => disputedChargeCoversSeat,
44
46
  drainMembershipEffects: () => drainMembershipEffects,
45
47
  grantIncludedNamedSeat: () => grantIncludedNamedSeat,
46
48
  liftSeatPurchaseWindow: () => liftSeatPurchaseWindow,
@@ -49,7 +51,12 @@ __export(worker_exports, {
49
51
  namedSeatInvitationOpen: () => namedSeatInvitationOpen,
50
52
  namedSeatRecipientState: () => namedSeatRecipientState,
51
53
  namedSeatRegiftable: () => namedSeatRegiftable,
54
+ readDisputedCharge: () => readDisputedCharge,
52
55
  readNamedSeatOffer: () => readNamedSeatOffer,
56
+ rechargeDisputedMembership: () => rechargeDisputedMembership,
57
+ rechargeDisputedSeat: () => rechargeDisputedSeat,
58
+ reconcileMembershipDispute: () => reconcileMembershipDispute,
59
+ reconcileNamedSeatDispute: () => reconcileNamedSeatDispute,
53
60
  refundNamedSeatByOperator: () => refundNamedSeatByOperator,
54
61
  regiftNamedSeat: () => regiftNamedSeat,
55
62
  releaseNamedSeatRecipientApplications: () => releaseNamedSeatRecipientApplications,
@@ -311,12 +318,37 @@ function networkCommercialAlerts(ctx, route) {
311
318
  };
312
319
  }
313
320
 
321
+ // src/worker-defer.ts
322
+ function deferWork(ctx, req, task) {
323
+ return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
324
+ }
325
+ function createDeferral() {
326
+ const executions = /* @__PURE__ */ new WeakMap();
327
+ const settle = (task) => task().then(() => void 0, (error) => {
328
+ console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
329
+ });
330
+ return {
331
+ /** Remember the runtime's ExecutionContext for this request. */
332
+ bind(req, execution) {
333
+ if (execution) executions.set(req, execution);
334
+ },
335
+ /** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
336
+ defer(req, task) {
337
+ const execution = executions.get(req);
338
+ if (!execution) return settle(task);
339
+ execution.waitUntil(settle(task));
340
+ return Promise.resolve();
341
+ }
342
+ };
343
+ }
344
+
314
345
  // src/worker-context.ts
315
346
  var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
316
347
  function createWorkerContext(options) {
317
348
  const { chapter } = options;
318
349
  const auth = chapter.auth;
319
350
  const crmBase = options.crmBasePath ?? "/api/crm";
351
+ const deferral = createDeferral();
320
352
  let publicConfigCache = null;
321
353
  const jwksByIssuer = /* @__PURE__ */ new Map();
322
354
  async function getPublicConfig(env) {
@@ -421,6 +453,8 @@ function createWorkerContext(options) {
421
453
  membershipAuthority: options.membershipAuthority,
422
454
  membershipAuthorityOperator: options.membershipAuthorityOperator,
423
455
  recordError: options.recordError ?? consoleErrorRecorder,
456
+ bindExecution: deferral.bind,
457
+ defer: deferral.defer,
424
458
  getPublicConfig,
425
459
  verifyUser,
426
460
  makeDb,
@@ -748,6 +782,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
748
782
  else if (eventType.startsWith("refund.")) objectKind = "refund";
749
783
  else if (eventType === "charge.refunded") objectKind = "refund";
750
784
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
785
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
751
786
  const customerId = refId(object2.customer, "cus_");
752
787
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
753
788
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -757,6 +792,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
757
792
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
758
793
  const currency = text(object2.currency)?.toLowerCase();
759
794
  const status2 = text(object2.status);
795
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
760
796
  return {
761
797
  eventId,
762
798
  digest,
@@ -775,6 +811,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
775
811
  ...invoiceId ? { invoiceId } : {},
776
812
  ...chargeId ? { chargeId } : {},
777
813
  ...paymentIntentId ? { paymentIntentId } : {},
814
+ ...disputeReason ? { disputeReason } : {},
778
815
  ...price,
779
816
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
780
817
  ...status2 ? { status: status2 } : {},
@@ -817,6 +854,7 @@ function ownRef(fact) {
817
854
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
818
855
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
819
856
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
857
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
820
858
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
821
859
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
822
860
  }
@@ -872,6 +910,7 @@ function projectNetworkCommercialEvents(input, sources) {
872
910
  continue;
873
911
  }
874
912
  const source = assignments.get(fact.eventId);
913
+ if (!source && fact.objectKind === "dispute") continue;
875
914
  if (!source) {
876
915
  const direct = directSource(fact, sources);
877
916
  const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -888,6 +927,7 @@ function projectNetworkCommercialEvents(input, sources) {
888
927
  const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
889
928
  const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
890
929
  const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
930
+ const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
891
931
  const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
892
932
  for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
893
933
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -908,6 +948,7 @@ function projectNetworkCommercialEvents(input, sources) {
908
948
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
909
949
  return {
910
950
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
951
+ ...disputes.length ? { disputes } : {},
911
952
  customers,
912
953
  subscriptions,
913
954
  invoices,
@@ -1244,6 +1285,17 @@ function findGiftRef(obj) {
1244
1285
  }
1245
1286
  function normalizeWebhookEvent(event2, options = {}) {
1246
1287
  const obj = event2.data?.object ?? {};
1288
+ if (event2.type.startsWith("charge.dispute.")) {
1289
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
1290
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
1291
+ return {
1292
+ kind: "disputed",
1293
+ chargeId,
1294
+ disputeId: obj.id,
1295
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
1296
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
1297
+ };
1298
+ }
1247
1299
  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 };
1248
1300
  const gift = findGiftRef(obj);
1249
1301
  if (gift.giftIntentId) {
@@ -1648,9 +1700,9 @@ async function backfillCrm(deps) {
1648
1700
  }
1649
1701
 
1650
1702
  // src/email-retirement.ts
1651
- function isRetiredEmailCopy(subject, text8) {
1703
+ function isRetiredEmailCopy(subject, text10) {
1652
1704
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1653
- const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1705
+ const bodyCopy = text10.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1654
1706
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1655
1707
  }
1656
1708
 
@@ -1728,10 +1780,10 @@ function planDelivery(input) {
1728
1780
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1729
1781
  if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1730
1782
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1731
- const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
1783
+ const text10 = redirect ? `(dev redirect; original recipient: ${input.to})
1732
1784
 
1733
1785
  ` + renderedText : renderedText;
1734
- return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
1786
+ return { deliver: true, transport, to, subject, text: text10, redirected: redirect };
1735
1787
  }
1736
1788
 
1737
1789
  // src/notify.ts
@@ -1879,8 +1931,8 @@ function formatInstant(at2, timeZone) {
1879
1931
  var adminUrlFor = (origin) => `${origin}/admin/`;
1880
1932
  var membersUrlFor = (origin) => `${origin}/members/`;
1881
1933
  function applicantVariables(app) {
1882
- const text8 = (v) => typeof v === "string" ? v : "";
1883
- return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
1934
+ const text10 = (v) => typeof v === "string" ? v : "";
1935
+ return { firstName: text10(app.firstName), lastName: text10(app.lastName), email: text10(app.email), phone: text10(app.phone), state: text10(app.state) };
1884
1936
  }
1885
1937
  function sampleVariables(origin, timeZone, now = Date.now()) {
1886
1938
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -2987,7 +3039,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
2987
3039
  if (!intent) throw new Error("membership checkout reconciliation pending");
2988
3040
  const target = await membershipTargetForIntent(intent, app, env, ctx);
2989
3041
  const row2 = await ctx.membershipAuthority.reconcileRenewal({ ...target, eventId }, env);
2990
- 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)) {
3042
+ 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)) {
2991
3043
  throw new Error("invalid network renewal receipt");
2992
3044
  }
2993
3045
  return row2;
@@ -3612,6 +3664,163 @@ function paymentLineItems(group, tier) {
3612
3664
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
3613
3665
  }
3614
3666
 
3667
+ // src/membership-dispute-authority.ts
3668
+ var import_stripe6 = require("@odla-ai/stripe");
3669
+
3670
+ // src/membership-dispute-transition.ts
3671
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
3672
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
3673
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
3674
+ function disputeTransition(row2, fact, eventId, now, terminal) {
3675
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
3676
+ const sameDispute = row2.disputeId === fact.objectId;
3677
+ if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
3678
+ const won = DISPUTE_WON_STATUSES.includes(status2);
3679
+ const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
3680
+ const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
3681
+ const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
3682
+ return {
3683
+ status: next,
3684
+ disputeId: fact.objectId,
3685
+ disputeStatus: status2,
3686
+ disputeEventId: eventId,
3687
+ disputeClosedAt: closed ? now : 0,
3688
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
3689
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
3690
+ ...sameDispute ? {} : { disputedAt: now },
3691
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
3692
+ revision: Number(row2.revision) + 1,
3693
+ updatedAt: now
3694
+ };
3695
+ }
3696
+
3697
+ // src/membership-seat-dispute.ts
3698
+ var text2 = (value) => typeof value === "string" && value ? value : void 0;
3699
+ var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
3700
+ var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text2(seat.disputeStatus) ? { disputeStatus: text2(seat.disputeStatus) } : {}, applied });
3701
+ function disputedChargeCoversSeat(charge, seat) {
3702
+ return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
3703
+ }
3704
+ async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
3705
+ if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
3706
+ const membership = async (applied, seat) => {
3707
+ const id7 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id7);
3708
+ requireMembershipScope(member, input);
3709
+ return {
3710
+ membershipId: id7,
3711
+ status: String(member.status),
3712
+ termEndsAt: Number(member.termEndsAt),
3713
+ graceEndsAt: Number(member.graceEndsAt ?? 0),
3714
+ autoRenew: member.autoRenew === true,
3715
+ applied,
3716
+ ...text2(member.disputeStatus) ? { disputeStatus: text2(member.disputeStatus) } : {},
3717
+ ...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
3718
+ };
3719
+ };
3720
+ for (let attempt = 0; attempt < 8; attempt++) {
3721
+ const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
3722
+ if (!seat) return membership(false);
3723
+ requireMembershipScope(seat, input);
3724
+ if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
3725
+ if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
3726
+ const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
3727
+ if (!attrs) return membership(false, seat);
3728
+ try {
3729
+ await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
3730
+ { ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
3731
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3732
+ { ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
3733
+ evidence.guard
3734
+ ] });
3735
+ } catch (error) {
3736
+ if (error.code !== "transact_guard_failed") throw error;
3737
+ continue;
3738
+ }
3739
+ const saved = await membershipLoad(db, "networkSeat", String(seat.id));
3740
+ requireMembershipScope(saved, input);
3741
+ return membership(true, saved);
3742
+ }
3743
+ return membershipFail("membership_dispute_retry_required");
3744
+ }
3745
+
3746
+ // src/membership-dispute-authority.ts
3747
+ var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
3748
+ var text3 = (value) => typeof value === "string" && value ? value : void 0;
3749
+ var refOf = (value) => typeof value === "string" ? text3(value) : text3(value?.id);
3750
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
3751
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
3752
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
3753
+ const charge = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/charges/${chargeId}`);
3754
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
3755
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
3756
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
3757
+ let subscriptionId;
3758
+ if (invoiceId) {
3759
+ const invoice = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/invoices/${invoiceId}`);
3760
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
3761
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
3762
+ }
3763
+ const seatId = text3(charge.body.metadata?.seatId);
3764
+ return {
3765
+ chargeId,
3766
+ customerId,
3767
+ ...invoiceId ? { invoiceId } : {},
3768
+ ...subscriptionId ? { subscriptionId } : {},
3769
+ ...paymentIntentId ? { paymentIntentId } : {},
3770
+ ...seatId ? { seatId } : {}
3771
+ };
3772
+ }
3773
+ function disputedChargeCoversMembership(charge, member) {
3774
+ 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);
3775
+ }
3776
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3777
+ membershipClock(now);
3778
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3779
+ const fact = row2?.fact;
3780
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3781
+ 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");
3782
+ const id7 = await networkMembershipId(input);
3783
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
3784
+ membershipId: id7,
3785
+ status: String(value.status),
3786
+ termEndsAt: Number(value.termEndsAt),
3787
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
3788
+ autoRenew: value.autoRenew === true,
3789
+ ...text3(value.disputeStatus) ? { disputeStatus: text3(value.disputeStatus) } : {},
3790
+ applied
3791
+ });
3792
+ const known = await membershipLoad(db, "networkMembership", id7);
3793
+ requireMembershipScope(known, input);
3794
+ if (known.disputeEventId === input.eventId) return receipt(known);
3795
+ const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
3796
+ if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
3797
+ const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
3798
+ if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
3799
+ if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
3800
+ for (let attempt = 0; attempt < 8; attempt++) {
3801
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
3802
+ requireMembershipScope(member, input);
3803
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
3804
+ const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
3805
+ if (!attrs) return receipt(member, false);
3806
+ try {
3807
+ await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
3808
+ { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
3809
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3810
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
3811
+ evidenceGuard
3812
+ ] });
3813
+ } catch (error) {
3814
+ if (error.code !== "transact_guard_failed") throw error;
3815
+ continue;
3816
+ }
3817
+ const saved = await membershipLoad(db, "networkMembership", id7);
3818
+ requireMembershipScope(saved, input);
3819
+ return receipt(saved, true);
3820
+ }
3821
+ return membershipFail("membership_dispute_retry_required");
3822
+ }
3823
+
3615
3824
  // src/member-system-contract.ts
3616
3825
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3617
3826
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3736,7 +3945,8 @@ async function firstRow2(db, ns, q) {
3736
3945
  }
3737
3946
  async function reconcileChapterStripeEvent(input, env, ctx) {
3738
3947
  const db = ctx.makeDb(env);
3739
- const { rawBody, eventId, event: event2 } = input;
3948
+ const { rawBody, eventId } = input;
3949
+ let { event: event2 } = input;
3740
3950
  if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
3741
3951
  const digest = await stripeEventDigest(rawBody);
3742
3952
  const legacyDigest = await stripePayloadDigest(rawBody);
@@ -3792,6 +4002,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3792
4002
  }
3793
4003
  return json({ ok: true, matched: Boolean(claim) });
3794
4004
  }
4005
+ if (event2.kind === "disputed" && !event2.customerId) {
4006
+ const secret = await getVaultSecret(db, "stripe_secret_key");
4007
+ const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
4008
+ if (charge) event2 = { ...event2, customerId: charge.customerId };
4009
+ }
3795
4010
  const app = await findPaymentApplication(db, event2);
3796
4011
  if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
3797
4012
  reportStripePartition("warn", eventId, event2, "ignore_sibling");
@@ -3829,11 +4044,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3829
4044
  }
3830
4045
  let authoritativeEvent = event2;
3831
4046
  let membershipProjection = {};
3832
- if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
4047
+ if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
3833
4048
  try {
3834
4049
  const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
3835
4050
  membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
3836
- authoritativeEvent = receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
4051
+ 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 };
3837
4052
  } catch {
3838
4053
  return json({ error: "network membership renewal reconciliation pending" }, 503);
3839
4054
  }
@@ -4482,10 +4697,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
4482
4697
  }
4483
4698
 
4484
4699
  // src/membership-seat-refund-stripe.ts
4485
- var import_stripe7 = require("@odla-ai/stripe");
4700
+ var import_stripe8 = require("@odla-ai/stripe");
4486
4701
 
4487
4702
  // src/membership-checkout-seat-refund.ts
4488
- var import_stripe6 = require("@odla-ai/stripe");
4703
+ var import_stripe7 = require("@odla-ai/stripe");
4489
4704
  var fail4 = (code = "named_seat_refund_mismatch") => {
4490
4705
  throw Object.assign(new Error(code), { code });
4491
4706
  };
@@ -4494,7 +4709,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
4494
4709
  const invoiceRef = seat.combinedInvoice;
4495
4710
  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();
4496
4711
  const call = async (method, path, params, key3) => {
4497
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4712
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4498
4713
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4499
4714
  return result.body;
4500
4715
  };
@@ -4567,7 +4782,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
4567
4782
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
4568
4783
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
4569
4784
  const call = async (method, path, params, idempotencyKey) => {
4570
- const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4785
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4571
4786
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
4572
4787
  return result.body;
4573
4788
  };
@@ -4709,6 +4924,9 @@ function namedSeatRecipientState(seat) {
4709
4924
  return seat.refundReason === "declined" ? "declined" : "refunded";
4710
4925
  case "canceled":
4711
4926
  return "canceled";
4927
+ // A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
4928
+ case "payment_disputed":
4929
+ return "disputed";
4712
4930
  default:
4713
4931
  return "payment_pending";
4714
4932
  }
@@ -5035,15 +5253,232 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
5035
5253
  }
5036
5254
  };
5037
5255
 
5256
+ // src/membership-dispute-recharge.ts
5257
+ var import_stripe9 = require("@odla-ai/stripe");
5258
+ var NS = { membership: "networkMembership", seat: "networkSeat" };
5259
+ var fail6 = (code, extra = {}) => {
5260
+ throw Object.assign(new Error(code), { code, ...extra });
5261
+ };
5262
+ var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
5263
+ async function defaultPaymentMethod(config, subscriptionId, customerId) {
5264
+ const subscription = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
5265
+ if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
5266
+ const onSubscription = refOf2(subscription.body.default_payment_method);
5267
+ if (onSubscription) return onSubscription;
5268
+ const customer = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
5269
+ if (!customer.ok) fail6("membership_dispute_provider_unavailable");
5270
+ return refOf2(customer.body.invoice_settings?.default_payment_method);
5271
+ }
5272
+ async function rechargeDisputed(db, kind, id7, secretKey, now) {
5273
+ membershipClock(now);
5274
+ const ns = NS[kind];
5275
+ let row2 = await membershipLoad(db, ns, id7);
5276
+ if (!row2) fail6("membership_not_found");
5277
+ const scope2 = { chapterId: String(row2.chapterId), environment: String(row2.environment) };
5278
+ const membershipId = kind === "membership" ? id7 : String(row2.primaryMembershipId);
5279
+ const amountCents = Number(row2.disputeAmountCents ?? row2.amountCents), currency = String(row2.currency);
5280
+ const done = (value, replayed) => ({
5281
+ membershipId,
5282
+ ...kind === "seat" ? { seatId: id7 } : {},
5283
+ status: String(value.status),
5284
+ paymentIntentId: String(value.rechargePaymentIntentId),
5285
+ amountCents,
5286
+ currency,
5287
+ ...replayed ? { replayed } : {}
5288
+ });
5289
+ if (row2.status !== MEMBERSHIP_DISPUTED_STATUS) {
5290
+ if (row2.disputeStatus === "recharged" && row2.rechargePaymentIntentId) return done(row2, true);
5291
+ fail6("membership_not_disputed");
5292
+ }
5293
+ if (row2.disputeStatus !== "lost") fail6("membership_dispute_open");
5294
+ if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
5295
+ let attempt = row2.rechargeAttempt;
5296
+ if (!attempt || attempt.finishedAt !== void 0) {
5297
+ attempt = { key: `${kind}-recharge:${id7}:${String(row2.disputeId)}:${now}`, startedAt: now };
5298
+ await db.transact(
5299
+ [membershipUpdate(ns, id7, { rechargeAttempt: attempt, revision: Number(row2.revision) + 1, updatedAt: now })],
5300
+ { mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{ ns, id: id7, exists: true, equals: { revision: row2.revision, status: MEMBERSHIP_DISPUTED_STATUS } }] }
5301
+ );
5302
+ row2 = await membershipLoad(db, ns, id7);
5303
+ if (!row2 || row2.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
5304
+ }
5305
+ const config = { secretKey };
5306
+ const paymentMethod = await defaultPaymentMethod(config, String(row2.providerSubscriptionId), String(row2.providerCustomerId));
5307
+ if (!paymentMethod) fail6("recharge_payment_method_unavailable");
5308
+ const intent = await (0, import_stripe9.stripeRequest)(config, "POST", "/v1/payment_intents", {
5309
+ amount: amountCents,
5310
+ currency,
5311
+ customer: String(row2.providerCustomerId),
5312
+ payment_method: paymentMethod,
5313
+ off_session: "true",
5314
+ confirm: "true",
5315
+ description: `${kind === "seat" ? "Named seat" : "Membership"} payment reissued after dispute ${String(row2.disputeId)}`,
5316
+ metadata: {
5317
+ ...stripePartitionMetadata({ appId: String(row2.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(row2.runtime) }),
5318
+ applicationId: String(row2.applicationId),
5319
+ membershipId,
5320
+ ...kind === "seat" ? { seatId: id7 } : {},
5321
+ disputeId: String(row2.disputeId),
5322
+ membershipRecharge: "true"
5323
+ }
5324
+ }, { idempotencyKey: attempt.key });
5325
+ const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
5326
+ const finish = async (outcome) => {
5327
+ const current = await membershipLoad(db, ns, id7);
5328
+ if (!current) return;
5329
+ await db.transact([membershipUpdate(ns, id7, {
5330
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome },
5331
+ revision: Number(current.revision) + 1,
5332
+ updatedAt: now
5333
+ })], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
5334
+ };
5335
+ if (!intent.ok) {
5336
+ if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
5337
+ await finish("declined");
5338
+ fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
5339
+ }
5340
+ fail6("membership_dispute_provider_unavailable");
5341
+ }
5342
+ if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
5343
+ await finish("declined");
5344
+ fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
5345
+ }
5346
+ if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
5347
+ if (refOf2(body.customer) !== row2.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
5348
+ for (let retry = 0; retry < 8; retry++) {
5349
+ const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, ns, id7);
5350
+ if (!current) fail6("membership_not_found");
5351
+ if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
5352
+ const status2 = String(current.disputePriorStatus ?? (kind === "seat" ? "active" : current.journeyApprovedAt ? "active" : "paid_pending_approval"));
5353
+ try {
5354
+ await db.transact(
5355
+ [membershipUpdate(ns, id7, {
5356
+ status: status2,
5357
+ disputeStatus: "recharged",
5358
+ rechargePaymentIntentId: String(body.id),
5359
+ rechargedAt: now,
5360
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
5361
+ revision: Number(current.revision) + 1,
5362
+ updatedAt: now
5363
+ })],
5364
+ { mutationId: `membership-recharged:${attempt.key}`, guards: [
5365
+ { ns, id: id7, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
5366
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
5367
+ ] }
5368
+ );
5369
+ } catch (cause) {
5370
+ if (cause.code !== "transact_guard_failed") throw cause;
5371
+ continue;
5372
+ }
5373
+ return done(await membershipLoad(db, ns, id7));
5374
+ }
5375
+ return fail6("membership_dispute_retry_required");
5376
+ }
5377
+ var rechargeDisputedMembership = (db, membershipId, secretKey, now) => rechargeDisputed(db, "membership", membershipId, secretKey, now);
5378
+ var rechargeDisputedSeat = (db, seatId, secretKey, now) => rechargeDisputed(db, "seat", seatId, secretKey, now);
5379
+
5380
+ // src/worker-routes-admin-membership-disputes.ts
5381
+ var LIST2 = "/api/admin/membership-disputes";
5382
+ var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
5383
+ var PUBLIC2 = [
5384
+ "id",
5385
+ "appId",
5386
+ "chapterId",
5387
+ "environment",
5388
+ "runtime",
5389
+ "applicationId",
5390
+ "skuId",
5391
+ "status",
5392
+ "memberNumber",
5393
+ "amountCents",
5394
+ "currency",
5395
+ "recipientName",
5396
+ "primaryMembershipId",
5397
+ "purchasedAt",
5398
+ "disputeId",
5399
+ "disputeStatus",
5400
+ "disputeReason",
5401
+ "disputeAmountCents",
5402
+ "disputedAt",
5403
+ "disputeClosedAt",
5404
+ "disputePriorStatus",
5405
+ "rechargePaymentIntentId",
5406
+ "rechargedAt",
5407
+ "termEndsAt",
5408
+ "updatedAt"
5409
+ ];
5410
+ var project2 = (row2) => ({
5411
+ ...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
5412
+ ...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
5413
+ startedAt: row2.rechargeAttempt.startedAt ?? null,
5414
+ finishedAt: row2.rechargeAttempt.finishedAt ?? null,
5415
+ outcome: row2.rechargeAttempt.outcome ?? null
5416
+ } } : {}
5417
+ });
5418
+ var FINAL = /* @__PURE__ */ new Set([
5419
+ "membership_not_found",
5420
+ "membership_not_disputed",
5421
+ "membership_dispute_open",
5422
+ "membership_dispute_amount_invalid",
5423
+ "recharge_payment_method_unavailable",
5424
+ "recharge_declined",
5425
+ "recharge_pending",
5426
+ "recharge_mismatch",
5427
+ "membership_dispute_changed"
5428
+ ]);
5429
+ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5430
+ const one = url.pathname.match(ONE2);
5431
+ if (url.pathname !== LIST2 && !one) return null;
5432
+ const operator = ctx.membershipAuthorityOperator;
5433
+ if (!operator) return json({ error: "not found" }, 404);
5434
+ if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
5435
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
5436
+ if (!user) return json({ error: "unauthorized" }, 401);
5437
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
5438
+ let db;
5439
+ try {
5440
+ db = operator.makeDb(env);
5441
+ } catch {
5442
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
5443
+ }
5444
+ if (!one) {
5445
+ const page = { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } };
5446
+ const found = await db.query({ networkMembership: page, networkSeat: page });
5447
+ const memberships = found.networkMembership ?? [], seats = found.networkSeat ?? [];
5448
+ const rows5 = [...memberships.map((row2) => ({ ...project2(row2), kind: "membership" })), ...seats.map((row2) => ({ ...project2(row2), kind: "seat" }))];
5449
+ const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5450
+ return json({ disputes, truncated: memberships.length >= 500 || seats.length >= 500 });
5451
+ }
5452
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
5453
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
5454
+ try {
5455
+ const receipt = await (one[1].startsWith("seat_") ? rechargeDisputedSeat : rechargeDisputedMembership)(db, one[1], secret, Date.now());
5456
+ return json({ ok: true, ...receipt });
5457
+ } catch (error) {
5458
+ const failure = error;
5459
+ const code = typeof failure.code === "string" ? failure.code : void 0;
5460
+ if (code === "membership_not_found") return json({ error: "not found", code }, 404);
5461
+ if (code && FINAL.has(code)) {
5462
+ return json({
5463
+ error: "the charge could not be re-issued",
5464
+ code,
5465
+ ...failure.declineCode ? { declineCode: failure.declineCode } : {},
5466
+ ...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
5467
+ }, 409);
5468
+ }
5469
+ return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
5470
+ }
5471
+ };
5472
+
5038
5473
  // src/membership-policy-versions.ts
5039
5474
  var LIMIT = 200;
5040
- var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
5475
+ var text4 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
5041
5476
  var instant = (value) => {
5042
5477
  const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
5043
5478
  return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
5044
5479
  };
5045
5480
  function provenance(row2) {
5046
- const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
5481
+ const source = text4(row2.source), actor = text4(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
5047
5482
  return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
5048
5483
  }
5049
5484
  var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
@@ -5137,16 +5572,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
5137
5572
  };
5138
5573
 
5139
5574
  // src/membership-discount-reterm.ts
5140
- var import_stripe9 = require("@odla-ai/stripe");
5575
+ var import_stripe11 = require("@odla-ai/stripe");
5141
5576
 
5142
5577
  // src/subscription-provider-contract.ts
5143
- var import_stripe8 = require("@odla-ai/stripe");
5578
+ var import_stripe10 = require("@odla-ai/stripe");
5144
5579
  function subscriptionContractMismatch() {
5145
5580
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5146
5581
  }
5147
5582
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
5148
5583
  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();
5149
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5584
+ const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5150
5585
  const price = result.body;
5151
5586
  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();
5152
5587
  return typeof price.product === "string" ? price.product : void 0;
@@ -5184,7 +5619,7 @@ async function retermPolicy(db, scope2, id7) {
5184
5619
  }
5185
5620
  async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
5186
5621
  const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
5187
- const read2 = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5622
+ const read2 = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5188
5623
  const sub = read2.body, items = sub?.items?.data;
5189
5624
  const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
5190
5625
  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) {
@@ -5195,7 +5630,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
5195
5630
  const present = discounts.map(couponOf);
5196
5631
  if (present.length === 1 && present[0] === couponId) return false;
5197
5632
  if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
5198
- const written = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "POST", path, {
5633
+ const written = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "POST", path, {
5199
5634
  "discounts[0][coupon]": couponId,
5200
5635
  "expand[0]": "discounts",
5201
5636
  "metadata[discountPolicyVersionId]": policy2.id
@@ -6044,8 +6479,8 @@ var APPLICATION_LABELS = {
6044
6479
  state: "State"
6045
6480
  };
6046
6481
  function humanize(id7) {
6047
- const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6048
- return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
6482
+ const text10 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6483
+ return text10 ? `${text10[0].toUpperCase()}${text10.slice(1)}` : id7;
6049
6484
  }
6050
6485
  function applicationDefs(chapter) {
6051
6486
  try {
@@ -6112,7 +6547,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
6112
6547
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
6113
6548
  var CURRENCY = /^[a-z]{3}$/;
6114
6549
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
6115
- var text3 = (value, path, errors, max) => {
6550
+ var text5 = (value, path, errors, max) => {
6116
6551
  if (typeof value !== "string" || !value.trim()) {
6117
6552
  errors.push(`${path} is required`);
6118
6553
  return "";
@@ -6140,23 +6575,23 @@ function validateGiftOfferCatalog(input, tierIds) {
6140
6575
  }
6141
6576
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6142
6577
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6143
- const id7 = text3(item.id, `${path}.id`, errors, 80);
6578
+ const id7 = text5(item.id, `${path}.id`, errors, 80);
6144
6579
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6145
6580
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
6146
6581
  ids2.add(id7);
6147
6582
  const targetRaw = record(item.target);
6148
6583
  let target = { type: "entitlement", entitlementKey: "invalid" };
6149
6584
  if (targetRaw?.type === "tier") {
6150
- const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6585
+ const tierId = text5(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6151
6586
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
6152
6587
  target = { type: "tier", tierId };
6153
6588
  } else if (targetRaw?.type === "entitlement") {
6154
- const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6589
+ const entitlementKey = text5(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6155
6590
  if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
6156
6591
  target = { type: "entitlement", entitlementKey };
6157
6592
  } else errors.push(`${path}.target.type must be tier or entitlement`);
6158
6593
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
6159
- const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6594
+ const stripePriceId = text5(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6160
6595
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
6161
6596
  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`);
6162
6597
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -6178,9 +6613,9 @@ function validateGiftOfferCatalog(input, tierIds) {
6178
6613
  redemptionLifetimeHours: lifetime,
6179
6614
  ...activeFrom !== void 0 ? { activeFrom } : {},
6180
6615
  ...activeUntil !== void 0 ? { activeUntil } : {},
6181
- title: text3(item.title, `${path}.title`, errors, 160),
6182
- description: text3(item.description, `${path}.description`, errors, 2e3),
6183
- ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6616
+ title: text5(item.title, `${path}.title`, errors, 160),
6617
+ description: text5(item.description, `${path}.description`, errors, 2e3),
6618
+ ctaLabel: text5(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6184
6619
  });
6185
6620
  }
6186
6621
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -6407,23 +6842,23 @@ async function createPaymentQuote(input) {
6407
6842
  // src/membership-quote-projection.ts
6408
6843
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
6409
6844
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
6410
- var fail6 = (code) => {
6845
+ var fail7 = (code) => {
6411
6846
  throw Object.assign(new Error(code), { code });
6412
6847
  };
6413
6848
  var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
6414
6849
  async function restore2(row2, target, now, allowExpired = false) {
6415
- 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");
6416
- if (row2.status !== "ready") return fail6("membership_quote_pending");
6850
+ 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");
6851
+ if (row2.status !== "ready") return fail7("membership_quote_pending");
6417
6852
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
6418
6853
  ...target,
6419
6854
  requestId: row2.requestId
6420
6855
  }), now, lifetimeMs: 6e4 });
6421
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
6856
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
6422
6857
  const tier = row2.tier, quote = row2.quote;
6423
6858
  const offering = offer.contract.offering;
6424
- if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
6859
+ if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
6425
6860
  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) {
6426
- return fail6("membership_catalog_mismatch");
6861
+ return fail7("membership_catalog_mismatch");
6427
6862
  }
6428
6863
  const expected = await createPaymentQuote({
6429
6864
  ...target,
@@ -6436,7 +6871,7 @@ async function restore2(row2, target, now, allowExpired = false) {
6436
6871
  seatOffer: quote.seatOffer,
6437
6872
  namedSeat: quote.namedSeat
6438
6873
  });
6439
- if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
6874
+ if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
6440
6875
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
6441
6876
  }
6442
6877
  async function readMembershipQuote(db, target, now) {
@@ -6444,9 +6879,9 @@ async function readMembershipQuote(db, target, now) {
6444
6879
  }
6445
6880
  async function prepareMembershipQuote(db, input, issue) {
6446
6881
  const { applicationId: id7, now, app } = input;
6447
- 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");
6882
+ 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");
6448
6883
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
6449
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
6884
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
6450
6885
  if (row2?.status === "ready") {
6451
6886
  try {
6452
6887
  return await restore2(row2, input, now);
@@ -6491,7 +6926,7 @@ async function prepareMembershipQuote(db, input, issue) {
6491
6926
  }
6492
6927
  if (row2?.status === "ready") return restore2(row2, input, now);
6493
6928
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
6494
- return fail6("membership_quote_pending");
6929
+ return fail7("membership_quote_pending");
6495
6930
  }
6496
6931
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
6497
6932
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -6540,7 +6975,7 @@ async function prepareMembershipQuote(db, input, issue) {
6540
6975
  return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
6541
6976
  }
6542
6977
  async function selectCheckoutNamedSeat(db, target, input) {
6543
- if (!input.enabled) return fail6("named_seat_purchase_unavailable");
6978
+ if (!input.enabled) return fail7("named_seat_purchase_unavailable");
6544
6979
  const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
6545
6980
  const current = await restore2(row2, target, input.now);
6546
6981
  const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
@@ -6596,17 +7031,17 @@ async function readBoundedRequestText(req, maxBytes) {
6596
7031
  if (!reader) return "";
6597
7032
  const decoder = new TextDecoder();
6598
7033
  let bytes = 0;
6599
- let text8 = "";
7034
+ let text10 = "";
6600
7035
  for (; ; ) {
6601
7036
  const { done, value } = await reader.read();
6602
7037
  if (req.signal.aborted) return invalid2();
6603
7038
  if (done) {
6604
7039
  complete = true;
6605
- return text8 + decoder.decode();
7040
+ return text10 + decoder.decode();
6606
7041
  }
6607
7042
  bytes += value.byteLength;
6608
7043
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
6609
- text8 += decoder.decode(value, { stream: true });
7044
+ text10 += decoder.decode(value, { stream: true });
6610
7045
  }
6611
7046
  } catch {
6612
7047
  return invalid2();
@@ -6710,9 +7145,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
6710
7145
  let selection;
6711
7146
  if (request?.body) {
6712
7147
  try {
6713
- const text8 = await readBoundedRequestText(request, 2048);
6714
- if (text8 instanceof Response) return text8;
6715
- const body = JSON.parse(text8);
7148
+ const text10 = await readBoundedRequestText(request, 2048);
7149
+ if (text10 instanceof Response) return text10;
7150
+ const body = JSON.parse(text10);
6716
7151
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
6717
7152
  selection = body.namedSeat;
6718
7153
  } catch {
@@ -7367,10 +7802,13 @@ var handleMember = async (req, url, env, ctx) => {
7367
7802
  const db = ctx.makeDb(env);
7368
7803
  const groupId = url.searchParams.get("group") ?? chapter.id;
7369
7804
  if (groupId !== chapter.id) return json({ error: "not found" }, 404);
7370
- const { groups } = await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } });
7805
+ const [{ groups }, stripeKey, activeControl] = await Promise.all([
7806
+ db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
7807
+ getVaultSecret(db, "stripe_secret_key"),
7808
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
7809
+ ]);
7371
7810
  const group = Array.isArray(groups) ? groups[0] : void 0;
7372
7811
  if (!group) return json({ error: "not found" }, 404);
7373
- const stripeKey = await getVaultSecret(db, "stripe_secret_key");
7374
7812
  const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
7375
7813
  const offered = await loadOfferableTiers(
7376
7814
  db,
@@ -7379,7 +7817,6 @@ var handleMember = async (req, url, env, ctx) => {
7379
7817
  );
7380
7818
  const seats = /* @__PURE__ */ new Map();
7381
7819
  const founding = await foundingPreviews(offered, env, ctx, seats);
7382
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7383
7820
  const application = controlledApplication(chapter.application, activeControl?.document);
7384
7821
  return json({
7385
7822
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
@@ -7417,11 +7854,14 @@ var handleMember = async (req, url, env, ctx) => {
7417
7854
  parsed.tierId = named.invitation.skuId;
7418
7855
  submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
7419
7856
  }
7420
- const eligibility = named ? null : await resolveSignupEligibility(db, chapter, parsed, submissionId);
7857
+ const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
7858
+ named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
7859
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
7860
+ typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
7861
+ db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
7862
+ ]);
7421
7863
  if (eligibility?.conflict) return eligibility.conflict;
7422
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7423
7864
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
7424
- const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
7425
7865
  const now = Date.now();
7426
7866
  const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
7427
7867
  const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
@@ -7460,18 +7900,16 @@ var handleMember = async (req, url, env, ctx) => {
7460
7900
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
7461
7901
  return json(provisioningFailure.body, provisioningFailure.status);
7462
7902
  }
7463
- if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
7464
- const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
7465
- const group = Array.isArray(groups) ? groups[0] : void 0;
7903
+ if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
7466
7904
  const notice = group ? { group, application: canonical3, origin: url.origin } : null;
7467
- if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
7905
+ if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
7468
7906
  const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
7469
7907
  if (!state) return json({
7470
7908
  error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
7471
7909
  code: "application_continuation_unavailable",
7472
7910
  id: result.id
7473
7911
  }, 503);
7474
- if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
7912
+ if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
7475
7913
  const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
7476
7914
  if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
7477
7915
  return json({ error: "application changed; reload its current state" }, 409);
@@ -7591,10 +8029,10 @@ function boundedString(value, path, errors, max, required = false) {
7591
8029
  errors.push(`${path} must be a string`);
7592
8030
  return void 0;
7593
8031
  }
7594
- const text8 = value.trim();
7595
- if (required && !text8) errors.push(`${path} is required`);
7596
- if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7597
- return text8;
8032
+ const text10 = value.trim();
8033
+ if (required && !text10) errors.push(`${path} is required`);
8034
+ if (text10.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
8035
+ return text10;
7598
8036
  }
7599
8037
  function validateMembershipPageDocument(input, options = {}) {
7600
8038
  const errors = [];
@@ -7643,8 +8081,8 @@ function validateMembershipPageDocument(input, options = {}) {
7643
8081
  else {
7644
8082
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
7645
8083
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
7646
- const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
7647
- return text8 ? [text8] : [];
8084
+ const text10 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8085
+ return text10 ? [text10] : [];
7648
8086
  });
7649
8087
  }
7650
8088
  }
@@ -7738,7 +8176,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
7738
8176
  function unknownKeys(value, allowed2, path, errors) {
7739
8177
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7740
8178
  }
7741
- function text4(value, path, errors, max = 65536) {
8179
+ function text6(value, path, errors, max = 65536) {
7742
8180
  if (typeof value !== "string" || !value.trim()) {
7743
8181
  errors.push(`${path} is required`);
7744
8182
  return "";
@@ -7761,11 +8199,11 @@ function tiersOf(value, errors) {
7761
8199
  return [];
7762
8200
  }
7763
8201
  unknownKeys(item, TIER_KEYS2, path, errors);
7764
- const id7 = text4(item.id, `${path}.id`, errors, 80);
8202
+ const id7 = text6(item.id, `${path}.id`, errors, 80);
7765
8203
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7766
8204
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
7767
8205
  seen.add(id7);
7768
- const name = text4(item.name, `${path}.name`, errors, 160);
8206
+ const name = text6(item.name, `${path}.name`, errors, 160);
7769
8207
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
7770
8208
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
7771
8209
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -7783,7 +8221,7 @@ function tiersOf(value, errors) {
7783
8221
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
7784
8222
  errors.push(`${path}.interval must be year or month`);
7785
8223
  }
7786
- const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
8224
+ const blurb = item.blurb === void 0 ? void 0 : text6(item.blurb, `${path}.blurb`, errors, 4e3);
7787
8225
  return [{
7788
8226
  id: id7,
7789
8227
  name,
@@ -7841,7 +8279,7 @@ function policyOf(value, errors, tiers) {
7841
8279
  const result = {};
7842
8280
  for (const key3 of allowed2) {
7843
8281
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
7844
- result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
8282
+ result[key3] = text6(input[key3], `document.policy.${key3}`, errors);
7845
8283
  }
7846
8284
  const tierPolicies = {};
7847
8285
  if (input.tierPolicies !== void 0) {
@@ -7856,8 +8294,8 @@ function policyOf(value, errors, tiers) {
7856
8294
  }
7857
8295
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
7858
8296
  tierPolicies[id7] = {
7859
- refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
7860
- ...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8297
+ refundPolicyText: text6(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8298
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text6(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
7861
8299
  };
7862
8300
  }
7863
8301
  }
@@ -7897,9 +8335,9 @@ function validateSignupControlDocument(input) {
7897
8335
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
7898
8336
  const source = recordOf(root.source);
7899
8337
  const target = recordOf(root.target);
7900
- const appId = text4(source?.appId, "document.source.appId", errors, 63);
7901
- const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
7902
- const environment = text4(target?.environment, "document.target.environment", errors, 63);
8338
+ const appId = text6(source?.appId, "document.source.appId", errors, 63);
8339
+ const chapterId = text6(target?.chapterId, "document.target.chapterId", errors, 63);
8340
+ const environment = text6(target?.environment, "document.target.environment", errors, 63);
7903
8341
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7904
8342
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
7905
8343
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9411,8 +9849,8 @@ async function bookSlot(req, env, ctx) {
9411
9849
  const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
9412
9850
  await db.transact([meetingOp, appOp]);
9413
9851
  const booked = { ...app, ...bookingAttrs };
9414
- await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
9415
- await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
9852
+ await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
9853
+ await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
9416
9854
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
9417
9855
  }
9418
9856
  var handleSchedule = async (req, url, env, ctx) => {
@@ -9436,28 +9874,28 @@ var handleSchedule = async (req, url, env, ctx) => {
9436
9874
  };
9437
9875
 
9438
9876
  // src/payments-stripe.ts
9439
- var import_stripe12 = require("@odla-ai/stripe");
9877
+ var import_stripe14 = require("@odla-ai/stripe");
9440
9878
 
9441
9879
  // src/payments-refund.ts
9442
- var import_stripe10 = require("@odla-ai/stripe");
9880
+ var import_stripe12 = require("@odla-ai/stripe");
9443
9881
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9444
9882
  var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
9445
9883
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
9446
- function fail7(code = "refund_target_mismatch") {
9884
+ function fail8(code = "refund_target_mismatch") {
9447
9885
  throw Object.assign(new Error(code), { code });
9448
9886
  }
9449
9887
  async function refundSubscriptionPayment(secretKey, input) {
9450
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
9888
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
9451
9889
  const call = async (method, path, params, key3) => {
9452
- const result = await (0, import_stripe10.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9453
- if (!result.ok) fail7("stripe_error");
9454
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
9890
+ const result = await (0, import_stripe12.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9891
+ if (!result.ok) fail8("stripe_error");
9892
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
9455
9893
  return result.body;
9456
9894
  };
9457
9895
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
9458
- if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
9896
+ if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
9459
9897
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
9460
- if (row(sub.metadata)[key3] !== value) fail7();
9898
+ if (row(sub.metadata)[key3] !== value) fail8();
9461
9899
  }
9462
9900
  let cursor;
9463
9901
  let earliest;
@@ -9470,66 +9908,66 @@ async function refundSubscriptionPayment(secretKey, input) {
9470
9908
  limit: 100,
9471
9909
  ...cursor ? { starting_after: cursor } : {}
9472
9910
  });
9473
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
9911
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
9474
9912
  for (const value of list.data) {
9475
9913
  const invoice2 = row(value);
9476
9914
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
9477
- 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();
9915
+ 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();
9478
9916
  seen.add(id3(invoice2));
9479
9917
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
9480
9918
  }
9481
9919
  if (!list.has_more) break;
9482
- if (!list.data.length || page === 9) fail7("refund_history_incomplete");
9920
+ if (!list.data.length || page === 9) fail8("refund_history_incomplete");
9483
9921
  cursor = id3(list.data[list.data.length - 1]);
9484
9922
  }
9485
- if (!earliest) fail7("no_charge");
9923
+ if (!earliest) fail8("no_charge");
9486
9924
  const invoice = earliest;
9487
- if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
9925
+ if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
9488
9926
  let chargeId = id3(invoice.charge);
9489
9927
  let paymentIntentId = id3(invoice.payment_intent);
9490
9928
  if (!chargeId && !paymentIntentId) {
9491
9929
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
9492
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
9930
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
9493
9931
  const payment = row(payments.data[0]);
9494
- if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
9932
+ if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
9495
9933
  paymentIntentId = id3(row(payment.payment).payment_intent);
9496
9934
  }
9497
9935
  if (!chargeId) {
9498
- if (!providerId2(paymentIntentId, "pi")) fail7();
9936
+ if (!providerId2(paymentIntentId, "pi")) fail8();
9499
9937
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
9500
- if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
9938
+ if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
9501
9939
  chargeId = id3(payment.latest_charge);
9502
9940
  }
9503
- if (!providerId2(chargeId, "ch")) fail7();
9941
+ if (!providerId2(chargeId, "ch")) fail8();
9504
9942
  const charge = await call("GET", `/v1/charges/${chargeId}`);
9505
- 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();
9506
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
9943
+ 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();
9944
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
9507
9945
  if (charge.amount_refunded !== charge.amount) {
9508
9946
  const refund = await call("POST", "/v1/refunds", {
9509
9947
  charge: chargeId,
9510
9948
  ...input.metadata ? { metadata: input.metadata } : {}
9511
9949
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
9512
- 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");
9950
+ 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");
9513
9951
  }
9514
9952
  let subscriptionCanceled = sub.status === "canceled";
9515
9953
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
9516
9954
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
9517
- if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
9955
+ if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
9518
9956
  subscriptionCanceled = true;
9519
9957
  }
9520
9958
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
9521
- 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");
9959
+ 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");
9522
9960
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
9523
9961
  }
9524
9962
 
9525
9963
  // src/subscription-payment-identity.ts
9526
- var import_stripe11 = require("@odla-ai/stripe");
9964
+ var import_stripe13 = require("@odla-ai/stripe");
9527
9965
  var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
9528
9966
  async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
9529
9967
  let paymentId = id4(invoice.payment_intent);
9530
9968
  if (!paymentId) {
9531
9969
  if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
9532
- const result2 = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9970
+ const result2 = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9533
9971
  const list = result2.body;
9534
9972
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
9535
9973
  const link = list.data[0];
@@ -9538,11 +9976,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9538
9976
  }
9539
9977
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
9540
9978
  const path = `/v1/payment_intents/${paymentId}`;
9541
- const result = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", path);
9979
+ const result = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", path);
9542
9980
  const payment = result.body;
9543
9981
  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();
9544
9982
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
9545
- const tagged = await (0, import_stripe11.stripeRequest)(
9983
+ const tagged = await (0, import_stripe13.stripeRequest)(
9546
9984
  { secretKey },
9547
9985
  "POST",
9548
9986
  path,
@@ -9554,7 +9992,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9554
9992
 
9555
9993
  // src/payments-stripe.ts
9556
9994
  async function stripeCall(sk, method, path, params, idempotencyKey) {
9557
- return (0, import_stripe12.stripeRequest)(
9995
+ return (0, import_stripe14.stripeRequest)(
9558
9996
  { secretKey: sk },
9559
9997
  method,
9560
9998
  path,
@@ -9562,7 +10000,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
9562
10000
  idempotencyKey ? { idempotencyKey } : {}
9563
10001
  );
9564
10002
  }
9565
- function fail8(op, r) {
10003
+ function fail9(op, r) {
9566
10004
  const err = new Error(`stripe ${op} failed: ${r.status}`);
9567
10005
  err.code = "stripe_error";
9568
10006
  throw err;
@@ -9621,7 +10059,7 @@ function createStripeProvider(config) {
9621
10059
  { email: input.email, name: input.name, metadata: meta },
9622
10060
  `cus:${input.applicationId}`
9623
10061
  );
9624
- if (!cust.ok) fail8("customer create", cust);
10062
+ if (!cust.ok) fail9("customer create", cust);
9625
10063
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
9626
10064
  customerId = cust.body.id;
9627
10065
  }
@@ -9666,7 +10104,7 @@ function createStripeProvider(config) {
9666
10104
  // retry between create and the db write can't orphan a second one.
9667
10105
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
9668
10106
  );
9669
- if (!sub.ok) fail8("subscription create", sub);
10107
+ if (!sub.ok) fail9("subscription create", sub);
9670
10108
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
9671
10109
  const subscriptionId = sub.body.id;
9672
10110
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -9676,14 +10114,14 @@ function createStripeProvider(config) {
9676
10114
  "expand[2]": "discounts",
9677
10115
  "expand[3]": "latest_invoice.discounts"
9678
10116
  });
9679
- if (!expanded.ok) fail8("subscription invoice evidence", expanded);
10117
+ if (!expanded.ok) fail9("subscription invoice evidence", expanded);
9680
10118
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
9681
10119
  sub = expanded;
9682
10120
  }
9683
10121
  verifySubscriptionResult(sub.body, input, customerId, meta);
9684
10122
  const invoice = sub.body.latest_invoice;
9685
10123
  const clientSecret2 = clientSecretOf(sub.body);
9686
- if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
10124
+ if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
9687
10125
  await tagSubscriptionPayment(
9688
10126
  sk,
9689
10127
  invoice,
@@ -9695,7 +10133,7 @@ function createStripeProvider(config) {
9695
10133
  return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
9696
10134
  },
9697
10135
  async ingestWebhook(rawBody, sigHeader) {
9698
- if (!webhookSecret || !await (0, import_stripe12.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
10136
+ if (!webhookSecret || !await (0, import_stripe14.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
9699
10137
  return { ok: false, reason: "bad_signature" };
9700
10138
  }
9701
10139
  const event2 = JSON.parse(rawBody);
@@ -10170,7 +10608,7 @@ async function startSubscription(req, env, ctx) {
10170
10608
 
10171
10609
  // src/worker-payment-receipt.ts
10172
10610
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10173
- var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10611
+ var escape = (text10) => text10.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10174
10612
  async function paymentReceiptRoute(req, url, env, ctx) {
10175
10613
  const id7 = url.searchParams.get("application");
10176
10614
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
@@ -10181,7 +10619,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10181
10619
  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);
10182
10620
  const receipt = intent.paymentReceipt;
10183
10621
  if (!receipt) return json2({ pending: true }, 202);
10184
- const text8 = paymentReceiptText(receipt, ctx.chapter.name);
10622
+ const text10 = paymentReceiptText(receipt, ctx.chapter.name);
10185
10623
  if (req.method === "POST") {
10186
10624
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10187
10625
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10199,7 +10637,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10199
10637
  template: "paymentReceipt",
10200
10638
  to: String(intent.purchaserEmailNormalized),
10201
10639
  applicationId: id7,
10202
- vars: { chapterName: ctx.chapter.name, receiptText: text8 },
10640
+ vars: { chapterName: ctx.chapter.name, receiptText: text10 },
10203
10641
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10204
10642
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10205
10643
  });
@@ -10209,7 +10647,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10209
10647
  const nonce = crypto.randomUUID();
10210
10648
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10211
10649
  <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>
10212
- <h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
10650
+ <h1>Payment receipt</h1><pre>${escape(text10)}</pre><button id="print">Print / save PDF</button>
10213
10651
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10214
10652
  headers: {
10215
10653
  "content-type": "text/html; charset=utf-8",
@@ -10679,16 +11117,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
10679
11117
  };
10680
11118
 
10681
11119
  // src/named-seat-accepted-notice.ts
10682
- var text5 = (value) => typeof value === "string" ? value : "";
11120
+ var text7 = (value) => typeof value === "string" ? value : "";
10683
11121
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
10684
11122
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
10685
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
11123
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text7(purchaser.email)) {
10686
11124
  return { sent: false, reason: "purchaser-missing" };
10687
11125
  }
10688
11126
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
10689
11127
  if (!group) return { sent: false, reason: "group-missing" };
10690
11128
  const config = emailGroupFrom(group);
10691
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
11129
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text7).join(" ").replace(/\s+/g, " ").trim();
10692
11130
  return sendTemplated({
10693
11131
  db,
10694
11132
  envName: env.ODLA_ENV,
@@ -10700,12 +11138,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
10700
11138
  }, {
10701
11139
  group: config,
10702
11140
  template: "seatAccepted",
10703
- to: text5(purchaser.email),
11141
+ to: text7(purchaser.email),
10704
11142
  dedupeKey: `named-seat-accepted:${input.seatId}`,
10705
11143
  applicationId: input.primaryApplicationId,
10706
11144
  tierId: input.skuId,
10707
11145
  vars: {
10708
- firstName: text5(purchaser.firstName),
11146
+ firstName: text7(purchaser.firstName),
10709
11147
  purchaserName,
10710
11148
  recipientName: input.recipientName,
10711
11149
  tierName: await tierName(db, group, input.skuId),
@@ -12172,12 +12610,12 @@ async function updateEmailSettings(db, ctx, env, body) {
12172
12610
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12173
12611
  const t = value;
12174
12612
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12175
- const subject = str2(t.subject).trim(), text8 = str2(t.text);
12176
- const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text8 }, { allowedVariables: events[key3].variables });
12613
+ const subject = str2(t.subject).trim(), text10 = str2(t.text);
12614
+ const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text10 }, { allowedVariables: events[key3].variables });
12177
12615
  if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
12178
12616
  return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
12179
12617
  }
12180
- clean[key3] = { subject, text: text8, enabled: t.enabled };
12618
+ clean[key3] = { subject, text: text10, enabled: t.enabled };
12181
12619
  }
12182
12620
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12183
12621
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -14381,9 +14819,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14381
14819
  if (req.method === "POST") {
14382
14820
  let body;
14383
14821
  try {
14384
- const text8 = await req.text();
14385
- if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14386
- const parsed = JSON.parse(text8);
14822
+ const text10 = await req.text();
14823
+ if (new TextEncoder().encode(text10).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14824
+ const parsed = JSON.parse(text10);
14387
14825
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14388
14826
  body = parsed;
14389
14827
  } catch {
@@ -14544,7 +14982,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14544
14982
  };
14545
14983
 
14546
14984
  // src/worker-routes-network-commercial.ts
14547
- var import_stripe13 = require("@odla-ai/stripe");
14985
+ var import_stripe15 = require("@odla-ai/stripe");
14548
14986
 
14549
14987
  // src/worker-routes-network-commercial-parity.ts
14550
14988
  var import_db13 = require("@odla-ai/db");
@@ -14552,7 +14990,7 @@ var import_db13 = require("@odla-ai/db");
14552
14990
  // src/network-commercial-parity.ts
14553
14991
  var DIGEST = /^[0-9a-f]{64}$/;
14554
14992
  var MAX_RECORDS = 500;
14555
- var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14993
+ var text8 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14556
14994
  function parityMessage(partition, kind, value) {
14557
14995
  return [
14558
14996
  "chapter-commercial-parity",
@@ -14584,18 +15022,18 @@ async function digestWithKey(key3, partition, kind, value) {
14584
15022
  async function buildChapterCommercialParitySnapshot(input) {
14585
15023
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
14586
15024
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
14587
- const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
15025
+ const eligible = input.rows.filter((row2) => text8(row2.id) && (text8(row2.stripeCustomerId) || text8(row2.stripeSubscriptionId)));
14588
15026
  const selected2 = eligible.slice(0, maxRecords);
14589
15027
  const key3 = await parityKey(input.secret);
14590
15028
  const records = await Promise.all(selected2.map(async (row2) => {
14591
- const applicationId = text6(row2.id);
14592
- const customerId = text6(row2.stripeCustomerId);
14593
- const subscriptionId = text6(row2.stripeSubscriptionId);
15029
+ const applicationId = text8(row2.id);
15030
+ const customerId = text8(row2.stripeCustomerId);
15031
+ const subscriptionId = text8(row2.stripeSubscriptionId);
14594
15032
  return {
14595
15033
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
14596
15034
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
14597
15035
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
14598
- status: text6(row2.status, 64),
15036
+ status: text8(row2.status, 64),
14599
15037
  canceled: row2.canceled === true
14600
15038
  };
14601
15039
  }));
@@ -14621,7 +15059,7 @@ function validSnapshot(snapshot, source) {
14621
15059
  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;
14622
15060
  const seen = /* @__PURE__ */ new Set();
14623
15061
  for (const record4 of records) {
14624
- 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;
15062
+ 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;
14625
15063
  const key3 = recordKey(record4);
14626
15064
  if (seen.has(key3)) return false;
14627
15065
  seen.add(key3);
@@ -14632,16 +15070,16 @@ async function reconcileCommercialParitySnapshot(input) {
14632
15070
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
14633
15071
  const sourceKey = networkCommercialSourceKey(input.source);
14634
15072
  const key3 = await parityKey(input.secret);
14635
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
14636
- const customerId = text6(row2.customerId);
14637
- const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
15073
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text8(row2.applicationId) && text8(row2.objectId)).map(async (row2) => {
15074
+ const customerId = text8(row2.customerId);
15075
+ const subscriptionId = text8(row2.subscriptionId) ?? text8(row2.objectId);
14638
15076
  return {
14639
15077
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
14640
15078
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
14641
15079
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
14642
15080
  status: null,
14643
15081
  canceled: false,
14644
- stripeStatus: text6(row2.status, 64),
15082
+ stripeStatus: text8(row2.status, 64),
14645
15083
  stripeCanceled: row2.status === "canceled"
14646
15084
  };
14647
15085
  }));
@@ -14838,6 +15276,40 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
14838
15276
  };
14839
15277
  }
14840
15278
 
15279
+ // src/worker-network-commercial-dispute.ts
15280
+ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15281
+ const operator = ctx.membershipAuthorityOperator;
15282
+ if (!operator || !fact.chargeId) return { disposition: "unhosted" };
15283
+ let db;
15284
+ try {
15285
+ db = operator.makeDb(env);
15286
+ } catch {
15287
+ return { disposition: "unhosted" };
15288
+ }
15289
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
15290
+ if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
15291
+ const charge = await readDisputedCharge(secret, fact.chargeId);
15292
+ const scopeOf = (row2) => ({
15293
+ appId: String(row2.appId),
15294
+ chapterId: String(row2.chapterId),
15295
+ environment: String(row2.environment),
15296
+ runtime: String(row2.runtime),
15297
+ applicationId: String(row2.applicationId),
15298
+ skuId: String(row2.skuId)
15299
+ });
15300
+ if (charge.seatId) {
15301
+ const seat = await membershipLoad(db, "networkSeat", charge.seatId);
15302
+ if (!seat || seat.environment !== env.ODLA_ENV || !disputedChargeCoversSeat(charge, seat)) return { disposition: "not_applicable" };
15303
+ const receipt2 = await reconcileMembershipDispute(db, { ...scopeOf(seat), eventId: fact.eventId }, now, secret, charge);
15304
+ return { disposition: "applied", membershipId: receipt2.membershipId, seatId: charge.seatId };
15305
+ }
15306
+ const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15307
+ const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
15308
+ if (!member) return { disposition: "not_applicable" };
15309
+ const receipt = await reconcileMembershipDispute(db, { ...scopeOf(member), eventId: fact.eventId }, now, secret, charge);
15310
+ return { disposition: "applied", membershipId: receipt.membershipId };
15311
+ }
15312
+
14841
15313
  // src/worker-routes-network-commercial.ts
14842
15314
  var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
14843
15315
  var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
@@ -14902,6 +15374,12 @@ function safeProjection(projection) {
14902
15374
  amountReceivedCents: seat.amountReceivedCents,
14903
15375
  refundedCents: seat.refundedCents ?? 0
14904
15376
  })),
15377
+ disputes: (projection.disputes ?? []).map((dispute2) => ({
15378
+ ...row2(dispute2),
15379
+ chargeId: dispute2.chargeId ?? null,
15380
+ paymentIntentId: dispute2.paymentIntentId ?? null,
15381
+ reason: dispute2.disputeReason ?? null
15382
+ })),
14905
15383
  exceptions: projection.exceptions
14906
15384
  };
14907
15385
  }
@@ -14920,7 +15398,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
14920
15398
  return json({ error: "commercial webhook is not configured" }, 503);
14921
15399
  }
14922
15400
  try {
14923
- const event2 = await (0, import_stripe13.constructStripeEvent)(body, signature, secret);
15401
+ const event2 = await (0, import_stripe15.constructStripeEvent)(body, signature, secret);
14924
15402
  const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
14925
15403
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
14926
15404
  const result = await ingestNetworkCommercialFact(
@@ -14934,14 +15412,24 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
14934
15412
  if (result.status === "collision") {
14935
15413
  console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
14936
15414
  }
15415
+ let dispute2;
15416
+ if (fact.objectKind === "dispute" && result.status !== "collision") {
15417
+ try {
15418
+ dispute2 = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
15419
+ } catch (error) {
15420
+ console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
15421
+ return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
15422
+ }
15423
+ }
14937
15424
  return json({
14938
15425
  ok: true,
14939
15426
  status: result.status,
14940
15427
  eventId: fact.eventId,
15428
+ ...dispute2 ? { dispute: dispute2.disposition } : {},
14941
15429
  ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
14942
15430
  });
14943
15431
  } catch (error) {
14944
- if (error instanceof import_stripe13.StripeError) return json({ error: error.message, code: error.code }, 400);
15432
+ if (error instanceof import_stripe15.StripeError) return json({ error: error.message, code: error.code }, 400);
14945
15433
  throw error;
14946
15434
  }
14947
15435
  };
@@ -15004,7 +15492,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
15004
15492
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
15005
15493
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
15006
15494
  try {
15007
- const stripe = (0, import_stripe13.initStripe)({ secretKey });
15495
+ const stripe = (0, import_stripe15.initStripe)({ secretKey });
15008
15496
  const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
15009
15497
  listStripeObjects(stripe, "/v1/customers"),
15010
15498
  listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
@@ -15018,7 +15506,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
15018
15506
  ...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
15019
15507
  });
15020
15508
  } catch (error) {
15021
- if (error instanceof import_stripe13.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
15509
+ if (error instanceof import_stripe15.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
15022
15510
  return json({ error: "Stripe reconciliation failed" }, 502);
15023
15511
  }
15024
15512
  }
@@ -15135,7 +15623,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
15135
15623
  function unknownKeys3(value, allowed2, path, errors) {
15136
15624
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
15137
15625
  }
15138
- function text7(value, path, errors, max = MAX_TEXT2, required = true) {
15626
+ function text9(value, path, errors, max = MAX_TEXT2, required = true) {
15139
15627
  if (value === void 0 && !required) return void 0;
15140
15628
  if (typeof value !== "string" || required && !value.trim()) {
15141
15629
  errors.push(`${path} must be a non-empty string`);
@@ -15161,7 +15649,7 @@ function scopeValue(value, path, errors) {
15161
15649
  return { kind: raw2.kind };
15162
15650
  }
15163
15651
  if (raw2.kind === "chapter") {
15164
- const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15652
+ const chapterId = text9(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15165
15653
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15166
15654
  return { kind: "chapter", chapterId };
15167
15655
  }
@@ -15175,22 +15663,22 @@ function resourceValue(value, path, errors) {
15175
15663
  return null;
15176
15664
  }
15177
15665
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15178
- const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
15666
+ const id7 = text9(raw2.id, `${path}.id`, errors, 80) ?? "";
15179
15667
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15180
- const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
15668
+ const kind = text9(raw2.kind, `${path}.kind`, errors) ?? "";
15181
15669
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15182
- const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
15183
- const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
15184
- const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
15670
+ const title = text9(raw2.title, `${path}.title`, errors, 240) ?? "";
15671
+ const summary = text9(raw2.summary, `${path}.summary`, errors, 1e3, false);
15672
+ const status2 = text9(raw2.status, `${path}.status`, errors) ?? "";
15185
15673
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15186
15674
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15187
15675
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15188
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15676
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text9(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15189
15677
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15190
15678
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15191
15679
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15192
- const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15193
- const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
15680
+ const bodyMarkdown = text9(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15681
+ const url = text9(raw2.url, `${path}.url`, errors, 2048, false);
15194
15682
  if (url) {
15195
15683
  try {
15196
15684
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15202,9 +15690,9 @@ function resourceValue(value, path, errors) {
15202
15690
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15203
15691
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15204
15692
  const brandAsset = asset ? {
15205
- bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15206
- assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15207
- ...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15693
+ bookId: text9(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15694
+ assetId: text9(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15695
+ ...asset.filename === void 0 ? {} : { filename: text9(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15208
15696
  } : void 0;
15209
15697
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15210
15698
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15219,17 +15707,17 @@ function resourceValue(value, path, errors) {
15219
15707
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15220
15708
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15221
15709
  const promotedFrom = promotedRaw ? {
15222
- chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15223
- resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15710
+ chapterId: text9(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15711
+ resourceId: text9(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15224
15712
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15225
15713
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15226
- promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15714
+ promotedBy: text9(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15227
15715
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15228
15716
  } : void 0;
15229
15717
  const provenance2 = {
15230
- createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15718
+ createdBy: text9(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15231
15719
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15232
- updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15720
+ updatedBy: text9(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15233
15721
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15234
15722
  ...promotedFrom ? { promotedFrom } : {}
15235
15723
  };
@@ -15248,7 +15736,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15248
15736
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15249
15737
  const sourceType = sourceRaw?.type;
15250
15738
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15251
- const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15739
+ const sourceAppId = text9(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15252
15740
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15253
15741
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15254
15742
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15256,8 +15744,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15256
15744
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15257
15745
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15258
15746
  const target = targetRaw ? {
15259
- appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15260
- environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15747
+ appId: text9(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15748
+ environment: text9(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15261
15749
  } : void 0;
15262
15750
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15263
15751
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15598,7 +16086,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
15598
16086
  };
15599
16087
 
15600
16088
  // src/membership-cancel-authority.ts
15601
- var import_stripe14 = require("@odla-ai/stripe");
16089
+ var import_stripe16 = require("@odla-ai/stripe");
15602
16090
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15603
16091
  membershipClock(now);
15604
16092
  const id7 = await networkMembershipId(target);
@@ -15609,14 +16097,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15609
16097
  membershipFail("membership_cancellation_unavailable");
15610
16098
  }
15611
16099
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
15612
- const read2 = await (0, import_stripe14.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
16100
+ const read2 = await (0, import_stripe16.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
15613
16101
  const sub = read2.body;
15614
16102
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
15615
16103
  ...stripePartitionMetadata(target),
15616
16104
  applicationId: target.applicationId
15617
16105
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
15618
16106
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
15619
- const write = await (0, import_stripe14.stripeRequest)(
16107
+ const write = await (0, import_stripe16.stripeRequest)(
15620
16108
  { secretKey: stripeSecret },
15621
16109
  "POST",
15622
16110
  path,
@@ -15838,7 +16326,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
15838
16326
  }
15839
16327
 
15840
16328
  // src/membership-provider-lifecycle.ts
15841
- var import_stripe15 = require("@odla-ai/stripe");
16329
+ var import_stripe17 = require("@odla-ai/stripe");
15842
16330
 
15843
16331
  // src/membership-renewal-authority.ts
15844
16332
  async function reconcileMembershipRenewal(db, input, now) {
@@ -15960,6 +16448,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15960
16448
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
15961
16449
  const fact = row2?.fact;
15962
16450
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
16451
+ if (fact?.objectKind === "dispute") {
16452
+ const secret = await getVaultSecret(db, stripeSecretName);
16453
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
16454
+ return reconcileMembershipDispute(db, input, now, secret);
16455
+ }
15963
16456
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
15964
16457
  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({
15965
16458
  appId: input.appId,
@@ -16002,7 +16495,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
16002
16495
  }
16003
16496
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
16004
16497
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
16005
- const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
16498
+ const result = await (0, import_stripe17.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
16006
16499
  const sub = result.body;
16007
16500
  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)) {
16008
16501
  membershipFail("membership_billing_provider_mismatch");
@@ -16040,7 +16533,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
16040
16533
  }
16041
16534
 
16042
16535
  // src/membership-seat-stripe.ts
16043
- var import_stripe16 = require("@odla-ai/stripe");
16536
+ var import_stripe18 = require("@odla-ai/stripe");
16044
16537
  var reject7 = () => {
16045
16538
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
16046
16539
  };
@@ -16052,7 +16545,7 @@ var metadataFor = (seat) => ({
16052
16545
  });
16053
16546
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
16054
16547
  async function read(sk, path, params) {
16055
- const response = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "GET", path, params);
16548
+ const response = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "GET", path, params);
16056
16549
  if (!response.ok) return reject7();
16057
16550
  return response.body;
16058
16551
  }
@@ -16089,11 +16582,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
16089
16582
  }
16090
16583
  await subscriptionForSeat(sk, seat);
16091
16584
  const metadata = metadataFor(seat);
16092
- const result = seat.providerPaymentIntentId ? await (0, import_stripe16.stripeRequest)(
16585
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe18.stripeRequest)(
16093
16586
  { secretKey: sk },
16094
16587
  "GET",
16095
16588
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
16096
- ) : await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
16589
+ ) : await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
16097
16590
  amount: seat.amountCents,
16098
16591
  currency: seat.currency,
16099
16592
  customer: seat.providerCustomerId,
@@ -16115,7 +16608,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
16115
16608
  return String(existing.id);
16116
16609
  }
16117
16610
  if (seat.combinedInvoice) reject7();
16118
- const result = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
16611
+ const result = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
16119
16612
  subscription: seat.providerSubscriptionId,
16120
16613
  price: seat.priceId,
16121
16614
  quantity: 1,
@@ -16777,6 +17270,19 @@ var id5 = () => attr("string", false, true, true);
16777
17270
  var key = (optional = false) => attr("string", optional, false, true);
16778
17271
  var scope = () => ({ appId: key(), chapterId: key(), environment: key(), runtime: key(), applicationId: key(), skuId: key() });
16779
17272
  var times = () => ({ revision: attr("number"), createdAt: attr("number"), updatedAt: attr("number") });
17273
+ var dispute = () => ({
17274
+ disputeId: key(true),
17275
+ disputeStatus: attr("string", true),
17276
+ disputeReason: attr("string", true),
17277
+ disputeAmountCents: attr("number", true),
17278
+ disputedAt: attr("number", true),
17279
+ disputeClosedAt: attr("number", true),
17280
+ disputePriorStatus: attr("string", true),
17281
+ disputeEventId: key(true),
17282
+ rechargePaymentIntentId: key(true),
17283
+ rechargedAt: attr("number", true),
17284
+ rechargeAttempt: attr("json", true)
17285
+ });
16780
17286
  var membershipAuthorityEntities = {
16781
17287
  networkMembership: { attrs: {
16782
17288
  id: id5(),
@@ -16816,7 +17322,8 @@ var membershipAuthorityEntities = {
16816
17322
  graceEndsAt: attr("number", true),
16817
17323
  ...times(),
16818
17324
  refundRequestedAt: attr("number", true),
16819
- restartId: key(true)
17325
+ restartId: key(true),
17326
+ ...dispute()
16820
17327
  } },
16821
17328
  membershipRestart: { attrs: {
16822
17329
  id: id5(),
@@ -16892,7 +17399,8 @@ var membershipAuthorityEntities = {
16892
17399
  refundedCents: attr("number", true),
16893
17400
  providerRefundIds: attr("json", true),
16894
17401
  regiftedAt: attr("number", true),
16895
- formerRecipients: attr("json", true)
17402
+ formerRecipients: attr("json", true),
17403
+ ...dispute()
16896
17404
  } },
16897
17405
  networkSeatRecipient: { attrs: { id: id5(), appId: key(), chapterId: key(), environment: key(), runtime: key(), seatId: key(), recipientEmailNormalized: key(), createdAt: attr("number") } },
16898
17406
  membershipEffect: { attrs: {
@@ -17013,7 +17521,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
17013
17521
  }
17014
17522
 
17015
17523
  // src/membership-seat-cancel.ts
17016
- var import_stripe17 = require("@odla-ai/stripe");
17524
+ var import_stripe19 = require("@odla-ai/stripe");
17017
17525
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
17018
17526
  membershipClock(now);
17019
17527
  const state = await loadMembershipAuthorityState(db, target);
@@ -17041,7 +17549,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
17041
17549
  }
17042
17550
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
17043
17551
  const request = async (method, path, params) => {
17044
- const result = await (0, import_stripe17.stripeRequest)(
17552
+ const result = await (0, import_stripe19.stripeRequest)(
17045
17553
  { secretKey },
17046
17554
  method,
17047
17555
  path,
@@ -17054,7 +17562,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
17054
17562
  let payment;
17055
17563
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
17056
17564
  else {
17057
- const snapshot = await listStripeObjects((0, import_stripe17.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
17565
+ const snapshot = await listStripeObjects((0, import_stripe19.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
17058
17566
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
17059
17567
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
17060
17568
  payment = matches2[0];
@@ -17229,7 +17737,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17229
17737
  };
17230
17738
  signal.addEventListener("abort", cancel, { once: true });
17231
17739
  let size = 0;
17232
- let text8 = "";
17740
+ let text10 = "";
17233
17741
  const decoder = new TextDecoder();
17234
17742
  try {
17235
17743
  while (true) {
@@ -17239,10 +17747,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17239
17747
  if (done) break;
17240
17748
  size += value.byteLength;
17241
17749
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17242
- text8 += decoder.decode(value, { stream: true });
17750
+ text10 += decoder.decode(value, { stream: true });
17243
17751
  }
17244
17752
  try {
17245
- return JSON.parse(text8 + decoder.decode());
17753
+ return JSON.parse(text10 + decoder.decode());
17246
17754
  } catch {
17247
17755
  throw new TypeError(`invalid ${label}`);
17248
17756
  }
@@ -17577,7 +18085,7 @@ async function requestMembershipRefund(options) {
17577
18085
  async function requestMembershipRenewal(options) {
17578
18086
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
17579
18087
  const row2 = result.renewal;
17580
- 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)) {
18088
+ 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)) {
17581
18089
  throw new Error("invalid membership renewal receipt");
17582
18090
  }
17583
18091
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -17929,6 +18437,7 @@ var BUILTIN_ROUTES = [
17929
18437
  handlePaymentRecovery,
17930
18438
  handleAdminNamedSeat,
17931
18439
  handleAdminMembershipSeats,
18440
+ handleAdminMembershipDisputes,
17932
18441
  handleAdminMembershipPolicyVersions,
17933
18442
  handleAdminMembershipReterm,
17934
18443
  handleAdminMeetings,
@@ -17977,8 +18486,9 @@ function chapterWorker(options) {
17977
18486
  const ctx = createWorkerContext(options);
17978
18487
  const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
17979
18488
  return {
17980
- async fetch(req, env) {
18489
+ async fetch(req, env, execution) {
17981
18490
  const url = new URL(req.url);
18491
+ ctx.bindExecution(req, execution);
17982
18492
  for (const route of routes) {
17983
18493
  let res;
17984
18494
  try {