@odla-ai/chapter 0.23.0 → 0.25.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.
- package/README.md +40 -29
- package/dist/{chunk-OYXNI6VR.js → chunk-GMGTJIY3.js} +28 -15
- package/dist/chunk-GMGTJIY3.js.map +1 -0
- package/dist/{chunk-NTZSXAFT.js → chunk-WGQO4WDJ.js} +3 -2
- package/dist/chunk-WGQO4WDJ.js.map +1 -0
- package/dist/{chunk-VCC4S6JU.js → chunk-ZPAJL7KG.js} +28 -21
- package/dist/chunk-ZPAJL7KG.js.map +1 -0
- package/dist/{copy-context-pDogTR4x.d.ts → copy-context-DI21CYQ3.d.ts} +11 -5
- package/dist/index.cjs +70 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -25
- package/dist/index.d.ts +72 -25
- package/dist/index.js +70 -21
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +308 -108
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +11 -5
- package/dist/worker/index.d.ts +11 -5
- package/dist/worker/index.js +308 -108
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +38 -9
- package/runbooks/greenfield.md +31 -31
- package/dist/chunk-NTZSXAFT.js.map +0 -1
- package/dist/chunk-OYXNI6VR.js.map +0 -1
- package/dist/chunk-VCC4S6JU.js.map +0 -1
package/dist/ui/admin/index.d.ts
CHANGED
|
@@ -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 } from '../../copy-context-
|
|
5
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
4
|
+
import { a as Chapter, b as ChapterCopy } from '../../copy-context-DI21CYQ3.js';
|
|
5
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DI21CYQ3.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. */
|
package/dist/ui/admin/index.js
CHANGED
|
@@ -24,11 +24,11 @@ import {
|
|
|
24
24
|
meetingsSection,
|
|
25
25
|
peopleSection,
|
|
26
26
|
useAdminResource
|
|
27
|
-
} from "../../chunk-
|
|
27
|
+
} from "../../chunk-ZPAJL7KG.js";
|
|
28
28
|
import {
|
|
29
29
|
ChapterCopyProvider,
|
|
30
30
|
useChapterCopy
|
|
31
|
-
} from "../../chunk-
|
|
31
|
+
} from "../../chunk-WGQO4WDJ.js";
|
|
32
32
|
export {
|
|
33
33
|
AdminNote,
|
|
34
34
|
AdminPage,
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AdminAccountMenuProps, AdminChrome, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AvailabilitySectionOptions, BillingSectionOptions, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, MeetingsSectionOptions, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, peopleSection, useAdminResource } from './admin/index.js';
|
|
2
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-
|
|
2
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-DI21CYQ3.js';
|
|
3
3
|
export { ApiFn, BrandStyle, BrandStyleProps, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinStepRenderContext, JoinSubmitRenderContext, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, PaymentPriceLines, PaymentStep, PaymentStepProps, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, timeLabel, tzShort } from './member/index.js';
|
|
4
4
|
import 'preact';
|
|
5
5
|
import '@odla-ai/crm';
|
package/dist/ui/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
loadJoinResume,
|
|
14
14
|
timeLabel,
|
|
15
15
|
tzShort
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-GMGTJIY3.js";
|
|
17
17
|
import {
|
|
18
18
|
AdminNote,
|
|
19
19
|
AdminPage,
|
|
@@ -40,12 +40,12 @@ import {
|
|
|
40
40
|
meetingsSection,
|
|
41
41
|
peopleSection,
|
|
42
42
|
useAdminResource
|
|
43
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-ZPAJL7KG.js";
|
|
44
44
|
import {
|
|
45
45
|
BrandStyle,
|
|
46
46
|
ChapterCopyProvider,
|
|
47
47
|
useChapterCopy
|
|
48
|
-
} from "../chunk-
|
|
48
|
+
} from "../chunk-WGQO4WDJ.js";
|
|
49
49
|
export {
|
|
50
50
|
AdminNote,
|
|
51
51
|
AdminPage,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
import { ComponentChildren, JSX } from 'preact';
|
|
3
|
-
import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-
|
|
4
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
3
|
+
import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-DI21CYQ3.js';
|
|
4
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-DI21CYQ3.js';
|
|
5
5
|
import '@odla-ai/crm';
|
|
6
6
|
|
|
7
7
|
/** A bookable slot: a start instant in epoch milliseconds. */
|
package/dist/ui/member/index.js
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
loadJoinResume,
|
|
14
14
|
timeLabel,
|
|
15
15
|
tzShort
|
|
16
|
-
} from "../../chunk-
|
|
16
|
+
} from "../../chunk-GMGTJIY3.js";
|
|
17
17
|
import {
|
|
18
18
|
BrandStyle,
|
|
19
19
|
ChapterCopyProvider,
|
|
20
20
|
useChapterCopy
|
|
21
|
-
} from "../../chunk-
|
|
21
|
+
} from "../../chunk-WGQO4WDJ.js";
|
|
22
22
|
export {
|
|
23
23
|
BrandStyle,
|
|
24
24
|
ChapterCopyProvider,
|
package/dist/worker/index.cjs
CHANGED
|
@@ -546,23 +546,30 @@ function clerkUserRequest(input) {
|
|
|
546
546
|
skip_password_requirement: true,
|
|
547
547
|
...input.firstName ? { first_name: input.firstName } : {},
|
|
548
548
|
...input.lastName ? { last_name: input.lastName } : {},
|
|
549
|
-
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
549
|
+
...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
|
|
550
|
+
...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
|
|
550
551
|
}
|
|
551
552
|
};
|
|
552
553
|
}
|
|
553
|
-
async function
|
|
554
|
+
async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fetch) {
|
|
555
|
+
const res = await fetchImpl(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}/metadata`, {
|
|
556
|
+
method: "PATCH",
|
|
557
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
558
|
+
body: JSON.stringify({
|
|
559
|
+
...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
|
|
560
|
+
...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
|
|
561
|
+
})
|
|
562
|
+
});
|
|
563
|
+
return res.ok;
|
|
564
|
+
}
|
|
565
|
+
async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
|
|
554
566
|
const auth = { authorization: `Bearer ${secretKey}` };
|
|
555
567
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
556
568
|
if (!found.ok) return false;
|
|
557
569
|
const users = await found.json().catch(() => null);
|
|
558
570
|
const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
559
571
|
if (!id) return false;
|
|
560
|
-
|
|
561
|
-
method: "PATCH",
|
|
562
|
-
headers: { ...auth, "content-type": "application/json" },
|
|
563
|
-
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
564
|
-
});
|
|
565
|
-
return patched.ok;
|
|
572
|
+
return updateClerkUserMetadata(secretKey, id, input, fetchImpl);
|
|
566
573
|
}
|
|
567
574
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
568
575
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -573,8 +580,15 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
573
580
|
});
|
|
574
581
|
if (res.ok) return { ok: true, status: res.status };
|
|
575
582
|
const healed = heal(res.status);
|
|
576
|
-
|
|
577
|
-
|
|
583
|
+
const repair = input.repairMetadata ?? {
|
|
584
|
+
...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
|
|
585
|
+
...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
|
|
586
|
+
};
|
|
587
|
+
if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
|
|
588
|
+
const refreshed = await updateClerkUserMetadataByEmail(secretKey, input.email, {
|
|
589
|
+
...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
|
|
590
|
+
...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
|
|
591
|
+
}, fetchImpl).catch(() => false);
|
|
578
592
|
return { ...healed, refreshed };
|
|
579
593
|
}
|
|
580
594
|
|
|
@@ -682,6 +696,41 @@ function emailGroupFrom(row) {
|
|
|
682
696
|
}
|
|
683
697
|
|
|
684
698
|
// src/worker-provisioning.ts
|
|
699
|
+
var applicantPrivateMetadata = (chapter, applicationId, fields, removeMissingProfile = false) => {
|
|
700
|
+
const profile = applicantProfile(chapter, fields);
|
|
701
|
+
return {
|
|
702
|
+
applicationId,
|
|
703
|
+
...profile ? { profile } : removeMissingProfile ? { profile: null } : {}
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
var publicProfileRemoval = { applicationId: null, profile: null };
|
|
707
|
+
async function markPrivateMetadataSynced(db, applicationId, clerkUserId) {
|
|
708
|
+
await db.transact([{
|
|
709
|
+
t: "update",
|
|
710
|
+
ns: "applications",
|
|
711
|
+
id: applicationId,
|
|
712
|
+
attrs: {
|
|
713
|
+
clerkPrivateMetadataSyncedAt: Date.now(),
|
|
714
|
+
...clerkUserId ? { clerkUserId } : {}
|
|
715
|
+
}
|
|
716
|
+
}]);
|
|
717
|
+
}
|
|
718
|
+
async function syncApplicantPrivateMetadata(db, chapter, applicationId, fields, clerkUserId) {
|
|
719
|
+
if (chapter.account === "none") return false;
|
|
720
|
+
try {
|
|
721
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
722
|
+
if (!secret) return false;
|
|
723
|
+
const ok = await updateClerkUserMetadata(secret, clerkUserId, {
|
|
724
|
+
publicMetadata: publicProfileRemoval,
|
|
725
|
+
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
726
|
+
});
|
|
727
|
+
if (!ok) return false;
|
|
728
|
+
await markPrivateMetadataSynced(db, applicationId, clerkUserId);
|
|
729
|
+
return true;
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
685
734
|
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
686
735
|
const email = typeof fields.email === "string" ? fields.email : "";
|
|
687
736
|
if (!email) return;
|
|
@@ -709,17 +758,30 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
|
709
758
|
try {
|
|
710
759
|
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
711
760
|
if (!secret) return;
|
|
712
|
-
const
|
|
713
|
-
const publicMetadata = { applicationId, ...profile ? { profile } : {} };
|
|
761
|
+
const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
|
|
714
762
|
if (chapter.account === "create") {
|
|
715
|
-
await createClerkUser(secret, {
|
|
763
|
+
const result = await createClerkUser(secret, {
|
|
716
764
|
email,
|
|
717
765
|
firstName: stringValue(fields.firstName),
|
|
718
766
|
lastName: stringValue(fields.lastName),
|
|
719
|
-
|
|
767
|
+
privateMetadata,
|
|
768
|
+
repairMetadata: {
|
|
769
|
+
publicMetadata: publicProfileRemoval,
|
|
770
|
+
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
771
|
+
}
|
|
720
772
|
});
|
|
773
|
+
if (result.ok && (!result.existed || result.refreshed)) {
|
|
774
|
+
await markPrivateMetadataSynced(db, applicationId);
|
|
775
|
+
}
|
|
721
776
|
} else {
|
|
722
|
-
await createClerkInvitation(secret, { email
|
|
777
|
+
const result = await createClerkInvitation(secret, { email });
|
|
778
|
+
if (result.existed) {
|
|
779
|
+
const refreshed = await updateClerkUserMetadataByEmail(secret, email, {
|
|
780
|
+
publicMetadata: publicProfileRemoval,
|
|
781
|
+
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
782
|
+
});
|
|
783
|
+
if (refreshed) await markPrivateMetadataSynced(db, applicationId);
|
|
784
|
+
}
|
|
723
785
|
}
|
|
724
786
|
} catch {
|
|
725
787
|
}
|
|
@@ -768,7 +830,10 @@ async function memberSessionApplication(db, chapterId, email) {
|
|
|
768
830
|
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
769
831
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
770
832
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
771
|
-
return
|
|
833
|
+
return {
|
|
834
|
+
application: memberApplication(app, meeting, timezone),
|
|
835
|
+
source: app
|
|
836
|
+
};
|
|
772
837
|
}
|
|
773
838
|
var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
|
|
774
839
|
var handleConfig = async (_req, url, env, ctx) => {
|
|
@@ -789,8 +854,26 @@ var handleMe = async (req, url, env, ctx) => {
|
|
|
789
854
|
const superAdmin = await ctx.isSuperAdminEmail(db, u.email);
|
|
790
855
|
const base = { authorized: isAdminRole(role, ctx.auth), role, superAdmin, email: u.email ?? null };
|
|
791
856
|
if (ctx.chapter.mode !== "chapter" || !u.email) return json(base);
|
|
792
|
-
const
|
|
793
|
-
|
|
857
|
+
const member = await memberSessionApplication(db, ctx.chapter.id, u.email);
|
|
858
|
+
let privateMetadataSynced = false;
|
|
859
|
+
if (member && ctx.chapter.account !== "none" && typeof member.source.clerkPrivateMetadataSyncedAt !== "number") {
|
|
860
|
+
privateMetadataSynced = await syncApplicantPrivateMetadata(
|
|
861
|
+
db,
|
|
862
|
+
ctx.chapter,
|
|
863
|
+
String(member.source.id),
|
|
864
|
+
member.source,
|
|
865
|
+
u.userId
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (member && !privateMetadataSynced && member.source.clerkUserId !== u.userId) {
|
|
869
|
+
await db.transact([{
|
|
870
|
+
t: "update",
|
|
871
|
+
ns: "applications",
|
|
872
|
+
id: String(member.source.id),
|
|
873
|
+
attrs: { clerkUserId: u.userId }
|
|
874
|
+
}]).catch(() => void 0);
|
|
875
|
+
}
|
|
876
|
+
return json({ ...base, application: member?.application ?? null });
|
|
794
877
|
};
|
|
795
878
|
var handleCrm = async (req, url, env, ctx) => {
|
|
796
879
|
const crmBase = ctx.crmBase;
|
|
@@ -948,6 +1031,93 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
948
1031
|
return null;
|
|
949
1032
|
};
|
|
950
1033
|
|
|
1034
|
+
// src/crm-sync.ts
|
|
1035
|
+
var import_crm3 = require("@odla-ai/crm");
|
|
1036
|
+
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1037
|
+
var crmDeps = (db, ctx) => ({
|
|
1038
|
+
crm: ctx.chapter.crm,
|
|
1039
|
+
db,
|
|
1040
|
+
now: () => Date.now(),
|
|
1041
|
+
newId: () => crypto.randomUUID(),
|
|
1042
|
+
chapter: ctx.chapter
|
|
1043
|
+
});
|
|
1044
|
+
function personInputFromApp(chapter, app) {
|
|
1045
|
+
const input = sharedPersonInput({
|
|
1046
|
+
email: str(app.email),
|
|
1047
|
+
firstName: str(app.firstName) || void 0,
|
|
1048
|
+
lastName: str(app.lastName) || void 0,
|
|
1049
|
+
phone: str(app.phone) || void 0,
|
|
1050
|
+
linkedin: str(app.linkedin) || void 0,
|
|
1051
|
+
hubRecordId: str(app.id)
|
|
1052
|
+
});
|
|
1053
|
+
for (const f of chapter.application.crmFields) {
|
|
1054
|
+
if (app[f] !== void 0) input[f] = app[f];
|
|
1055
|
+
}
|
|
1056
|
+
if (app.id !== void 0) input.applicationId = str(app.id);
|
|
1057
|
+
return input;
|
|
1058
|
+
}
|
|
1059
|
+
function billingColumns(app) {
|
|
1060
|
+
const status = str(app.status);
|
|
1061
|
+
const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
|
|
1062
|
+
const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
|
|
1063
|
+
const cols = { billingStatus };
|
|
1064
|
+
if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
|
|
1065
|
+
if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
|
|
1066
|
+
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
1067
|
+
return cols;
|
|
1068
|
+
}
|
|
1069
|
+
async function syncApplicationToCrm(deps, opts) {
|
|
1070
|
+
const emailKey = str(opts.app.email).toLowerCase();
|
|
1071
|
+
if (!emailKey) return null;
|
|
1072
|
+
const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1073
|
+
const input = personInputFromApp(deps.chapter, opts.app);
|
|
1074
|
+
const { crm_record } = await deps.db.query({
|
|
1075
|
+
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
1076
|
+
});
|
|
1077
|
+
const existing = crm_record?.[0] ?? null;
|
|
1078
|
+
const stage = opts.stage || void 0;
|
|
1079
|
+
let recordId;
|
|
1080
|
+
if (existing && typeof existing.id === "string") {
|
|
1081
|
+
recordId = existing.id;
|
|
1082
|
+
await (0, import_crm3.updateRecord)(recordDeps, { id: recordId, input });
|
|
1083
|
+
} else {
|
|
1084
|
+
const created = await (0, import_crm3.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
|
|
1085
|
+
recordId = created.id;
|
|
1086
|
+
}
|
|
1087
|
+
if (existing && stage && existing.stage !== stage) {
|
|
1088
|
+
await (0, import_crm3.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1089
|
+
}
|
|
1090
|
+
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1091
|
+
await (0, import_crm3.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1092
|
+
return recordId;
|
|
1093
|
+
}
|
|
1094
|
+
async function backfillCrm(deps) {
|
|
1095
|
+
const [appsRes, usersRes] = await Promise.all([
|
|
1096
|
+
deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
|
|
1097
|
+
deps.db.query({ $users: { $: { limit: 1e3 } } })
|
|
1098
|
+
]);
|
|
1099
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1100
|
+
let synced = 0;
|
|
1101
|
+
const errors = [];
|
|
1102
|
+
const run = async (app, stage) => {
|
|
1103
|
+
const key = str(app.email).toLowerCase();
|
|
1104
|
+
if (!key || seen.has(key)) return;
|
|
1105
|
+
seen.add(key);
|
|
1106
|
+
try {
|
|
1107
|
+
await syncApplicationToCrm(deps, { app, stage });
|
|
1108
|
+
synced += 1;
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
for (const a of appsRes.applications ?? []) await run(a, str(a.status));
|
|
1114
|
+
for (const u of usersRes.$users ?? []) {
|
|
1115
|
+
if (u.deleted === true) continue;
|
|
1116
|
+
await run({ email: u.email, firstName: str(u.name) });
|
|
1117
|
+
}
|
|
1118
|
+
return { synced, errors };
|
|
1119
|
+
}
|
|
1120
|
+
|
|
951
1121
|
// src/worker-routes-schedule.ts
|
|
952
1122
|
var import_calendar = require("@odla-ai/calendar");
|
|
953
1123
|
function errCode(err) {
|
|
@@ -1051,8 +1221,11 @@ async function bookSlot(req, env, ctx) {
|
|
|
1051
1221
|
if (code === "calendar_slot_unavailable") return json({ error: "slot no longer available", code }, 409);
|
|
1052
1222
|
return json({ error: "booking failed", code }, 502);
|
|
1053
1223
|
}
|
|
1054
|
-
const
|
|
1224
|
+
const bookingAttrs = applicationBookingUpdate(status, startAt, htmlLink);
|
|
1225
|
+
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: bookingAttrs };
|
|
1055
1226
|
await db.transact([meetingOp, appOp]);
|
|
1227
|
+
const booked = { ...app, ...bookingAttrs };
|
|
1228
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: booked, stage: String(booked.status ?? "") }).catch(() => void 0);
|
|
1056
1229
|
if (typeof app.email === "string" && app.email) {
|
|
1057
1230
|
await sendTemplated(
|
|
1058
1231
|
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
@@ -1402,6 +1575,8 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
1402
1575
|
const patch = webhookPatch(event, String(app.status ?? ""));
|
|
1403
1576
|
if (Object.keys(patch).length) {
|
|
1404
1577
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
|
|
1578
|
+
const next = { ...app, ...patch };
|
|
1579
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: next, stage: String(next.status ?? "") }).catch(() => void 0);
|
|
1405
1580
|
}
|
|
1406
1581
|
if (event.kind === "first_payment") {
|
|
1407
1582
|
await notifyPaymentConfirmed(db, env, eventId, app);
|
|
@@ -1553,9 +1728,21 @@ var PAGE = 100;
|
|
|
1553
1728
|
function toRecord(u) {
|
|
1554
1729
|
if (typeof u.id !== "string") return null;
|
|
1555
1730
|
const pm = u.public_metadata ?? {};
|
|
1731
|
+
const privateMetadata = u.private_metadata ?? {};
|
|
1732
|
+
const rawProfile = privateMetadata.profile;
|
|
1733
|
+
const profile = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
|
|
1734
|
+
const applicationId = typeof privateMetadata.applicationId === "string" ? privateMetadata.applicationId : void 0;
|
|
1556
1735
|
const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
|
|
1557
1736
|
const email = u.email_addresses?.[0]?.email_address;
|
|
1558
|
-
return {
|
|
1737
|
+
return {
|
|
1738
|
+
id: u.id,
|
|
1739
|
+
email: typeof email === "string" ? email : void 0,
|
|
1740
|
+
role,
|
|
1741
|
+
publicMetadata: pm,
|
|
1742
|
+
privateMetadata,
|
|
1743
|
+
profile,
|
|
1744
|
+
...applicationId ? { applicationId } : {}
|
|
1745
|
+
};
|
|
1559
1746
|
}
|
|
1560
1747
|
async function clerkGet(path, secretKey, fetchImpl) {
|
|
1561
1748
|
const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
|
|
@@ -1593,86 +1780,6 @@ async function clerkSetRole(secretKey, id, role, fetchImpl = fetch) {
|
|
|
1593
1780
|
return res.ok;
|
|
1594
1781
|
}
|
|
1595
1782
|
|
|
1596
|
-
// src/crm-sync.ts
|
|
1597
|
-
var import_crm3 = require("@odla-ai/crm");
|
|
1598
|
-
var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1599
|
-
function personInputFromApp(chapter, app) {
|
|
1600
|
-
const input = sharedPersonInput({
|
|
1601
|
-
email: str(app.email),
|
|
1602
|
-
firstName: str(app.firstName) || void 0,
|
|
1603
|
-
lastName: str(app.lastName) || void 0,
|
|
1604
|
-
phone: str(app.phone) || void 0,
|
|
1605
|
-
linkedin: str(app.linkedin) || void 0,
|
|
1606
|
-
hubRecordId: str(app.id)
|
|
1607
|
-
});
|
|
1608
|
-
for (const f of chapter.application.crmFields) {
|
|
1609
|
-
if (app[f] !== void 0) input[f] = app[f];
|
|
1610
|
-
}
|
|
1611
|
-
if (app.id !== void 0) input.applicationId = str(app.id);
|
|
1612
|
-
return input;
|
|
1613
|
-
}
|
|
1614
|
-
function billingColumns(app) {
|
|
1615
|
-
const status = str(app.status);
|
|
1616
|
-
const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
|
|
1617
|
-
const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
|
|
1618
|
-
const cols = { billingStatus };
|
|
1619
|
-
if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
|
|
1620
|
-
if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
|
|
1621
|
-
if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
|
|
1622
|
-
return cols;
|
|
1623
|
-
}
|
|
1624
|
-
async function syncApplicationToCrm(deps, opts) {
|
|
1625
|
-
const emailKey = str(opts.app.email).toLowerCase();
|
|
1626
|
-
if (!emailKey) return null;
|
|
1627
|
-
const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
1628
|
-
const input = personInputFromApp(deps.chapter, opts.app);
|
|
1629
|
-
const { crm_record } = await deps.db.query({
|
|
1630
|
-
crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
|
|
1631
|
-
});
|
|
1632
|
-
const existing = crm_record?.[0] ?? null;
|
|
1633
|
-
const stage = opts.stage || void 0;
|
|
1634
|
-
let recordId;
|
|
1635
|
-
if (existing && typeof existing.id === "string") {
|
|
1636
|
-
recordId = existing.id;
|
|
1637
|
-
await (0, import_crm3.updateRecord)(crmDeps3, { id: recordId, input });
|
|
1638
|
-
} else {
|
|
1639
|
-
const created = await (0, import_crm3.createRecord)(crmDeps3, { type: "person", input, ...stage ? { stage } : {} });
|
|
1640
|
-
recordId = created.id;
|
|
1641
|
-
}
|
|
1642
|
-
if (existing && stage && existing.stage !== stage) {
|
|
1643
|
-
await (0, import_crm3.setStage)(crmDeps3, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
|
|
1644
|
-
}
|
|
1645
|
-
await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
|
|
1646
|
-
await (0, import_crm3.linkIdentity)(crmDeps3, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
|
|
1647
|
-
return recordId;
|
|
1648
|
-
}
|
|
1649
|
-
async function backfillCrm(deps) {
|
|
1650
|
-
const [appsRes, usersRes] = await Promise.all([
|
|
1651
|
-
deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
|
|
1652
|
-
deps.db.query({ $users: { $: { limit: 1e3 } } })
|
|
1653
|
-
]);
|
|
1654
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1655
|
-
let synced = 0;
|
|
1656
|
-
const errors = [];
|
|
1657
|
-
const run = async (app, stage) => {
|
|
1658
|
-
const key = str(app.email).toLowerCase();
|
|
1659
|
-
if (!key || seen.has(key)) return;
|
|
1660
|
-
seen.add(key);
|
|
1661
|
-
try {
|
|
1662
|
-
await syncApplicationToCrm(deps, { app, stage });
|
|
1663
|
-
synced += 1;
|
|
1664
|
-
} catch (err) {
|
|
1665
|
-
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
1666
|
-
}
|
|
1667
|
-
};
|
|
1668
|
-
for (const a of appsRes.applications ?? []) await run(a, str(a.status));
|
|
1669
|
-
for (const u of usersRes.$users ?? []) {
|
|
1670
|
-
if (u.deleted === true) continue;
|
|
1671
|
-
await run({ email: u.email, firstName: str(u.name) });
|
|
1672
|
-
}
|
|
1673
|
-
return { synced, errors };
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
1783
|
// src/worker-routes-admin-people.ts
|
|
1677
1784
|
async function adminGate(req, env, ctx) {
|
|
1678
1785
|
const rawDb = ctx.makeDb(env);
|
|
@@ -1681,7 +1788,7 @@ async function adminGate(req, env, ctx) {
|
|
|
1681
1788
|
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1682
1789
|
return { db: rawDb, actor: { userId: u.userId, email: u.email ?? void 0 } };
|
|
1683
1790
|
}
|
|
1684
|
-
var
|
|
1791
|
+
var crmDeps2 = (db, ctx) => ({
|
|
1685
1792
|
crm: ctx.chapter.crm,
|
|
1686
1793
|
db,
|
|
1687
1794
|
now: () => Date.now(),
|
|
@@ -1692,7 +1799,7 @@ var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
|
1692
1799
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
1693
1800
|
const gate5 = await adminGate(req, env, ctx);
|
|
1694
1801
|
if (gate5 instanceof Response) return gate5;
|
|
1695
|
-
const result = await backfillCrm(
|
|
1802
|
+
const result = await backfillCrm(crmDeps2(gate5.db, ctx));
|
|
1696
1803
|
return json({ ok: true, ...result });
|
|
1697
1804
|
};
|
|
1698
1805
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
@@ -1706,17 +1813,20 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
1706
1813
|
db.query({ $users: { $: { limit: 200 } } }),
|
|
1707
1814
|
sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
|
|
1708
1815
|
]);
|
|
1709
|
-
const
|
|
1816
|
+
const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
|
|
1710
1817
|
const people = /* @__PURE__ */ new Map();
|
|
1711
1818
|
for (const u of usersRes.$users ?? []) {
|
|
1712
1819
|
if (u.deleted === true) continue;
|
|
1713
1820
|
const email = typeof u.email === "string" ? u.email : "";
|
|
1714
1821
|
if (!email) continue;
|
|
1822
|
+
const clerk = clerkByUserId.get(String(u.id));
|
|
1715
1823
|
people.set(email.toLowerCase(), {
|
|
1716
1824
|
email,
|
|
1717
1825
|
name: typeof u.name === "string" ? u.name : "",
|
|
1718
1826
|
userId: typeof u.id === "string" ? u.id : null,
|
|
1719
|
-
role:
|
|
1827
|
+
role: clerk?.role ?? "provisional",
|
|
1828
|
+
profile: clerk ? clerk.profile : null,
|
|
1829
|
+
clerkApplicationId: clerk?.applicationId ?? null,
|
|
1720
1830
|
application: null
|
|
1721
1831
|
});
|
|
1722
1832
|
}
|
|
@@ -1729,7 +1839,15 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
1729
1839
|
if (!row.application) row.application = applicationSummary(a);
|
|
1730
1840
|
if (!row.name) row.name = name;
|
|
1731
1841
|
} else {
|
|
1732
|
-
people.set(key, {
|
|
1842
|
+
people.set(key, {
|
|
1843
|
+
email: String(a.email),
|
|
1844
|
+
name,
|
|
1845
|
+
userId: null,
|
|
1846
|
+
role: null,
|
|
1847
|
+
profile: null,
|
|
1848
|
+
clerkApplicationId: null,
|
|
1849
|
+
application: applicationSummary(a)
|
|
1850
|
+
});
|
|
1733
1851
|
}
|
|
1734
1852
|
}
|
|
1735
1853
|
const rows2 = [...people.values()].sort(
|
|
@@ -1781,6 +1899,88 @@ var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
|
1781
1899
|
return json({ ok: true });
|
|
1782
1900
|
};
|
|
1783
1901
|
|
|
1902
|
+
// src/worker-routes-admin-clerk.ts
|
|
1903
|
+
var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
1904
|
+
function canonical(value) {
|
|
1905
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
1906
|
+
if (value && typeof value === "object") {
|
|
1907
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
|
|
1908
|
+
}
|
|
1909
|
+
return JSON.stringify(value) ?? "undefined";
|
|
1910
|
+
}
|
|
1911
|
+
function positiveInt(raw, fallback, max) {
|
|
1912
|
+
const parsed = Number(raw);
|
|
1913
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? Math.min(parsed, max) : fallback;
|
|
1914
|
+
}
|
|
1915
|
+
var handleAdminClerkPrivateProfileSync = async (req, url, env, ctx) => {
|
|
1916
|
+
if (req.method !== "POST" || url.pathname !== "/api/admin/clerk/private-profiles/sync") return null;
|
|
1917
|
+
const rawDb = ctx.makeDb(env);
|
|
1918
|
+
const actor = await ctx.verifyUser(req, env);
|
|
1919
|
+
if (!actor) return json({ error: "unauthorized" }, 401);
|
|
1920
|
+
if (!await ctx.isAdmin(rawDb, actor)) return json({ error: "forbidden" }, 403);
|
|
1921
|
+
if (ctx.chapter.mode !== "chapter" || ctx.chapter.account === "none") {
|
|
1922
|
+
return json({ error: "private profile sync requires a side-effecting chapter account model" }, 409);
|
|
1923
|
+
}
|
|
1924
|
+
const db = rawDb;
|
|
1925
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
1926
|
+
if (!secret) return json({ error: "clerk_secret_key missing from vault" }, 503);
|
|
1927
|
+
let all;
|
|
1928
|
+
try {
|
|
1929
|
+
all = await clerkListUsers(secret);
|
|
1930
|
+
} catch {
|
|
1931
|
+
return json({ error: "Clerk user listing failed" }, 502);
|
|
1932
|
+
}
|
|
1933
|
+
const offset = positiveInt(url.searchParams.get("offset"), 0, Number.MAX_SAFE_INTEGER);
|
|
1934
|
+
const limit = positiveInt(url.searchParams.get("limit"), 50, 100) || 50;
|
|
1935
|
+
const users = all.slice(offset, offset + limit);
|
|
1936
|
+
const result = { updated: 0, alreadyCurrent: 0, noApplication: 0, failed: 0 };
|
|
1937
|
+
for (const user of users) {
|
|
1938
|
+
if (!user.email) {
|
|
1939
|
+
result.noApplication += 1;
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
const apps = (await db.query({
|
|
1943
|
+
applications: { $: { where: { email: user.email }, order: { createdAt: "desc" }, limit: 1 } }
|
|
1944
|
+
})).applications;
|
|
1945
|
+
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
1946
|
+
if (!app || typeof app.id !== "string") {
|
|
1947
|
+
result.noApplication += 1;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
const desired = applicantPrivateMetadata(ctx.chapter, app.id, app);
|
|
1951
|
+
const desiredProfile = desired.profile ?? {};
|
|
1952
|
+
const legacyPublic = own(user.publicMetadata, "applicationId") || own(user.publicMetadata, "profile");
|
|
1953
|
+
const current = user.applicationId === app.id && canonical(user.profile) === canonical(desiredProfile);
|
|
1954
|
+
try {
|
|
1955
|
+
let updated = false;
|
|
1956
|
+
if (!current || legacyPublic) {
|
|
1957
|
+
const ok = await updateClerkUserMetadata(secret, user.id, {
|
|
1958
|
+
publicMetadata: publicProfileRemoval,
|
|
1959
|
+
privateMetadata: applicantPrivateMetadata(ctx.chapter, app.id, app, true)
|
|
1960
|
+
});
|
|
1961
|
+
if (!ok) {
|
|
1962
|
+
result.failed += 1;
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
updated = true;
|
|
1966
|
+
}
|
|
1967
|
+
await markPrivateMetadataSynced(db, app.id, user.id);
|
|
1968
|
+
if (updated) result.updated += 1;
|
|
1969
|
+
else result.alreadyCurrent += 1;
|
|
1970
|
+
} catch {
|
|
1971
|
+
result.failed += 1;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
const nextOffset = offset + users.length < all.length ? offset + users.length : null;
|
|
1975
|
+
return json({
|
|
1976
|
+
totalUsers: all.length,
|
|
1977
|
+
offset,
|
|
1978
|
+
processed: users.length,
|
|
1979
|
+
nextOffset,
|
|
1980
|
+
...result
|
|
1981
|
+
});
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1784
1984
|
// src/series.ts
|
|
1785
1985
|
function periodSeries(points, now, count, periodMs) {
|
|
1786
1986
|
const start = now - count * periodMs;
|
|
@@ -1972,7 +2172,6 @@ async function gate2(req, env, ctx) {
|
|
|
1972
2172
|
return rawDb;
|
|
1973
2173
|
}
|
|
1974
2174
|
var calFor = (env) => (0, import_calendar3.initCalendar)({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
|
|
1975
|
-
var crmDeps2 = (db, ctx) => ({ crm: ctx.chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID(), chapter: ctx.chapter });
|
|
1976
2175
|
var readJson = async (req) => {
|
|
1977
2176
|
try {
|
|
1978
2177
|
return await req.json();
|
|
@@ -2058,7 +2257,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
2058
2257
|
if (!canApprove(String(app.status), ctx.chapter.pipeline)) return json({ error: `cannot approve from status "${String(app.status)}"` }, 409);
|
|
2059
2258
|
const target = "approved";
|
|
2060
2259
|
await db.transact([{ t: "update", ns: "applications", id, attrs: { status: target } }]);
|
|
2061
|
-
await syncApplicationToCrm(
|
|
2260
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
|
|
2062
2261
|
const { promoteTo, send } = ctx.chapter.operations.onApprove;
|
|
2063
2262
|
let rolePromoted = false;
|
|
2064
2263
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -2139,7 +2338,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
2139
2338
|
if (Object.keys(attrs).length === 0) return json({ error: "nothing to update" }, 400);
|
|
2140
2339
|
await db.transact([{ t: "update", ns: "applications", id, attrs }]);
|
|
2141
2340
|
if (attrs.status !== void 0) {
|
|
2142
|
-
await syncApplicationToCrm(
|
|
2341
|
+
await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, ...attrs }, stage: String(attrs.status) }).catch(() => void 0);
|
|
2143
2342
|
}
|
|
2144
2343
|
return json({ ok: true });
|
|
2145
2344
|
};
|
|
@@ -2398,6 +2597,7 @@ var BUILTIN_ROUTES = [
|
|
|
2398
2597
|
handleAdminPeopleAccess,
|
|
2399
2598
|
handleAdminPeopleRole,
|
|
2400
2599
|
handleAdminCrmSync,
|
|
2600
|
+
handleAdminClerkPrivateProfileSync,
|
|
2401
2601
|
// Aggregation
|
|
2402
2602
|
handleAdminDashboard,
|
|
2403
2603
|
handleAdminBilling,
|