@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.
@@ -24,6 +24,7 @@ __export(worker_exports, {
24
24
  DECLINABLE_FROM: () => DECLINABLE_FROM,
25
25
  MEMBERSHIP_AUTHORITY_PATH: () => MEMBERSHIP_AUTHORITY_PATH,
26
26
  MEMBERSHIP_CHAPTER_EFFECT_KINDS: () => MEMBERSHIP_CHAPTER_EFFECT_KINDS,
27
+ MEMBERSHIP_DISPUTED_STATUS: () => MEMBERSHIP_DISPUTED_STATUS,
27
28
  NetworkTargetTransportError: () => NetworkTargetTransportError,
28
29
  REGIFTABLE_SEAT_STATUSES: () => REGIFTABLE_SEAT_STATUSES,
29
30
  chapterMembershipAccess: () => chapterMembershipAccess,
@@ -49,7 +50,10 @@ __export(worker_exports, {
49
50
  namedSeatInvitationOpen: () => namedSeatInvitationOpen,
50
51
  namedSeatRecipientState: () => namedSeatRecipientState,
51
52
  namedSeatRegiftable: () => namedSeatRegiftable,
53
+ readDisputedCharge: () => readDisputedCharge,
52
54
  readNamedSeatOffer: () => readNamedSeatOffer,
55
+ rechargeDisputedMembership: () => rechargeDisputedMembership,
56
+ reconcileMembershipDispute: () => reconcileMembershipDispute,
53
57
  refundNamedSeatByOperator: () => refundNamedSeatByOperator,
54
58
  regiftNamedSeat: () => regiftNamedSeat,
55
59
  releaseNamedSeatRecipientApplications: () => releaseNamedSeatRecipientApplications,
@@ -311,12 +315,37 @@ function networkCommercialAlerts(ctx, route) {
311
315
  };
312
316
  }
313
317
 
318
+ // src/worker-defer.ts
319
+ function deferWork(ctx, req, task) {
320
+ return ctx.defer ? ctx.defer(req, task) : task().then(() => void 0, () => void 0);
321
+ }
322
+ function createDeferral() {
323
+ const executions = /* @__PURE__ */ new WeakMap();
324
+ const settle = (task) => task().then(() => void 0, (error) => {
325
+ console.warn("chapter_deferred_task_failed", JSON.stringify({ message: String(error?.message ?? error).slice(0, 200) }));
326
+ });
327
+ return {
328
+ /** Remember the runtime's ExecutionContext for this request. */
329
+ bind(req, execution) {
330
+ if (execution) executions.set(req, execution);
331
+ },
332
+ /** Run `task` after the response when the request has an ExecutionContext, else now. Resolves once the task is scheduled or done. */
333
+ defer(req, task) {
334
+ const execution = executions.get(req);
335
+ if (!execution) return settle(task);
336
+ execution.waitUntil(settle(task));
337
+ return Promise.resolve();
338
+ }
339
+ };
340
+ }
341
+
314
342
  // src/worker-context.ts
315
343
  var json = (body, status2 = 200) => new Response(JSON.stringify(body), { status: status2, headers: { "content-type": "application/json" } });
316
344
  function createWorkerContext(options) {
317
345
  const { chapter } = options;
318
346
  const auth = chapter.auth;
319
347
  const crmBase = options.crmBasePath ?? "/api/crm";
348
+ const deferral = createDeferral();
320
349
  let publicConfigCache = null;
321
350
  const jwksByIssuer = /* @__PURE__ */ new Map();
322
351
  async function getPublicConfig(env) {
@@ -421,6 +450,8 @@ function createWorkerContext(options) {
421
450
  membershipAuthority: options.membershipAuthority,
422
451
  membershipAuthorityOperator: options.membershipAuthorityOperator,
423
452
  recordError: options.recordError ?? consoleErrorRecorder,
453
+ bindExecution: deferral.bind,
454
+ defer: deferral.defer,
424
455
  getPublicConfig,
425
456
  verifyUser,
426
457
  makeDb,
@@ -748,6 +779,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
748
779
  else if (eventType.startsWith("refund.")) objectKind = "refund";
749
780
  else if (eventType === "charge.refunded") objectKind = "refund";
750
781
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
782
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
751
783
  const customerId = refId(object2.customer, "cus_");
752
784
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
753
785
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -757,6 +789,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
757
789
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
758
790
  const currency = text(object2.currency)?.toLowerCase();
759
791
  const status2 = text(object2.status);
792
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
760
793
  return {
761
794
  eventId,
762
795
  digest,
@@ -775,6 +808,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
775
808
  ...invoiceId ? { invoiceId } : {},
776
809
  ...chargeId ? { chargeId } : {},
777
810
  ...paymentIntentId ? { paymentIntentId } : {},
811
+ ...disputeReason ? { disputeReason } : {},
778
812
  ...price,
779
813
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
780
814
  ...status2 ? { status: status2 } : {},
@@ -817,6 +851,7 @@ function ownRef(fact) {
817
851
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
818
852
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
819
853
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
854
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
820
855
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
821
856
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
822
857
  }
@@ -872,6 +907,7 @@ function projectNetworkCommercialEvents(input, sources) {
872
907
  continue;
873
908
  }
874
909
  const source = assignments.get(fact.eventId);
910
+ if (!source && fact.objectKind === "dispute") continue;
875
911
  if (!source) {
876
912
  const direct = directSource(fact, sources);
877
913
  const keys3 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -888,6 +924,7 @@ function projectNetworkCommercialEvents(input, sources) {
888
924
  const subscriptions = rows5.filter((row2) => row2.objectKind === "subscription");
889
925
  const invoices = rows5.filter((row2) => row2.objectKind === "invoice");
890
926
  const seatPayments = rows5.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
927
+ const disputes = rows5.filter((row2) => row2.objectKind === "dispute");
891
928
  const refunds = rows5.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
892
929
  for (const charge of rows5.filter((row2) => row2.eventType === "charge.refunded")) {
893
930
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -908,6 +945,7 @@ function projectNetworkCommercialEvents(input, sources) {
908
945
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
909
946
  return {
910
947
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
948
+ ...disputes.length ? { disputes } : {},
911
949
  customers,
912
950
  subscriptions,
913
951
  invoices,
@@ -1244,6 +1282,17 @@ function findGiftRef(obj) {
1244
1282
  }
1245
1283
  function normalizeWebhookEvent(event2, options = {}) {
1246
1284
  const obj = event2.data?.object ?? {};
1285
+ if (event2.type.startsWith("charge.dispute.")) {
1286
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
1287
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
1288
+ return {
1289
+ kind: "disputed",
1290
+ chargeId,
1291
+ disputeId: obj.id,
1292
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
1293
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
1294
+ };
1295
+ }
1247
1296
  if (event2.type === "charge.refunded" && (!Number.isSafeInteger(obj.amount) || Number(obj.amount) <= 0 || obj.amount_refunded !== obj.amount || obj.refunded !== true)) return { kind: "ignored", type: event2.type };
1248
1297
  const gift = findGiftRef(obj);
1249
1298
  if (gift.giftIntentId) {
@@ -1648,9 +1697,9 @@ async function backfillCrm(deps) {
1648
1697
  }
1649
1698
 
1650
1699
  // src/email-retirement.ts
1651
- function isRetiredEmailCopy(subject, text8) {
1700
+ function isRetiredEmailCopy(subject, text9) {
1652
1701
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
1653
- const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1702
+ const bodyCopy = text9.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
1654
1703
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
1655
1704
  }
1656
1705
 
@@ -1728,10 +1777,10 @@ function planDelivery(input) {
1728
1777
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
1729
1778
  if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
1730
1779
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
1731
- const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
1780
+ const text9 = redirect ? `(dev redirect; original recipient: ${input.to})
1732
1781
 
1733
1782
  ` + renderedText : renderedText;
1734
- return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
1783
+ return { deliver: true, transport, to, subject, text: text9, redirected: redirect };
1735
1784
  }
1736
1785
 
1737
1786
  // src/notify.ts
@@ -1879,8 +1928,8 @@ function formatInstant(at2, timeZone) {
1879
1928
  var adminUrlFor = (origin) => `${origin}/admin/`;
1880
1929
  var membersUrlFor = (origin) => `${origin}/members/`;
1881
1930
  function applicantVariables(app) {
1882
- const text8 = (v) => typeof v === "string" ? v : "";
1883
- return { firstName: text8(app.firstName), lastName: text8(app.lastName), email: text8(app.email), phone: text8(app.phone), state: text8(app.state) };
1931
+ const text9 = (v) => typeof v === "string" ? v : "";
1932
+ return { firstName: text9(app.firstName), lastName: text9(app.lastName), email: text9(app.email), phone: text9(app.phone), state: text9(app.state) };
1884
1933
  }
1885
1934
  function sampleVariables(origin, timeZone, now = Date.now()) {
1886
1935
  const startAt = formatInstant(now + 864e5, timeZone);
@@ -2987,7 +3036,7 @@ async function renewChapterNetworkMembership(db, env, ctx, app, eventId) {
2987
3036
  if (!intent) throw new Error("membership checkout reconciliation pending");
2988
3037
  const target = await membershipTargetForIntent(intent, app, env, ctx);
2989
3038
  const row2 = await ctx.membershipAuthority.reconcileRenewal({ ...target, eventId }, env);
2990
- if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
3039
+ if (!row2 || row2.membershipId !== await networkMembershipId(target) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
2991
3040
  throw new Error("invalid network renewal receipt");
2992
3041
  }
2993
3042
  return row2;
@@ -3612,6 +3661,102 @@ function paymentLineItems(group, tier) {
3612
3661
  return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
3613
3662
  }
3614
3663
 
3664
+ // src/membership-dispute-authority.ts
3665
+ var import_stripe6 = require("@odla-ai/stripe");
3666
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
3667
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
3668
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
3669
+ var TERMINAL = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
3670
+ var text2 = (value) => typeof value === "string" && value ? value : void 0;
3671
+ var refOf = (value) => typeof value === "string" ? text2(value) : text2(value?.id);
3672
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
3673
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
3674
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
3675
+ const charge = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/charges/${chargeId}`);
3676
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
3677
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
3678
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
3679
+ let subscriptionId;
3680
+ if (invoiceId) {
3681
+ const invoice = await (0, import_stripe6.stripeRequest)(config, "GET", `/v1/invoices/${invoiceId}`);
3682
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
3683
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
3684
+ }
3685
+ const seatId = text2(charge.body.metadata?.seatId);
3686
+ return {
3687
+ chargeId,
3688
+ customerId,
3689
+ ...invoiceId ? { invoiceId } : {},
3690
+ ...subscriptionId ? { subscriptionId } : {},
3691
+ ...paymentIntentId ? { paymentIntentId } : {},
3692
+ ...seatId ? { seatId } : {}
3693
+ };
3694
+ }
3695
+ function disputedChargeCoversMembership(charge, member) {
3696
+ 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);
3697
+ }
3698
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
3699
+ membershipClock(now);
3700
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
3701
+ const fact = row2?.fact;
3702
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
3703
+ 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");
3704
+ const id7 = await networkMembershipId(input);
3705
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
3706
+ membershipId: id7,
3707
+ status: String(value.status),
3708
+ termEndsAt: Number(value.termEndsAt),
3709
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
3710
+ autoRenew: value.autoRenew === true,
3711
+ ...text2(value.disputeStatus) ? { disputeStatus: text2(value.disputeStatus) } : {},
3712
+ applied
3713
+ });
3714
+ const known = await membershipLoad(db, "networkMembership", id7);
3715
+ requireMembershipScope(known, input);
3716
+ if (known.disputeEventId === input.eventId) return receipt(known);
3717
+ const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
3718
+ for (let attempt = 0; attempt < 8; attempt++) {
3719
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id7);
3720
+ requireMembershipScope(member, input);
3721
+ if (charge.customerId !== member.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
3722
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
3723
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
3724
+ const sameDispute = member.disputeId === fact.objectId;
3725
+ if (sameDispute && Number(member.disputeClosedAt ?? 0) > 0 && !closed) return receipt(member, false);
3726
+ const won = DISPUTE_WON_STATUSES.includes(status2);
3727
+ const suspended = member.status === MEMBERSHIP_DISPUTED_STATUS, terminal = TERMINAL.includes(String(member.status));
3728
+ const next = terminal ? String(member.status) : won ? suspended ? String(member.disputePriorStatus ?? (member.journeyApprovedAt ? "active" : "paid_pending_approval")) : String(member.status) : MEMBERSHIP_DISPUTED_STATUS;
3729
+ const attrs = {
3730
+ status: next,
3731
+ disputeId: fact.objectId,
3732
+ disputeStatus: status2,
3733
+ disputeEventId: input.eventId,
3734
+ disputeClosedAt: closed ? now : 0,
3735
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
3736
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
3737
+ ...sameDispute ? {} : { disputedAt: now },
3738
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(member.status) } : {},
3739
+ revision: Number(member.revision) + 1,
3740
+ updatedAt: now
3741
+ };
3742
+ try {
3743
+ await db.transact([membershipUpdate("networkMembership", id7, attrs)], { mutationId: `membership-dispute:${id7}:${input.eventId}`, guards: [
3744
+ { ns: "networkMembership", id: id7, exists: true, equals: { revision: member.revision } },
3745
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
3746
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
3747
+ evidenceGuard
3748
+ ] });
3749
+ } catch (error) {
3750
+ if (error.code !== "transact_guard_failed") throw error;
3751
+ continue;
3752
+ }
3753
+ const saved = await membershipLoad(db, "networkMembership", id7);
3754
+ requireMembershipScope(saved, input);
3755
+ return receipt(saved, true);
3756
+ }
3757
+ return membershipFail("membership_dispute_retry_required");
3758
+ }
3759
+
3615
3760
  // src/member-system-contract.ts
3616
3761
  function classifyMemberStripeEvent(identity2, target, hasProvenLegacyLocalLink) {
3617
3762
  const keys3 = ["appId", "environment", "dataEnvironment", "runtime", "chapterId"];
@@ -3736,7 +3881,8 @@ async function firstRow2(db, ns, q) {
3736
3881
  }
3737
3882
  async function reconcileChapterStripeEvent(input, env, ctx) {
3738
3883
  const db = ctx.makeDb(env);
3739
- const { rawBody, eventId, event: event2 } = input;
3884
+ const { rawBody, eventId } = input;
3885
+ let { event: event2 } = input;
3740
3886
  if (event2.kind === "ignored") return json({ ok: true, ignored: event2.type });
3741
3887
  const digest = await stripeEventDigest(rawBody);
3742
3888
  const legacyDigest = await stripePayloadDigest(rawBody);
@@ -3792,6 +3938,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3792
3938
  }
3793
3939
  return json({ ok: true, matched: Boolean(claim) });
3794
3940
  }
3941
+ if (event2.kind === "disputed" && !event2.customerId) {
3942
+ const secret = await getVaultSecret(db, "stripe_secret_key");
3943
+ const charge = secret ? await readDisputedCharge(secret, event2.chargeId).catch(() => null) : null;
3944
+ if (charge) event2 = { ...event2, customerId: charge.customerId };
3945
+ }
3795
3946
  const app = await findPaymentApplication(db, event2);
3796
3947
  if (app && typeof app.stripeRuntime === "string" && app.stripeRuntime !== target.runtime) {
3797
3948
  reportStripePartition("warn", eventId, event2, "ignore_sibling");
@@ -3829,11 +3980,11 @@ async function reconcileChapterStripeEvent(input, env, ctx) {
3829
3980
  }
3830
3981
  let authoritativeEvent = event2;
3831
3982
  let membershipProjection = {};
3832
- if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded"].includes(event2.kind)) {
3983
+ if (ctx.membershipAuthority && ["renewal", "renewal_failed", "subscription_changed", "refunded", "disputed"].includes(event2.kind)) {
3833
3984
  try {
3834
3985
  const receipt = await renewChapterNetworkMembership(db, env, ctx, app, eventId);
3835
3986
  membershipProjection = { membershipStatus: receipt.status, membershipGraceEndsAt: receipt.graceEndsAt, membershipAutoRenew: receipt.autoRenew };
3836
- authoritativeEvent = receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
3987
+ authoritativeEvent = event2.kind === "disputed" ? event2 : receipt.status === "refunded" ? { kind: "refunded" } : { kind: "renewal", renewalAt: ["refunded", "refund_pending", "canceled", "declined", "restarting"].includes(receipt.status) ? 0 : receipt.termEndsAt };
3837
3988
  } catch {
3838
3989
  return json({ error: "network membership renewal reconciliation pending" }, 503);
3839
3990
  }
@@ -4482,10 +4633,10 @@ async function closeRecipientApplication(db, env, ctx, seatId) {
4482
4633
  }
4483
4634
 
4484
4635
  // src/membership-seat-refund-stripe.ts
4485
- var import_stripe7 = require("@odla-ai/stripe");
4636
+ var import_stripe8 = require("@odla-ai/stripe");
4486
4637
 
4487
4638
  // src/membership-checkout-seat-refund.ts
4488
- var import_stripe6 = require("@odla-ai/stripe");
4639
+ var import_stripe7 = require("@odla-ai/stripe");
4489
4640
  var fail4 = (code = "named_seat_refund_mismatch") => {
4490
4641
  throw Object.assign(new Error(code), { code });
4491
4642
  };
@@ -4494,7 +4645,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
4494
4645
  const invoiceRef = seat.combinedInvoice;
4495
4646
  if (!invoiceRef || !/^in_[A-Za-z0-9_]+$/.test(invoiceRef.invoiceId) || !Number.isSafeInteger(invoiceRef.totalCents) || invoiceRef.totalCents <= seat.amountCents || seat.amountCents <= 0 || seat.status !== "refund_pending") fail4();
4496
4647
  const call = async (method, path, params, key3) => {
4497
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4648
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
4498
4649
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
4499
4650
  return result.body;
4500
4651
  };
@@ -4567,7 +4718,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
4567
4718
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
4568
4719
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
4569
4720
  const call = async (method, path, params, idempotencyKey) => {
4570
- const result = await (0, import_stripe7.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4721
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
4571
4722
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
4572
4723
  return result.body;
4573
4724
  };
@@ -5035,15 +5186,224 @@ var handleAdminMembershipSeats = async (req, url, env, ctx) => {
5035
5186
  }
5036
5187
  };
5037
5188
 
5189
+ // src/membership-dispute-recharge.ts
5190
+ var import_stripe9 = require("@odla-ai/stripe");
5191
+ var fail6 = (code, extra = {}) => {
5192
+ throw Object.assign(new Error(code), { code, ...extra });
5193
+ };
5194
+ var refOf2 = (value) => typeof value === "string" && value ? value : typeof value?.id === "string" ? String(value.id) : void 0;
5195
+ async function defaultPaymentMethod(config, subscriptionId, customerId) {
5196
+ const subscription = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
5197
+ if (!subscription.ok) fail6("membership_dispute_provider_unavailable");
5198
+ const onSubscription = refOf2(subscription.body.default_payment_method);
5199
+ if (onSubscription) return onSubscription;
5200
+ const customer = await (0, import_stripe9.stripeRequest)(config, "GET", `/v1/customers/${encodeURIComponent(customerId)}`);
5201
+ if (!customer.ok) fail6("membership_dispute_provider_unavailable");
5202
+ return refOf2(customer.body.invoice_settings?.default_payment_method);
5203
+ }
5204
+ async function rechargeDisputedMembership(db, membershipId, secretKey, now) {
5205
+ membershipClock(now);
5206
+ let member = await membershipLoad(db, "networkMembership", membershipId);
5207
+ if (!member) fail6("membership_not_found");
5208
+ const scope2 = { chapterId: String(member.chapterId), environment: String(member.environment) };
5209
+ const amountCents = Number(member.disputeAmountCents ?? member.amountCents), currency = String(member.currency);
5210
+ const done = (value, replayed) => ({
5211
+ membershipId,
5212
+ status: String(value.status),
5213
+ paymentIntentId: String(value.rechargePaymentIntentId),
5214
+ amountCents,
5215
+ currency,
5216
+ ...replayed ? { replayed } : {}
5217
+ });
5218
+ if (member.status !== MEMBERSHIP_DISPUTED_STATUS) {
5219
+ if (member.disputeStatus === "recharged" && member.rechargePaymentIntentId) return done(member, true);
5220
+ fail6("membership_not_disputed");
5221
+ }
5222
+ if (member.disputeStatus !== "lost") fail6("membership_dispute_open");
5223
+ if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !/^[a-z]{3}$/.test(currency)) fail6("membership_dispute_amount_invalid");
5224
+ let attempt = member.rechargeAttempt;
5225
+ if (!attempt || attempt.finishedAt !== void 0) {
5226
+ attempt = { key: `membership-recharge:${membershipId}:${String(member.disputeId)}:${now}`, startedAt: now };
5227
+ await db.transact(
5228
+ [membershipUpdate("networkMembership", membershipId, { rechargeAttempt: attempt, revision: Number(member.revision) + 1, updatedAt: now })],
5229
+ { mutationId: `membership-recharge-attempt:${attempt.key}`, guards: [{
5230
+ ns: "networkMembership",
5231
+ id: membershipId,
5232
+ exists: true,
5233
+ equals: { revision: member.revision, status: MEMBERSHIP_DISPUTED_STATUS }
5234
+ }] }
5235
+ );
5236
+ member = await membershipLoad(db, "networkMembership", membershipId);
5237
+ if (!member || member.status !== MEMBERSHIP_DISPUTED_STATUS) fail6("membership_dispute_changed");
5238
+ }
5239
+ const config = { secretKey };
5240
+ const paymentMethod = await defaultPaymentMethod(config, String(member.providerSubscriptionId), String(member.providerCustomerId));
5241
+ if (!paymentMethod) fail6("recharge_payment_method_unavailable");
5242
+ const intent = await (0, import_stripe9.stripeRequest)(config, "POST", "/v1/payment_intents", {
5243
+ amount: amountCents,
5244
+ currency,
5245
+ customer: String(member.providerCustomerId),
5246
+ payment_method: paymentMethod,
5247
+ off_session: "true",
5248
+ confirm: "true",
5249
+ description: `Membership payment reissued after dispute ${String(member.disputeId)}`,
5250
+ metadata: {
5251
+ ...stripePartitionMetadata({ appId: String(member.appId), chapterId: scope2.chapterId, environment: scope2.environment, runtime: String(member.runtime) }),
5252
+ applicationId: String(member.applicationId),
5253
+ membershipId,
5254
+ disputeId: String(member.disputeId),
5255
+ membershipRecharge: "true"
5256
+ }
5257
+ }, { idempotencyKey: attempt.key });
5258
+ const body = intent.body, error = body.error ?? {}, paymentStatus = String(body.status ?? "");
5259
+ const finish = async (outcome) => {
5260
+ const current = await membershipLoad(db, "networkMembership", membershipId);
5261
+ if (!current) return;
5262
+ await db.transact([membershipUpdate("networkMembership", membershipId, {
5263
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome },
5264
+ revision: Number(current.revision) + 1,
5265
+ updatedAt: now
5266
+ })], { mutationId: `membership-recharge-finish:${attempt.key}:${outcome}` });
5267
+ };
5268
+ if (!intent.ok) {
5269
+ if (intent.status === 402 || error.type === "card_error" || typeof error.decline_code === "string") {
5270
+ await finish("declined");
5271
+ fail6("recharge_declined", { declineCode: typeof error.decline_code === "string" ? error.decline_code : String(error.code ?? "card_declined") });
5272
+ }
5273
+ fail6("membership_dispute_provider_unavailable");
5274
+ }
5275
+ if (["requires_payment_method", "canceled"].includes(paymentStatus)) {
5276
+ await finish("declined");
5277
+ fail6("recharge_declined", { declineCode: String(body.last_payment_error?.decline_code ?? paymentStatus) });
5278
+ }
5279
+ if (paymentStatus !== "succeeded") fail6("recharge_pending", { paymentStatus });
5280
+ if (refOf2(body.customer) !== member.providerCustomerId || body.amount !== amountCents) fail6("recharge_mismatch");
5281
+ for (let retry = 0; retry < 8; retry++) {
5282
+ const state = await loadMembershipAuthorityState(db, scope2), current = await membershipLoad(db, "networkMembership", membershipId);
5283
+ if (!current) fail6("membership_not_found");
5284
+ if (current.rechargePaymentIntentId === body.id && current.status !== MEMBERSHIP_DISPUTED_STATUS) return done(current, true);
5285
+ const status2 = String(current.disputePriorStatus ?? (current.journeyApprovedAt ? "active" : "paid_pending_approval"));
5286
+ try {
5287
+ await db.transact(
5288
+ [membershipUpdate("networkMembership", membershipId, {
5289
+ status: status2,
5290
+ disputeStatus: "recharged",
5291
+ rechargePaymentIntentId: String(body.id),
5292
+ rechargedAt: now,
5293
+ rechargeAttempt: { ...attempt, finishedAt: now, outcome: "succeeded" },
5294
+ revision: Number(current.revision) + 1,
5295
+ updatedAt: now
5296
+ })],
5297
+ { mutationId: `membership-recharged:${attempt.key}`, guards: [
5298
+ { ns: "networkMembership", id: membershipId, exists: true, equals: { revision: current.revision, status: MEMBERSHIP_DISPUTED_STATUS } },
5299
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
5300
+ ] }
5301
+ );
5302
+ } catch (cause) {
5303
+ if (cause.code !== "transact_guard_failed") throw cause;
5304
+ continue;
5305
+ }
5306
+ return done(await membershipLoad(db, "networkMembership", membershipId));
5307
+ }
5308
+ return fail6("membership_dispute_retry_required");
5309
+ }
5310
+
5311
+ // src/worker-routes-admin-membership-disputes.ts
5312
+ var LIST2 = "/api/admin/membership-disputes";
5313
+ var ONE2 = /^\/api\/admin\/membership-disputes\/([A-Za-z0-9][A-Za-z0-9_.:-]{2,199})\/recharge$/;
5314
+ var PUBLIC2 = [
5315
+ "id",
5316
+ "appId",
5317
+ "chapterId",
5318
+ "environment",
5319
+ "runtime",
5320
+ "applicationId",
5321
+ "skuId",
5322
+ "status",
5323
+ "memberNumber",
5324
+ "amountCents",
5325
+ "currency",
5326
+ "disputeId",
5327
+ "disputeStatus",
5328
+ "disputeReason",
5329
+ "disputeAmountCents",
5330
+ "disputedAt",
5331
+ "disputeClosedAt",
5332
+ "disputePriorStatus",
5333
+ "rechargePaymentIntentId",
5334
+ "rechargedAt",
5335
+ "termEndsAt",
5336
+ "updatedAt"
5337
+ ];
5338
+ var project2 = (row2) => ({
5339
+ ...Object.fromEntries(PUBLIC2.filter((key3) => row2[key3] !== void 0).map((key3) => [key3, row2[key3]])),
5340
+ ...row2.rechargeAttempt && typeof row2.rechargeAttempt === "object" ? { rechargeAttempt: {
5341
+ startedAt: row2.rechargeAttempt.startedAt ?? null,
5342
+ finishedAt: row2.rechargeAttempt.finishedAt ?? null,
5343
+ outcome: row2.rechargeAttempt.outcome ?? null
5344
+ } } : {}
5345
+ });
5346
+ var FINAL = /* @__PURE__ */ new Set([
5347
+ "membership_not_found",
5348
+ "membership_not_disputed",
5349
+ "membership_dispute_open",
5350
+ "membership_dispute_amount_invalid",
5351
+ "recharge_payment_method_unavailable",
5352
+ "recharge_declined",
5353
+ "recharge_pending",
5354
+ "recharge_mismatch",
5355
+ "membership_dispute_changed"
5356
+ ]);
5357
+ var handleAdminMembershipDisputes = async (req, url, env, ctx) => {
5358
+ const one = url.pathname.match(ONE2);
5359
+ if (url.pathname !== LIST2 && !one) return null;
5360
+ const operator = ctx.membershipAuthorityOperator;
5361
+ if (!operator) return json({ error: "not found" }, 404);
5362
+ if (req.method !== (one ? "POST" : "GET")) return json({ error: "method not allowed" }, 405);
5363
+ const raw2 = ctx.makeDb(env), user = await ctx.verifyUser(req, env);
5364
+ if (!user) return json({ error: "unauthorized" }, 401);
5365
+ if (!await ctx.isSuperAdminEmail(raw2, user.email, user.userId)) return json({ error: "super-admin required", code: "super_admin_required" }, 403);
5366
+ let db;
5367
+ try {
5368
+ db = operator.makeDb(env);
5369
+ } catch {
5370
+ return json({ error: "the membership authority is not hosted by this deployment" }, 404);
5371
+ }
5372
+ if (!one) {
5373
+ const rows5 = (await db.query({ networkMembership: { $: { where: { environment: env.ODLA_ENV }, order: { updatedAt: "desc" }, limit: 500 } } })).networkMembership ?? [];
5374
+ const disputes = rows5.filter((row2) => typeof row2.disputeId === "string").sort((a, b) => Number(b.disputedAt ?? 0) - Number(a.disputedAt ?? 0));
5375
+ return json({ disputes: disputes.map(project2), truncated: rows5.length >= 500 });
5376
+ }
5377
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
5378
+ if (!secret) return json({ error: "network payment provider unavailable" }, 503);
5379
+ try {
5380
+ const receipt = await rechargeDisputedMembership(db, one[1], secret, Date.now());
5381
+ return json({ ok: true, ...receipt });
5382
+ } catch (error) {
5383
+ const failure = error;
5384
+ const code = typeof failure.code === "string" ? failure.code : void 0;
5385
+ if (code === "membership_not_found") return json({ error: "not found", code }, 404);
5386
+ if (code && FINAL.has(code)) {
5387
+ return json({
5388
+ error: "the charge could not be re-issued",
5389
+ code,
5390
+ ...failure.declineCode ? { declineCode: failure.declineCode } : {},
5391
+ ...failure.paymentStatus ? { paymentStatus: failure.paymentStatus } : {}
5392
+ }, 409);
5393
+ }
5394
+ return json({ error: "re-charge is pending; retry to confirm", ...code ? { code } : {} }, 503);
5395
+ }
5396
+ };
5397
+
5038
5398
  // src/membership-policy-versions.ts
5039
5399
  var LIMIT = 200;
5040
- var text2 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
5400
+ var text3 = (value) => typeof value === "string" && value.length > 0 && value.length <= 200 ? value : void 0;
5041
5401
  var instant = (value) => {
5042
5402
  const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
5043
5403
  return Number.isSafeInteger(at2) && at2 >= 0 ? at2 : void 0;
5044
5404
  };
5045
5405
  function provenance(row2) {
5046
- const source = text2(row2.source), actor = text2(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
5406
+ const source = text3(row2.source), actor = text3(row2.actor ?? row2.$createdBy), recordedAt = instant(row2.recordedAt ?? row2.createdAt ?? row2.$createdAt);
5047
5407
  return { ...source ? { source } : {}, ...actor ? { actor } : {}, ...recordedAt !== void 0 ? { recordedAt } : {} };
5048
5408
  }
5049
5409
  var live = (w, now) => w.from <= now && (w.until === void 0 || now < w.until);
@@ -5137,16 +5497,16 @@ var handleAdminMembershipPolicyVersions = async (req, url, env, ctx) => {
5137
5497
  };
5138
5498
 
5139
5499
  // src/membership-discount-reterm.ts
5140
- var import_stripe9 = require("@odla-ai/stripe");
5500
+ var import_stripe11 = require("@odla-ai/stripe");
5141
5501
 
5142
5502
  // src/subscription-provider-contract.ts
5143
- var import_stripe8 = require("@odla-ai/stripe");
5503
+ var import_stripe10 = require("@odla-ai/stripe");
5144
5504
  function subscriptionContractMismatch() {
5145
5505
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5146
5506
  }
5147
5507
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
5148
5508
  if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
5149
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5509
+ const result = await (0, import_stripe10.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5150
5510
  const price = result.body;
5151
5511
  if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
5152
5512
  return typeof price.product === "string" ? price.product : void 0;
@@ -5184,7 +5544,7 @@ async function retermPolicy(db, scope2, id7) {
5184
5544
  }
5185
5545
  async function attachProviderDiscount(sk, scope2, member, policy2, couponId, productId) {
5186
5546
  const subId = String(member.providerSubscriptionId), path = `/v1/subscriptions/${encodeURIComponent(subId)}`;
5187
- const read2 = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5547
+ const read2 = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "GET", path, { "expand[0]": "discounts" });
5188
5548
  const sub = read2.body, items = sub?.items?.data;
5189
5549
  const metadata = { ...stripePartitionMetadata(scope2), applicationId: String(member.applicationId) };
5190
5550
  if (!read2.ok || sub.id !== subId || sub.customer !== member.providerCustomerId || sub.livemode !== (scope2.environment === "prod") || !PROVIDER_LIVE.has(sub.status) || !Object.entries(metadata).every(([key3, value]) => sub.metadata?.[key3] === value) || sub.items?.has_more !== false || !Array.isArray(items) || items.length < 1 || items.length > 2 || items.filter((item) => item.price?.id === member.providerPriceId && item.quantity === 1 && item.price?.product === productId).length !== 1) {
@@ -5195,7 +5555,7 @@ async function attachProviderDiscount(sk, scope2, member, policy2, couponId, pro
5195
5555
  const present = discounts.map(couponOf);
5196
5556
  if (present.length === 1 && present[0] === couponId) return false;
5197
5557
  if (present.length > 0) throw Object.assign(new Error("provider subscription carries an unrelated discount"), { code: "provider_mismatch" });
5198
- const written = await (0, import_stripe9.stripeRequest)({ secretKey: sk }, "POST", path, {
5558
+ const written = await (0, import_stripe11.stripeRequest)({ secretKey: sk }, "POST", path, {
5199
5559
  "discounts[0][coupon]": couponId,
5200
5560
  "expand[0]": "discounts",
5201
5561
  "metadata[discountPolicyVersionId]": policy2.id
@@ -6044,8 +6404,8 @@ var APPLICATION_LABELS = {
6044
6404
  state: "State"
6045
6405
  };
6046
6406
  function humanize(id7) {
6047
- const text8 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6048
- return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id7;
6407
+ const text9 = id7.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
6408
+ return text9 ? `${text9[0].toUpperCase()}${text9.slice(1)}` : id7;
6049
6409
  }
6050
6410
  function applicationDefs(chapter) {
6051
6411
  try {
@@ -6112,7 +6472,7 @@ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
6112
6472
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
6113
6473
  var CURRENCY = /^[a-z]{3}$/;
6114
6474
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
6115
- var text3 = (value, path, errors, max) => {
6475
+ var text4 = (value, path, errors, max) => {
6116
6476
  if (typeof value !== "string" || !value.trim()) {
6117
6477
  errors.push(`${path} is required`);
6118
6478
  return "";
@@ -6140,23 +6500,23 @@ function validateGiftOfferCatalog(input, tierIds) {
6140
6500
  }
6141
6501
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
6142
6502
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
6143
- const id7 = text3(item.id, `${path}.id`, errors, 80);
6503
+ const id7 = text4(item.id, `${path}.id`, errors, 80);
6144
6504
  if (id7 && !ID3.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
6145
6505
  if (ids2.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
6146
6506
  ids2.add(id7);
6147
6507
  const targetRaw = record(item.target);
6148
6508
  let target = { type: "entitlement", entitlementKey: "invalid" };
6149
6509
  if (targetRaw?.type === "tier") {
6150
- const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6510
+ const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
6151
6511
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
6152
6512
  target = { type: "tier", tierId };
6153
6513
  } else if (targetRaw?.type === "entitlement") {
6154
- const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6514
+ const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
6155
6515
  if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
6156
6516
  target = { type: "entitlement", entitlementKey };
6157
6517
  } else errors.push(`${path}.target.type must be tier or entitlement`);
6158
6518
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
6159
- const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6519
+ const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
6160
6520
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
6161
6521
  if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
6162
6522
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -6178,9 +6538,9 @@ function validateGiftOfferCatalog(input, tierIds) {
6178
6538
  redemptionLifetimeHours: lifetime,
6179
6539
  ...activeFrom !== void 0 ? { activeFrom } : {},
6180
6540
  ...activeUntil !== void 0 ? { activeUntil } : {},
6181
- title: text3(item.title, `${path}.title`, errors, 160),
6182
- description: text3(item.description, `${path}.description`, errors, 2e3),
6183
- ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6541
+ title: text4(item.title, `${path}.title`, errors, 160),
6542
+ description: text4(item.description, `${path}.description`, errors, 2e3),
6543
+ ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
6184
6544
  });
6185
6545
  }
6186
6546
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -6407,23 +6767,23 @@ async function createPaymentQuote(input) {
6407
6767
  // src/membership-quote-projection.ts
6408
6768
  var MEMBERSHIP_QUOTE_NAMESPACE = "membershipQuoteProjections";
6409
6769
  var keys2 = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
6410
- var fail6 = (code) => {
6770
+ var fail7 = (code) => {
6411
6771
  throw Object.assign(new Error(code), { code });
6412
6772
  };
6413
6773
  var load3 = async (db, ns, id7) => (await db.query({ [ns]: { $: { where: { id: id7 }, limit: 1 } } }))[ns]?.[0];
6414
6774
  async function restore2(row2, target, now, allowExpired = false) {
6415
- if (!row2 || keys2.some((key3) => row2[key3] !== target[key3]) || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1 || typeof row2.requestId !== "string") return fail6("membership_quote_unavailable");
6416
- if (row2.status !== "ready") return fail6("membership_quote_pending");
6775
+ 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");
6776
+ if (row2.status !== "ready") return fail7("membership_quote_pending");
6417
6777
  const offer = await verifyIssuedMembershipOffer(row2.offer, { ...target, offerId: await membershipOfferIdForRequest({
6418
6778
  ...target,
6419
6779
  requestId: row2.requestId
6420
6780
  }), now, lifetimeMs: 6e4 });
6421
- if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail6("membership_quote_expired");
6781
+ if (offer.issuedAt > now + 3e4 || !allowExpired && offer.expiresAt <= now) return fail7("membership_quote_expired");
6422
6782
  const tier = row2.tier, quote = row2.quote;
6423
6783
  const offering = offer.contract.offering;
6424
- if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail6("membership_quote_conflict");
6784
+ if (quote?.seatOffer && quote.seatOffer.amountCents !== offering.seatPriceCents) return fail7("membership_quote_conflict");
6425
6785
  if (!tier || tier.id !== target.skuId || tier.groupId !== target.chapterId || !tier.stripePriceId || tier.priceCents !== offering.basePriceCents || tier.currency !== offering.currency || tier.interval !== offering.interval || !quote) {
6426
- return fail6("membership_catalog_mismatch");
6786
+ return fail7("membership_catalog_mismatch");
6427
6787
  }
6428
6788
  const expected = await createPaymentQuote({
6429
6789
  ...target,
@@ -6436,7 +6796,7 @@ async function restore2(row2, target, now, allowExpired = false) {
6436
6796
  seatOffer: quote.seatOffer,
6437
6797
  namedSeat: quote.namedSeat
6438
6798
  });
6439
- if (quote.digest !== expected.digest) return fail6("membership_quote_conflict");
6799
+ if (quote.digest !== expected.digest) return fail7("membership_quote_conflict");
6440
6800
  return { tier, quote: expected, billingTerms: offer.contract.billingTerms, membershipOffer: offer, projectionRevision: Number(row2.revision) };
6441
6801
  }
6442
6802
  async function readMembershipQuote(db, target, now) {
@@ -6444,9 +6804,9 @@ async function readMembershipQuote(db, target, now) {
6444
6804
  }
6445
6805
  async function prepareMembershipQuote(db, input, issue) {
6446
6806
  const { applicationId: id7, now, app } = input;
6447
- if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail6("membership_quote_unavailable");
6807
+ if (app.id !== id7 || app.groupId !== input.chapterId || app.tierId !== input.skuId || app.stripeRuntime !== input.runtime || app.status !== "submitted" || app.canceled === true || app.giftOfferId || app.giftClaimId || app.namedSeatId || app.namedSeatClaimPending) return fail7("membership_quote_unavailable");
6448
6808
  let row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7);
6449
- if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail6("membership_quote_conflict");
6809
+ if (row2 && keys2.some((key3) => row2[key3] !== input[key3])) return fail7("membership_quote_conflict");
6450
6810
  if (row2?.status === "ready") {
6451
6811
  try {
6452
6812
  return await restore2(row2, input, now);
@@ -6491,7 +6851,7 @@ async function prepareMembershipQuote(db, input, issue) {
6491
6851
  }
6492
6852
  if (row2?.status === "ready") return restore2(row2, input, now);
6493
6853
  if (!row2 || row2.status !== "pending" || keys2.some((key3) => row2[key3] !== input[key3]) || typeof row2.requestId !== "string") {
6494
- return fail6("membership_quote_pending");
6854
+ return fail7("membership_quote_pending");
6495
6855
  }
6496
6856
  const target = Object.fromEntries(keys2.map((key3) => [key3, input[key3]]));
6497
6857
  const offer = await verifyIssuedMembershipOffer(await issue({ ...target, requestId: row2.requestId }), {
@@ -6540,7 +6900,7 @@ async function prepareMembershipQuote(db, input, issue) {
6540
6900
  return restore2(await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, id7), target, now);
6541
6901
  }
6542
6902
  async function selectCheckoutNamedSeat(db, target, input) {
6543
- if (!input.enabled) return fail6("named_seat_purchase_unavailable");
6903
+ if (!input.enabled) return fail7("named_seat_purchase_unavailable");
6544
6904
  const row2 = await load3(db, MEMBERSHIP_QUOTE_NAMESPACE, target.applicationId);
6545
6905
  const current = await restore2(row2, target, input.now);
6546
6906
  const namedSeat = input.selection === null ? void 0 : checkoutNamedSeatSelection(input.selection, input.purchaserEmail);
@@ -6596,17 +6956,17 @@ async function readBoundedRequestText(req, maxBytes) {
6596
6956
  if (!reader) return "";
6597
6957
  const decoder = new TextDecoder();
6598
6958
  let bytes = 0;
6599
- let text8 = "";
6959
+ let text9 = "";
6600
6960
  for (; ; ) {
6601
6961
  const { done, value } = await reader.read();
6602
6962
  if (req.signal.aborted) return invalid2();
6603
6963
  if (done) {
6604
6964
  complete = true;
6605
- return text8 + decoder.decode();
6965
+ return text9 + decoder.decode();
6606
6966
  }
6607
6967
  bytes += value.byteLength;
6608
6968
  if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
6609
- text8 += decoder.decode(value, { stream: true });
6969
+ text9 += decoder.decode(value, { stream: true });
6610
6970
  }
6611
6971
  } catch {
6612
6972
  return invalid2();
@@ -6710,9 +7070,9 @@ async function paymentQuoteRoute(url, env, ctx, prepare = false, request) {
6710
7070
  let selection;
6711
7071
  if (request?.body) {
6712
7072
  try {
6713
- const text8 = await readBoundedRequestText(request, 2048);
6714
- if (text8 instanceof Response) return text8;
6715
- const body = JSON.parse(text8);
7073
+ const text9 = await readBoundedRequestText(request, 2048);
7074
+ if (text9 instanceof Response) return text9;
7075
+ const body = JSON.parse(text9);
6716
7076
  if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key3) => key3 !== "namedSeat")) throw new Error("invalid body");
6717
7077
  selection = body.namedSeat;
6718
7078
  } catch {
@@ -7367,10 +7727,13 @@ var handleMember = async (req, url, env, ctx) => {
7367
7727
  const db = ctx.makeDb(env);
7368
7728
  const groupId = url.searchParams.get("group") ?? chapter.id;
7369
7729
  if (groupId !== chapter.id) return json({ error: "not found" }, 404);
7370
- const { groups } = await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } });
7730
+ const [{ groups }, stripeKey, activeControl] = await Promise.all([
7731
+ db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } }),
7732
+ getVaultSecret(db, "stripe_secret_key"),
7733
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : Promise.resolve(null)
7734
+ ]);
7371
7735
  const group = Array.isArray(groups) ? groups[0] : void 0;
7372
7736
  if (!group) return json({ error: "not found" }, 404);
7373
- const stripeKey = await getVaultSecret(db, "stripe_secret_key");
7374
7737
  const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
7375
7738
  const offered = await loadOfferableTiers(
7376
7739
  db,
@@ -7379,7 +7742,6 @@ var handleMember = async (req, url, env, ctx) => {
7379
7742
  );
7380
7743
  const seats = /* @__PURE__ */ new Map();
7381
7744
  const founding = await foundingPreviews(offered, env, ctx, seats);
7382
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7383
7745
  const application = controlledApplication(chapter.application, activeControl?.document);
7384
7746
  return json({
7385
7747
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered, { currency: typeof group.currency === "string" ? group.currency : void 0, interval: group.interval === "month" ? "month" : "year" }, founding, seats), application.conditions),
@@ -7417,11 +7779,14 @@ var handleMember = async (req, url, env, ctx) => {
7417
7779
  parsed.tierId = named.invitation.skuId;
7418
7780
  submissionId = `named-seat:${named.invitation.seatId}:${named.userId}`;
7419
7781
  }
7420
- const eligibility = named ? null : await resolveSignupEligibility(db, chapter, parsed, submissionId);
7782
+ const [eligibility, activeControl, offeredTierIds, group] = await Promise.all([
7783
+ named ? null : resolveSignupEligibility(db, chapter, parsed, submissionId),
7784
+ chapter.signupControl ? loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null,
7785
+ typeof parsed.tierId === "string" && parsed.tierId ? loadTiers(db, { id: chapter.id }).then((tiers) => tiers.map((tier) => tier.id)) : void 0,
7786
+ db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } }).then(({ groups }) => Array.isArray(groups) ? groups[0] : void 0)
7787
+ ]);
7421
7788
  if (eligibility?.conflict) return eligibility.conflict;
7422
- const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
7423
7789
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
7424
- const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
7425
7790
  const now = Date.now();
7426
7791
  const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
7427
7792
  const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
@@ -7460,18 +7825,16 @@ var handleMember = async (req, url, env, ctx) => {
7460
7825
  if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical3.status);
7461
7826
  return json(provisioningFailure.body, provisioningFailure.status);
7462
7827
  }
7463
- if (predecessor) await syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0);
7464
- const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
7465
- const group = Array.isArray(groups) ? groups[0] : void 0;
7828
+ if (predecessor) await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: canonical3, stage: String(canonical3.status) }).catch(() => void 0));
7466
7829
  const notice = group ? { group, application: canonical3, origin: url.origin } : null;
7467
- if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await notifyAdminOfApplication(db, env, notice);
7830
+ if (notice && !result.duplicate && effectiveChapter.sends.adminNotification === "submit") await deferWork(ctx, req, () => notifyAdminOfApplication(db, env, notice));
7468
7831
  const state = notice ? await continueJoin(db, ctx, env, url.origin, canonical3, notice.group) : null;
7469
7832
  if (!state) return json({
7470
7833
  error: "Application saved; its next step is temporarily unavailable. Retry this submission.",
7471
7834
  code: "application_continuation_unavailable",
7472
7835
  id: result.id
7473
7836
  }, 503);
7474
- if (notice && !result.duplicate) await notifySubmitConfirmed(db, env, { ...notice, state });
7837
+ if (notice && !result.duplicate) await deferWork(ctx, req, () => notifySubmitConfirmed(db, env, { ...notice, state }));
7475
7838
  const final = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications?.[0];
7476
7839
  if (!final || final.groupId !== chapter.id || state === "accepted" && final.status !== "approved") {
7477
7840
  return json({ error: "application changed; reload its current state" }, 409);
@@ -7591,10 +7954,10 @@ function boundedString(value, path, errors, max, required = false) {
7591
7954
  errors.push(`${path} must be a string`);
7592
7955
  return void 0;
7593
7956
  }
7594
- const text8 = value.trim();
7595
- if (required && !text8) errors.push(`${path} is required`);
7596
- if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7597
- return text8;
7957
+ const text9 = value.trim();
7958
+ if (required && !text9) errors.push(`${path} is required`);
7959
+ if (text9.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
7960
+ return text9;
7598
7961
  }
7599
7962
  function validateMembershipPageDocument(input, options = {}) {
7600
7963
  const errors = [];
@@ -7643,8 +8006,8 @@ function validateMembershipPageDocument(input, options = {}) {
7643
8006
  else {
7644
8007
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
7645
8008
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
7646
- const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
7647
- return text8 ? [text8] : [];
8009
+ const text9 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
8010
+ return text9 ? [text9] : [];
7648
8011
  });
7649
8012
  }
7650
8013
  }
@@ -7738,7 +8101,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
7738
8101
  function unknownKeys(value, allowed2, path, errors) {
7739
8102
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7740
8103
  }
7741
- function text4(value, path, errors, max = 65536) {
8104
+ function text5(value, path, errors, max = 65536) {
7742
8105
  if (typeof value !== "string" || !value.trim()) {
7743
8106
  errors.push(`${path} is required`);
7744
8107
  return "";
@@ -7761,11 +8124,11 @@ function tiersOf(value, errors) {
7761
8124
  return [];
7762
8125
  }
7763
8126
  unknownKeys(item, TIER_KEYS2, path, errors);
7764
- const id7 = text4(item.id, `${path}.id`, errors, 80);
8127
+ const id7 = text5(item.id, `${path}.id`, errors, 80);
7765
8128
  if (id7 && !TIER.test(id7)) errors.push(`${path}.id must be a lowercase kebab-case id`);
7766
8129
  if (seen.has(id7)) errors.push(`${path}.id duplicates "${id7}"`);
7767
8130
  seen.add(id7);
7768
- const name = text4(item.name, `${path}.name`, errors, 160);
8131
+ const name = text5(item.name, `${path}.name`, errors, 160);
7769
8132
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
7770
8133
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
7771
8134
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -7783,7 +8146,7 @@ function tiersOf(value, errors) {
7783
8146
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
7784
8147
  errors.push(`${path}.interval must be year or month`);
7785
8148
  }
7786
- const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
8149
+ const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
7787
8150
  return [{
7788
8151
  id: id7,
7789
8152
  name,
@@ -7841,7 +8204,7 @@ function policyOf(value, errors, tiers) {
7841
8204
  const result = {};
7842
8205
  for (const key3 of allowed2) {
7843
8206
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
7844
- result[key3] = text4(input[key3], `document.policy.${key3}`, errors);
8207
+ result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
7845
8208
  }
7846
8209
  const tierPolicies = {};
7847
8210
  if (input.tierPolicies !== void 0) {
@@ -7856,8 +8219,8 @@ function policyOf(value, errors, tiers) {
7856
8219
  }
7857
8220
  unknownKeys(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
7858
8221
  tierPolicies[id7] = {
7859
- refundPolicyText: text4(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
7860
- ...policy2.trustCopy !== void 0 ? { trustCopy: text4(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
8222
+ refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
8223
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
7861
8224
  };
7862
8225
  }
7863
8226
  }
@@ -7897,9 +8260,9 @@ function validateSignupControlDocument(input) {
7897
8260
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
7898
8261
  const source = recordOf(root.source);
7899
8262
  const target = recordOf(root.target);
7900
- const appId = text4(source?.appId, "document.source.appId", errors, 63);
7901
- const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
7902
- const environment = text4(target?.environment, "document.target.environment", errors, 63);
8263
+ const appId = text5(source?.appId, "document.source.appId", errors, 63);
8264
+ const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
8265
+ const environment = text5(target?.environment, "document.target.environment", errors, 63);
7903
8266
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
7904
8267
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
7905
8268
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9411,8 +9774,8 @@ async function bookSlot(req, env, ctx) {
9411
9774
  const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
9412
9775
  await db.transact([meetingOp, appOp]);
9413
9776
  const booked = { ...app, ...bookingAttrs };
9414
- await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
9415
- await notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } });
9777
+ await deferWork(ctx, req, () => syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0));
9778
+ await deferWork(ctx, req, () => notifyBookingConfirmed(db, env, { group, application: app, meeting: { startAt, endAt, timezone: cfg.timezone, meetUrl, htmlLink } }));
9416
9779
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
9417
9780
  }
9418
9781
  var handleSchedule = async (req, url, env, ctx) => {
@@ -9436,28 +9799,28 @@ var handleSchedule = async (req, url, env, ctx) => {
9436
9799
  };
9437
9800
 
9438
9801
  // src/payments-stripe.ts
9439
- var import_stripe12 = require("@odla-ai/stripe");
9802
+ var import_stripe14 = require("@odla-ai/stripe");
9440
9803
 
9441
9804
  // src/payments-refund.ts
9442
- var import_stripe10 = require("@odla-ai/stripe");
9805
+ var import_stripe12 = require("@odla-ai/stripe");
9443
9806
  var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9444
9807
  var id3 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
9445
9808
  var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
9446
- function fail7(code = "refund_target_mismatch") {
9809
+ function fail8(code = "refund_target_mismatch") {
9447
9810
  throw Object.assign(new Error(code), { code });
9448
9811
  }
9449
9812
  async function refundSubscriptionPayment(secretKey, input) {
9450
- if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail7();
9813
+ if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail8();
9451
9814
  const call = async (method, path, params, key3) => {
9452
- const result = await (0, import_stripe10.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9453
- if (!result.ok) fail7("stripe_error");
9454
- if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail7("stripe_invalid_response");
9815
+ const result = await (0, import_stripe12.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
9816
+ if (!result.ok) fail8("stripe_error");
9817
+ if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail8("stripe_invalid_response");
9455
9818
  return result.body;
9456
9819
  };
9457
9820
  const sub = await call("GET", `/v1/subscriptions/${input.subscriptionId}`);
9458
- if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail7();
9821
+ if (id3(sub) !== input.subscriptionId || id3(sub.customer) !== input.customerId) fail8();
9459
9822
  for (const [key3, value] of Object.entries(input.metadata ?? {})) {
9460
- if (row(sub.metadata)[key3] !== value) fail7();
9823
+ if (row(sub.metadata)[key3] !== value) fail8();
9461
9824
  }
9462
9825
  let cursor;
9463
9826
  let earliest;
@@ -9470,66 +9833,66 @@ async function refundSubscriptionPayment(secretKey, input) {
9470
9833
  limit: 100,
9471
9834
  ...cursor ? { starting_after: cursor } : {}
9472
9835
  });
9473
- if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail7("stripe_invalid_response");
9836
+ if (!Array.isArray(list.data) || typeof list.has_more !== "boolean") fail8("stripe_invalid_response");
9474
9837
  for (const value of list.data) {
9475
9838
  const invoice2 = row(value);
9476
9839
  const subscription = invoice2.subscription ?? row(row(invoice2.parent).subscription_details).subscription;
9477
- if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail7();
9840
+ if (!providerId2(id3(invoice2), "in") || seen.has(id3(invoice2)) || id3(subscription) !== input.subscriptionId || id3(invoice2.customer) !== input.customerId || invoice2.status !== "paid" || !Number.isSafeInteger(invoice2.created) || !Number.isSafeInteger(invoice2.amount_paid) || Number(invoice2.amount_paid) < 0) fail8();
9478
9841
  seen.add(id3(invoice2));
9479
9842
  if (Number(invoice2.amount_paid) > 0 && (!earliest || Number(invoice2.created) < Number(earliest.created))) earliest = invoice2;
9480
9843
  }
9481
9844
  if (!list.has_more) break;
9482
- if (!list.data.length || page === 9) fail7("refund_history_incomplete");
9845
+ if (!list.data.length || page === 9) fail8("refund_history_incomplete");
9483
9846
  cursor = id3(list.data[list.data.length - 1]);
9484
9847
  }
9485
- if (!earliest) fail7("no_charge");
9848
+ if (!earliest) fail8("no_charge");
9486
9849
  const invoice = earliest;
9487
- if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail7();
9850
+ if (input.expectedInvoiceId !== void 0 && id3(invoice) !== input.expectedInvoiceId || input.expectedAmountCents !== void 0 && invoice.amount_paid !== input.expectedAmountCents) fail8();
9488
9851
  let chargeId = id3(invoice.charge);
9489
9852
  let paymentIntentId = id3(invoice.payment_intent);
9490
9853
  if (!chargeId && !paymentIntentId) {
9491
9854
  const payments = await call("GET", "/v1/invoice_payments", { invoice: id3(invoice), status: "paid", limit: 100 });
9492
- if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail7("refund_payment_ambiguous");
9855
+ if (payments.has_more !== false || !Array.isArray(payments.data) || payments.data.length !== 1) fail8("refund_payment_ambiguous");
9493
9856
  const payment = row(payments.data[0]);
9494
- if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail7();
9857
+ if (id3(payment.invoice) !== id3(invoice) || payment.status !== "paid" || row(payment.payment).type !== "payment_intent") fail8();
9495
9858
  paymentIntentId = id3(row(payment.payment).payment_intent);
9496
9859
  }
9497
9860
  if (!chargeId) {
9498
- if (!providerId2(paymentIntentId, "pi")) fail7();
9861
+ if (!providerId2(paymentIntentId, "pi")) fail8();
9499
9862
  const payment = await call("GET", `/v1/payment_intents/${paymentIntentId}`);
9500
- if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail7();
9863
+ if (id3(payment) !== paymentIntentId || id3(payment.customer) !== input.customerId || payment.status !== "succeeded") fail8();
9501
9864
  chargeId = id3(payment.latest_charge);
9502
9865
  }
9503
- if (!providerId2(chargeId, "ch")) fail7();
9866
+ if (!providerId2(chargeId, "ch")) fail8();
9504
9867
  const charge = await call("GET", `/v1/charges/${chargeId}`);
9505
- if (id3(charge) !== chargeId || id3(charge.customer) !== input.customerId || charge.status !== "succeeded" || charge.invoice != null && id3(charge.invoice) !== id3(invoice) || paymentIntentId && id3(charge.payment_intent) !== paymentIntentId || !Number.isSafeInteger(charge.amount) || Number(charge.amount) <= 0 || !Number.isSafeInteger(charge.amount_refunded) || Number(charge.amount_refunded) < 0 || Number(charge.amount_refunded) > Number(charge.amount) || charge.currency !== invoice.currency || Number(charge.amount) !== Number(invoice.amount_paid)) fail7();
9506
- if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail7();
9868
+ 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();
9869
+ if (input.alreadyRefundedChargeId !== void 0 && (chargeId !== input.alreadyRefundedChargeId || charge.amount_refunded !== charge.amount || charge.refunded !== true)) fail8();
9507
9870
  if (charge.amount_refunded !== charge.amount) {
9508
9871
  const refund = await call("POST", "/v1/refunds", {
9509
9872
  charge: chargeId,
9510
9873
  ...input.metadata ? { metadata: input.metadata } : {}
9511
9874
  }, `chapter-initial-refund:${input.subscriptionId}:${chargeId}`);
9512
- if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail7("refund_not_confirmed");
9875
+ if (id3(refund.charge) !== chargeId || !["succeeded", "pending"].includes(String(refund.status)) || !Number.isSafeInteger(refund.amount) || Number(refund.amount) !== Number(charge.amount) - Number(charge.amount_refunded)) fail8("refund_not_confirmed");
9513
9876
  }
9514
9877
  let subscriptionCanceled = sub.status === "canceled";
9515
9878
  if (input.cancelSubscription !== false && !subscriptionCanceled) {
9516
9879
  const canceled = await call("DELETE", `/v1/subscriptions/${input.subscriptionId}`);
9517
- if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail7("refund_cancel_incomplete");
9880
+ if (id3(canceled) !== input.subscriptionId || canceled.status !== "canceled") fail8("refund_cancel_incomplete");
9518
9881
  subscriptionCanceled = true;
9519
9882
  }
9520
9883
  const confirmed = await call("GET", `/v1/charges/${chargeId}`);
9521
- if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail7("refund_not_confirmed");
9884
+ if (id3(confirmed) !== chargeId || id3(confirmed.customer) !== input.customerId || confirmed.amount !== charge.amount || confirmed.currency !== charge.currency || confirmed.amount_refunded !== charge.amount || confirmed.refunded !== true) fail8("refund_not_confirmed");
9522
9885
  return { refundedCents: Number(charge.amount), subscriptionCanceled };
9523
9886
  }
9524
9887
 
9525
9888
  // src/subscription-payment-identity.ts
9526
- var import_stripe11 = require("@odla-ai/stripe");
9889
+ var import_stripe13 = require("@odla-ai/stripe");
9527
9890
  var id4 = (value) => typeof value === "string" ? value : String(value?.id ?? "");
9528
9891
  async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata, clientSecret2, applicationId) {
9529
9892
  let paymentId = id4(invoice.payment_intent);
9530
9893
  if (!paymentId) {
9531
9894
  if (!/^in_[A-Za-z0-9]+$/.test(id4(invoice))) subscriptionContractMismatch();
9532
- const result2 = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9895
+ const result2 = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", "/v1/invoice_payments", { invoice: id4(invoice), limit: 100 });
9533
9896
  const list = result2.body;
9534
9897
  if (!result2.ok || list?.has_more !== false || !Array.isArray(list.data) || list.data.length !== 1) subscriptionContractMismatch();
9535
9898
  const link = list.data[0];
@@ -9538,11 +9901,11 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9538
9901
  }
9539
9902
  if (!/^pi_[A-Za-z0-9]+$/.test(paymentId)) subscriptionContractMismatch();
9540
9903
  const path = `/v1/payment_intents/${paymentId}`;
9541
- const result = await (0, import_stripe11.stripeRequest)({ secretKey }, "GET", path);
9904
+ const result = await (0, import_stripe13.stripeRequest)({ secretKey }, "GET", path);
9542
9905
  const payment = result.body;
9543
9906
  if (!result.ok || payment?.id !== paymentId || id4(payment.customer) !== customerId || payment.amount !== invoice.total || payment.currency !== invoice.currency || payment.client_secret !== clientSecret2 || payment.livemode !== (metadata.odlaEnvironment === "prod") || !Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === void 0 || payment.metadata[key3] === value)) subscriptionContractMismatch();
9544
9907
  if (Object.entries(metadata).every(([key3, value]) => payment.metadata?.[key3] === value)) return;
9545
- const tagged = await (0, import_stripe11.stripeRequest)(
9908
+ const tagged = await (0, import_stripe13.stripeRequest)(
9546
9909
  { secretKey },
9547
9910
  "POST",
9548
9911
  path,
@@ -9554,7 +9917,7 @@ async function tagSubscriptionPayment(secretKey, invoice, customerId, metadata,
9554
9917
 
9555
9918
  // src/payments-stripe.ts
9556
9919
  async function stripeCall(sk, method, path, params, idempotencyKey) {
9557
- return (0, import_stripe12.stripeRequest)(
9920
+ return (0, import_stripe14.stripeRequest)(
9558
9921
  { secretKey: sk },
9559
9922
  method,
9560
9923
  path,
@@ -9562,7 +9925,7 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
9562
9925
  idempotencyKey ? { idempotencyKey } : {}
9563
9926
  );
9564
9927
  }
9565
- function fail8(op, r) {
9928
+ function fail9(op, r) {
9566
9929
  const err = new Error(`stripe ${op} failed: ${r.status}`);
9567
9930
  err.code = "stripe_error";
9568
9931
  throw err;
@@ -9621,7 +9984,7 @@ function createStripeProvider(config) {
9621
9984
  { email: input.email, name: input.name, metadata: meta },
9622
9985
  `cus:${input.applicationId}`
9623
9986
  );
9624
- if (!cust.ok) fail8("customer create", cust);
9987
+ if (!cust.ok) fail9("customer create", cust);
9625
9988
  if (typeof cust.body.id !== "string" || !/^cus_[A-Za-z0-9]+$/.test(cust.body.id)) subscriptionContractMismatch();
9626
9989
  customerId = cust.body.id;
9627
9990
  }
@@ -9666,7 +10029,7 @@ function createStripeProvider(config) {
9666
10029
  // retry between create and the db write can't orphan a second one.
9667
10030
  `sub:${input.applicationId}${input.purchaseAttemptId ? `:${input.purchaseAttemptId}` : ""}`
9668
10031
  );
9669
- if (!sub.ok) fail8("subscription create", sub);
10032
+ if (!sub.ok) fail9("subscription create", sub);
9670
10033
  if (input.billingTerms?.discount?.duration === "once" && Array.isArray(sub.body.discounts) && sub.body.discounts.length === 0) {
9671
10034
  const subscriptionId = sub.body.id;
9672
10035
  if (typeof subscriptionId !== "string" || !/^sub_[A-Za-z0-9]+$/.test(subscriptionId)) subscriptionContractMismatch();
@@ -9676,14 +10039,14 @@ function createStripeProvider(config) {
9676
10039
  "expand[2]": "discounts",
9677
10040
  "expand[3]": "latest_invoice.discounts"
9678
10041
  });
9679
- if (!expanded.ok) fail8("subscription invoice evidence", expanded);
10042
+ if (!expanded.ok) fail9("subscription invoice evidence", expanded);
9680
10043
  if (expanded.body.id !== subscriptionId) subscriptionContractMismatch();
9681
10044
  sub = expanded;
9682
10045
  }
9683
10046
  verifySubscriptionResult(sub.body, input, customerId, meta);
9684
10047
  const invoice = sub.body.latest_invoice;
9685
10048
  const clientSecret2 = clientSecretOf(sub.body);
9686
- if (!clientSecret2) fail8("subscription confirmation-secret missing", sub);
10049
+ if (!clientSecret2) fail9("subscription confirmation-secret missing", sub);
9687
10050
  await tagSubscriptionPayment(
9688
10051
  sk,
9689
10052
  invoice,
@@ -9695,7 +10058,7 @@ function createStripeProvider(config) {
9695
10058
  return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
9696
10059
  },
9697
10060
  async ingestWebhook(rawBody, sigHeader) {
9698
- if (!webhookSecret || !await (0, import_stripe12.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
10061
+ if (!webhookSecret || !await (0, import_stripe14.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
9699
10062
  return { ok: false, reason: "bad_signature" };
9700
10063
  }
9701
10064
  const event2 = JSON.parse(rawBody);
@@ -10170,7 +10533,7 @@ async function startSubscription(req, env, ctx) {
10170
10533
 
10171
10534
  // src/worker-payment-receipt.ts
10172
10535
  var json2 = (value, status2 = 200) => Response.json(value, { status: status2, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
10173
- var escape = (text8) => text8.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10536
+ var escape = (text9) => text9.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
10174
10537
  async function paymentReceiptRoute(req, url, env, ctx) {
10175
10538
  const id7 = url.searchParams.get("application");
10176
10539
  if (!id7 || id7.length > 160) return json2({ error: "application required" }, 400);
@@ -10181,7 +10544,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10181
10544
  if (!app || app.groupId !== ctx.chapter.id || !intent || intent.applicationId !== id7 || intent.appId !== env.ODLA_APP_ID || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV || intent.runtime !== stripeRuntimeIdentity(env.ODLA_RUNTIME) || app.stripeRuntime !== intent.runtime || app.stripeSubscriptionId !== intent.providerSubscriptionId) return json2({ error: "receipt not found" }, 404);
10182
10545
  const receipt = intent.paymentReceipt;
10183
10546
  if (!receipt) return json2({ pending: true }, 202);
10184
- const text8 = paymentReceiptText(receipt, ctx.chapter.name);
10547
+ const text9 = paymentReceiptText(receipt, ctx.chapter.name);
10185
10548
  if (req.method === "POST") {
10186
10549
  if (!env.SEND_EMAIL || !env.EMAIL_FROM) return json2({ error: "Email is unavailable. You can print or save your receipt." }, 503);
10187
10550
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
@@ -10199,7 +10562,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10199
10562
  template: "paymentReceipt",
10200
10563
  to: String(intent.purchaserEmailNormalized),
10201
10564
  applicationId: id7,
10202
- vars: { chapterName: ctx.chapter.name, receiptText: text8 },
10565
+ vars: { chapterName: ctx.chapter.name, receiptText: text9 },
10203
10566
  dedupeKey: `requested-payment-receipt:${receipt.invoiceId}`,
10204
10567
  event: { label: "Payment receipt", trigger: "Purchaser requests their receipt", audience: "Purchaser", variables: ["chapterName", "receiptText"] }
10205
10568
  });
@@ -10209,7 +10572,7 @@ async function paymentReceiptRoute(req, url, env, ctx) {
10209
10572
  const nonce = crypto.randomUUID();
10210
10573
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Payment receipt</title>
10211
10574
  <style>body{font:16px/1.6 system-ui;max-width:680px;margin:48px auto;padding:24px;color:#17252d}pre{font:inherit;white-space:pre-wrap}button{font:inherit;padding:10px 16px}@media print{button{display:none}body{margin:0}}</style>
10212
- <h1>Payment receipt</h1><pre>${escape(text8)}</pre><button id="print">Print / save PDF</button>
10575
+ <h1>Payment receipt</h1><pre>${escape(text9)}</pre><button id="print">Print / save PDF</button>
10213
10576
  <script nonce="${nonce}">document.getElementById('print').addEventListener('click',()=>window.print())</script></html>`, {
10214
10577
  headers: {
10215
10578
  "content-type": "text/html; charset=utf-8",
@@ -10679,16 +11042,16 @@ var handleNamedSeat = async (req, url, env, ctx) => {
10679
11042
  };
10680
11043
 
10681
11044
  // src/named-seat-accepted-notice.ts
10682
- var text5 = (value) => typeof value === "string" ? value : "";
11045
+ var text6 = (value) => typeof value === "string" ? value : "";
10683
11046
  async function notifyNamedSeatAccepted(db, env, ctx, input) {
10684
11047
  const purchaser = await membershipLoad(db, "applications", input.primaryApplicationId);
10685
- if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text5(purchaser.email)) {
11048
+ if (!purchaser || !chapterBillingApplication(purchaser, ctx.chapter.id, env.ODLA_RUNTIME) || !text6(purchaser.email)) {
10686
11049
  return { sent: false, reason: "purchaser-missing" };
10687
11050
  }
10688
11051
  const group = await membershipLoad(db, "groups", ctx.chapter.id);
10689
11052
  if (!group) return { sent: false, reason: "group-missing" };
10690
11053
  const config = emailGroupFrom(group);
10691
- const purchaserName = [purchaser.firstName, purchaser.lastName].map(text5).join(" ").replace(/\s+/g, " ").trim();
11054
+ const purchaserName = [purchaser.firstName, purchaser.lastName].map(text6).join(" ").replace(/\s+/g, " ").trim();
10692
11055
  return sendTemplated({
10693
11056
  db,
10694
11057
  envName: env.ODLA_ENV,
@@ -10700,12 +11063,12 @@ async function notifyNamedSeatAccepted(db, env, ctx, input) {
10700
11063
  }, {
10701
11064
  group: config,
10702
11065
  template: "seatAccepted",
10703
- to: text5(purchaser.email),
11066
+ to: text6(purchaser.email),
10704
11067
  dedupeKey: `named-seat-accepted:${input.seatId}`,
10705
11068
  applicationId: input.primaryApplicationId,
10706
11069
  tierId: input.skuId,
10707
11070
  vars: {
10708
- firstName: text5(purchaser.firstName),
11071
+ firstName: text6(purchaser.firstName),
10709
11072
  purchaserName,
10710
11073
  recipientName: input.recipientName,
10711
11074
  tierName: await tierName(db, group, input.skuId),
@@ -11149,7 +11512,7 @@ var handleAdminScheduling = async (req, url, env, ctx) => {
11149
11512
  const checked = validateScheduling({ ...stored, ...body });
11150
11513
  if (!checked.ok) return json({ error: "invalid scheduling config", errors: checked.errors }, 400);
11151
11514
  try {
11152
- await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
11515
+ await db.transact([{ t: "update", ns: "groups", id: { ns: "groups", attr: "id", value: String(group.id) }, attrs: { schedulingJson: checked.value } }]);
11153
11516
  } catch (error) {
11154
11517
  const code = error.code;
11155
11518
  console.error("admin_scheduling_write_failed", { code: typeof code === "string" ? code : "unknown" });
@@ -12172,12 +12535,12 @@ async function updateEmailSettings(db, ctx, env, body) {
12172
12535
  if (!value || typeof value !== "object" || Array.isArray(value)) return json({ error: `Invalid template: ${key3}` }, 400);
12173
12536
  const t = value;
12174
12537
  if (typeof t.enabled !== "boolean") return json({ error: `Choose whether ${key3} is enabled.` }, 400);
12175
- const subject = str2(t.subject).trim(), text8 = str2(t.text);
12176
- const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text8 }, { allowedVariables: events[key3].variables });
12538
+ const subject = str2(t.subject).trim(), text9 = str2(t.text);
12539
+ const issues = (0, import_crm16.validateEmailCopy)({ subject, text: text9 }, { allowedVariables: events[key3].variables });
12177
12540
  if (t.enabled && issues.length || issues.some((issue) => ["subject-invalid", "body-invalid"].includes(issue.code))) {
12178
12541
  return json({ error: `${events[key3].label} is incomplete.`, errors: issues.map((issue) => issue.message) }, 400);
12179
12542
  }
12180
- clean[key3] = { subject, text: text8, enabled: t.enabled };
12543
+ clean[key3] = { subject, text: text9, enabled: t.enabled };
12181
12544
  }
12182
12545
  const notificationEmail = str2(body.notificationEmail).trim(), replyTo = str2(body.replyTo).trim(), debugEmail = str2(body.debugEmail).trim();
12183
12546
  const emailish = /^[^\s@,;<>]+@[^\s@,;<>]+\.[^\s@,;<>]+$/;
@@ -14381,9 +14744,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14381
14744
  if (req.method === "POST") {
14382
14745
  let body;
14383
14746
  try {
14384
- const text8 = await req.text();
14385
- if (new TextEncoder().encode(text8).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14386
- const parsed = JSON.parse(text8);
14747
+ const text9 = await req.text();
14748
+ if (new TextEncoder().encode(text9).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
14749
+ const parsed = JSON.parse(text9);
14387
14750
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
14388
14751
  body = parsed;
14389
14752
  } catch {
@@ -14544,7 +14907,7 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
14544
14907
  };
14545
14908
 
14546
14909
  // src/worker-routes-network-commercial.ts
14547
- var import_stripe13 = require("@odla-ai/stripe");
14910
+ var import_stripe15 = require("@odla-ai/stripe");
14548
14911
 
14549
14912
  // src/worker-routes-network-commercial-parity.ts
14550
14913
  var import_db13 = require("@odla-ai/db");
@@ -14552,7 +14915,7 @@ var import_db13 = require("@odla-ai/db");
14552
14915
  // src/network-commercial-parity.ts
14553
14916
  var DIGEST = /^[0-9a-f]{64}$/;
14554
14917
  var MAX_RECORDS = 500;
14555
- var text6 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14918
+ var text7 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
14556
14919
  function parityMessage(partition, kind, value) {
14557
14920
  return [
14558
14921
  "chapter-commercial-parity",
@@ -14584,18 +14947,18 @@ async function digestWithKey(key3, partition, kind, value) {
14584
14947
  async function buildChapterCommercialParitySnapshot(input) {
14585
14948
  const requestedMax = input.maxRecords ?? MAX_RECORDS;
14586
14949
  const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
14587
- const eligible = input.rows.filter((row2) => text6(row2.id) && (text6(row2.stripeCustomerId) || text6(row2.stripeSubscriptionId)));
14950
+ const eligible = input.rows.filter((row2) => text7(row2.id) && (text7(row2.stripeCustomerId) || text7(row2.stripeSubscriptionId)));
14588
14951
  const selected2 = eligible.slice(0, maxRecords);
14589
14952
  const key3 = await parityKey(input.secret);
14590
14953
  const records = await Promise.all(selected2.map(async (row2) => {
14591
- const applicationId = text6(row2.id);
14592
- const customerId = text6(row2.stripeCustomerId);
14593
- const subscriptionId = text6(row2.stripeSubscriptionId);
14954
+ const applicationId = text7(row2.id);
14955
+ const customerId = text7(row2.stripeCustomerId);
14956
+ const subscriptionId = text7(row2.stripeSubscriptionId);
14594
14957
  return {
14595
14958
  applicationDigest: await digestWithKey(key3, input.partition, "application", applicationId),
14596
14959
  customerDigest: customerId ? await digestWithKey(key3, input.partition, "customer", customerId) : null,
14597
14960
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.partition, "subscription", subscriptionId) : null,
14598
- status: text6(row2.status, 64),
14961
+ status: text7(row2.status, 64),
14599
14962
  canceled: row2.canceled === true
14600
14963
  };
14601
14964
  }));
@@ -14621,7 +14984,7 @@ function validSnapshot(snapshot, source) {
14621
14984
  if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
14622
14985
  const seen = /* @__PURE__ */ new Set();
14623
14986
  for (const record4 of records) {
14624
- if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text6(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
14987
+ 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;
14625
14988
  const key3 = recordKey(record4);
14626
14989
  if (seen.has(key3)) return false;
14627
14990
  seen.add(key3);
@@ -14632,16 +14995,16 @@ async function reconcileCommercialParitySnapshot(input) {
14632
14995
  if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
14633
14996
  const sourceKey = networkCommercialSourceKey(input.source);
14634
14997
  const key3 = await parityKey(input.secret);
14635
- const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text6(row2.applicationId) && text6(row2.objectId)).map(async (row2) => {
14636
- const customerId = text6(row2.customerId);
14637
- const subscriptionId = text6(row2.subscriptionId) ?? text6(row2.objectId);
14998
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row2) => row2.sourceKey === sourceKey && text7(row2.applicationId) && text7(row2.objectId)).map(async (row2) => {
14999
+ const customerId = text7(row2.customerId);
15000
+ const subscriptionId = text7(row2.subscriptionId) ?? text7(row2.objectId);
14638
15001
  return {
14639
15002
  applicationDigest: await digestWithKey(key3, input.snapshot.partition, "application", row2.applicationId),
14640
15003
  customerDigest: customerId ? await digestWithKey(key3, input.snapshot.partition, "customer", customerId) : null,
14641
15004
  subscriptionDigest: subscriptionId ? await digestWithKey(key3, input.snapshot.partition, "subscription", subscriptionId) : null,
14642
15005
  status: null,
14643
15006
  canceled: false,
14644
- stripeStatus: text6(row2.status, 64),
15007
+ stripeStatus: text7(row2.status, 64),
14645
15008
  stripeCanceled: row2.status === "canceled"
14646
15009
  };
14647
15010
  }));
@@ -14838,6 +15201,34 @@ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
14838
15201
  };
14839
15202
  }
14840
15203
 
15204
+ // src/worker-network-commercial-dispute.ts
15205
+ async function applyHostedMembershipDispute(env, ctx, fact, now) {
15206
+ const operator = ctx.membershipAuthorityOperator;
15207
+ if (!operator || !fact.chargeId) return { disposition: "unhosted" };
15208
+ let db;
15209
+ try {
15210
+ db = operator.makeDb(env);
15211
+ } catch {
15212
+ return { disposition: "unhosted" };
15213
+ }
15214
+ const secret = await getVaultSecret(db, operator.stripeSecretName ?? "stripe_secret_key");
15215
+ if (!secret) throw Object.assign(new Error("membership_dispute_provider_unavailable"), { code: "membership_dispute_provider_unavailable" });
15216
+ const charge = await readDisputedCharge(secret, fact.chargeId);
15217
+ const rows5 = (await db.query({ networkMembership: { $: { where: { providerCustomerId: charge.customerId, environment: env.ODLA_ENV }, limit: 5 } } })).networkMembership ?? [];
15218
+ const member = rows5.find((row2) => disputedChargeCoversMembership(charge, row2));
15219
+ if (!member) return { disposition: "not_applicable" };
15220
+ const target = {
15221
+ appId: String(member.appId),
15222
+ chapterId: String(member.chapterId),
15223
+ environment: String(member.environment),
15224
+ runtime: String(member.runtime),
15225
+ applicationId: String(member.applicationId),
15226
+ skuId: String(member.skuId)
15227
+ };
15228
+ const receipt = await reconcileMembershipDispute(db, { ...target, eventId: fact.eventId }, now, secret, charge);
15229
+ return { disposition: "applied", membershipId: receipt.membershipId };
15230
+ }
15231
+
14841
15232
  // src/worker-routes-network-commercial.ts
14842
15233
  var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
14843
15234
  var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
@@ -14902,6 +15293,12 @@ function safeProjection(projection) {
14902
15293
  amountReceivedCents: seat.amountReceivedCents,
14903
15294
  refundedCents: seat.refundedCents ?? 0
14904
15295
  })),
15296
+ disputes: (projection.disputes ?? []).map((dispute) => ({
15297
+ ...row2(dispute),
15298
+ chargeId: dispute.chargeId ?? null,
15299
+ paymentIntentId: dispute.paymentIntentId ?? null,
15300
+ reason: dispute.disputeReason ?? null
15301
+ })),
14905
15302
  exceptions: projection.exceptions
14906
15303
  };
14907
15304
  }
@@ -14920,7 +15317,7 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
14920
15317
  return json({ error: "commercial webhook is not configured" }, 503);
14921
15318
  }
14922
15319
  try {
14923
- const event2 = await (0, import_stripe13.constructStripeEvent)(body, signature, secret);
15320
+ const event2 = await (0, import_stripe15.constructStripeEvent)(body, signature, secret);
14924
15321
  const fact = normalizeNetworkCommercialStripeEvent(event2, await stripeEventDigest(body));
14925
15322
  if (!fact.eventId || !fact.eventType || !fact.objectId) return json({ error: "Stripe event identity is invalid" }, 400);
14926
15323
  const result = await ingestNetworkCommercialFact(
@@ -14934,14 +15331,24 @@ var handleNetworkCommercialStripeWebhook = async (req, url, env, ctx) => {
14934
15331
  if (result.status === "collision") {
14935
15332
  console.error("network commercial event collision", JSON.stringify({ eventId: fact.eventId, eventType: fact.eventType }));
14936
15333
  }
15334
+ let dispute;
15335
+ if (fact.objectKind === "dispute" && result.status !== "collision") {
15336
+ try {
15337
+ dispute = await applyHostedMembershipDispute(env, ctx, fact, Date.now());
15338
+ } catch (error) {
15339
+ console.warn("membership dispute pending", JSON.stringify({ eventId: fact.eventId, code: error.code ?? "unknown" }));
15340
+ return json({ error: "membership dispute reconciliation pending", eventId: fact.eventId }, 503);
15341
+ }
15342
+ }
14937
15343
  return json({
14938
15344
  ok: true,
14939
15345
  status: result.status,
14940
15346
  eventId: fact.eventId,
15347
+ ...dispute ? { dispute: dispute.disposition } : {},
14941
15348
  ...result.status === "replayed" && result.resolvedCollision ? { resolvedCollision: true } : {}
14942
15349
  });
14943
15350
  } catch (error) {
14944
- if (error instanceof import_stripe13.StripeError) return json({ error: error.message, code: error.code }, 400);
15351
+ if (error instanceof import_stripe15.StripeError) return json({ error: error.message, code: error.code }, 400);
14945
15352
  throw error;
14946
15353
  }
14947
15354
  };
@@ -15004,7 +15411,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
15004
15411
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
15005
15412
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
15006
15413
  try {
15007
- const stripe = (0, import_stripe13.initStripe)({ secretKey });
15414
+ const stripe = (0, import_stripe15.initStripe)({ secretKey });
15008
15415
  const [customers, subscriptions, invoices, refunds, payments] = await Promise.all([
15009
15416
  listStripeObjects(stripe, "/v1/customers"),
15010
15417
  listStripeObjects(stripe, "/v1/subscriptions", { status: "all" }),
@@ -15018,7 +15425,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
15018
15425
  ...reconcileStripeSnapshot(projection2, { customers, subscriptions, invoices, refunds, payments }, ctx.chapter.networkCommercialReadModel.sources)
15019
15426
  });
15020
15427
  } catch (error) {
15021
- if (error instanceof import_stripe13.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
15428
+ if (error instanceof import_stripe15.StripeError) return json({ error: "Stripe reconciliation failed", code: error.code }, 502);
15022
15429
  return json({ error: "Stripe reconciliation failed" }, 502);
15023
15430
  }
15024
15431
  }
@@ -15135,7 +15542,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
15135
15542
  function unknownKeys3(value, allowed2, path, errors) {
15136
15543
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
15137
15544
  }
15138
- function text7(value, path, errors, max = MAX_TEXT2, required = true) {
15545
+ function text8(value, path, errors, max = MAX_TEXT2, required = true) {
15139
15546
  if (value === void 0 && !required) return void 0;
15140
15547
  if (typeof value !== "string" || required && !value.trim()) {
15141
15548
  errors.push(`${path} must be a non-empty string`);
@@ -15161,7 +15568,7 @@ function scopeValue(value, path, errors) {
15161
15568
  return { kind: raw2.kind };
15162
15569
  }
15163
15570
  if (raw2.kind === "chapter") {
15164
- const chapterId = text7(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15571
+ const chapterId = text8(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
15165
15572
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
15166
15573
  return { kind: "chapter", chapterId };
15167
15574
  }
@@ -15175,22 +15582,22 @@ function resourceValue(value, path, errors) {
15175
15582
  return null;
15176
15583
  }
15177
15584
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
15178
- const id7 = text7(raw2.id, `${path}.id`, errors, 80) ?? "";
15585
+ const id7 = text8(raw2.id, `${path}.id`, errors, 80) ?? "";
15179
15586
  if (id7 && !ID5.test(id7)) errors.push(`${path}.id must be lowercase kebab-case`);
15180
- const kind = text7(raw2.kind, `${path}.kind`, errors) ?? "";
15587
+ const kind = text8(raw2.kind, `${path}.kind`, errors) ?? "";
15181
15588
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
15182
- const title = text7(raw2.title, `${path}.title`, errors, 240) ?? "";
15183
- const summary = text7(raw2.summary, `${path}.summary`, errors, 1e3, false);
15184
- const status2 = text7(raw2.status, `${path}.status`, errors) ?? "";
15589
+ const title = text8(raw2.title, `${path}.title`, errors, 240) ?? "";
15590
+ const summary = text8(raw2.summary, `${path}.summary`, errors, 1e3, false);
15591
+ const status2 = text8(raw2.status, `${path}.status`, errors) ?? "";
15185
15592
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
15186
15593
  const revision = integer4(raw2.revision, `${path}.revision`, errors);
15187
15594
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
15188
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text7(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15595
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text8(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
15189
15596
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
15190
15597
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
15191
15598
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
15192
- const bodyMarkdown = text7(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15193
- const url = text7(raw2.url, `${path}.url`, errors, 2048, false);
15599
+ const bodyMarkdown = text8(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
15600
+ const url = text8(raw2.url, `${path}.url`, errors, 2048, false);
15194
15601
  if (url) {
15195
15602
  try {
15196
15603
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -15202,9 +15609,9 @@ function resourceValue(value, path, errors) {
15202
15609
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
15203
15610
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
15204
15611
  const brandAsset = asset ? {
15205
- bookId: text7(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15206
- assetId: text7(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15207
- ...asset.filename === void 0 ? {} : { filename: text7(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15612
+ bookId: text8(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
15613
+ assetId: text8(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
15614
+ ...asset.filename === void 0 ? {} : { filename: text8(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
15208
15615
  } : void 0;
15209
15616
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
15210
15617
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -15219,17 +15626,17 @@ function resourceValue(value, path, errors) {
15219
15626
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
15220
15627
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
15221
15628
  const promotedFrom = promotedRaw ? {
15222
- chapterId: text7(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15223
- resourceId: text7(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15629
+ chapterId: text8(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
15630
+ resourceId: text8(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
15224
15631
  resourceRevision: integer4(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
15225
15632
  documentRevision: integer4(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
15226
- promotedBy: text7(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15633
+ promotedBy: text8(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
15227
15634
  promotedAt: integer4(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
15228
15635
  } : void 0;
15229
15636
  const provenance2 = {
15230
- createdBy: text7(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15637
+ createdBy: text8(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
15231
15638
  createdAt: integer4(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
15232
- updatedBy: text7(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15639
+ updatedBy: text8(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
15233
15640
  updatedAt: integer4(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
15234
15641
  ...promotedFrom ? { promotedFrom } : {}
15235
15642
  };
@@ -15248,7 +15655,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
15248
15655
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
15249
15656
  const sourceType = sourceRaw?.type;
15250
15657
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
15251
- const sourceAppId = text7(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15658
+ const sourceAppId = text8(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
15252
15659
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
15253
15660
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
15254
15661
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -15256,8 +15663,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
15256
15663
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
15257
15664
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
15258
15665
  const target = targetRaw ? {
15259
- appId: text7(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15260
- environment: text7(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15666
+ appId: text8(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
15667
+ environment: text8(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
15261
15668
  } : void 0;
15262
15669
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
15263
15670
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -15598,7 +16005,7 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
15598
16005
  };
15599
16006
 
15600
16007
  // src/membership-cancel-authority.ts
15601
- var import_stripe14 = require("@odla-ai/stripe");
16008
+ var import_stripe16 = require("@odla-ai/stripe");
15602
16009
  async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15603
16010
  membershipClock(now);
15604
16011
  const id7 = await networkMembershipId(target);
@@ -15609,14 +16016,14 @@ async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
15609
16016
  membershipFail("membership_cancellation_unavailable");
15610
16017
  }
15611
16018
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
15612
- const read2 = await (0, import_stripe14.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
16019
+ const read2 = await (0, import_stripe16.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
15613
16020
  const sub = read2.body;
15614
16021
  if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
15615
16022
  ...stripePartitionMetadata(target),
15616
16023
  applicationId: target.applicationId
15617
16024
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
15618
16025
  if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
15619
- const write = await (0, import_stripe14.stripeRequest)(
16026
+ const write = await (0, import_stripe16.stripeRequest)(
15620
16027
  { secretKey: stripeSecret },
15621
16028
  "POST",
15622
16029
  path,
@@ -15838,7 +16245,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
15838
16245
  }
15839
16246
 
15840
16247
  // src/membership-provider-lifecycle.ts
15841
- var import_stripe15 = require("@odla-ai/stripe");
16248
+ var import_stripe17 = require("@odla-ai/stripe");
15842
16249
 
15843
16250
  // src/membership-renewal-authority.ts
15844
16251
  async function reconcileMembershipRenewal(db, input, now) {
@@ -15960,6 +16367,11 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
15960
16367
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
15961
16368
  const fact = row2?.fact;
15962
16369
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
16370
+ if (fact?.objectKind === "dispute") {
16371
+ const secret = await getVaultSecret(db, stripeSecretName);
16372
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
16373
+ return reconcileMembershipDispute(db, input, now, secret);
16374
+ }
15963
16375
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
15964
16376
  if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
15965
16377
  appId: input.appId,
@@ -16002,7 +16414,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
16002
16414
  }
16003
16415
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
16004
16416
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
16005
- const result = await (0, import_stripe15.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
16417
+ const result = await (0, import_stripe17.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
16006
16418
  const sub = result.body;
16007
16419
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
16008
16420
  membershipFail("membership_billing_provider_mismatch");
@@ -16040,7 +16452,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
16040
16452
  }
16041
16453
 
16042
16454
  // src/membership-seat-stripe.ts
16043
- var import_stripe16 = require("@odla-ai/stripe");
16455
+ var import_stripe18 = require("@odla-ai/stripe");
16044
16456
  var reject7 = () => {
16045
16457
  throw Object.assign(new Error("named seat provider contract mismatch"), { code: "named_seat_provider_mismatch" });
16046
16458
  };
@@ -16052,7 +16464,7 @@ var metadataFor = (seat) => ({
16052
16464
  });
16053
16465
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
16054
16466
  async function read(sk, path, params) {
16055
- const response = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "GET", path, params);
16467
+ const response = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "GET", path, params);
16056
16468
  if (!response.ok) return reject7();
16057
16469
  return response.body;
16058
16470
  }
@@ -16089,11 +16501,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
16089
16501
  }
16090
16502
  await subscriptionForSeat(sk, seat);
16091
16503
  const metadata = metadataFor(seat);
16092
- const result = seat.providerPaymentIntentId ? await (0, import_stripe16.stripeRequest)(
16504
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe18.stripeRequest)(
16093
16505
  { secretKey: sk },
16094
16506
  "GET",
16095
16507
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
16096
- ) : await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
16508
+ ) : await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
16097
16509
  amount: seat.amountCents,
16098
16510
  currency: seat.currency,
16099
16511
  customer: seat.providerCustomerId,
@@ -16115,7 +16527,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
16115
16527
  return String(existing.id);
16116
16528
  }
16117
16529
  if (seat.combinedInvoice) reject7();
16118
- const result = await (0, import_stripe16.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
16530
+ const result = await (0, import_stripe18.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
16119
16531
  subscription: seat.providerSubscriptionId,
16120
16532
  price: seat.priceId,
16121
16533
  quantity: 1,
@@ -16816,7 +17228,20 @@ var membershipAuthorityEntities = {
16816
17228
  graceEndsAt: attr("number", true),
16817
17229
  ...times(),
16818
17230
  refundRequestedAt: attr("number", true),
16819
- restartId: key(true)
17231
+ restartId: key(true),
17232
+ // A card-network dispute on this membership's payment: Stripe's dispute id and status, the reason the cardholder gave,
17233
+ // the status the membership held before it was suspended, and the re-issued charge once an operator collected again.
17234
+ disputeId: key(true),
17235
+ disputeStatus: attr("string", true),
17236
+ disputeReason: attr("string", true),
17237
+ disputeAmountCents: attr("number", true),
17238
+ disputedAt: attr("number", true),
17239
+ disputeClosedAt: attr("number", true),
17240
+ disputePriorStatus: attr("string", true),
17241
+ disputeEventId: key(true),
17242
+ rechargePaymentIntentId: key(true),
17243
+ rechargedAt: attr("number", true),
17244
+ rechargeAttempt: attr("json", true)
16820
17245
  } },
16821
17246
  membershipRestart: { attrs: {
16822
17247
  id: id5(),
@@ -17013,7 +17438,7 @@ function createMembershipAuthorityStorage(db, secretNames) {
17013
17438
  }
17014
17439
 
17015
17440
  // src/membership-seat-cancel.ts
17016
- var import_stripe17 = require("@odla-ai/stripe");
17441
+ var import_stripe19 = require("@odla-ai/stripe");
17017
17442
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
17018
17443
  membershipClock(now);
17019
17444
  const state = await loadMembershipAuthorityState(db, target);
@@ -17041,7 +17466,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
17041
17466
  }
17042
17467
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
17043
17468
  const request = async (method, path, params) => {
17044
- const result = await (0, import_stripe17.stripeRequest)(
17469
+ const result = await (0, import_stripe19.stripeRequest)(
17045
17470
  { secretKey },
17046
17471
  method,
17047
17472
  path,
@@ -17054,7 +17479,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
17054
17479
  let payment;
17055
17480
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
17056
17481
  else {
17057
- const snapshot = await listStripeObjects((0, import_stripe17.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
17482
+ const snapshot = await listStripeObjects((0, import_stripe19.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
17058
17483
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
17059
17484
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
17060
17485
  payment = matches2[0];
@@ -17229,7 +17654,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17229
17654
  };
17230
17655
  signal.addEventListener("abort", cancel, { once: true });
17231
17656
  let size = 0;
17232
- let text8 = "";
17657
+ let text9 = "";
17233
17658
  const decoder = new TextDecoder();
17234
17659
  try {
17235
17660
  while (true) {
@@ -17239,10 +17664,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
17239
17664
  if (done) break;
17240
17665
  size += value.byteLength;
17241
17666
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
17242
- text8 += decoder.decode(value, { stream: true });
17667
+ text9 += decoder.decode(value, { stream: true });
17243
17668
  }
17244
17669
  try {
17245
- return JSON.parse(text8 + decoder.decode());
17670
+ return JSON.parse(text9 + decoder.decode());
17246
17671
  } catch {
17247
17672
  throw new TypeError(`invalid ${label}`);
17248
17673
  }
@@ -17577,7 +18002,7 @@ async function requestMembershipRefund(options) {
17577
18002
  async function requestMembershipRenewal(options) {
17578
18003
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
17579
18004
  const row2 = result.renewal;
17580
- if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
18005
+ if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting", "payment_disputed"].includes(row2.status)) {
17581
18006
  throw new Error("invalid membership renewal receipt");
17582
18007
  }
17583
18008
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -17929,6 +18354,7 @@ var BUILTIN_ROUTES = [
17929
18354
  handlePaymentRecovery,
17930
18355
  handleAdminNamedSeat,
17931
18356
  handleAdminMembershipSeats,
18357
+ handleAdminMembershipDisputes,
17932
18358
  handleAdminMembershipPolicyVersions,
17933
18359
  handleAdminMembershipReterm,
17934
18360
  handleAdminMeetings,
@@ -17977,8 +18403,9 @@ function chapterWorker(options) {
17977
18403
  const ctx = createWorkerContext(options);
17978
18404
  const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
17979
18405
  return {
17980
- async fetch(req, env) {
18406
+ async fetch(req, env, execution) {
17981
18407
  const url = new URL(req.url);
18408
+ ctx.bindExecution(req, execution);
17982
18409
  for (const route of routes) {
17983
18410
  let res;
17984
18411
  try {