@odla-ai/chapter 0.5.0 → 0.8.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 +111 -28
- package/dist/index.cjs +175 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +157 -9
- package/dist/index.d.ts +157 -9
- package/dist/index.js +175 -17
- package/dist/index.js.map +1 -1
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.js +3 -2
- package/dist/ui/index.js.map +1 -1
- package/dist/worker/index.cjs +369 -17
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +69 -6
- package/dist/worker/index.d.ts +69 -6
- package/dist/worker/index.js +369 -17
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -226,7 +226,7 @@ Warmly,
|
|
|
226
226
|
${name}`;
|
|
227
227
|
return {
|
|
228
228
|
adminNotification: {
|
|
229
|
-
subject: `New application
|
|
229
|
+
subject: `New application: {{firstName}} {{lastName}}`,
|
|
230
230
|
text: `A new application came in for ${name}.
|
|
231
231
|
|
|
232
232
|
Name: {{firstName}} {{lastName}}
|
|
@@ -245,7 +245,7 @@ Your membership payment is confirmed. We'll be in touch to schedule your intro c
|
|
|
245
245
|
Looking forward to our call at {{meetingTime}}. {{meetingLink}}${sign}`
|
|
246
246
|
},
|
|
247
247
|
onboardingInvite: {
|
|
248
|
-
subject: `You're in
|
|
248
|
+
subject: `You're in, ${name}`,
|
|
249
249
|
text: `Hi {{firstName}},
|
|
250
250
|
|
|
251
251
|
Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
|
|
@@ -470,6 +470,10 @@ function defineChapter(config) {
|
|
|
470
470
|
const application = resolveApplication(config.application);
|
|
471
471
|
const { schema, rules } = chapterDb(mode, auth);
|
|
472
472
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
473
|
+
const account = config.account ?? "invite";
|
|
474
|
+
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
475
|
+
throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
|
|
476
|
+
}
|
|
473
477
|
const chapter = {
|
|
474
478
|
config,
|
|
475
479
|
id: id2,
|
|
@@ -482,6 +486,7 @@ function defineChapter(config) {
|
|
|
482
486
|
schema,
|
|
483
487
|
rules,
|
|
484
488
|
services,
|
|
489
|
+
account,
|
|
485
490
|
groupSeed: () => mode === "chapter" ? buildGroupSeed(config) : null
|
|
486
491
|
};
|
|
487
492
|
if (config.url !== void 0) chapter.url = config.url;
|
|
@@ -508,7 +513,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
508
513
|
}
|
|
509
514
|
return {
|
|
510
515
|
id: "chapter",
|
|
511
|
-
title: `Chapter
|
|
516
|
+
title: `Chapter: ${chapter.name}`,
|
|
512
517
|
npm: "@odla-ai/chapter",
|
|
513
518
|
schema: {
|
|
514
519
|
entities: { ...crmDesc.schema.entities, ...chapter.schema.entities },
|
|
@@ -556,6 +561,72 @@ function planDelivery(input) {
|
|
|
556
561
|
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
557
562
|
}
|
|
558
563
|
|
|
564
|
+
// src/notify.ts
|
|
565
|
+
async function sendTemplated(deps, input) {
|
|
566
|
+
const { emailLog: emailLog2 } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
567
|
+
const prior = Array.isArray(emailLog2) ? emailLog2 : [];
|
|
568
|
+
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
569
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
570
|
+
const decision = planDelivery({
|
|
571
|
+
envName: deps.envName,
|
|
572
|
+
group: input.group,
|
|
573
|
+
template: input.template,
|
|
574
|
+
to: input.to,
|
|
575
|
+
vars: input.vars,
|
|
576
|
+
cloudflareReady,
|
|
577
|
+
force: input.force
|
|
578
|
+
});
|
|
579
|
+
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
580
|
+
let error;
|
|
581
|
+
let messageId;
|
|
582
|
+
if (decision.transport === "cloudflare" && deps.sender && deps.from) {
|
|
583
|
+
try {
|
|
584
|
+
const res = await deps.sender.send({
|
|
585
|
+
from: deps.from,
|
|
586
|
+
to: [decision.to],
|
|
587
|
+
subject: decision.subject,
|
|
588
|
+
text: decision.text,
|
|
589
|
+
replyTo: input.group.replyTo
|
|
590
|
+
});
|
|
591
|
+
messageId = res.messageId;
|
|
592
|
+
} catch (e) {
|
|
593
|
+
error = e instanceof Error ? e.message : String(e);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const id2 = deps.newId();
|
|
597
|
+
const row = {
|
|
598
|
+
id: id2,
|
|
599
|
+
groupId: input.group.id,
|
|
600
|
+
to: decision.to,
|
|
601
|
+
template: input.template,
|
|
602
|
+
subject: decision.subject,
|
|
603
|
+
body: decision.text,
|
|
604
|
+
transport: decision.transport,
|
|
605
|
+
redirected: decision.redirected,
|
|
606
|
+
dedupeKey: input.dedupeKey,
|
|
607
|
+
sentAt: deps.now(),
|
|
608
|
+
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
609
|
+
...messageId ? { messageId } : {},
|
|
610
|
+
...error ? { error } : {}
|
|
611
|
+
};
|
|
612
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id2, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
613
|
+
return error ? { sent: false, reason: error } : { sent: true };
|
|
614
|
+
}
|
|
615
|
+
function emailGroupFrom(row) {
|
|
616
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
617
|
+
const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
|
|
618
|
+
return {
|
|
619
|
+
id: String(row.id),
|
|
620
|
+
name: String(row.name ?? ""),
|
|
621
|
+
replyTo: str(row.replyTo) ?? "",
|
|
622
|
+
debugEmail: str(row.debugEmail),
|
|
623
|
+
refundPolicyText: str(row.refundPolicyText),
|
|
624
|
+
commitmentText: str(row.commitmentText),
|
|
625
|
+
normsText: str(row.normsText),
|
|
626
|
+
emailTemplates: templates
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
559
630
|
// src/payments.ts
|
|
560
631
|
function parseSigHeader(header) {
|
|
561
632
|
const parts = {};
|
|
@@ -665,21 +736,73 @@ function sharedPersonInput(person) {
|
|
|
665
736
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
666
737
|
return input;
|
|
667
738
|
}
|
|
668
|
-
async function
|
|
669
|
-
const email =
|
|
670
|
-
const input = sharedPersonInput(person);
|
|
739
|
+
async function upsertPerson(deps, opts) {
|
|
740
|
+
const email = opts.email.toLowerCase();
|
|
671
741
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
672
|
-
const { crm_record } = await deps.db.query({
|
|
673
|
-
crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } }
|
|
674
|
-
});
|
|
742
|
+
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
675
743
|
const existing = crm_record?.[0];
|
|
676
744
|
if (existing && typeof existing.id === "string") {
|
|
677
|
-
await updateRecord(crmDeps, { id: existing.id, input });
|
|
745
|
+
await updateRecord(crmDeps, { id: existing.id, input: opts.input });
|
|
678
746
|
return { recordId: existing.id };
|
|
679
747
|
}
|
|
680
|
-
const created = await createRecord(crmDeps, { type: "person", input, mutationId:
|
|
748
|
+
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
681
749
|
return { recordId: created.id };
|
|
682
750
|
}
|
|
751
|
+
async function projectSharedRecord(deps, person) {
|
|
752
|
+
return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
|
|
753
|
+
}
|
|
754
|
+
async function projectApplicant(deps, applicant) {
|
|
755
|
+
const input = sharedPersonInput({
|
|
756
|
+
email: applicant.email,
|
|
757
|
+
firstName: applicant.firstName,
|
|
758
|
+
lastName: applicant.lastName,
|
|
759
|
+
phone: applicant.phone,
|
|
760
|
+
linkedin: applicant.linkedin,
|
|
761
|
+
hubRecordId: applicant.applicationId
|
|
762
|
+
});
|
|
763
|
+
return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/clerk.ts
|
|
767
|
+
function clerkInviteRequest(input) {
|
|
768
|
+
return {
|
|
769
|
+
path: "/v1/invitations",
|
|
770
|
+
body: {
|
|
771
|
+
email_address: input.email,
|
|
772
|
+
notify: true,
|
|
773
|
+
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
778
|
+
const { path, body } = clerkInviteRequest(input);
|
|
779
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
780
|
+
method: "POST",
|
|
781
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
782
|
+
body: JSON.stringify(body)
|
|
783
|
+
});
|
|
784
|
+
return { ok: res.ok, status: res.status };
|
|
785
|
+
}
|
|
786
|
+
function clerkUserRequest(input) {
|
|
787
|
+
return {
|
|
788
|
+
path: "/v1/users",
|
|
789
|
+
body: {
|
|
790
|
+
email_address: [input.email],
|
|
791
|
+
skip_password_requirement: true,
|
|
792
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
793
|
+
...input.lastName ? { last_name: input.lastName } : {}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
798
|
+
const { path, body } = clerkUserRequest(input);
|
|
799
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
800
|
+
method: "POST",
|
|
801
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
802
|
+
body: JSON.stringify(body)
|
|
803
|
+
});
|
|
804
|
+
return { ok: res.ok, status: res.status };
|
|
805
|
+
}
|
|
683
806
|
|
|
684
807
|
// src/session.ts
|
|
685
808
|
function applicationSummary(app) {
|
|
@@ -745,6 +868,38 @@ function brandTokens(brand) {
|
|
|
745
868
|
` : "";
|
|
746
869
|
}
|
|
747
870
|
|
|
871
|
+
// src/reconcile.ts
|
|
872
|
+
function isReconcilable(meeting, now) {
|
|
873
|
+
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
874
|
+
}
|
|
875
|
+
function reconcileMeetings(meetings2, events, now) {
|
|
876
|
+
const byEvent = new Map(events.map((e) => [e.eventId, e]));
|
|
877
|
+
const decisions = [];
|
|
878
|
+
for (const m of meetings2) {
|
|
879
|
+
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
880
|
+
const g = byEvent.get(m.googleEventId);
|
|
881
|
+
if (!g || g.status === "cancelled") {
|
|
882
|
+
decisions.push({
|
|
883
|
+
meetingId: m.id,
|
|
884
|
+
applicationId: m.applicationId,
|
|
885
|
+
kind: "cancelled",
|
|
886
|
+
meetingPatch: { status: "cancelled", drift: "none", adoptedFromGoogleAt: now },
|
|
887
|
+
applicationPatch: { meetingAt: 0, meetingLink: "" }
|
|
888
|
+
});
|
|
889
|
+
} else if (g.startAt !== void 0 && g.startAt !== m.startAt) {
|
|
890
|
+
const duration = (m.endAt ?? 0) - (m.startAt ?? 0);
|
|
891
|
+
decisions.push({
|
|
892
|
+
meetingId: m.id,
|
|
893
|
+
applicationId: m.applicationId,
|
|
894
|
+
kind: "moved",
|
|
895
|
+
meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: "none", adoptedFromGoogleAt: now },
|
|
896
|
+
applicationPatch: { meetingAt: g.startAt }
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return decisions;
|
|
901
|
+
}
|
|
902
|
+
|
|
748
903
|
// src/scheduling.ts
|
|
749
904
|
var SCHEDULING_DEFAULTS = {
|
|
750
905
|
slotMinutes: 45,
|
|
@@ -791,10 +946,6 @@ function resolveScheduling(config) {
|
|
|
791
946
|
if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
|
|
792
947
|
return { ...c, days };
|
|
793
948
|
}
|
|
794
|
-
var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
795
|
-
function canBookFrom(status) {
|
|
796
|
-
return BOOKABLE_STATUSES.includes(status);
|
|
797
|
-
}
|
|
798
949
|
function slotWindow(now, windowDays) {
|
|
799
950
|
return { from: now, to: now + windowDays * 864e5 };
|
|
800
951
|
}
|
|
@@ -841,7 +992,6 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
841
992
|
};
|
|
842
993
|
}
|
|
843
994
|
export {
|
|
844
|
-
BOOKABLE_STATUSES,
|
|
845
995
|
SCHEDULING_DEFAULTS,
|
|
846
996
|
applicationBookingUpdate,
|
|
847
997
|
applicationSummary,
|
|
@@ -850,14 +1000,18 @@ export {
|
|
|
850
1000
|
buildGroupSeed,
|
|
851
1001
|
canApprove,
|
|
852
1002
|
canBook,
|
|
853
|
-
canBookFrom,
|
|
854
1003
|
canChangeRole,
|
|
855
1004
|
canTransition,
|
|
856
1005
|
canceledPatch,
|
|
857
1006
|
chapterDb,
|
|
1007
|
+
clerkInviteRequest,
|
|
1008
|
+
clerkUserRequest,
|
|
858
1009
|
createChapterIntegration,
|
|
1010
|
+
createClerkInvitation,
|
|
1011
|
+
createClerkUser,
|
|
859
1012
|
defaultCrm,
|
|
860
1013
|
defineChapter,
|
|
1014
|
+
emailGroupFrom,
|
|
861
1015
|
endForSlot,
|
|
862
1016
|
findApplicationRef,
|
|
863
1017
|
firstPaymentPatch,
|
|
@@ -865,6 +1019,7 @@ export {
|
|
|
865
1019
|
introIdempotencyKey,
|
|
866
1020
|
isAdminRole,
|
|
867
1021
|
isAlreadySent,
|
|
1022
|
+
isReconcilable,
|
|
868
1023
|
isSlotAvailable,
|
|
869
1024
|
joinConfig,
|
|
870
1025
|
meetingCreateRow,
|
|
@@ -874,7 +1029,9 @@ export {
|
|
|
874
1029
|
normalizeWebhookEvent,
|
|
875
1030
|
paymentsReady,
|
|
876
1031
|
planDelivery,
|
|
1032
|
+
projectApplicant,
|
|
877
1033
|
projectSharedRecord,
|
|
1034
|
+
reconcileMeetings,
|
|
878
1035
|
refundedPatch,
|
|
879
1036
|
render,
|
|
880
1037
|
renderSummary,
|
|
@@ -885,6 +1042,7 @@ export {
|
|
|
885
1042
|
resolvePipeline,
|
|
886
1043
|
resolveScheduling,
|
|
887
1044
|
roleFromClaim,
|
|
1045
|
+
sendTemplated,
|
|
888
1046
|
sharedPersonInput,
|
|
889
1047
|
slotWindow,
|
|
890
1048
|
stageIndex,
|