@odla-ai/chapter 0.38.0 → 0.39.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.
@@ -1225,10 +1225,10 @@ async function syncApplicationToCrm(deps, opts) {
1225
1225
  return recordId;
1226
1226
  }
1227
1227
  async function backfillCrm(deps) {
1228
- const [appsRes, usersRes] = await Promise.all([
1229
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1230
- deps.db.query({ $users: { $: { limit: 1e3 } } })
1231
- ]);
1228
+ const data = await deps.db.query({
1229
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
1230
+ $users: { $: { limit: 1e3 } }
1231
+ });
1232
1232
  const seen = /* @__PURE__ */ new Set();
1233
1233
  let synced = 0;
1234
1234
  const errors = [];
@@ -1243,8 +1243,8 @@ async function backfillCrm(deps) {
1243
1243
  errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1244
1244
  }
1245
1245
  };
1246
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1247
- for (const u of usersRes.$users ?? []) {
1246
+ for (const a of data.applications ?? []) await run(a, str(a.status));
1247
+ for (const u of data.$users ?? []) {
1248
1248
  if (u.deleted === true) continue;
1249
1249
  await run({ email: u.email, firstName: str(u.name) });
1250
1250
  }
@@ -3373,9 +3373,21 @@ function webhookMutationId(eventId) {
3373
3373
  function findApplicationRef(obj) {
3374
3374
  const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
3375
3375
  const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
3376
- const applicationId = pick(metaOf(obj)) ?? pick(metaOf(obj.subscription_details)) ?? pick(metaOf(obj.parent?.subscription_details));
3376
+ const candidates = [
3377
+ metaOf(obj),
3378
+ metaOf(obj.subscription_details),
3379
+ metaOf(obj.parent?.subscription_details)
3380
+ ];
3381
+ const applicationId = candidates.map(pick).find(Boolean);
3382
+ const appId = candidates.map((metadata) => typeof metadata.odlaAppId === "string" ? metadata.odlaAppId : void 0).find(Boolean);
3383
+ const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
3377
3384
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
3378
- return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
3385
+ return {
3386
+ ...applicationId ? { applicationId } : {},
3387
+ ...customerId ? { customerId } : {},
3388
+ ...appId ? { appId } : {},
3389
+ ...environment ? { environment } : {}
3390
+ };
3379
3391
  }
3380
3392
  function findGiftRef(obj) {
3381
3393
  const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
@@ -3466,7 +3478,13 @@ function createStripeProvider(config) {
3466
3478
  return {
3467
3479
  async createSubscription(input) {
3468
3480
  const sk = requireSecret(secretKey);
3469
- const meta = { applicationId: input.applicationId, groupId: input.groupId, email: input.email };
3481
+ const meta = {
3482
+ applicationId: input.applicationId,
3483
+ groupId: input.groupId,
3484
+ email: input.email,
3485
+ odlaAppId: input.appId,
3486
+ odlaEnvironment: input.environment
3487
+ };
3470
3488
  let customerId = input.existingCustomerId;
3471
3489
  if (!customerId) {
3472
3490
  const cust = await stripeCall(
@@ -3986,6 +4004,8 @@ async function startSubscription(req, env, ctx) {
3986
4004
  let result;
3987
4005
  try {
3988
4006
  result = await provider.createSubscription({
4007
+ appId: env.ODLA_APP_ID,
4008
+ environment: env.ODLA_ENV,
3989
4009
  applicationId,
3990
4010
  groupId: String(group.id),
3991
4011
  email: String(app.email ?? ""),
@@ -4015,6 +4035,10 @@ async function ingestWebhook(req, env, ctx) {
4015
4035
  if (!ingest.ok) return json({ error: "invalid signature" }, 400);
4016
4036
  const { eventId, event } = ingest;
4017
4037
  if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
4038
+ const identifiedEnvironment = event.appId !== void 0 || event.environment !== void 0;
4039
+ if (env.ODLA_APP_ID && identifiedEnvironment && (event.appId !== env.ODLA_APP_ID || event.environment !== env.ODLA_ENV)) {
4040
+ return json({ ok: true, matched: false });
4041
+ }
4018
4042
  if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
4019
4043
  const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id: event.giftIntentId }, limit: 1 });
4020
4044
  if (!intent || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV) {
@@ -4369,14 +4393,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4369
4393
  if (gate9 instanceof Response) return gate9;
4370
4394
  const { db } = gate9;
4371
4395
  const sk = await getVaultSecret(db, "clerk_secret_key");
4372
- const [appsRes, usersRes, roleList] = await Promise.all([
4373
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
4374
- db.query({ $users: { $: { limit: 200 } } }),
4396
+ const [data, roleList] = await Promise.all([
4397
+ db.query({
4398
+ applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
4399
+ $users: { $: { limit: 200 } }
4400
+ }),
4375
4401
  sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
4376
4402
  ]);
4377
4403
  const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
4378
4404
  const people = /* @__PURE__ */ new Map();
4379
- for (const u of usersRes.$users ?? []) {
4405
+ for (const u of data.$users ?? []) {
4380
4406
  if (u.deleted === true) continue;
4381
4407
  const email = typeof u.email === "string" ? u.email : "";
4382
4408
  if (!email) continue;
@@ -4391,7 +4417,7 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4391
4417
  application: null
4392
4418
  });
4393
4419
  }
4394
- for (const a of appsRes.applications ?? []) {
4420
+ for (const a of data.applications ?? []) {
4395
4421
  const key = String(a.email ?? "").toLowerCase();
4396
4422
  if (!key) continue;
4397
4423
  const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
@@ -4596,13 +4622,19 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4596
4622
  const now = Date.now();
4597
4623
  const d7 = now - 7 * 864e5;
4598
4624
  const d30 = now - 30 * 864e5;
4599
- const [appsRes, meetingsRes, recsRes, groupRes] = await Promise.all([
4600
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
4601
- db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } }),
4602
- db.query({ crm_record: { $: { where: { type: "person" }, limit: 1e3 } } }),
4603
- db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
4604
- ]);
4605
- const apps = rows3(appsRes.applications);
4625
+ const data = await db.query({
4626
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
4627
+ meetings: {
4628
+ $: {
4629
+ where: { status: "scheduled" },
4630
+ order: { startAt: "asc" },
4631
+ limit: 500
4632
+ }
4633
+ },
4634
+ crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
4635
+ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
4636
+ });
4637
+ const apps = rows3(data.applications);
4606
4638
  const applications = {
4607
4639
  total: apps.length,
4608
4640
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -4614,7 +4646,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4614
4646
  pipeline[s] = 0;
4615
4647
  pipelineDelta[s] = 0;
4616
4648
  }
4617
- for (const r of rows3(recsRes.crm_record)) {
4649
+ for (const r of rows3(data.crm_record)) {
4618
4650
  const s = r.stage;
4619
4651
  if (s in pipeline) {
4620
4652
  pipeline[s] = (pipeline[s] ?? 0) + 1;
@@ -4622,7 +4654,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4622
4654
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
4623
4655
  }
4624
4656
  }
4625
- const meetings = rows3(meetingsRes.meetings);
4657
+ const meetings = rows3(data.meetings);
4626
4658
  const appById = new Map(apps.map((a) => [a.id, a]));
4627
4659
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
4628
4660
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
@@ -4638,7 +4670,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4638
4670
  email: a?.email ?? null
4639
4671
  };
4640
4672
  });
4641
- const group = groupRes.groups?.[0];
4673
+ const group = data.groups?.[0];
4642
4674
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
4643
4675
  let revenue = { billingReady: false };
4644
4676
  let revenueSeries = null;
@@ -4672,16 +4704,18 @@ var handleAdminBilling = async (req, url, env, ctx) => {
4672
4704
  const db = got;
4673
4705
  const sk = await getVaultSecret(db, "stripe_secret_key");
4674
4706
  if (!sk) return json({ billingReady: false, rows: [], summary: null });
4675
- const [appsRes, subsRes, groupRes] = await Promise.all([
4676
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
4677
- stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" }),
4678
- db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
4707
+ const [data, subsRes] = await Promise.all([
4708
+ db.query({
4709
+ applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
4710
+ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
4711
+ }),
4712
+ stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" })
4679
4713
  ]);
4680
- const testMode = String(groupRes.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
4714
+ const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
4681
4715
  if (!subsRes.ok) return json({ error: "billing lookup failed upstream" }, 502);
4682
4716
  const subById = new Map(rows3(subsRes.body.data).map((s) => [s.id, s]));
4683
4717
  const billingRows = [];
4684
- for (const a of rows3(appsRes.applications)) {
4718
+ for (const a of rows3(data.applications)) {
4685
4719
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
4686
4720
  const sub = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
4687
4721
  const items = sub?.items?.data ?? [];
@@ -5021,21 +5055,27 @@ var handleAdminComms = async (req, url, env, ctx) => {
5021
5055
  if (got instanceof Response) return got;
5022
5056
  const db = got;
5023
5057
  const appId = m[1];
5024
- const [emailRes, meetingRes, appRes] = await Promise.all([
5025
- db.query({ emailLog: { $: { where: { applicationId: appId }, order: { sentAt: "desc" }, limit: 100 } } }),
5026
- db.query({ meetings: { $: { where: { applicationId: appId } } } }),
5027
- db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })
5028
- ]);
5029
- const app = appRes.applications?.[0] ?? null;
5058
+ const data = await db.query({
5059
+ emailLog: {
5060
+ $: {
5061
+ where: { applicationId: appId },
5062
+ order: { sentAt: "desc" },
5063
+ limit: 100
5064
+ }
5065
+ },
5066
+ meetings: { $: { where: { applicationId: appId } } },
5067
+ applications: { $: { where: { id: appId }, limit: 1 } }
5068
+ });
5069
+ const app = data.applications?.[0] ?? null;
5030
5070
  const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
5031
5071
  const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl: `${url.origin}/admin/`, membersUrl: `${url.origin}/members/` } : null;
5032
- const emails = (emailRes.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
5072
+ const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
5033
5073
  let mailBody = typeof r.body === "string" ? r.body : null;
5034
5074
  if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
5035
5075
  const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
5036
5076
  return { kind: "email", channel, label: String(r.template), subject: str2(r.subject), to: r.to ?? null, body: mailBody, at: r.sentAt, error: r.error ?? null };
5037
5077
  });
5038
- const calendar = (meetingRes.meetings ?? []).map((m2) => ({
5078
+ const calendar = (data.meetings ?? []).map((m2) => ({
5039
5079
  kind: "calendar",
5040
5080
  channel: "Google Calendar",
5041
5081
  label: m2.status === "cancelled" ? "Invitation (call later cancelled)" : "Meeting invitation",