@odla-ai/chapter 0.39.2 → 0.40.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.
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-Dbu9Bbq1.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dbu9Bbq1.js';
4
+ import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-B14OOiYq.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-B14OOiYq.js';
6
6
  import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
7
7
 
8
8
  /** Normalized public auth configuration consumed by the Clerk gate. */
@@ -1,5 +1,5 @@
1
1
  export { AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-Dbu9Bbq1.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-B14OOiYq.js';
3
3
  export { ApiFn, BrandStyle, BrandStyleProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
package/dist/ui/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  timeLabel,
23
23
  tzShort,
24
24
  useTierSelection
25
- } from "../chunk-BIYWPPH6.js";
25
+ } from "../chunk-E5OXQ3DC.js";
26
26
  import {
27
27
  AdminNote,
28
28
  AdminPage,
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-Dbu9Bbq1.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-Dbu9Bbq1.js';
3
+ import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-B14OOiYq.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-B14OOiYq.js';
5
5
  import { FieldType, FieldConditions } from '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -164,6 +164,8 @@ interface PaymentStepProps {
164
164
  applicationId: string;
165
165
  /** Refund-policy copy the applicant must acknowledge to start payment. */
166
166
  refundPolicyText: string;
167
+ /** Plain-language merchant identity shown before the applicant consents. */
168
+ merchantDisclosureText?: string;
167
169
  /** Called once the payment succeeds (or is processing) — advance to booking. */
168
170
  onPaid: (result: {
169
171
  gift: boolean;
@@ -340,6 +342,8 @@ interface JoinConfig {
340
342
  name: string;
341
343
  paymentsReady: boolean;
342
344
  refundPolicyText?: string;
345
+ /** Plain-language merchant identity displayed before payment consent. */
346
+ merchantDisclosureText?: string;
343
347
  /** Offered membership tiers, in display order. Empty on a chapter that
344
348
  * declares none, in which case no tier is posted and nothing renders. */
345
349
  tiers?: readonly JoinTier[];
@@ -22,7 +22,7 @@ import {
22
22
  timeLabel,
23
23
  tzShort,
24
24
  useTierSelection
25
- } from "../../chunk-BIYWPPH6.js";
25
+ } from "../../chunk-E5OXQ3DC.js";
26
26
  import {
27
27
  BrandStyle,
28
28
  ChapterCopyProvider,
@@ -350,6 +350,7 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
350
350
  conditions,
351
351
  disclaimerText: group.disclaimerText ?? "",
352
352
  refundPolicyText: group.refundPolicyText ?? "",
353
+ merchantDisclosureText: group.merchantDisclosureText ?? "",
353
354
  trustCopy: group.trustCopy ?? "",
354
355
  commitmentText: group.commitmentText ?? "",
355
356
  normsText: group.normsText ?? "",
@@ -2353,7 +2354,14 @@ function policyOf(value, errors) {
2353
2354
  errors.push("document.policy must be an object");
2354
2355
  return void 0;
2355
2356
  }
2356
- const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2357
+ const allowed2 = /* @__PURE__ */ new Set([
2358
+ "disclaimerText",
2359
+ "refundPolicyText",
2360
+ "merchantDisclosureText",
2361
+ "trustCopy",
2362
+ "commitmentText",
2363
+ "normsText"
2364
+ ]);
2357
2365
  unknownKeys(input, allowed2, "document.policy", errors);
2358
2366
  const result = {};
2359
2367
  for (const key of allowed2) {
@@ -3382,12 +3390,16 @@ function findApplicationRef(obj) {
3382
3390
  const applicationId = candidates.map(pick).find(Boolean);
3383
3391
  const appId = candidates.map((metadata) => typeof metadata.odlaAppId === "string" ? metadata.odlaAppId : void 0).find(Boolean);
3384
3392
  const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
3393
+ const dataEnvironment = candidates.map((metadata) => typeof metadata.odlaDataEnvironment === "string" ? metadata.odlaDataEnvironment : void 0).find(Boolean);
3394
+ const runtime = candidates.map((metadata) => typeof metadata.odlaRuntime === "string" ? metadata.odlaRuntime : void 0).find(Boolean);
3385
3395
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
3386
3396
  return {
3387
3397
  ...applicationId ? { applicationId } : {},
3388
3398
  ...customerId ? { customerId } : {},
3389
3399
  ...appId ? { appId } : {},
3390
- ...environment ? { environment } : {}
3400
+ ...environment ? { environment } : {},
3401
+ ...dataEnvironment ? { dataEnvironment } : {},
3402
+ ...runtime ? { runtime } : {}
3391
3403
  };
3392
3404
  }
3393
3405
  function findGiftRef(obj) {
@@ -3397,17 +3409,31 @@ function findGiftRef(obj) {
3397
3409
  const subscription = obj.subscription_details ?? parent?.subscription_details;
3398
3410
  const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
3399
3411
  const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
3400
- return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
3412
+ const candidates = [metaOf(obj), metaOf(subscription)];
3413
+ const pickIdentity = (name) => candidates.map((metadata) => typeof metadata[name] === "string" ? String(metadata[name]) : void 0).find(Boolean);
3414
+ const appId = pickIdentity("odlaAppId");
3415
+ const environment = pickIdentity("odlaEnvironment");
3416
+ const dataEnvironment = pickIdentity("odlaDataEnvironment");
3417
+ const runtime = pickIdentity("odlaRuntime");
3418
+ return {
3419
+ ...giftIntentId ? { giftIntentId } : {},
3420
+ providerObjectId: subscriptionId ?? String(obj.id ?? ""),
3421
+ ...appId ? { appId } : {},
3422
+ ...environment ? { environment } : {},
3423
+ ...dataEnvironment ? { dataEnvironment } : {},
3424
+ ...runtime ? { runtime } : {}
3425
+ };
3401
3426
  }
3402
3427
  function normalizeWebhookEvent(event) {
3403
3428
  const obj = event.data?.object ?? {};
3404
3429
  const gift = findGiftRef(obj);
3405
3430
  if (gift.giftIntentId) {
3431
+ const { giftIntentId, providerObjectId, ...identity } = gift;
3406
3432
  if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
3407
- return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
3433
+ return { kind: "gift_paid", giftIntentId, providerObjectId, ...identity };
3408
3434
  }
3409
- if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
3410
- if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
3435
+ if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId, ...identity, reason: "refunded" };
3436
+ if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId, ...identity, reason: "canceled" };
3411
3437
  return { kind: "ignored", type: event.type };
3412
3438
  }
3413
3439
  const ref = findApplicationRef(obj);
@@ -3484,7 +3510,9 @@ function createStripeProvider(config) {
3484
3510
  groupId: input.groupId,
3485
3511
  email: input.email,
3486
3512
  odlaAppId: input.appId,
3487
- odlaEnvironment: input.environment
3513
+ odlaEnvironment: input.environment,
3514
+ odlaDataEnvironment: input.environment,
3515
+ ...input.runtime ? { odlaRuntime: input.runtime } : {}
3488
3516
  };
3489
3517
  let customerId = input.existingCustomerId;
3490
3518
  if (!customerId) {
@@ -3772,12 +3800,16 @@ var clientSecret = (value) => {
3772
3800
  const nested = confirmation?.client_secret ?? intent?.client_secret;
3773
3801
  return typeof nested === "string" && nested ? nested : null;
3774
3802
  };
3775
- async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId) {
3803
+ async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId, runtime) {
3776
3804
  const offer = intent.offerSnapshot;
3777
3805
  const metadata = {
3778
3806
  giftIntentId: intent.id,
3779
3807
  chapterId: intent.chapterId,
3780
3808
  environment: intent.environment,
3809
+ odlaAppId: intent.chapterId,
3810
+ odlaEnvironment: intent.environment,
3811
+ odlaDataEnvironment: intent.environment,
3812
+ ...runtime ? { odlaRuntime: runtime } : {},
3781
3813
  offerId: intent.offerId
3782
3814
  };
3783
3815
  let customerId = existingCustomerId;
@@ -3827,6 +3859,7 @@ async function startGiftIntent(db, input) {
3827
3859
  id: input.id,
3828
3860
  chapterId: input.chapterId,
3829
3861
  environment: input.environment,
3862
+ ...input.runtime ? { runtime: input.runtime } : {},
3830
3863
  purchaserType: input.purchaserType,
3831
3864
  purchaserEmailNormalized: input.purchaserEmailNormalized,
3832
3865
  ...input.purchaserApplicationId ? { purchaserApplicationId: input.purchaserApplicationId } : {},
@@ -3846,7 +3879,7 @@ async function startGiftIntent(db, input) {
3846
3879
  stored = write.duplicate ? first2((await db.query({ giftPurchaseIntents: { $: { where: { id: input.id }, limit: 1 } } })).giftPurchaseIntents) : attrs;
3847
3880
  if (!write.duplicate) giftCode = prepared.code;
3848
3881
  }
3849
- if (!stored || stored.chapterId !== input.chapterId || stored.environment !== input.environment || stored.offerId !== input.offer.id || stored.purchaserEmailNormalized !== input.purchaserEmailNormalized) {
3882
+ if (!stored || stored.chapterId !== input.chapterId || stored.environment !== input.environment || stored.runtime !== input.runtime || stored.offerId !== input.offer.id || stored.purchaserEmailNormalized !== input.purchaserEmailNormalized) {
3850
3883
  throw new Error("gift purchase intent does not match this checkout");
3851
3884
  }
3852
3885
  if (stored.status === "paid") throw new Error("gift purchase is already paid");
@@ -3854,7 +3887,8 @@ async function startGiftIntent(db, input) {
3854
3887
  input.stripeSecret,
3855
3888
  stored,
3856
3889
  input.purchaserEmail,
3857
- typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
3890
+ typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId,
3891
+ input.runtime
3858
3892
  );
3859
3893
  await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs: {
3860
3894
  status: "checkout_ready",
@@ -3873,18 +3907,29 @@ async function startGiftIntent(db, input) {
3873
3907
  };
3874
3908
  }
3875
3909
 
3910
+ // src/stripe-runtime.ts
3911
+ function stripeWebhookSecretName(runtime) {
3912
+ if (!runtime) return "stripe_webhook_secret";
3913
+ if (!/^[a-z0-9]{2,12}$/.test(runtime) || runtime === "prod" || runtime === "production") {
3914
+ throw new Error("ODLA_RUNTIME is invalid");
3915
+ }
3916
+ return `stripe_webhook_secret__${runtime}`;
3917
+ }
3918
+
3919
+ // src/payments-route-support.ts
3920
+ var paymentErrorCode = (error) => error && typeof error === "object" && typeof error.code === "string" ? error.code : "unknown";
3921
+ function paymentLineItems(group) {
3922
+ const standard = Number(group.standardPriceCents ?? 0);
3923
+ const discount = Number(group.foundingDiscountCents ?? 0);
3924
+ return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
3925
+ }
3926
+
3876
3927
  // src/worker-routes-payments.ts
3877
- var codeOf = (err) => err && typeof err === "object" && typeof err.code === "string" ? err.code : "unknown";
3878
3928
  async function firstRow2(db, ns, q) {
3879
3929
  const res = await db.query({ [ns]: { $: q } });
3880
3930
  const rows4 = res[ns];
3881
3931
  return Array.isArray(rows4) ? rows4[0] : void 0;
3882
3932
  }
3883
- function lineItems(group) {
3884
- const standard = Number(group.standardPriceCents ?? 0);
3885
- const discount = Number(group.foundingDiscountCents ?? 0);
3886
- return { standardCents: standard, discountCents: discount, dueTodayCents: standard - discount };
3887
- }
3888
3933
  async function findApplication(db, event) {
3889
3934
  if ("applicationId" in event && event.applicationId) {
3890
3935
  return firstRow2(db, "applications", { where: { id: event.applicationId }, limit: 1 });
@@ -3975,6 +4020,7 @@ async function startSubscription(req, env, ctx) {
3975
4020
  chapterId: ctx.chapter.id,
3976
4021
  environment: env.ODLA_ENV,
3977
4022
  offer,
4023
+ ...env.ODLA_RUNTIME ? { runtime: env.ODLA_RUNTIME } : {},
3978
4024
  purchaserType: "application",
3979
4025
  purchaserEmail: String(app.email ?? ""),
3980
4026
  purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
@@ -4007,6 +4053,7 @@ async function startSubscription(req, env, ctx) {
4007
4053
  result = await provider.createSubscription({
4008
4054
  appId: env.ODLA_APP_ID,
4009
4055
  environment: env.ODLA_ENV,
4056
+ ...env.ODLA_RUNTIME ? { runtime: env.ODLA_RUNTIME } : {},
4010
4057
  applicationId,
4011
4058
  groupId: String(group.id),
4012
4059
  email: String(app.email ?? ""),
@@ -4015,29 +4062,35 @@ async function startSubscription(req, env, ctx) {
4015
4062
  existingCustomerId: typeof app.stripeCustomerId === "string" ? app.stripeCustomerId : void 0
4016
4063
  });
4017
4064
  } catch (err) {
4018
- return json({ error: "payment setup failed", code: codeOf(err) }, 502);
4065
+ return json({ error: "payment setup failed", code: paymentErrorCode(err) }, 502);
4019
4066
  }
4020
4067
  await db.transact([
4021
4068
  {
4022
4069
  t: "update",
4023
4070
  ns: "applications",
4024
4071
  id: applicationId,
4025
- attrs: { stripeCustomerId: result.customerId, stripeSubscriptionId: result.subscriptionId, refundPolicyAckAt: Date.now() }
4072
+ attrs: {
4073
+ stripeCustomerId: result.customerId,
4074
+ stripeSubscriptionId: result.subscriptionId,
4075
+ ...env.ODLA_RUNTIME ? { stripeRuntime: env.ODLA_RUNTIME } : {},
4076
+ refundPolicyAckAt: Date.now()
4077
+ }
4026
4078
  }
4027
4079
  ]);
4028
- return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: lineItems(group) });
4080
+ return json({ clientSecret: result.clientSecret, publishableKey: group.stripePublishableKey ?? null, lineItems: paymentLineItems(group) });
4029
4081
  }
4030
4082
  async function ingestWebhook(req, env, ctx) {
4031
4083
  const db = ctx.makeDb(env);
4032
- const webhookSecret = await getVaultSecret(db, "stripe_webhook_secret");
4084
+ const webhookSecret = await getVaultSecret(db, stripeWebhookSecretName(env.ODLA_RUNTIME));
4033
4085
  if (!webhookSecret) return json({ error: "webhook not configured" }, 503);
4034
4086
  const rawBody = await req.text();
4035
4087
  const ingest = await createStripeProvider({ webhookSecret }).ingestWebhook(rawBody, req.headers.get("stripe-signature") ?? "");
4036
4088
  if (!ingest.ok) return json({ error: "invalid signature" }, 400);
4037
4089
  const { eventId, event } = ingest;
4038
4090
  if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
4039
- const identifiedEnvironment = event.appId !== void 0 || event.environment !== void 0;
4040
- if (env.ODLA_APP_ID && identifiedEnvironment && (event.appId !== env.ODLA_APP_ID || event.environment !== env.ODLA_ENV)) {
4091
+ const identifiedEnvironment = event.appId !== void 0 || event.environment !== void 0 || event.dataEnvironment !== void 0 || event.runtime !== void 0;
4092
+ const eventDataEnvironment = event.dataEnvironment ?? event.environment;
4093
+ if (env.ODLA_APP_ID && identifiedEnvironment && (event.appId !== env.ODLA_APP_ID || eventDataEnvironment !== env.ODLA_ENV || event.runtime !== void 0 && event.runtime !== env.ODLA_RUNTIME)) {
4041
4094
  return json({ ok: true, matched: false });
4042
4095
  }
4043
4096
  if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
@@ -4045,6 +4098,9 @@ async function ingestWebhook(req, env, ctx) {
4045
4098
  if (!intent || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV) {
4046
4099
  return json({ ok: true, matched: false });
4047
4100
  }
4101
+ if (event.runtime === void 0 && typeof intent.runtime === "string" && intent.runtime !== env.ODLA_RUNTIME) {
4102
+ return json({ ok: true, matched: false });
4103
+ }
4048
4104
  if (event.kind === "gift_paid") {
4049
4105
  if (intent.providerObjectId !== event.providerObjectId) return json({ ok: true, matched: false });
4050
4106
  if (intent.status === "refunded" || intent.status === "canceled") return json({ ok: true, matched: false });
@@ -4068,6 +4124,9 @@ async function ingestWebhook(req, env, ctx) {
4068
4124
  }
4069
4125
  const app = await findApplication(db, event);
4070
4126
  if (!app) return json({ ok: true, matched: false });
4127
+ if (event.runtime === void 0 && typeof app.stripeRuntime === "string" && app.stripeRuntime !== env.ODLA_RUNTIME) {
4128
+ return json({ ok: true, matched: false });
4129
+ }
4071
4130
  const patch = webhookPatch(event, String(app.status ?? ""));
4072
4131
  let next = app;
4073
4132
  if (Object.keys(patch).length) {
@@ -4139,7 +4198,8 @@ var handleGifts = async (req, url, env, ctx) => {
4139
4198
  return json({
4140
4199
  offers: publicGiftOffers(control3?.document.giftOffers, Date.now()),
4141
4200
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
4142
- refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
4201
+ refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : "",
4202
+ merchantDisclosureText: typeof group?.merchantDisclosureText === "string" ? group.merchantDisclosureText : ""
4143
4203
  });
4144
4204
  }
4145
4205
  if (req.method === "POST" && url.pathname === "/api/gifts/checkout") {
@@ -4170,6 +4230,7 @@ var handleGifts = async (req, url, env, ctx) => {
4170
4230
  offer,
4171
4231
  purchaserType: "member",
4172
4232
  purchaserEmail: user.email,
4233
+ ...env.ODLA_RUNTIME ? { runtime: env.ODLA_RUNTIME } : {},
4173
4234
  purchaserEmailNormalized: emailNormalized,
4174
4235
  purchaserUserId: user.userId,
4175
4236
  ...typeof app?.stripeCustomerId === "string" ? { existingCustomerId: app.stripeCustomerId } : {},