@odla-ai/chapter 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -246,12 +246,37 @@ function networkCommercialAlerts(ctx, route) {
246
246
  };
247
247
  }
248
248
 
249
+ // src/worker-defer.ts
250
+ function deferWork(ctx, req, task) {
251
+ return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
252
+ }
253
+ function createDeferral() {
254
+ const executions = /* @__PURE__ */ new WeakMap();
255
+ const settle = (task) => task().then(() => void 0, (error) => {
256
+ console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
257
+ });
258
+ return {
259
+ /** Remember the runtime's ExecutionContext for this request. */
260
+ bind(req, execution) {
261
+ if (execution) executions.set(req, execution);
262
+ },
263
+ /** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
264
+ defer(req, task) {
265
+ const execution = executions.get(req);
266
+ if (!execution) return settle(task);
267
+ execution.waitUntil(settle(task));
268
+ return Promise.resolve();
269
+ }
270
+ };
271
+ }
272
+
249
273
  // src/worker-context.ts
250
274
  var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
251
275
  function createWorkerContext(options) {
252
276
  const { chapter } = options;
253
277
  const auth = chapter.auth;
254
278
  const crmBase = options.crmBasePath ?? "/api/crm";
279
+ const deferral = createDeferral();
255
280
  let publicConfigCache = null;
256
281
  const jwksByIssuer = /* @__PURE__ */ new Map();
257
282
  async function getPublicConfig(env) {
@@ -356,6 +381,8 @@ function createWorkerContext(options) {
356
381
  membershipAuthority: options.membershipAuthority,
357
382
  membershipAuthorityOperator: options.membershipAuthorityOperator,
358
383
  recordError: options.recordError ?? consoleErrorRecorder,
384
+ bindExecution: deferral.bind,
385
+ defer: deferral.defer,
359
386
  getPublicConfig,
360
387
  verifyUser,
361
388
  makeDb,
@@ -683,6 +710,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
683
710
  else if (eventType.startsWith("refund.")) objectKind = "refund";
684
711
  else if (eventType === "charge.refunded") objectKind = "refund";
685
712
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
713
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
686
714
  const customerId = refId(object2.customer, "cus_");
687
715
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
688
716
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -692,6 +720,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
692
720
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
693
721
  const currency = text(object2.currency)?.toLowerCase();
694
722
  const status2 = text(object2.status);
723
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
695
724
  return {
696
725
  eventId,
697
726
  digest,
@@ -710,6 +739,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
710
739
  ...invoiceId ? { invoiceId } : {},
711
740
  ...chargeId ? { chargeId } : {},
712
741
  ...paymentIntentId ? { paymentIntentId } : {},
742
+ ...disputeReason ? { disputeReason } : {},
713
743
  ...price,
714
744
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
715
745
  ...status2 ? { status: status2 } : {},
@@ -752,6 +782,7 @@ function ownRef(fact) {
752
782
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
753
783
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
754
784
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
785
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
755
786
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
756
787
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
757
788
  }
@@ -807,6 +838,7 @@ function projectNetworkCommercialEvents(input, sources) {
807
838
  continue;
808
839
  }
809
840
  const source = assignments.get(fact.eventId);
841
+ if (!source && fact.objectKind === "dispute") continue;
810
842
  if (!source) {
811
843
  const direct = directSource(fact, sources);
812
844
  const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -823,6 +855,7 @@ function projectNetworkCommercialEvents(input, sources) {
823
855
  const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
824
856
  const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
825
857
  const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
858
+ const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
826
859
  const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
827
860
  for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
828
861
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -843,6 +876,7 @@ function projectNetworkCommercialEvents(input, sources) {
843
876
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
844
877
  return {
845
878
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
879
+ ...disputes.length ? { disputes } : {},
846
880
  customers,
847
881
  subscriptions,
848
882
  invoices,
@@ -1179,6 +1213,17 @@ function findGiftRef(obj) {
1179
1213
  }
1180
1214
  function normalizeWebhookEvent(event2, options = {}) {
1181
1215
  const obj = event2.data?.object ?? {};
1216
+ if (event2.type.startsWith("charge.dispute.")) {
1217
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
1218
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
1219
+ return {
1220
+ kind: "disputed",
1221
+ chargeId,
1222
+ disputeId: obj.id,
1223
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
1224
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
1225
+ };
1226
+ }
1182
1227
  if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
1183
1228
  const gift = findGiftRef(obj);
1184
1229
  if (gift.giftIntentId) {
@@ -1588,9 +1633,9 @@ async function backfillCrm(deps) {
1588
1633
  }
1589
1634
 
1590
1635
  // src/email-retirement.ts
1591
- function isRetiredEmailCopy(subject, text8) {
1636
+ function isRetiredEmailCopy(subject, text9) {
1592
1637
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1593
- const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1638
+ const bodyCopy = text9.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1594
1639
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1595
1640
  }
1596
1641
 
@@ -1668,10 +1713,10 @@ function planDelivery(input) {
1668
1713
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1669
1714
  if (validateEmailCopy({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1670
1715
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1671
- const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
1716
+ const text9 = redirect ? `(dev redirect; original recipient: ${input.to})
1672
1717
 
1673
1718
  ` + renderedText : renderedText;
1674
- return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
1719
+ return { deliver: true, transport, to, subject, text: text9, redirected: redirect };
1675
1720
  }
1676
1721
 
1677
1722
  // src/notify.ts
@@ -1819,8 +1864,8 @@ function formatInstant(at2, timeZone) {
1819
1864
  var adminUrlFor = (origin) => `${origin}/admin/`;
1820
1865
  var membersUrlFor = (origin) => `${origin}/members/`;
1821
1866
  function applicantVariables(app) {
1822
- const text8 = (v) => typeof v === "string" ? v : "";
1823
- return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
1867
+ const text9 = (v) => typeof v === "string" ? v : "";
1868
+ return { firstName: text9(app.firstName), lastName: text9(app.lastName), email: text9(app.email), phone: text9(app.phone), state: text9(app.state) };
1824
1869
  }
1825
1870
  function sampleVariables(origin, timeZone, now = Date.now()) {
1826
1871
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -2927,7 +2972,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
2927
2972
  if (!intent) throw new Error("membership checkout reconciliation pending");
2928
2973
  const target = await membershipTargetForIntent(intent, app, env, ctx);
2929
2974
  const row2 = await ctx.membershipAuthority.reconcileRenewal({ ...target, eventId }, env);
2930
- if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
2975
+ if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
2931
2976
  throw new Error("invalid network renewal receipt");
2932
2977
  }
2933
2978
  return row2;
@@ -3552,6 +3597,102 @@ function paymentLineItems(group, tier) {
3552
3597
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
3553
3598
  }
3554
3599
 
3600
+ // src/membership-dispute-authority.ts
3601
+ import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
3602
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
3603
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
3604
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
3605
+ var TERMINAL = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
3606
+ var text2 = (value) => typeof value === "string" && value ? value : void 0;
3607
+ var refOf = (value) => typeof value === "string" ? text2(value) : text2(value?.id);
3608
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
3609
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
3610
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
3611
+ const charge = await stripeRequest4(config, "GET", `/v1/charges/${chargeId}`);
3612
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
3613
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
3614
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
3615
+ let subscriptionId;
3616
+ if (invoiceId) {
3617
+ const invoice = await stripeRequest4(config, "GET", `/v1/invoices/${invoiceId}`);
3618
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
3619
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
3620
+ }
3621
+ const seatId = text2(charge.body.metadata?.seatId);
3622
+ return {
3623
+ chargeId,
3624
+ customerId,
3625
+ ...invoiceId ? { invoiceId } : {},
3626
+ ...subscriptionId ? { subscriptionId } : {},
3627
+ ...paymentIntentId ? { paymentIntentId } : {},
3628
+ ...seatId ? { seatId } : {}
3629
+ };
3630
+ }
3631
+ function disputedChargeCoversMembership(charge, member) {
3632
+ 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);
3633
+ }
3634
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3635
+ membershipClock(now);
3636
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3637
+ const fact = row2?.fact;
3638
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3639
+ 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");
3640
+ const id7 = await networkMembershipId(input);
3641
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
3642
+ membershipId: id7,
3643
+ status: String(value.status),
3644
+ termEndsAt: Number(value.termEndsAt),
3645
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
3646
+ autoRenew: value.autoRenew === true,
3647
+ ...text2(value.disputeStatus) ? { disputeStatus: text2(value.disputeStatus) } : {},
3648
+ applied
3649
+ });
3650
+ const known = await membershipLoad(db, "networkMembership", id7);
3651
+ requireMembershipScope(known, input);
3652
+ if (known.disputeEventId === input.eventId) return receipt(known);
3653
+ const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
3654
+ for (let attempt = 0; attempt < 8; attempt++) {
3655
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
3656
+ requireMembershipScope(member, input);
3657
+ if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
3658
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
3659
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
3660
+ const sameDispute = member.disputeId === fact.objectId;
3661
+ if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
3662
+ const won = DISPUTE_WON_STATUSES.includes(status2);
3663
+ const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
3664
+ const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
3665
+ const attrs = {
3666
+ status: next,
3667
+ disputeId: fact.objectId,
3668
+ disputeStatus: status2,
3669
+ disputeEventId: input.eventId,
3670
+ disputeClosedAt: closed ? now : 0,
3671
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
3672
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
3673
+ ...sameDispute ? {} : { disputedAt: now },
3674
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
3675
+ revision: Number(member.revision) + 1,
3676
+ updatedAt: now
3677
+ };
3678
+ try {
3679
+ await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
3680
+ { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
3681
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3682
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
3683
+ evidenceGuard
3684
+ ] });
3685
+ } catch (error) {
3686
+ if (error.code !== "transact_guard_failed") throw error;
3687
+ continue;
3688
+ }
3689
+ const saved = await membershipLoad(db, "networkMembership", id7);
3690
+ requireMembershipScope(saved, input);
3691
+ return receipt(saved, true);
3692
+ }
3693
+ return membershipFail("membership_dispute_retry_required");
3694
+ }
3695
+
3555
3696
  // src/member-system-contract.ts
3556
3697
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3557
3698
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3676,7 +3817,8 @@ async function firstRow2(db, ns, q) {
3676
3817
  }
3677
3818
  async function reconcileChapterStripeEvent(input, env, ctx) {
3678
3819
  const db = ctx.makeDb(env);
3679
- const { rawBody, eventId, event: event2 } = input;
3820
+ const { rawBody, eventId } = input;
3821
+ let { event: event2 } = input;
3680
3822
  if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
3681
3823
  const digest = await stripeEventDigest(rawBody);
3682
3824
  const legacyDigest = await stripePayloadDigest(rawBody);
@@ -3732,6 +3874,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3732
3874
  }
3733
3875
  return json({ ok: true, matched: Boolean(claim) });
3734
3876
  }
3877
+ if (event2.kind === "disputed" && !event2.customerId) {
3878
+ const secret = await getVaultSecret(db, "stripe_secret_key");
3879
+ const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
3880
+ if (charge) event2 = { ...event2, customerId: charge.customerId };
3881
+ }
3735
3882
  const app = await findPaymentApplication(db, event2);
3736
3883
  if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
3737
3884
  reportStripePartition("warn", eventId, event2, "ignore_sibling");
@@ -3769,11 +3916,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3769
3916
  }
3770
3917
  let authoritativeEvent = event2;
3771
3918
  let membershipProjection = {};
3772
- if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
3919
+ if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
3773
3920
  try {
3774
3921
  const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
3775
3922
  membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
3776
- authoritativeEvent = receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
3923
+ authoritativeEvent = event2.kind === "disputed" ? event2 : receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
3777
3924
  } catch {
3778
3925
  return json({ error: "network membership renewal reconciliation pending" }, 503);
3779
3926
  }
@@ -4422,10 +4569,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
4422
4569
  }
4423
4570
 
4424
4571
  // src/membership-seat-refund-stripe.ts
4425
- import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4572
+ import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
4426
4573
 
4427
4574
  // src/membership-checkout-seat-refund.ts
4428
- import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
4575
+ import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
4429
4576
  var fail4 = (code = "named_seat_refund_mismatch") => {
4430
4577
  throw Object.assign(new Error(code), { code });
4431
4578
  };
@@ -4434,7 +4581,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
4434
4581
  const invoiceRef = seat.combinedInvoice;
4435
4582
  if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
4436
4583
  const call = async (method, path, params, key3) => {
4437
- const result = await stripeRequest4({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4584
+ const result = await stripeRequest5({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4438
4585
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4439
4586
  return result.body;
4440
4587
  };
@@ -4507,7 +4654,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
4507
4654
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
4508
4655
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
4509
4656
  const call = async (method, path, params, idempotencyKey) => {
4510
- const result = await stripeRequest5({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4657
+ const result = await stripeRequest6({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4511
4658
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
4512
4659
  return result.body;
4513
4660
  };
@@ -4975,15 +5122,224 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
4975
5122
  }
4976
5123
  };
4977
5124
 
5125
+ // src/membership-dispute-recharge.ts
5126
+ import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5127
+ var fail6 = (code, extra = {}) => {
5128
+ throw Object.assign(new Error(code), { code, ...extra });
5129
+ };
5130
+ var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
5131
+ async function defaultPaymentMethod(config, subscriptionId, customerId) {
5132
+ const subscription = await stripeRequest7(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
5133
+ if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
5134
+ const onSubscription = refOf2(subscription.body.default_payment_method);
5135
+ if (onSubscription) return onSubscription;
5136
+ const customer = await stripeRequest7(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
5137
+ if (!customer.ok) fail6("membership_dispute_provider_unavailable");
5138
+ return refOf2(customer.body.invoice_settings?.default_payment_method);
5139
+ }
5140
+ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
5141
+ membershipClock(now);
5142
+ let member = await membershipLoad(db, "networkMembership", membershipId);
5143
+ if (!member) fail6("membership_not_found");
5144
+ const scope2 = { chapterId: String(member.chapterId), environment: String(member.environment) };
5145
+ const amountCents = Number(member.disputeAmountCents ?? member.amountCents), currency = String(member.currency);
5146
+ const done = (value, replayed) => ({
5147
+ membershipId,
5148
+ status: String(value.status),
5149
+ paymentIntentId: String(value.rechargePaymentIntentId),
5150
+ amountCents,
5151
+ currency,
5152
+ ...replayed ? { replayed } : {}
5153
+ });
5154
+ if (member.status !== MEMBERSHIP_DISPUTED_STATUS) {
5155
+ if (member.disputeStatus === "recharged" && member.rechargePaymentIntentId) return done(member, true);
5156
+ fail6("membership_not_disputed");
5157
+ }
5158
+ if (member.disputeStatus !== "lost") fail6("membership_dispute_open");
5159
+ if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
5160
+ let attempt = member.rechargeAttempt;
5161
+ if (!attempt || attempt.finishedAt !== void 0) {
5162
+ attempt = { key: `membership-recharge:${membershipId}:${String(member.disputeId)}:${now}`, startedAt: now };
5163
+ await db.transact(
5164
+ [membershipUpdate("networkMembership", membershipId, { rechargeAttempt: attempt, revision: Number(member.revision) + 1, updatedAt: now })],
5165
+ { mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{
5166
+ ns: "networkMembership",
5167
+ id: membershipId,
5168
+ exists: true,
5169
+ equals: { revision: member.revision, status: MEMBERSHIP_DISPUTED_STATUS }
5170
+ }] }
5171
+ );
5172
+ member = await membershipLoad(db, "networkMembership", membershipId);
5173
+ if (!member || member.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
5174
+ }
5175
+ const config = { secretKey };
5176
+ const paymentMethod = await defaultPaymentMethod(config, String(member.providerSubscriptionId), String(member.providerCustomerId));
5177
+ if (!paymentMethod) fail6("recharge_payment_method_unavailable");
5178
+ const intent = await stripeRequest7(config, "POST", "/v1/payment_intents", {
5179
+ amount: amountCents,
5180
+ currency,
5181
+ customer: String(member.providerCustomerId),
5182
+ payment_method: paymentMethod,
5183
+ off_session: "true",
5184
+ confirm: "true",
5185
+ description: `Membership payment reissued after dispute ${String(member.disputeId)}`,
5186
+ metadata: {
5187
+ ...stripePartitionMetadata({ appId: String(member.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(member.runtime) }),
5188
+ applicationId: String(member.applicationId),
5189
+ membershipId,
5190
+ disputeId: String(member.disputeId),
5191
+ membershipRecharge: "true"
5192
+ }
5193
+ }, { idempotencyKey: attempt.key });
5194
+ const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
5195
+ const finish = async (outcome) => {
5196
+ const current = await membershipLoad(db, "networkMembership", membershipId);
5197
+ if (!current) return;
5198
+ await db.transact([membershipUpdate("networkMembership", membershipId, {
5199
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome },
5200
+ revision: Number(current.revision) + 1,
5201
+ updatedAt: now
5202
+ })], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
5203
+ };
5204
+ if (!intent.ok) {
5205
+ if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
5206
+ await finish("declined");
5207
+ fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
5208
+ }
5209
+ fail6("membership_dispute_provider_unavailable");
5210
+ }
5211
+ if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
5212
+ await finish("declined");
5213
+ fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
5214
+ }
5215
+ if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
5216
+ if (refOf2(body.customer) !== member.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
5217
+ for (let retry = 0; retry < 8; retry++) {
5218
+ const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, "networkMembership", membershipId);
5219
+ if (!current) fail6("membership_not_found");
5220
+ if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
5221
+ const status2 = String(current.disputePriorStatus ?? (current.journeyApprovedAt ? "active" : "paid_pending_approval"));
5222
+ try {
5223
+ await db.transact(
5224
+ [membershipUpdate("networkMembership", membershipId, {
5225
+ status: status2,
5226
+ disputeStatus: "recharged",
5227
+ rechargePaymentIntentId: String(body.id),
5228
+ rechargedAt: now,
5229
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
5230
+ revision: Number(current.revision) + 1,
5231
+ updatedAt: now
5232
+ })],
5233
+ { mutationId: `membership-recharged:${attempt.key}`, guards: [
5234
+ { ns: "networkMembership", id: membershipId, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
5235
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
5236
+ ] }
5237
+ );
5238
+ } catch (cause) {
5239
+ if (cause.code !== "transact_guard_failed") throw cause;
5240
+ continue;
5241
+ }
5242
+ return done(await membershipLoad(db, "networkMembership", membershipId));
5243
+ }
5244
+ return fail6("membership_dispute_retry_required");
5245
+ }
5246
+
5247
+ // src/worker-routes-admin-membership-disputes.ts
5248
+ var LIST2 = "/api/admin/membership-disputes";
5249
+ var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
5250
+ var PUBLIC2 = [
5251
+ "id",
5252
+ "appId",
5253
+ "chapterId",
5254
+ "environment",
5255
+ "runtime",
5256
+ "applicationId",
5257
+ "skuId",
5258
+ "status",
5259
+ "memberNumber",
5260
+ "amountCents",
5261
+ "currency",
5262
+ "disputeId",
5263
+ "disputeStatus",
5264
+ "disputeReason",
5265
+ "disputeAmountCents",
5266
+ "disputedAt",
5267
+ "disputeClosedAt",
5268
+ "disputePriorStatus",
5269
+ "rechargePaymentIntentId",
5270
+ "rechargedAt",
5271
+ "termEndsAt",
5272
+ "updatedAt"
5273
+ ];
5274
+ var project2 = (row2) => ({
5275
+ ...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
5276
+ ...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
5277
+ startedAt: row2.rechargeAttempt.startedAt ?? null,
5278
+ finishedAt: row2.rechargeAttempt.finishedAt ?? null,
5279
+ outcome: row2.rechargeAttempt.outcome ?? null
5280
+ } } : {}
5281
+ });
5282
+ var FINAL = /* @__PURE__ */ new Set([
5283
+ "membership_not_found",
5284
+ "membership_not_disputed",
5285
+ "membership_dispute_open",
5286
+ "membership_dispute_amount_invalid",
5287
+ "recharge_payment_method_unavailable",
5288
+ "recharge_declined",
5289
+ "recharge_pending",
5290
+ "recharge_mismatch",
5291
+ "membership_dispute_changed"
5292
+ ]);
5293
+ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5294
+ const one = url.pathname.match(ONE2);
5295
+ if (url.pathname !== LIST2 && !one) return null;
5296
+ const operator = ctx.membershipAuthorityOperator;
5297
+ if (!operator) return json({ error: "not found" }, 404);
5298
+ if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
5299
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
5300
+ if (!user) return json({ error: "unauthorized" }, 401);
5301
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
5302
+ let db;
5303
+ try {
5304
+ db = operator.makeDb(env);
5305
+ } catch {
5306
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
5307
+ }
5308
+ if (!one) {
5309
+ const rows5 = (await db.query({ networkMembership: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } } })).networkMembership ?? [];
5310
+ const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5311
+ return json({ disputes: disputes.map(project2), truncated: rows5.length >= 500 });
5312
+ }
5313
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
5314
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
5315
+ try {
5316
+ const receipt = await rechargeDisputedMembership(db, one[1], secret, Date.now());
5317
+ return json({ ok: true, ...receipt });
5318
+ } catch (error) {
5319
+ const failure = error;
5320
+ const code = typeof failure.code === "string" ? failure.code : void 0;
5321
+ if (code === "membership_not_found") return json({ error: "not found", code }, 404);
5322
+ if (code && FINAL.has(code)) {
5323
+ return json({
5324
+ error: "the charge could not be re-issued",
5325
+ code,
5326
+ ...failure.declineCode ? { declineCode: failure.declineCode } : {},
5327
+ ...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
5328
+ }, 409);
5329
+ }
5330
+ return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
5331
+ }
5332
+ };
5333
+
4978
5334
  // src/membership-policy-versions.ts
4979
5335
  var LIMIT = 200;
4980
- var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
5336
+ var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
4981
5337
  var instant = (value) => {
4982
5338
  const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
4983
5339
  return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
4984
5340
  };
4985
5341
  function provenance(row2) {
4986
- const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
5342
+ const source = text3(row2.source), actor = text3(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
4987
5343
  return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
4988
5344
  }
4989
5345
  var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
@@ -5077,16 +5433,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
5077
5433
  };
5078
5434
 
5079
5435
  // src/membership-discount-reterm.ts
5080
- import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
5436
+ import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
5081
5437
 
5082
5438
  // src/subscription-provider-contract.ts
5083
- import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
5439
+ import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
5084
5440
  function subscriptionContractMismatch() {
5085
5441
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5086
5442
  }
5087
5443
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
5088
5444
  if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
5089
- const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5445
+ const result = await stripeRequest8({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5090
5446
  const price = result.body;
5091
5447
  if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
5092
5448
  return typeof price.product === "string" ? price.product : void 0;
@@ -5124,7 +5480,7 @@ async function retermPolicy(db, scope2, id7) {
5124
5480
  }
5125
5481
  async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
5126
5482
  const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
5127
- const read2 = await stripeRequest7({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5483
+ const read2 = await stripeRequest9({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5128
5484
  const sub = read2.body, items = sub?.items?.data;
5129
5485
  const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
5130
5486
  if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
@@ -5135,7 +5491,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
5135
5491
  const present = discounts.map(couponOf);
5136
5492
  if (present.length === 1 && present[0] === couponId) return false;
5137
5493
  if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
5138
- const written = await stripeRequest7({ secretKey: sk }, "POST", path, {
5494
+ const written = await stripeRequest9({ secretKey: sk }, "POST", path, {
5139
5495
  "discounts[0][coupon]": couponId,
5140
5496
  "expand[0]": "discounts",
5141
5497
  "metadata[discountPolicyVersionId]": policy2.id
@@ -5984,8 +6340,8 @@ var APPLICATION_LABELS = {
5984
6340
  state: "State"
5985
6341
  };
5986
6342
  function humanize(id7) {
5987
- const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
5988
- return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
6343
+ const text9 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6344
+ return text9 ? `${text9[0].toUpperCase()}${text9.slice(1)}` : id7;
5989
6345
  }
5990
6346
  function applicationDefs(chapter) {
5991
6347
  try {
@@ -6052,7 +6408,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
6052
6408
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
6053
6409
  var CURRENCY = /^[a-z]{3}$/;
6054
6410
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
6055
- var text3 = (value, path, errors, max) => {
6411
+ var text4 = (value, path, errors, max) => {
6056
6412
  if (typeof value !== "string" || !value.trim()) {
6057
6413
  errors.push(`${path} is required`);
6058
6414
  return "";
@@ -6080,23 +6436,23 @@ function validateGiftOfferCatalog(input, tierIds) {
6080
6436
  }
6081
6437
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6082
6438
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6083
- const id7 = text3(item.id, `${path}.id`, errors, 80);
6439
+ const id7 = text4(item.id, `${path}.id`, errors, 80);
6084
6440
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6085
6441
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
6086
6442
  ids2.add(id7);
6087
6443
  const targetRaw = record(item.target);
6088
6444
  let target = { type: "entitlement", entitlementKey: "invalid" };
6089
6445
  if (targetRaw?.type === "tier") {
6090
- const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6446
+ const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6091
6447
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
6092
6448
  target = { type: "tier", tierId };
6093
6449
  } else if (targetRaw?.type === "entitlement") {
6094
- const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6450
+ const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6095
6451
  if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
6096
6452
  target = { type: "entitlement", entitlementKey };
6097
6453
  } else errors.push(`${path}.target.type must be tier or entitlement`);
6098
6454
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
6099
- const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6455
+ const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6100
6456
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
6101
6457
  if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
6102
6458
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -6118,9 +6474,9 @@ function validateGiftOfferCatalog(input, tierIds) {
6118
6474
  redemptionLifetimeHours: lifetime,
6119
6475
  ...activeFrom !== void 0 ? { activeFrom } : {},
6120
6476
  ...activeUntil !== void 0 ? { activeUntil } : {},
6121
- title: text3(item.title, `${path}.title`, errors, 160),
6122
- description: text3(item.description, `${path}.description`, errors, 2e3),
6123
- ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6477
+ title: text4(item.title, `${path}.title`, errors, 160),
6478
+ description: text4(item.description, `${path}.description`, errors, 2e3),
6479
+ ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6124
6480
  });
6125
6481
  }
6126
6482
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -6347,23 +6703,23 @@ async function createPaymentQuote(input) {
6347
6703
  // src/membership-quote-projection.ts
6348
6704
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
6349
6705
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
6350
- var fail6 = (code) => {
6706
+ var fail7 = (code) => {
6351
6707
  throw Object.assign(new Error(code), { code });
6352
6708
  };
6353
6709
  var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
6354
6710
  async function restore2(row2, target, now, allowExpired = false) {
6355
- if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail6("membership_quote_unavailable");
6356
- if (row2.status !== "ready") return fail6("membership_quote_pending");
6711
+ 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");
6712
+ if (row2.status !== "ready") return fail7("membership_quote_pending");
6357
6713
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
6358
6714
  ...target,
6359
6715
  requestId: row2.requestId
6360
6716
  }), now, lifetimeMs: 6e4 });
6361
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
6717
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
6362
6718
  const tier = row2.tier, quote = row2.quote;
6363
6719
  const offering = offer.contract.offering;
6364
- if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
6720
+ if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
6365
6721
  if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
6366
- return fail6("membership_catalog_mismatch");
6722
+ return fail7("membership_catalog_mismatch");
6367
6723
  }
6368
6724
  const expected = await createPaymentQuote({
6369
6725
  ...target,
@@ -6376,7 +6732,7 @@ async function restore2(row2, target, now, allowExpired = false) {
6376
6732
  seatOffer: quote.seatOffer,
6377
6733
  namedSeat: quote.namedSeat
6378
6734
  });
6379
- if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
6735
+ if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
6380
6736
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
6381
6737
  }
6382
6738
  async function readMembershipQuote(db, target, now) {
@@ -6384,9 +6740,9 @@ async function readMembershipQuote(db, target, now) {
6384
6740
  }
6385
6741
  async function prepareMembershipQuote(db, input, issue) {
6386
6742
  const { applicationId: id7, now, app } = input;
6387
- if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail6("membership_quote_unavailable");
6743
+ if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail7("membership_quote_unavailable");
6388
6744
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
6389
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
6745
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
6390
6746
  if (row2?.status === "ready") {
6391
6747
  try {
6392
6748
  return await restore2(row2, input, now);
@@ -6431,7 +6787,7 @@ async function prepareMembershipQuote(db, input, issue) {
6431
6787
  }
6432
6788
  if (row2?.status === "ready") return restore2(row2, input, now);
6433
6789
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
6434
- return fail6("membership_quote_pending");
6790
+ return fail7("membership_quote_pending");
6435
6791
  }
6436
6792
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
6437
6793
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -6480,7 +6836,7 @@ async function prepareMembershipQuote(db, input, issue) {
6480
6836
  return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
6481
6837
  }
6482
6838
  async function selectCheckoutNamedSeat(db, target, input) {
6483
- if (!input.enabled) return fail6("named_seat_purchase_unavailable");
6839
+ if (!input.enabled) return fail7("named_seat_purchase_unavailable");
6484
6840
  const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
6485
6841
  const current = await restore2(row2, target, input.now);
6486
6842
  const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
@@ -6536,17 +6892,17 @@ async function readBoundedRequestText(req, maxBytes) {
6536
6892
  if (!reader) return "";
6537
6893
  const decoder = new TextDecoder();
6538
6894
  let bytes = 0;
6539
- let text8 = "";
6895
+ let text9 = "";
6540
6896
  for (; ; ) {
6541
6897
  const { done, value } = await reader.read();
6542
6898
  if (req.signal.aborted) return invalid2();
6543
6899
  if (done) {
6544
6900
  complete = true;
6545
- return text8 + decoder.decode();
6901
+ return text9 + decoder.decode();
6546
6902
  }
6547
6903
  bytes += value.byteLength;
6548
6904
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
6549
- text8 += decoder.decode(value, { stream: true });
6905
+ text9 += decoder.decode(value, { stream: true });
6550
6906
  }
6551
6907
  } catch {
6552
6908
  return invalid2();
@@ -6650,9 +7006,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
6650
7006
  let selection;
6651
7007
  if (request?.body) {
6652
7008
  try {
6653
- const text8 = await readBoundedRequestText(request, 2048);
6654
- if (text8 instanceof Response) return text8;
6655
- const body = JSON.parse(text8);
7009
+ const text9 = await readBoundedRequestText(request, 2048);
7010
+ if (text9 instanceof Response) return text9;
7011
+ const body = JSON.parse(text9);
6656
7012
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
6657
7013
  selection = body.namedSeat;
6658
7014
  } catch {
@@ -7307,10 +7663,13 @@ var handleMember = async (req, url, env, ctx) => {
7307
7663
  const db = ctx.makeDb(env);
7308
7664
  const groupId = url.searchParams.get("group") ?? chapter.id;
7309
7665
  if (groupId !== chapter.id) return json({ error: "not found" }, 404);
7310
- const { groups } = await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } });
7666
+ const [{ groups }, stripeKey, activeControl] = await Promise.all([
7667
+ db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
7668
+ getVaultSecret(db, "stripe_secret_key"),
7669
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
7670
+ ]);
7311
7671
  const group = Array.isArray(groups) ? groups[0] : void 0;
7312
7672
  if (!group) return json({ error: "not found" }, 404);
7313
- const stripeKey = await getVaultSecret(db, "stripe_secret_key");
7314
7673
  const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
7315
7674
  const offered = await loadOfferableTiers(
7316
7675
  db,
@@ -7319,7 +7678,6 @@ var handleMember = async (req, url, env, ctx) => {
7319
7678
  );
7320
7679
  const seats = /* @__PURE__ */ new Map();
7321
7680
  const founding = await foundingPreviews(offered, env, ctx, seats);
7322
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7323
7681
  const application = controlledApplication(chapter.application, activeControl?.document);
7324
7682
  return json({
7325
7683
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
@@ -7357,11 +7715,14 @@ var handleMember = async (req, url, env, ctx) => {
7357
7715
  parsed.tierId = named.invitation.skuId;
7358
7716
  submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
7359
7717
  }
7360
- const eligibility = named ? null : await resolveSignupEligibility(db, chapter, parsed, submissionId);
7718
+ const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
7719
+ named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
7720
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
7721
+ typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
7722
+ db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
7723
+ ]);
7361
7724
  if (eligibility?.conflict) return eligibility.conflict;
7362
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7363
7725
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
7364
- const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
7365
7726
  const now = Date.now();
7366
7727
  const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
7367
7728
  const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
@@ -7400,18 +7761,16 @@ var handleMember = async (req, url, env, ctx) => {
7400
7761
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
7401
7762
  return json(provisioningFailure.body, provisioningFailure.status);
7402
7763
  }
7403
- if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
7404
- const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
7405
- const group = Array.isArray(groups) ? groups[0] : void 0;
7764
+ if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
7406
7765
  const notice = group ? { group, application: canonical3, origin: url.origin } : null;
7407
- if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
7766
+ if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
7408
7767
  const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
7409
7768
  if (!state) return json({
7410
7769
  error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
7411
7770
  code: "application_continuation_unavailable",
7412
7771
  id: result.id
7413
7772
  }, 503);
7414
- if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
7773
+ if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
7415
7774
  const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
7416
7775
  if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
7417
7776
  return json({ error: "application changed; reload its current state" }, 409);
@@ -7531,10 +7890,10 @@ function boundedString(value, path, errors, max, required = false) {
7531
7890
  errors.push(`${path} must be a string`);
7532
7891
  return void 0;
7533
7892
  }
7534
- const text8 = value.trim();
7535
- if (required && !text8) errors.push(`${path} is required`);
7536
- if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7537
- return text8;
7893
+ const text9 = value.trim();
7894
+ if (required && !text9) errors.push(`${path} is required`);
7895
+ if (text9.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7896
+ return text9;
7538
7897
  }
7539
7898
  function validateMembershipPageDocument(input, options = {}) {
7540
7899
  const errors = [];
@@ -7583,8 +7942,8 @@ function validateMembershipPageDocument(input, options = {}) {
7583
7942
  else {
7584
7943
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
7585
7944
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
7586
- const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
7587
- return text8 ? [text8] : [];
7945
+ const text9 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
7946
+ return text9 ? [text9] : [];
7588
7947
  });
7589
7948
  }
7590
7949
  }
@@ -7678,7 +8037,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
7678
8037
  function unknownKeys(value, allowed2, path, errors) {
7679
8038
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7680
8039
  }
7681
- function text4(value, path, errors, max = 65536) {
8040
+ function text5(value, path, errors, max = 65536) {
7682
8041
  if (typeof value !== "string" || !value.trim()) {
7683
8042
  errors.push(`${path} is required`);
7684
8043
  return "";
@@ -7701,11 +8060,11 @@ function tiersOf(value, errors) {
7701
8060
  return [];
7702
8061
  }
7703
8062
  unknownKeys(item, TIER_KEYS2, path, errors);
7704
- const id7 = text4(item.id, `${path}.id`, errors, 80);
8063
+ const id7 = text5(item.id, `${path}.id`, errors, 80);
7705
8064
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7706
8065
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
7707
8066
  seen.add(id7);
7708
- const name = text4(item.name, `${path}.name`, errors, 160);
8067
+ const name = text5(item.name, `${path}.name`, errors, 160);
7709
8068
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
7710
8069
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
7711
8070
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -7723,7 +8082,7 @@ function tiersOf(value, errors) {
7723
8082
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
7724
8083
  errors.push(`${path}.interval must be year or month`);
7725
8084
  }
7726
- const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
8085
+ const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
7727
8086
  return [{
7728
8087
  id: id7,
7729
8088
  name,
@@ -7781,7 +8140,7 @@ function policyOf(value, errors, tiers) {
7781
8140
  const result = {};
7782
8141
  for (const key3 of allowed2) {
7783
8142
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
7784
- result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
8143
+ result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
7785
8144
  }
7786
8145
  const tierPolicies = {};
7787
8146
  if (input.tierPolicies !== void 0) {
@@ -7796,8 +8155,8 @@ function policyOf(value, errors, tiers) {
7796
8155
  }
7797
8156
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
7798
8157
  tierPolicies[id7] = {
7799
- refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
7800
- ...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8158
+ refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8159
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
7801
8160
  };
7802
8161
  }
7803
8162
  }
@@ -7837,9 +8196,9 @@ function validateSignupControlDocument(input) {
7837
8196
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
7838
8197
  const source = recordOf(root.source);
7839
8198
  const target = recordOf(root.target);
7840
- const appId = text4(source?.appId, "document.source.appId", errors, 63);
7841
- const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
7842
- const environment = text4(target?.environment, "document.target.environment", errors, 63);
8199
+ const appId = text5(source?.appId, "document.source.appId", errors, 63);
8200
+ const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
8201
+ const environment = text5(target?.environment, "document.target.environment", errors, 63);
7843
8202
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7844
8203
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
7845
8204
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9351,8 +9710,8 @@ async function bookSlot(req, env, ctx) {
9351
9710
  const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
9352
9711
  await db.transact([meetingOp, appOp]);
9353
9712
  const booked = { ...app, ...bookingAttrs };
9354
- await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
9355
- await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
9713
+ await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
9714
+ await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
9356
9715
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
9357
9716
  }
9358
9717
  var handleSchedule = async (req, url, env, ctx) => {
@@ -9376,28 +9735,28 @@ var handleSchedule = async (req, url, env, ctx) => {
9376
9735
  };
9377
9736
 
9378
9737
  // src/payments-stripe.ts
9379
- import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
9738
+ import { stripeRequest as stripeRequest12, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
9380
9739
 
9381
9740
  // src/payments-refund.ts
9382
- import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
9741
+ import { stripeRequest as stripeRequest10 } from "@odla-ai/stripe";
9383
9742
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9384
9743
  var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
9385
9744
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
9386
- function fail7(code = "refund_target_mismatch") {
9745
+ function fail8(code = "refund_target_mismatch") {
9387
9746
  throw Object.assign(new Error(code), { code });
9388
9747
  }
9389
9748
  async function refundSubscriptionPayment(secretKey, input) {
9390
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
9749
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
9391
9750
  const call = async (method, path, params, key3) => {
9392
- const result = await stripeRequest8({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9393
- if (!result.ok) fail7("stripe_error");
9394
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
9751
+ const result = await stripeRequest10({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9752
+ if (!result.ok) fail8("stripe_error");
9753
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
9395
9754
  return result.body;
9396
9755
  };
9397
9756
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
9398
- if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
9757
+ if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
9399
9758
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
9400
- if (row(sub.metadata)[key3] !== value) fail7();
9759
+ if (row(sub.metadata)[key3] !== value) fail8();
9401
9760
  }
9402
9761
  let cursor;
9403
9762
  let earliest;
@@ -9410,66 +9769,66 @@ async function refundSubscriptionPayment(secretKey, input) {
9410
9769
  limit: 100,
9411
9770
  ...cursor ? { starting_after: cursor } : {}
9412
9771
  });
9413
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
9772
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
9414
9773
  for (const value of list.data) {
9415
9774
  const invoice2 = row(value);
9416
9775
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
9417
- if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail7();
9776
+ if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail8();
9418
9777
  seen.add(id3(invoice2));
9419
9778
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
9420
9779
  }
9421
9780
  if (!list.has_more) break;
9422
- if (!list.data.length || page === 9) fail7("refund_history_incomplete");
9781
+ if (!list.data.length || page === 9) fail8("refund_history_incomplete");
9423
9782
  cursor = id3(list.data[list.data.length - 1]);
9424
9783
  }
9425
- if (!earliest) fail7("no_charge");
9784
+ if (!earliest) fail8("no_charge");
9426
9785
  const invoice = earliest;
9427
- if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
9786
+ if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
9428
9787
  let chargeId = id3(invoice.charge);
9429
9788
  let paymentIntentId = id3(invoice.payment_intent);
9430
9789
  if (!chargeId && !paymentIntentId) {
9431
9790
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
9432
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
9791
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
9433
9792
  const payment = row(payments.data[0]);
9434
- if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
9793
+ if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
9435
9794
  paymentIntentId = id3(row(payment.payment).payment_intent);
9436
9795
  }
9437
9796
  if (!chargeId) {
9438
- if (!providerId2(paymentIntentId, "pi")) fail7();
9797
+ if (!providerId2(paymentIntentId, "pi")) fail8();
9439
9798
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
9440
- if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
9799
+ if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
9441
9800
  chargeId = id3(payment.latest_charge);
9442
9801
  }
9443
- if (!providerId2(chargeId, "ch")) fail7();
9802
+ if (!providerId2(chargeId, "ch")) fail8();
9444
9803
  const charge = await call("GET", `/v1/charges/${chargeId}`);
9445
- if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail7();
9446
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
9804
+ 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();
9805
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
9447
9806
  if (charge.amount_refunded !== charge.amount) {
9448
9807
  const refund = await call("POST", "/v1/refunds", {
9449
9808
  charge: chargeId,
9450
9809
  ...input.metadata ? { metadata: input.metadata } : {}
9451
9810
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
9452
- if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail7("refund_not_confirmed");
9811
+ if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail8("refund_not_confirmed");
9453
9812
  }
9454
9813
  let subscriptionCanceled = sub.status === "canceled";
9455
9814
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
9456
9815
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
9457
- if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
9816
+ if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
9458
9817
  subscriptionCanceled = true;
9459
9818
  }
9460
9819
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
9461
- if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail7("refund_not_confirmed");
9820
+ if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail8("refund_not_confirmed");
9462
9821
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
9463
9822
  }
9464
9823
 
9465
9824
  // src/subscription-payment-identity.ts
9466
- import { stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
9825
+ import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
9467
9826
  var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
9468
9827
  async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
9469
9828
  let paymentId = id4(invoice.payment_intent);
9470
9829
  if (!paymentId) {
9471
9830
  if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
9472
- const result2 = await stripeRequest9({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9831
+ const result2 = await stripeRequest11({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9473
9832
  const list = result2.body;
9474
9833
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
9475
9834
  const link = list.data[0];
@@ -9478,11 +9837,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9478
9837
  }
9479
9838
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
9480
9839
  const path = `/v1/payment_intents/${paymentId}`;
9481
- const result = await stripeRequest9({ secretKey }, "GET", path);
9840
+ const result = await stripeRequest11({ secretKey }, "GET", path);
9482
9841
  const payment = result.body;
9483
9842
  if (!result.ok || payment?.id !== paymentId || id4(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
9484
9843
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
9485
- const tagged = await stripeRequest9(
9844
+ const tagged = await stripeRequest11(
9486
9845
  { secretKey },
9487
9846
  "POST",
9488
9847
  path,
@@ -9494,7 +9853,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9494
9853
 
9495
9854
  // src/payments-stripe.ts
9496
9855
  async function stripeCall(sk, method, path, params, idempotencyKey) {
9497
- return stripeRequest10(
9856
+ return stripeRequest12(
9498
9857
  { secretKey: sk },
9499
9858
  method,
9500
9859
  path,
@@ -9502,7 +9861,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
9502
9861
  idempotencyKey ? { idempotencyKey } : {}
9503
9862
  );
9504
9863
  }
9505
- function fail8(op, r) {
9864
+ function fail9(op, r) {
9506
9865
  const err = new Error(`stripe ${op} failed: ${r.status}`);
9507
9866
  err.code = "stripe_error";
9508
9867
  throw err;
@@ -9561,7 +9920,7 @@ function createStripeProvider(config) {
9561
9920
  { email: input.email, name: input.name, metadata: meta },
9562
9921
  `cus:${input.applicationId}`
9563
9922
  );
9564
- if (!cust.ok) fail8("customer create", cust);
9923
+ if (!cust.ok) fail9("customer create", cust);
9565
9924
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
9566
9925
  customerId = cust.body.id;
9567
9926
  }
@@ -9606,7 +9965,7 @@ function createStripeProvider(config) {
9606
9965
  // retry between create and the db write can't orphan a second one.
9607
9966
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
9608
9967
  );
9609
- if (!sub.ok) fail8("subscription create", sub);
9968
+ if (!sub.ok) fail9("subscription create", sub);
9610
9969
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
9611
9970
  const subscriptionId = sub.body.id;
9612
9971
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -9616,14 +9975,14 @@ function createStripeProvider(config) {
9616
9975
  "expand[2]": "discounts",
9617
9976
  "expand[3]": "latest_invoice.discounts"
9618
9977
  });
9619
- if (!expanded.ok) fail8("subscription invoice evidence", expanded);
9978
+ if (!expanded.ok) fail9("subscription invoice evidence", expanded);
9620
9979
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
9621
9980
  sub = expanded;
9622
9981
  }
9623
9982
  verifySubscriptionResult(sub.body, input, customerId, meta);
9624
9983
  const invoice = sub.body.latest_invoice;
9625
9984
  const clientSecret2 = clientSecretOf(sub.body);
9626
- if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
9985
+ if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
9627
9986
  await tagSubscriptionPayment(
9628
9987
  sk,
9629
9988
  invoice,
@@ -10110,7 +10469,7 @@ async function startSubscription(req, env, ctx) {
10110
10469
 
10111
10470
  // src/worker-payment-receipt.ts
10112
10471
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10113
- var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10472
+ var escape = (text9) => text9.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10114
10473
  async function paymentReceiptRoute(req, url, env, ctx) {
10115
10474
  const id7 = url.searchParams.get("application");
10116
10475
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
@@ -10121,7 +10480,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10121
10480
  if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
10122
10481
  const receipt = intent.paymentReceipt;
10123
10482
  if (!receipt) return json2({ pending: true }, 202);
10124
- const text8 = paymentReceiptText(receipt, ctx.chapter.name);
10483
+ const text9 = paymentReceiptText(receipt, ctx.chapter.name);
10125
10484
  if (req.method === "POST") {
10126
10485
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10127
10486
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10139,7 +10498,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10139
10498
  template: "paymentReceipt",
10140
10499
  to: String(intent.purchaserEmailNormalized),
10141
10500
  applicationId: id7,
10142
- vars: { chapterName: ctx.chapter.name, receiptText: text8 },
10501
+ vars: { chapterName: ctx.chapter.name, receiptText: text9 },
10143
10502
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10144
10503
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10145
10504
  });
@@ -10149,7 +10508,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10149
10508
  const nonce = crypto.randomUUID();
10150
10509
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10151
10510
  <style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
10152
- <h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
10511
+ <h1>Payment receipt</h1><pre>${escape(text9)}</pre><button id="print">Print / save PDF</button>
10153
10512
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10154
10513
  headers: {
10155
10514
  "content-type": "text/html; charset=utf-8",
@@ -10619,16 +10978,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
10619
10978
  };
10620
10979
 
10621
10980
  // src/named-seat-accepted-notice.ts
10622
- var text5 = (value) => typeof value === "string" ? value : "";
10981
+ var text6 = (value) => typeof value === "string" ? value : "";
10623
10982
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
10624
10983
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
10625
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
10984
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text6(purchaser.email)) {
10626
10985
  return { sent: false, reason: "purchaser-missing" };
10627
10986
  }
10628
10987
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
10629
10988
  if (!group) return { sent: false, reason: "group-missing" };
10630
10989
  const config = emailGroupFrom(group);
10631
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
10990
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text6).join(" ").replace(/\s+/g, " ").trim();
10632
10991
  return sendTemplated({
10633
10992
  db,
10634
10993
  envName: env.ODLA_ENV,
@@ -10640,12 +10999,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
10640
10999
  }, {
10641
11000
  group: config,
10642
11001
  template: "seatAccepted",
10643
- to: text5(purchaser.email),
11002
+ to: text6(purchaser.email),
10644
11003
  dedupeKey: `named-seat-accepted:${input.seatId}`,
10645
11004
  applicationId: input.primaryApplicationId,
10646
11005
  tierId: input.skuId,
10647
11006
  vars: {
10648
- firstName: text5(purchaser.firstName),
11007
+ firstName: text6(purchaser.firstName),
10649
11008
  purchaserName,
10650
11009
  recipientName: input.recipientName,
10651
11010
  tierName: await tierName(db, group, input.skuId),
@@ -11089,7 +11448,7 @@ var handleAdminScheduling = async (req, url, env, ctx) => {
11089
11448
  const checked = validateScheduling({ ...stored, ...body });
11090
11449
  if (!checked.ok) return json({ error: "invalid scheduling config", errors: checked.errors }, 400);
11091
11450
  try {
11092
- await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
11451
+ await db.transact([{ t: "update", ns: "groups", id: { ns: "groups", attr: "id", value: String(group.id) }, attrs: { schedulingJson: checked.value } }]);
11093
11452
  } catch (error) {
11094
11453
  const code = error.code;
11095
11454
  console.error("admin_scheduling_write_failed", { code: typeof code === "string" ? code : "unknown" });
@@ -12112,12 +12471,12 @@ async function updateEmailSettings(db, ctx, env, body) {
12112
12471
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12113
12472
  const t = value;
12114
12473
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12115
- const subject = str2(t.subject).trim(), text8 = str2(t.text);
12116
- const issues = validateEmailCopy4({ subject, text: text8 }, { allowedVariables: events[key3].variables });
12474
+ const subject = str2(t.subject).trim(), text9 = str2(t.text);
12475
+ const issues = validateEmailCopy4({ subject, text: text9 }, { allowedVariables: events[key3].variables });
12117
12476
  if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
12118
12477
  return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
12119
12478
  }
12120
- clean[key3] = { subject, text: text8, enabled: t.enabled };
12479
+ clean[key3] = { subject, text: text9, enabled: t.enabled };
12121
12480
  }
12122
12481
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12123
12482
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -14330,9 +14689,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14330
14689
  if (req.method === "POST") {
14331
14690
  let body;
14332
14691
  try {
14333
- const text8 = await req.text();
14334
- if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14335
- const parsed = JSON.parse(text8);
14692
+ const text9 = await req.text();
14693
+ if (new TextEncoder().encode(text9).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14694
+ const parsed = JSON.parse(text9);
14336
14695
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14337
14696
  body = parsed;
14338
14697
  } catch {
@@ -14501,7 +14860,7 @@ import { signFederatedRequest as signFederatedRequest7, verifyFederatedRequest a
14501
14860
  // src/network-commercial-parity.ts
14502
14861
  var DIGEST = /^[0-9a-f]{64}$/;
14503
14862
  var MAX_RECORDS = 500;
14504
- var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14863
+ var text7 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14505
14864
  function parityMessage(partition, kind, value) {
14506
14865
  return [
14507
14866
  "chapter-commercial-parity",
@@ -14533,18 +14892,18 @@ async function digestWithKey(key3, partition, kind, value) {
14533
14892
  async function buildChapterCommercialParitySnapshot(input) {
14534
14893
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
14535
14894
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
14536
- const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
14895
+ const eligible = input.rows.filter((row2) => text7(row2.id) && (text7(row2.stripeCustomerId) || text7(row2.stripeSubscriptionId)));
14537
14896
  const selected2 = eligible.slice(0, maxRecords);
14538
14897
  const key3 = await parityKey(input.secret);
14539
14898
  const records = await Promise.all(selected2.map(async (row2) => {
14540
- const applicationId = text6(row2.id);
14541
- const customerId = text6(row2.stripeCustomerId);
14542
- const subscriptionId = text6(row2.stripeSubscriptionId);
14899
+ const applicationId = text7(row2.id);
14900
+ const customerId = text7(row2.stripeCustomerId);
14901
+ const subscriptionId = text7(row2.stripeSubscriptionId);
14543
14902
  return {
14544
14903
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
14545
14904
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
14546
14905
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
14547
- status: text6(row2.status, 64),
14906
+ status: text7(row2.status, 64),
14548
14907
  canceled: row2.canceled === true
14549
14908
  };
14550
14909
  }));
@@ -14570,7 +14929,7 @@ function validSnapshot(snapshot, source) {
14570
14929
  if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
14571
14930
  const seen = /* @__PURE__ */ new Set();
14572
14931
  for (const record4 of records) {
14573
- if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text6(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
14932
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text7(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
14574
14933
  const key3 = recordKey(record4);
14575
14934
  if (seen.has(key3)) return false;
14576
14935
  seen.add(key3);
@@ -14581,16 +14940,16 @@ async function reconcileCommercialParitySnapshot(input) {
14581
14940
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
14582
14941
  const sourceKey = networkCommercialSourceKey(input.source);
14583
14942
  const key3 = await parityKey(input.secret);
14584
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
14585
- const customerId = text6(row2.customerId);
14586
- const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
14943
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text7(row2.applicationId) && text7(row2.objectId)).map(async (row2) => {
14944
+ const customerId = text7(row2.customerId);
14945
+ const subscriptionId = text7(row2.subscriptionId) ?? text7(row2.objectId);
14587
14946
  return {
14588
14947
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
14589
14948
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
14590
14949
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
14591
14950
  status: null,
14592
14951
  canceled: false,
14593
- stripeStatus: text6(row2.status, 64),
14952
+ stripeStatus: text7(row2.status, 64),
14594
14953
  stripeCanceled: row2.status === "canceled"
14595
14954
  };
14596
14955
  }));
@@ -14787,6 +15146,34 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
14787
15146
  };
14788
15147
  }
14789
15148
 
15149
+ // src/worker-network-commercial-dispute.ts
15150
+ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15151
+ const operator = ctx.membershipAuthorityOperator;
15152
+ if (!operator || !fact.chargeId) return { disposition: "unhosted" };
15153
+ let db;
15154
+ try {
15155
+ db = operator.makeDb(env);
15156
+ } catch {
15157
+ return { disposition: "unhosted" };
15158
+ }
15159
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
15160
+ if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
15161
+ const charge = await readDisputedCharge(secret, fact.chargeId);
15162
+ const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15163
+ const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
15164
+ if (!member) return { disposition: "not_applicable" };
15165
+ const target = {
15166
+ appId: String(member.appId),
15167
+ chapterId: String(member.chapterId),
15168
+ environment: String(member.environment),
15169
+ runtime: String(member.runtime),
15170
+ applicationId: String(member.applicationId),
15171
+ skuId: String(member.skuId)
15172
+ };
15173
+ const receipt = await reconcileMembershipDispute(db, { ...target, eventId: fact.eventId }, now, secret, charge);
15174
+ return { disposition: "applied", membershipId: receipt.membershipId };
15175
+ }
15176
+
14790
15177
  // src/worker-routes-network-commercial.ts
14791
15178
  var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
14792
15179
  var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
@@ -14851,6 +15238,12 @@ function safeProjection(projection) {
14851
15238
  amountReceivedCents: seat.amountReceivedCents,
14852
15239
  refundedCents: seat.refundedCents ?? 0
14853
15240
  })),
15241
+ disputes: (projection.disputes ?? []).map((dispute) => ({
15242
+ ...row2(dispute),
15243
+ chargeId: dispute.chargeId ?? null,
15244
+ paymentIntentId: dispute.paymentIntentId ?? null,
15245
+ reason: dispute.disputeReason ?? null
15246
+ })),
14854
15247
  exceptions: projection.exceptions
14855
15248
  };
14856
15249
  }
@@ -14883,10 +15276,20 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
14883
15276
  if (result.status === "collision") {
14884
15277
  console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
14885
15278
  }
15279
+ let dispute;
15280
+ if (fact.objectKind === "dispute" && result.status !== "collision") {
15281
+ try {
15282
+ dispute = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
15283
+ } catch (error) {
15284
+ console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
15285
+ return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
15286
+ }
15287
+ }
14886
15288
  return json({
14887
15289
  ok: true,
14888
15290
  status: result.status,
14889
15291
  eventId: fact.eventId,
15292
+ ...dispute ? { dispute: dispute.disposition } : {},
14890
15293
  ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
14891
15294
  });
14892
15295
  } catch (error) {
@@ -15084,7 +15487,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
15084
15487
  function unknownKeys3(value, allowed2, path, errors) {
15085
15488
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
15086
15489
  }
15087
- function text7(value, path, errors, max = MAX_TEXT2, required = true) {
15490
+ function text8(value, path, errors, max = MAX_TEXT2, required = true) {
15088
15491
  if (value === void 0 && !required) return void 0;
15089
15492
  if (typeof value !== "string" || required && !value.trim()) {
15090
15493
  errors.push(`${path} must be a non-empty string`);
@@ -15110,7 +15513,7 @@ function scopeValue(value, path, errors) {
15110
15513
  return { kind: raw2.kind };
15111
15514
  }
15112
15515
  if (raw2.kind === "chapter") {
15113
- const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15516
+ const chapterId = text8(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15114
15517
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15115
15518
  return { kind: "chapter", chapterId };
15116
15519
  }
@@ -15124,22 +15527,22 @@ function resourceValue(value, path, errors) {
15124
15527
  return null;
15125
15528
  }
15126
15529
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15127
- const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
15530
+ const id7 = text8(raw2.id, `${path}.id`, errors, 80) ?? "";
15128
15531
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15129
- const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
15532
+ const kind = text8(raw2.kind, `${path}.kind`, errors) ?? "";
15130
15533
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15131
- const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
15132
- const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
15133
- const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
15534
+ const title = text8(raw2.title, `${path}.title`, errors, 240) ?? "";
15535
+ const summary = text8(raw2.summary, `${path}.summary`, errors, 1e3, false);
15536
+ const status2 = text8(raw2.status, `${path}.status`, errors) ?? "";
15134
15537
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15135
15538
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15136
15539
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15137
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15540
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text8(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15138
15541
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15139
15542
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15140
15543
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15141
- const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15142
- const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
15544
+ const bodyMarkdown = text8(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15545
+ const url = text8(raw2.url, `${path}.url`, errors, 2048, false);
15143
15546
  if (url) {
15144
15547
  try {
15145
15548
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15151,9 +15554,9 @@ function resourceValue(value, path, errors) {
15151
15554
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15152
15555
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15153
15556
  const brandAsset = asset ? {
15154
- bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15155
- assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15156
- ...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15557
+ bookId: text8(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15558
+ assetId: text8(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15559
+ ...asset.filename === void 0 ? {} : { filename: text8(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15157
15560
  } : void 0;
15158
15561
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15159
15562
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15168,17 +15571,17 @@ function resourceValue(value, path, errors) {
15168
15571
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15169
15572
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15170
15573
  const promotedFrom = promotedRaw ? {
15171
- chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15172
- resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15574
+ chapterId: text8(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15575
+ resourceId: text8(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15173
15576
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15174
15577
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15175
- promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15578
+ promotedBy: text8(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15176
15579
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15177
15580
  } : void 0;
15178
15581
  const provenance2 = {
15179
- createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15582
+ createdBy: text8(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15180
15583
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15181
- updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15584
+ updatedBy: text8(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15182
15585
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15183
15586
  ...promotedFrom ? { promotedFrom } : {}
15184
15587
  };
@@ -15197,7 +15600,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15197
15600
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15198
15601
  const sourceType = sourceRaw?.type;
15199
15602
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15200
- const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15603
+ const sourceAppId = text8(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15201
15604
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15202
15605
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15203
15606
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15205,8 +15608,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15205
15608
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15206
15609
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15207
15610
  const target = targetRaw ? {
15208
- appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15209
- environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15611
+ appId: text8(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15612
+ environment: text8(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15210
15613
  } : void 0;
15211
15614
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15212
15615
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15547,7 +15950,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
15547
15950
  };
15548
15951
 
15549
15952
  // src/membership-cancel-authority.ts
15550
- import { stripeRequest as stripeRequest11 } from "@odla-ai/stripe";
15953
+ import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
15551
15954
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15552
15955
  membershipClock(now);
15553
15956
  const id7 = await networkMembershipId(target);
@@ -15558,14 +15961,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15558
15961
  membershipFail("membership_cancellation_unavailable");
15559
15962
  }
15560
15963
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
15561
- const read2 = await stripeRequest11({ secretKey: stripeSecret }, "GET", path);
15964
+ const read2 = await stripeRequest13({ secretKey: stripeSecret }, "GET", path);
15562
15965
  const sub = read2.body;
15563
15966
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
15564
15967
  ...stripePartitionMetadata(target),
15565
15968
  applicationId: target.applicationId
15566
15969
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
15567
15970
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
15568
- const write = await stripeRequest11(
15971
+ const write = await stripeRequest13(
15569
15972
  { secretKey: stripeSecret },
15570
15973
  "POST",
15571
15974
  path,
@@ -15787,7 +16190,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
15787
16190
  }
15788
16191
 
15789
16192
  // src/membership-provider-lifecycle.ts
15790
- import { stripeRequest as stripeRequest12 } from "@odla-ai/stripe";
16193
+ import { stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
15791
16194
 
15792
16195
  // src/membership-renewal-authority.ts
15793
16196
  async function reconcileMembershipRenewal(db, input, now) {
@@ -15909,6 +16312,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15909
16312
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
15910
16313
  const fact = row2?.fact;
15911
16314
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
16315
+ if (fact?.objectKind === "dispute") {
16316
+ const secret = await getVaultSecret(db, stripeSecretName);
16317
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
16318
+ return reconcileMembershipDispute(db, input, now, secret);
16319
+ }
15912
16320
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
15913
16321
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
15914
16322
  appId: input.appId,
@@ -15951,7 +16359,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15951
16359
  }
15952
16360
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
15953
16361
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
15954
- const result = await stripeRequest12({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
16362
+ const result = await stripeRequest14({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
15955
16363
  const sub = result.body;
15956
16364
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
15957
16365
  membershipFail("membership_billing_provider_mismatch");
@@ -15989,7 +16397,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15989
16397
  }
15990
16398
 
15991
16399
  // src/membership-seat-stripe.ts
15992
- import { stripeRequest as stripeRequest13 } from "@odla-ai/stripe";
16400
+ import { stripeRequest as stripeRequest15 } from "@odla-ai/stripe";
15993
16401
  var reject7 = () => {
15994
16402
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
15995
16403
  };
@@ -16001,7 +16409,7 @@ var metadataFor = (seat) => ({
16001
16409
  });
16002
16410
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
16003
16411
  async function read(sk, path, params) {
16004
- const response = await stripeRequest13({ secretKey: sk }, "GET", path, params);
16412
+ const response = await stripeRequest15({ secretKey: sk }, "GET", path, params);
16005
16413
  if (!response.ok) return reject7();
16006
16414
  return response.body;
16007
16415
  }
@@ -16038,11 +16446,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
16038
16446
  }
16039
16447
  await subscriptionForSeat(sk, seat);
16040
16448
  const metadata = metadataFor(seat);
16041
- const result = seat.providerPaymentIntentId ? await stripeRequest13(
16449
+ const result = seat.providerPaymentIntentId ? await stripeRequest15(
16042
16450
  { secretKey: sk },
16043
16451
  "GET",
16044
16452
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
16045
- ) : await stripeRequest13({ secretKey: sk }, "POST", "/v1/payment_intents", {
16453
+ ) : await stripeRequest15({ secretKey: sk }, "POST", "/v1/payment_intents", {
16046
16454
  amount: seat.amountCents,
16047
16455
  currency: seat.currency,
16048
16456
  customer: seat.providerCustomerId,
@@ -16064,7 +16472,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
16064
16472
  return String(existing.id);
16065
16473
  }
16066
16474
  if (seat.combinedInvoice) reject7();
16067
- const result = await stripeRequest13({ secretKey: sk }, "POST", "/v1/subscription_items", {
16475
+ const result = await stripeRequest15({ secretKey: sk }, "POST", "/v1/subscription_items", {
16068
16476
  subscription: seat.providerSubscriptionId,
16069
16477
  price: seat.priceId,
16070
16478
  quantity: 1,
@@ -16765,7 +17173,20 @@ var membershipAuthorityEntities = {
16765
17173
  graceEndsAt: attr("number", true),
16766
17174
  ...times(),
16767
17175
  refundRequestedAt: attr("number", true),
16768
- restartId: key(true)
17176
+ restartId: key(true),
17177
+ // A card-network dispute on this membership's payment: Stripe's dispute id and status, the reason the cardholder gave,
17178
+ // the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
17179
+ disputeId: key(true),
17180
+ disputeStatus: attr("string", true),
17181
+ disputeReason: attr("string", true),
17182
+ disputeAmountCents: attr("number", true),
17183
+ disputedAt: attr("number", true),
17184
+ disputeClosedAt: attr("number", true),
17185
+ disputePriorStatus: attr("string", true),
17186
+ disputeEventId: key(true),
17187
+ rechargePaymentIntentId: key(true),
17188
+ rechargedAt: attr("number", true),
17189
+ rechargeAttempt: attr("json", true)
16769
17190
  } },
16770
17191
  membershipRestart: { attrs: {
16771
17192
  id: id5(),
@@ -16962,7 +17383,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
16962
17383
  }
16963
17384
 
16964
17385
  // src/membership-seat-cancel.ts
16965
- import { initStripe as initStripe3, stripeRequest as stripeRequest14 } from "@odla-ai/stripe";
17386
+ import { initStripe as initStripe3, stripeRequest as stripeRequest16 } from "@odla-ai/stripe";
16966
17387
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
16967
17388
  membershipClock(now);
16968
17389
  const state = await loadMembershipAuthorityState(db, target);
@@ -16990,7 +17411,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
16990
17411
  }
16991
17412
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
16992
17413
  const request = async (method, path, params) => {
16993
- const result = await stripeRequest14(
17414
+ const result = await stripeRequest16(
16994
17415
  { secretKey },
16995
17416
  method,
16996
17417
  path,
@@ -17178,7 +17599,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17178
17599
  };
17179
17600
  signal.addEventListener("abort", cancel, { once: true });
17180
17601
  let size = 0;
17181
- let text8 = "";
17602
+ let text9 = "";
17182
17603
  const decoder = new TextDecoder();
17183
17604
  try {
17184
17605
  while (true) {
@@ -17188,10 +17609,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17188
17609
  if (done) break;
17189
17610
  size += value.byteLength;
17190
17611
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17191
- text8 += decoder.decode(value, { stream: true });
17612
+ text9 += decoder.decode(value, { stream: true });
17192
17613
  }
17193
17614
  try {
17194
- return JSON.parse(text8 + decoder.decode());
17615
+ return JSON.parse(text9 + decoder.decode());
17195
17616
  } catch {
17196
17617
  throw new TypeError(`invalid ${label}`);
17197
17618
  }
@@ -17526,7 +17947,7 @@ async function requestMembershipRefund(options) {
17526
17947
  async function requestMembershipRenewal(options) {
17527
17948
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
17528
17949
  const row2 = result.renewal;
17529
- if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
17950
+ if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
17530
17951
  throw new Error("invalid membership renewal receipt");
17531
17952
  }
17532
17953
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -17878,6 +18299,7 @@ var BUILTIN_ROUTES = [
17878
18299
  handlePaymentRecovery,
17879
18300
  handleAdminNamedSeat,
17880
18301
  handleAdminMembershipSeats,
18302
+ handleAdminMembershipDisputes,
17881
18303
  handleAdminMembershipPolicyVersions,
17882
18304
  handleAdminMembershipReterm,
17883
18305
  handleAdminMeetings,
@@ -17926,8 +18348,9 @@ function chapterWorker(options) {
17926
18348
  const ctx = createWorkerContext(options);
17927
18349
  const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
17928
18350
  return {
17929
- async fetch(req, env) {
18351
+ async fetch(req, env, execution) {
17930
18352
  const url = new URL(req.url);
18353
+ ctx.bindExecution(req, execution);
17931
18354
  for (const route of routes) {
17932
18355
  let res;
17933
18356
  try {
@@ -17949,6 +18372,7 @@ export {
17949
18372
  DECLINABLE_FROM,
17950
18373
  MEMBERSHIP_AUTHORITY_PATH,
17951
18374
  MEMBERSHIP_CHAPTER_EFFECT_KINDS,
18375
+ MEMBERSHIP_DISPUTED_STATUS,
17952
18376
  NetworkTargetTransportError,
17953
18377
  REGIFTABLE_SEAT_STATUSES,
17954
18378
  chapterMembershipAccess,
@@ -17974,7 +18398,10 @@ export {
17974
18398
  namedSeatInvitationOpen,
17975
18399
  namedSeatRecipientState,
17976
18400
  namedSeatRegiftable,
18401
+ readDisputedCharge,
17977
18402
  readNamedSeatOffer,
18403
+ rechargeDisputedMembership,
18404
+ reconcileMembershipDispute,
17978
18405
  refundNamedSeatByOperator,
17979
18406
  regiftNamedSeat,
17980
18407
  releaseNamedSeatRecipientApplications,