@odla-ai/chapter 0.58.1 → 0.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -254,7 +254,7 @@ __export(src_exports, {
254
254
  slotWindow: () => slotWindow,
255
255
  stageIndex: () => stageIndex,
256
256
  stripeCall: () => stripeCall,
257
- stripeForm: () => import_stripe11.stripeForm,
257
+ stripeForm: () => import_stripe12.stripeForm,
258
258
  subAnnualCents: () => subAnnualCents,
259
259
  submitApplication: () => submitApplication,
260
260
  subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
@@ -276,7 +276,7 @@ __export(src_exports, {
276
276
  verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
277
277
  verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
278
278
  verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
279
- verifyStripeSignature: () => import_stripe11.verifyStripeSignature,
279
+ verifyStripeSignature: () => import_stripe12.verifyStripeSignature,
280
280
  webhookMutationId: () => webhookMutationId
281
281
  });
282
282
  module.exports = __toCommonJS(src_exports);
@@ -1843,7 +1843,9 @@ var DEFAULT_JOIN_COPY = {
1843
1843
  failed: "The booking could not be completed.",
1844
1844
  loading: "Loading available times\u2026",
1845
1845
  book: "Book this time",
1846
- booking: "Booking\u2026"
1846
+ booking: "Booking\u2026",
1847
+ pickDate: "Pick a date",
1848
+ pickTime: "Pick a time"
1847
1849
  },
1848
1850
  payment: {
1849
1851
  setupFailed: "Payment could not be set up. Please try again.",
@@ -1995,8 +1997,8 @@ var APPLICATION_LABELS = {
1995
1997
  state: "State"
1996
1998
  };
1997
1999
  function humanize(id11) {
1998
- const text6 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1999
- return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id11;
2000
+ const text8 = id11.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
2001
+ return text8 ? `${text8[0].toUpperCase()}${text8.slice(1)}` : id11;
2000
2002
  }
2001
2003
  function applicationDefs(chapter) {
2002
2004
  try {
@@ -2795,6 +2797,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2795
2797
  else if (eventType.startsWith("refund.")) objectKind = "refund";
2796
2798
  else if (eventType === "charge.refunded") objectKind = "refund";
2797
2799
  else if (eventType.startsWith("payment_intent.")) objectKind = "payment_intent";
2800
+ else if (eventType.startsWith("charge.dispute.")) objectKind = "dispute";
2798
2801
  const customerId = refId(object2.customer, "cus_");
2799
2802
  const subscriptionId = objectKind === "subscription" ? providerId(objectId, "sub_") : subscriptionRef(object2);
2800
2803
  const invoiceId = objectKind === "invoice" ? providerId(objectId, "in_") : refId(object2.invoice, "in_");
@@ -2804,6 +2807,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2804
2807
  const amountCents = integer(eventType === "charge.refunded" ? object2.amount_refunded : object2.amount);
2805
2808
  const currency = text(object2.currency)?.toLowerCase();
2806
2809
  const status2 = text(object2.status);
2810
+ const disputeReason = objectKind === "dispute" ? text(object2.reason) : void 0;
2807
2811
  return {
2808
2812
  eventId,
2809
2813
  digest,
@@ -2822,6 +2826,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
2822
2826
  ...invoiceId ? { invoiceId } : {},
2823
2827
  ...chargeId ? { chargeId } : {},
2824
2828
  ...paymentIntentId ? { paymentIntentId } : {},
2829
+ ...disputeReason ? { disputeReason } : {},
2825
2830
  ...price,
2826
2831
  ...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
2827
2832
  ...status2 ? { status: status2 } : {},
@@ -2864,6 +2869,7 @@ function ownRef(fact) {
2864
2869
  if (fact.objectKind === "subscription") return `subscription:${fact.objectId}`;
2865
2870
  if (fact.objectKind === "invoice") return `invoice:${fact.objectId}`;
2866
2871
  if (fact.objectKind === "payment_intent") return `payment_intent:${fact.objectId}`;
2872
+ if (fact.objectKind === "dispute") return `dispute:${fact.objectId}`;
2867
2873
  if (fact.eventType.startsWith("refund.")) return `refund:${fact.objectId}`;
2868
2874
  return fact.eventType === "charge.refunded" ? `charge:${fact.objectId}` : null;
2869
2875
  }
@@ -2919,6 +2925,7 @@ function projectNetworkCommercialEvents(input, sources) {
2919
2925
  continue;
2920
2926
  }
2921
2927
  const source = assignments.get(fact.eventId);
2928
+ if (!source && fact.objectKind === "dispute") continue;
2922
2929
  if (!source) {
2923
2930
  const direct = directSource(fact, sources);
2924
2931
  const keys2 = new Set(objectRefs(fact).flatMap((ref) => [...refs.get(ref) ?? []]));
@@ -2935,6 +2942,7 @@ function projectNetworkCommercialEvents(input, sources) {
2935
2942
  const subscriptions = rows3.filter((row2) => row2.objectKind === "subscription");
2936
2943
  const invoices = rows3.filter((row2) => row2.objectKind === "invoice");
2937
2944
  const seatPayments = rows3.filter((row2) => row2.objectKind === "payment_intent" && row2.seatId && row2.membershipId && !row2.invoiceId);
2945
+ const disputes = rows3.filter((row2) => row2.objectKind === "dispute");
2938
2946
  const refunds = rows3.filter((row2) => row2.objectKind === "refund" && row2.eventType.startsWith("refund."));
2939
2947
  for (const charge of rows3.filter((row2) => row2.eventType === "charge.refunded")) {
2940
2948
  if (!refunds.some((refund) => refund.chargeId === charge.objectId)) {
@@ -2955,6 +2963,7 @@ function projectNetworkCommercialEvents(input, sources) {
2955
2963
  for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
2956
2964
  return {
2957
2965
  ...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
2966
+ ...disputes.length ? { disputes } : {},
2958
2967
  customers,
2959
2968
  subscriptions,
2960
2969
  invoices,
@@ -4558,7 +4567,7 @@ async function refundNetworkMembership(db, input, secretKey, now) {
4558
4567
  }
4559
4568
 
4560
4569
  // src/membership-provider-lifecycle.ts
4561
- var import_stripe4 = require("@odla-ai/stripe");
4570
+ var import_stripe5 = require("@odla-ai/stripe");
4562
4571
 
4563
4572
  // src/membership-seat-reservation.ts
4564
4573
  async function namedSeatRecipientClaimId(input, email) {
@@ -4656,6 +4665,9 @@ function namedSeatRecipientState(seat) {
4656
4665
  return seat.refundReason === "declined" ? "declined" : "refunded";
4657
4666
  case "canceled":
4658
4667
  return "canceled";
4668
+ // A card-network dispute on the seat's own payment pauses the recipient until it is won or re-charged.
4669
+ case "payment_disputed":
4670
+ return "disputed";
4659
4671
  default:
4660
4672
  return "payment_pending";
4661
4673
  }
@@ -4849,12 +4861,174 @@ async function reconcileMembershipRenewal(db, input, now) {
4849
4861
  return membershipFail("membership_renewal_retry_required");
4850
4862
  }
4851
4863
 
4864
+ // src/membership-dispute-authority.ts
4865
+ var import_stripe4 = require("@odla-ai/stripe");
4866
+
4867
+ // src/membership-dispute-transition.ts
4868
+ var MEMBERSHIP_DISPUTED_STATUS = "payment_disputed";
4869
+ var DISPUTE_WON_STATUSES = ["won", "warning_closed"];
4870
+ var CLOSED_STATUSES = [...DISPUTE_WON_STATUSES, "lost"];
4871
+ function disputeTransition(row2, fact, eventId, now, terminal) {
4872
+ const status2 = String(fact.status ?? "unknown"), closed = CLOSED_STATUSES.includes(status2);
4873
+ const sameDispute = row2.disputeId === fact.objectId;
4874
+ if (sameDispute && Number(row2.disputeClosedAt ?? 0) > 0 && !closed) return null;
4875
+ const won = DISPUTE_WON_STATUSES.includes(status2);
4876
+ const suspended = row2.status === MEMBERSHIP_DISPUTED_STATUS, done = terminal.includes(String(row2.status));
4877
+ const restored = String(row2.disputePriorStatus ?? (row2.journeyApprovedAt ? "active" : "paid_pending_approval"));
4878
+ const next = done ? String(row2.status) : won ? suspended ? restored : String(row2.status) : MEMBERSHIP_DISPUTED_STATUS;
4879
+ return {
4880
+ status: next,
4881
+ disputeId: fact.objectId,
4882
+ disputeStatus: status2,
4883
+ disputeEventId: eventId,
4884
+ disputeClosedAt: closed ? now : 0,
4885
+ ...fact.disputeReason ? { disputeReason: fact.disputeReason } : {},
4886
+ ...fact.amountCents !== void 0 ? { disputeAmountCents: fact.amountCents } : {},
4887
+ ...sameDispute ? {} : { disputedAt: now },
4888
+ ...next === MEMBERSHIP_DISPUTED_STATUS && !suspended ? { disputePriorStatus: String(row2.status) } : {},
4889
+ revision: Number(row2.revision) + 1,
4890
+ updatedAt: now
4891
+ };
4892
+ }
4893
+
4894
+ // src/membership-seat-dispute.ts
4895
+ var text3 = (value) => typeof value === "string" && value ? value : void 0;
4896
+ var TERMINAL = ["payment_pending", "payment_processing", "payment_canceling", "refund_pending", "refunded", "canceled"];
4897
+ var namedSeatDisputeStanding = (seat, applied) => ({ seatId: String(seat.id), status: String(seat.status), ...text3(seat.disputeStatus) ? { disputeStatus: text3(seat.disputeStatus) } : {}, applied });
4898
+ function disputedChargeCoversSeat(charge, seat) {
4899
+ return charge.seatId === seat.id && charge.customerId === seat.providerCustomerId && charge.paymentIntentId !== void 0 && (charge.paymentIntentId === seat.providerPaymentIntentId || charge.paymentIntentId === seat.rechargePaymentIntentId);
4900
+ }
4901
+ async function reconcileNamedSeatDispute(db, input, now, charge, evidence) {
4902
+ if (!charge.seatId) membershipFail("membership_dispute_scope_mismatch");
4903
+ const membership = async (applied, seat) => {
4904
+ const id11 = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", id11);
4905
+ requireMembershipScope(member, input);
4906
+ return {
4907
+ membershipId: id11,
4908
+ status: String(member.status),
4909
+ termEndsAt: Number(member.termEndsAt),
4910
+ graceEndsAt: Number(member.graceEndsAt ?? 0),
4911
+ autoRenew: member.autoRenew === true,
4912
+ applied,
4913
+ ...text3(member.disputeStatus) ? { disputeStatus: text3(member.disputeStatus) } : {},
4914
+ ...seat ? { seat: namedSeatDisputeStanding(seat, applied) } : {}
4915
+ };
4916
+ };
4917
+ for (let attempt = 0; attempt < 8; attempt++) {
4918
+ const state = await loadMembershipAuthorityState(db, input), seat = await membershipLoad(db, "networkSeat", charge.seatId);
4919
+ if (!seat) return membership(false);
4920
+ requireMembershipScope(seat, input);
4921
+ if (charge.customerId !== seat.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
4922
+ if (!disputedChargeCoversSeat(charge, seat) || seat.disputeEventId === input.eventId) return membership(seat.disputeEventId === input.eventId, seat);
4923
+ const attrs = disputeTransition(seat, evidence.fact, input.eventId, now, TERMINAL);
4924
+ if (!attrs) return membership(false, seat);
4925
+ try {
4926
+ await db.transact([membershipUpdate("networkSeat", String(seat.id), attrs)], { mutationId: `seat-dispute:${seat.id}:${input.eventId}`, guards: [
4927
+ { ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } },
4928
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
4929
+ { ns: "networkCommercialEvents", id: String(evidence.row.id), exists: true, equals: { digest: evidence.fact.digest } },
4930
+ evidence.guard
4931
+ ] });
4932
+ } catch (error) {
4933
+ if (error.code !== "transact_guard_failed") throw error;
4934
+ continue;
4935
+ }
4936
+ const saved = await membershipLoad(db, "networkSeat", String(seat.id));
4937
+ requireMembershipScope(saved, input);
4938
+ return membership(true, saved);
4939
+ }
4940
+ return membershipFail("membership_dispute_retry_required");
4941
+ }
4942
+
4943
+ // src/membership-dispute-authority.ts
4944
+ var TERMINAL2 = ["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"];
4945
+ var text4 = (value) => typeof value === "string" && value ? value : void 0;
4946
+ var refOf = (value) => typeof value === "string" ? text4(value) : text4(value?.id);
4947
+ async function readDisputedCharge(secretKey, chargeId, fetch2) {
4948
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId)) membershipFail("invalid_dispute_charge");
4949
+ const config = { secretKey, ...fetch2 ? { fetch: fetch2 } : {} };
4950
+ const charge = await (0, import_stripe4.stripeRequest)(config, "GET", `/v1/charges/${chargeId}`);
4951
+ if (!charge.ok || charge.body.id !== chargeId) membershipFail("membership_dispute_provider_unavailable");
4952
+ const customerId = refOf(charge.body.customer), invoiceId = refOf(charge.body.invoice), paymentIntentId = refOf(charge.body.payment_intent);
4953
+ if (!customerId) membershipFail("membership_dispute_provider_unavailable");
4954
+ let subscriptionId;
4955
+ if (invoiceId) {
4956
+ const invoice = await (0, import_stripe4.stripeRequest)(config, "GET", `/v1/invoices/${invoiceId}`);
4957
+ if (!invoice.ok || invoice.body.id !== invoiceId) membershipFail("membership_dispute_provider_unavailable");
4958
+ subscriptionId = refOf(invoice.body.subscription) ?? refOf(invoice.body.parent?.subscription_details?.subscription);
4959
+ }
4960
+ const seatId = text4(charge.body.metadata?.seatId);
4961
+ return {
4962
+ chargeId,
4963
+ customerId,
4964
+ ...invoiceId ? { invoiceId } : {},
4965
+ ...subscriptionId ? { subscriptionId } : {},
4966
+ ...paymentIntentId ? { paymentIntentId } : {},
4967
+ ...seatId ? { seatId } : {}
4968
+ };
4969
+ }
4970
+ function disputedChargeCoversMembership(charge, member) {
4971
+ 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);
4972
+ }
4973
+ async function reconcileMembershipDispute(db, input, now, secretKey, resolved) {
4974
+ membershipClock(now);
4975
+ const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4976
+ const fact = row2?.fact;
4977
+ const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4978
+ 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");
4979
+ const id11 = await networkMembershipId(input);
4980
+ const receipt = (value, applied = value.disputeEventId === input.eventId) => ({
4981
+ membershipId: id11,
4982
+ status: String(value.status),
4983
+ termEndsAt: Number(value.termEndsAt),
4984
+ graceEndsAt: Number(value.graceEndsAt ?? 0),
4985
+ autoRenew: value.autoRenew === true,
4986
+ ...text4(value.disputeStatus) ? { disputeStatus: text4(value.disputeStatus) } : {},
4987
+ applied
4988
+ });
4989
+ const known = await membershipLoad(db, "networkMembership", id11);
4990
+ requireMembershipScope(known, input);
4991
+ if (known.disputeEventId === input.eventId) return receipt(known);
4992
+ const seatKnown = known.namedSeatId ? await membershipLoad(db, "networkSeat", String(known.namedSeatId)) : void 0;
4993
+ if (seatKnown?.disputeEventId === input.eventId) return { ...receipt(known, true), seat: namedSeatDisputeStanding(seatKnown, true) };
4994
+ const charge = resolved && resolved.chargeId === fact.chargeId ? resolved : await readDisputedCharge(secretKey, fact.chargeId);
4995
+ if (charge.customerId !== known.providerCustomerId) membershipFail("membership_dispute_scope_mismatch");
4996
+ if (charge.seatId) return reconcileNamedSeatDispute(db, input, now, charge, { row: row2, fact, guard: evidenceGuard });
4997
+ for (let attempt = 0; attempt < 8; attempt++) {
4998
+ const state = await loadMembershipAuthorityState(db, input), member = await membershipLoad(db, "networkMembership", id11);
4999
+ requireMembershipScope(member, input);
5000
+ if (!disputedChargeCoversMembership(charge, member) || member.disputeEventId === input.eventId) return receipt(member);
5001
+ const attrs = disputeTransition(member, fact, input.eventId, now, TERMINAL2);
5002
+ if (!attrs) return receipt(member, false);
5003
+ try {
5004
+ await db.transact([membershipUpdate("networkMembership", id11, attrs)], { mutationId: `membership-dispute:${id11}:${input.eventId}`, guards: [
5005
+ { ns: "networkMembership", id: id11, exists: true, equals: { revision: member.revision } },
5006
+ { ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
5007
+ { ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
5008
+ evidenceGuard
5009
+ ] });
5010
+ } catch (error) {
5011
+ if (error.code !== "transact_guard_failed") throw error;
5012
+ continue;
5013
+ }
5014
+ const saved = await membershipLoad(db, "networkMembership", id11);
5015
+ requireMembershipScope(saved, input);
5016
+ return receipt(saved, true);
5017
+ }
5018
+ return membershipFail("membership_dispute_retry_required");
5019
+ }
5020
+
4852
5021
  // src/membership-provider-lifecycle.ts
4853
5022
  async function reconcileMembershipBilling(db, input, now, stripeSecretName = "stripe_secret_key") {
4854
5023
  membershipClock(now);
4855
5024
  const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
4856
5025
  const fact = row2?.fact;
4857
5026
  if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
5027
+ if (fact?.objectKind === "dispute") {
5028
+ const secret = await getVaultSecret(db, stripeSecretName);
5029
+ if (!secret) membershipFail("membership_billing_provider_unavailable");
5030
+ return reconcileMembershipDispute(db, input, now, secret);
5031
+ }
4858
5032
  const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
4859
5033
  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({
4860
5034
  appId: input.appId,
@@ -4897,7 +5071,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
4897
5071
  }
4898
5072
  if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
4899
5073
  if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
4900
- const result = await (0, import_stripe4.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
5074
+ const result = await (0, import_stripe5.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
4901
5075
  const sub = result.body;
4902
5076
  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)) {
4903
5077
  membershipFail("membership_billing_provider_mismatch");
@@ -4954,7 +5128,7 @@ async function expireRestartedMembershipSeat(db, target, member, now) {
4954
5128
  }
4955
5129
 
4956
5130
  // src/membership-restart-authority.ts
4957
- var import_stripe5 = require("@odla-ai/stripe");
5131
+ var import_stripe6 = require("@odla-ai/stripe");
4958
5132
 
4959
5133
  // src/stripe-event-digest.ts
4960
5134
  async function stripePayloadDigest(value) {
@@ -4969,14 +5143,14 @@ function membershipCanRestart(member, now) {
4969
5143
  }
4970
5144
  async function stopPreviousSubscription(member, target, secret) {
4971
5145
  const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
4972
- const result = await (0, import_stripe5.stripeRequest)({ secretKey: secret }, "GET", path);
5146
+ const result = await (0, import_stripe6.stripeRequest)({ secretKey: secret }, "GET", path);
4973
5147
  const sub = result.body;
4974
5148
  if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
4975
5149
  ...stripePartitionMetadata(target),
4976
5150
  applicationId: target.applicationId
4977
5151
  }).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
4978
5152
  if (sub.status === "canceled") return;
4979
- const canceled = await (0, import_stripe5.stripeRequest)(
5153
+ const canceled = await (0, import_stripe6.stripeRequest)(
4980
5154
  { secretKey: secret },
4981
5155
  "DELETE",
4982
5156
  path,
@@ -5077,16 +5251,16 @@ async function verifyMembershipAccessReceipt(value, input, now) {
5077
5251
  }
5078
5252
 
5079
5253
  // src/membership-seat-stripe.ts
5080
- var import_stripe7 = require("@odla-ai/stripe");
5254
+ var import_stripe8 = require("@odla-ai/stripe");
5081
5255
 
5082
5256
  // src/subscription-provider-contract.ts
5083
- var import_stripe6 = require("@odla-ai/stripe");
5257
+ var import_stripe7 = require("@odla-ai/stripe");
5084
5258
  function subscriptionContractMismatch() {
5085
5259
  throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
5086
5260
  }
5087
5261
  async function verifySubscriptionPrice(secretKey, input, resume = false) {
5088
5262
  if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
5089
- const result = await (0, import_stripe6.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5263
+ const result = await (0, import_stripe7.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
5090
5264
  const price = result.body;
5091
5265
  if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
5092
5266
  return typeof price.product === "string" ? price.product : void 0;
@@ -5104,7 +5278,7 @@ var metadataFor = (seat) => ({
5104
5278
  });
5105
5279
  var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
5106
5280
  async function read(sk, path, params) {
5107
- const response = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "GET", path, params);
5281
+ const response = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "GET", path, params);
5108
5282
  if (!response.ok) return reject3();
5109
5283
  return response.body;
5110
5284
  }
@@ -5141,11 +5315,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
5141
5315
  }
5142
5316
  await subscriptionForSeat(sk, seat);
5143
5317
  const metadata = metadataFor(seat);
5144
- const result = seat.providerPaymentIntentId ? await (0, import_stripe7.stripeRequest)(
5318
+ const result = seat.providerPaymentIntentId ? await (0, import_stripe8.stripeRequest)(
5145
5319
  { secretKey: sk },
5146
5320
  "GET",
5147
5321
  `/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
5148
- ) : await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
5322
+ ) : await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
5149
5323
  amount: seat.amountCents,
5150
5324
  currency: seat.currency,
5151
5325
  customer: seat.providerCustomerId,
@@ -5167,7 +5341,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
5167
5341
  return String(existing.id);
5168
5342
  }
5169
5343
  if (seat.combinedInvoice) reject3();
5170
- const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
5344
+ const result = await (0, import_stripe8.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
5171
5345
  subscription: seat.providerSubscriptionId,
5172
5346
  price: seat.priceId,
5173
5347
  quantity: 1,
@@ -5442,10 +5616,10 @@ async function recordRecipient(db, input, now, approve) {
5442
5616
  }
5443
5617
 
5444
5618
  // src/membership-seat-refund-stripe.ts
5445
- var import_stripe9 = require("@odla-ai/stripe");
5619
+ var import_stripe10 = require("@odla-ai/stripe");
5446
5620
 
5447
5621
  // src/membership-checkout-seat-refund.ts
5448
- var import_stripe8 = require("@odla-ai/stripe");
5622
+ var import_stripe9 = require("@odla-ai/stripe");
5449
5623
  var fail4 = (code = "named_seat_refund_mismatch") => {
5450
5624
  throw Object.assign(new Error(code), { code });
5451
5625
  };
@@ -5454,7 +5628,7 @@ async function refundCheckoutNamedSeat(secretKey, seat) {
5454
5628
  const invoiceRef = seat.combinedInvoice;
5455
5629
  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();
5456
5630
  const call = async (method, path, params, key3) => {
5457
- const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5631
+ const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
5458
5632
  if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
5459
5633
  return result.body;
5460
5634
  };
@@ -5527,7 +5701,7 @@ async function refundNamedSeatStripePayment(secretKey, seat) {
5527
5701
  if (seat.combinedInvoice) return refundCheckoutNamedSeat(secretKey, seat);
5528
5702
  if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail5();
5529
5703
  const call = async (method, path, params, idempotencyKey) => {
5530
- const result = await (0, import_stripe9.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5704
+ const result = await (0, import_stripe10.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
5531
5705
  if (!result.ok || !result.body) return fail5("named_seat_refund_provider_unavailable");
5532
5706
  return result.body;
5533
5707
  };
@@ -6229,7 +6403,7 @@ async function readBoundedJson(response, signal, maxBytes, label) {
6229
6403
  };
6230
6404
  signal.addEventListener("abort", cancel, { once: true });
6231
6405
  let size = 0;
6232
- let text6 = "";
6406
+ let text8 = "";
6233
6407
  const decoder = new TextDecoder();
6234
6408
  try {
6235
6409
  while (true) {
@@ -6239,10 +6413,10 @@ async function readBoundedJson(response, signal, maxBytes, label) {
6239
6413
  if (done) break;
6240
6414
  size += value.byteLength;
6241
6415
  if (size > maxBytes) throw new TypeError(`${label} exceeds limit`);
6242
- text6 += decoder.decode(value, { stream: true });
6416
+ text8 += decoder.decode(value, { stream: true });
6243
6417
  }
6244
6418
  try {
6245
- return JSON.parse(text6 + decoder.decode());
6419
+ return JSON.parse(text8 + decoder.decode());
6246
6420
  } catch {
6247
6421
  throw new TypeError(`invalid ${label}`);
6248
6422
  }
@@ -6456,7 +6630,7 @@ async function requestMembershipRefund(options) {
6456
6630
  async function requestMembershipRenewal(options) {
6457
6631
  const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
6458
6632
  const row2 = result.renewal;
6459
- 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)) {
6633
+ 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)) {
6460
6634
  throw new Error("invalid membership renewal receipt");
6461
6635
  }
6462
6636
  return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
@@ -6516,7 +6690,7 @@ async function requestMembershipCancellation(options) {
6516
6690
  }
6517
6691
 
6518
6692
  // src/membership-seat-cancel.ts
6519
- var import_stripe10 = require("@odla-ai/stripe");
6693
+ var import_stripe11 = require("@odla-ai/stripe");
6520
6694
  async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6521
6695
  membershipClock(now);
6522
6696
  const state = await loadMembershipAuthorityState(db, target);
@@ -6544,7 +6718,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6544
6718
  }
6545
6719
  const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
6546
6720
  const request = async (method, path, params) => {
6547
- const result = await (0, import_stripe10.stripeRequest)(
6721
+ const result = await (0, import_stripe11.stripeRequest)(
6548
6722
  { secretKey },
6549
6723
  method,
6550
6724
  path,
@@ -6557,7 +6731,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
6557
6731
  let payment;
6558
6732
  if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
6559
6733
  else {
6560
- const snapshot = await listStripeObjects((0, import_stripe10.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
6734
+ const snapshot = await listStripeObjects((0, import_stripe11.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
6561
6735
  const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
6562
6736
  if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
6563
6737
  payment = matches2[0];
@@ -6729,6 +6903,19 @@ var id10 = () => attr9("string", false, true, true);
6729
6903
  var key2 = (optional = false) => attr9("string", optional, false, true);
6730
6904
  var scope = () => ({ appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), applicationId: key2(), skuId: key2() });
6731
6905
  var times = () => ({ revision: attr9("number"), createdAt: attr9("number"), updatedAt: attr9("number") });
6906
+ var dispute = () => ({
6907
+ disputeId: key2(true),
6908
+ disputeStatus: attr9("string", true),
6909
+ disputeReason: attr9("string", true),
6910
+ disputeAmountCents: attr9("number", true),
6911
+ disputedAt: attr9("number", true),
6912
+ disputeClosedAt: attr9("number", true),
6913
+ disputePriorStatus: attr9("string", true),
6914
+ disputeEventId: key2(true),
6915
+ rechargePaymentIntentId: key2(true),
6916
+ rechargedAt: attr9("number", true),
6917
+ rechargeAttempt: attr9("json", true)
6918
+ });
6732
6919
  var membershipAuthorityEntities = {
6733
6920
  networkMembership: { attrs: {
6734
6921
  id: id10(),
@@ -6768,7 +6955,8 @@ var membershipAuthorityEntities = {
6768
6955
  graceEndsAt: attr9("number", true),
6769
6956
  ...times(),
6770
6957
  refundRequestedAt: attr9("number", true),
6771
- restartId: key2(true)
6958
+ restartId: key2(true),
6959
+ ...dispute()
6772
6960
  } },
6773
6961
  membershipRestart: { attrs: {
6774
6962
  id: id10(),
@@ -6844,7 +7032,8 @@ var membershipAuthorityEntities = {
6844
7032
  refundedCents: attr9("number", true),
6845
7033
  providerRefundIds: attr9("json", true),
6846
7034
  regiftedAt: attr9("number", true),
6847
- formerRecipients: attr9("json", true)
7035
+ formerRecipients: attr9("json", true),
7036
+ ...dispute()
6848
7037
  } },
6849
7038
  networkSeatRecipient: { attrs: { id: id10(), appId: key2(), chapterId: key2(), environment: key2(), runtime: key2(), seatId: key2(), recipientEmailNormalized: key2(), createdAt: attr9("number") } },
6850
7039
  membershipEffect: { attrs: {
@@ -7204,7 +7393,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
7204
7393
  function unknownKeys2(value, allowed2, path, errors) {
7205
7394
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
7206
7395
  }
7207
- function text3(value, path, errors, max = MAX_TEXT2, required = true) {
7396
+ function text5(value, path, errors, max = MAX_TEXT2, required = true) {
7208
7397
  if (value === void 0 && !required) return void 0;
7209
7398
  if (typeof value !== "string" || required && !value.trim()) {
7210
7399
  errors.push(`${path} must be a non-empty string`);
@@ -7230,7 +7419,7 @@ function scopeValue(value, path, errors) {
7230
7419
  return { kind: raw2.kind };
7231
7420
  }
7232
7421
  if (raw2.kind === "chapter") {
7233
- const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7422
+ const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
7234
7423
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
7235
7424
  return { kind: "chapter", chapterId };
7236
7425
  }
@@ -7244,22 +7433,22 @@ function resourceValue(value, path, errors) {
7244
7433
  return null;
7245
7434
  }
7246
7435
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
7247
- const id11 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
7436
+ const id11 = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
7248
7437
  if (id11 && !ID2.test(id11)) errors.push(`${path}.id must be lowercase kebab-case`);
7249
- const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
7438
+ const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
7250
7439
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
7251
- const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
7252
- const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
7253
- const status2 = text3(raw2.status, `${path}.status`, errors) ?? "";
7440
+ const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
7441
+ const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
7442
+ const status2 = text5(raw2.status, `${path}.status`, errors) ?? "";
7254
7443
  if (!STATUSES.has(status2)) errors.push(`${path}.status must be draft, published, or archived`);
7255
7444
  const revision = integer3(raw2.revision, `${path}.revision`, errors);
7256
7445
  const scope2 = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
7257
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
7446
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
7258
7447
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
7259
7448
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
7260
7449
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
7261
- const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
7262
- const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
7450
+ const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
7451
+ const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
7263
7452
  if (url) {
7264
7453
  try {
7265
7454
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -7271,9 +7460,9 @@ function resourceValue(value, path, errors) {
7271
7460
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
7272
7461
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
7273
7462
  const brandAsset = asset ? {
7274
- bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
7275
- assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
7276
- ...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
7463
+ bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
7464
+ assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
7465
+ ...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
7277
7466
  } : void 0;
7278
7467
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
7279
7468
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -7288,17 +7477,17 @@ function resourceValue(value, path, errors) {
7288
7477
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
7289
7478
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
7290
7479
  const promotedFrom = promotedRaw ? {
7291
- chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7292
- resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
7480
+ chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
7481
+ resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
7293
7482
  resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
7294
7483
  documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
7295
- promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7484
+ promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
7296
7485
  promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
7297
7486
  } : void 0;
7298
7487
  const provenance = {
7299
- createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7488
+ createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
7300
7489
  createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
7301
- updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7490
+ updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
7302
7491
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
7303
7492
  ...promotedFrom ? { promotedFrom } : {}
7304
7493
  };
@@ -7317,7 +7506,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
7317
7506
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
7318
7507
  const sourceType = sourceRaw?.type;
7319
7508
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
7320
- const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7509
+ const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
7321
7510
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
7322
7511
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
7323
7512
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -7325,8 +7514,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
7325
7514
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
7326
7515
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
7327
7516
  const target = targetRaw ? {
7328
- appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7329
- environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
7517
+ appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
7518
+ environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
7330
7519
  } : void 0;
7331
7520
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
7332
7521
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -7421,9 +7610,9 @@ async function createBuilderLibraryEnvelope(document) {
7421
7610
  }
7422
7611
 
7423
7612
  // src/email-retirement.ts
7424
- function isRetiredEmailCopy(subject, text6) {
7613
+ function isRetiredEmailCopy(subject, text8) {
7425
7614
  const subjectCopy = subject.trimStart().replace(/^\[dev\]\s*/i, "");
7426
- const bodyCopy = text6.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7615
+ const bodyCopy = text8.trimStart().replace(/^\(dev redirect; original recipient: [^\r\n]*\)\r?\n\r?\n/, "");
7427
7616
  return /^\(\s*unused\s*:/i.test(subjectCopy) || /^Unused\.\s*Switched off\b/i.test(bodyCopy);
7428
7617
  }
7429
7618
 
@@ -7501,10 +7690,10 @@ function planDelivery(input) {
7501
7690
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
7502
7691
  if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7503
7692
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
7504
- const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
7693
+ const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
7505
7694
 
7506
7695
  ` + renderedText : renderedText;
7507
- return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
7696
+ return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
7508
7697
  }
7509
7698
 
7510
7699
  // src/email-readiness.ts
@@ -7778,7 +7967,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
7778
7967
  }
7779
7968
 
7780
7969
  // src/payments.ts
7781
- var import_stripe11 = require("@odla-ai/stripe");
7970
+ var import_stripe12 = require("@odla-ai/stripe");
7782
7971
  function paymentsReady(group, hasSecretKey) {
7783
7972
  return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
7784
7973
  }
@@ -7839,6 +8028,17 @@ function findGiftRef(obj) {
7839
8028
  }
7840
8029
  function normalizeWebhookEvent(event2, options = {}) {
7841
8030
  const obj = event2.data?.object ?? {};
8031
+ if (event2.type.startsWith("charge.dispute.")) {
8032
+ const chargeId = typeof obj.charge === "string" ? obj.charge : String(obj.charge?.id ?? "");
8033
+ if (!/^ch_[A-Za-z0-9]+$/.test(chargeId) || typeof obj.id !== "string" || !obj.id) return { kind: "ignored", type: event2.type };
8034
+ return {
8035
+ kind: "disputed",
8036
+ chargeId,
8037
+ disputeId: obj.id,
8038
+ disputeStatus: typeof obj.status === "string" ? obj.status : "unknown",
8039
+ ...typeof obj.payment_intent === "string" ? { paymentIntentId: obj.payment_intent } : {}
8040
+ };
8041
+ }
7842
8042
  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 };
7843
8043
  const gift = findGiftRef(obj);
7844
8044
  if (gift.giftIntentId) {
@@ -8149,7 +8349,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
8149
8349
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
8150
8350
  var CURRENCY = /^[a-z]{3}$/;
8151
8351
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
8152
- var text4 = (value, path, errors, max) => {
8352
+ var text6 = (value, path, errors, max) => {
8153
8353
  if (typeof value !== "string" || !value.trim()) {
8154
8354
  errors.push(`${path} is required`);
8155
8355
  return "";
@@ -8177,23 +8377,23 @@ function validateGiftOfferCatalog(input, tierIds) {
8177
8377
  }
8178
8378
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
8179
8379
  for (const key3 of Object.keys(item)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
8180
- const id11 = text4(item.id, `${path}.id`, errors, 80);
8380
+ const id11 = text6(item.id, `${path}.id`, errors, 80);
8181
8381
  if (id11 && !ID4.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
8182
8382
  if (ids2.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
8183
8383
  ids2.add(id11);
8184
8384
  const targetRaw = record3(item.target);
8185
8385
  let target = { type: "entitlement", entitlementKey: "invalid" };
8186
8386
  if (targetRaw?.type === "tier") {
8187
- const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8387
+ const tierId = text6(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
8188
8388
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
8189
8389
  target = { type: "tier", tierId };
8190
8390
  } else if (targetRaw?.type === "entitlement") {
8191
- const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8391
+ const entitlementKey = text6(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
8192
8392
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
8193
8393
  target = { type: "entitlement", entitlementKey };
8194
8394
  } else errors.push(`${path}.target.type must be tier or entitlement`);
8195
8395
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
8196
- const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8396
+ const stripePriceId = text6(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
8197
8397
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
8198
8398
  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`);
8199
8399
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -8215,9 +8415,9 @@ function validateGiftOfferCatalog(input, tierIds) {
8215
8415
  redemptionLifetimeHours: lifetime,
8216
8416
  ...activeFrom !== void 0 ? { activeFrom } : {},
8217
8417
  ...activeUntil !== void 0 ? { activeUntil } : {},
8218
- title: text4(item.title, `${path}.title`, errors, 160),
8219
- description: text4(item.description, `${path}.description`, errors, 2e3),
8220
- ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
8418
+ title: text6(item.title, `${path}.title`, errors, 160),
8419
+ description: text6(item.description, `${path}.description`, errors, 2e3),
8420
+ ctaLabel: text6(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
8221
8421
  });
8222
8422
  }
8223
8423
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -8676,9 +8876,9 @@ function subAnnualCents(sub) {
8676
8876
  }
8677
8877
 
8678
8878
  // src/payments-stripe.ts
8679
- var import_stripe12 = require("@odla-ai/stripe");
8879
+ var import_stripe13 = require("@odla-ai/stripe");
8680
8880
  async function stripeCall(sk, method, path, params, idempotencyKey) {
8681
- return (0, import_stripe12.stripeRequest)(
8881
+ return (0, import_stripe13.stripeRequest)(
8682
8882
  { secretKey: sk },
8683
8883
  method,
8684
8884
  path,
@@ -9300,10 +9500,10 @@ function boundedString(value, path, errors, max, required = false) {
9300
9500
  errors.push(`${path} must be a string`);
9301
9501
  return void 0;
9302
9502
  }
9303
- const text6 = value.trim();
9304
- if (required && !text6) errors.push(`${path} is required`);
9305
- if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9306
- return text6;
9503
+ const text8 = value.trim();
9504
+ if (required && !text8) errors.push(`${path} is required`);
9505
+ if (text8.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
9506
+ return text8;
9307
9507
  }
9308
9508
  function validateMembershipPageDocument(input, options = {}) {
9309
9509
  const errors = [];
@@ -9352,8 +9552,8 @@ function validateMembershipPageDocument(input, options = {}) {
9352
9552
  else {
9353
9553
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
9354
9554
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
9355
- const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9356
- return text6 ? [text6] : [];
9555
+ const text8 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
9556
+ return text8 ? [text8] : [];
9357
9557
  });
9358
9558
  }
9359
9559
  }
@@ -9452,7 +9652,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
9452
9652
  function unknownKeys3(value, allowed2, path, errors) {
9453
9653
  for (const key3 of Object.keys(value)) if (!allowed2.has(key3)) errors.push(`${path}.${key3} is not supported`);
9454
9654
  }
9455
- function text5(value, path, errors, max = 65536) {
9655
+ function text7(value, path, errors, max = 65536) {
9456
9656
  if (typeof value !== "string" || !value.trim()) {
9457
9657
  errors.push(`${path} is required`);
9458
9658
  return "";
@@ -9475,11 +9675,11 @@ function tiersOf(value, errors) {
9475
9675
  return [];
9476
9676
  }
9477
9677
  unknownKeys3(item, TIER_KEYS2, path, errors);
9478
- const id11 = text5(item.id, `${path}.id`, errors, 80);
9678
+ const id11 = text7(item.id, `${path}.id`, errors, 80);
9479
9679
  if (id11 && !TIER.test(id11)) errors.push(`${path}.id must be a lowercase kebab-case id`);
9480
9680
  if (seen.has(id11)) errors.push(`${path}.id duplicates "${id11}"`);
9481
9681
  seen.add(id11);
9482
- const name = text5(item.name, `${path}.name`, errors, 160);
9682
+ const name = text7(item.name, `${path}.name`, errors, 160);
9483
9683
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
9484
9684
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
9485
9685
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -9497,7 +9697,7 @@ function tiersOf(value, errors) {
9497
9697
  if (item.interval !== void 0 && item.interval !== "year" && item.interval !== "month") {
9498
9698
  errors.push(`${path}.interval must be year or month`);
9499
9699
  }
9500
- const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
9700
+ const blurb = item.blurb === void 0 ? void 0 : text7(item.blurb, `${path}.blurb`, errors, 4e3);
9501
9701
  return [{
9502
9702
  id: id11,
9503
9703
  name,
@@ -9555,7 +9755,7 @@ function policyOf(value, errors, tiers2) {
9555
9755
  const result = {};
9556
9756
  for (const key3 of allowed2) {
9557
9757
  if (input[key3] === void 0 && key3 !== "disclaimerText" && key3 !== "refundPolicyText") continue;
9558
- result[key3] = text5(input[key3], `document.policy.${key3}`, errors);
9758
+ result[key3] = text7(input[key3], `document.policy.${key3}`, errors);
9559
9759
  }
9560
9760
  const tierPolicies = {};
9561
9761
  if (input.tierPolicies !== void 0) {
@@ -9570,8 +9770,8 @@ function policyOf(value, errors, tiers2) {
9570
9770
  }
9571
9771
  unknownKeys3(policy2, /* @__PURE__ */ new Set(["refundPolicyText", "trustCopy"]), path, errors);
9572
9772
  tierPolicies[id11] = {
9573
- refundPolicyText: text5(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9574
- ...policy2.trustCopy !== void 0 ? { trustCopy: text5(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9773
+ refundPolicyText: text7(policy2.refundPolicyText, `${path}.refundPolicyText`, errors),
9774
+ ...policy2.trustCopy !== void 0 ? { trustCopy: text7(policy2.trustCopy, `${path}.trustCopy`, errors) } : {}
9575
9775
  };
9576
9776
  }
9577
9777
  }
@@ -9611,9 +9811,9 @@ function validateSignupControlDocument(input) {
9611
9811
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
9612
9812
  const source = recordOf(root.source);
9613
9813
  const target = recordOf(root.target);
9614
- const appId = text5(source?.appId, "document.source.appId", errors, 63);
9615
- const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
9616
- const environment = text5(target?.environment, "document.target.environment", errors, 63);
9814
+ const appId = text7(source?.appId, "document.source.appId", errors, 63);
9815
+ const chapterId = text7(target?.chapterId, "document.target.chapterId", errors, 63);
9816
+ const environment = text7(target?.environment, "document.target.environment", errors, 63);
9617
9817
  if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
9618
9818
  if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
9619
9819
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -9790,9 +9990,9 @@ function markdownBenefits(lines, path, errors) {
9790
9990
  errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
9791
9991
  continue;
9792
9992
  }
9793
- const text6 = (match[1] ?? "").trim();
9794
- if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9795
- if (text6) benefits.push(text6);
9993
+ const text8 = (match[1] ?? "").trim();
9994
+ if (text8.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
9995
+ if (text8) benefits.push(text8);
9796
9996
  }
9797
9997
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
9798
9998
  return benefits.slice(0, MAX_BENEFITS2);