@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/worker/index.js
CHANGED
|
@@ -94,6 +94,47 @@ function createWorkerContext(options) {
|
|
|
94
94
|
// src/worker-routes.ts
|
|
95
95
|
import { createCrmRoutes } from "@odla-ai/crm";
|
|
96
96
|
|
|
97
|
+
// src/clerk.ts
|
|
98
|
+
function clerkInviteRequest(input) {
|
|
99
|
+
return {
|
|
100
|
+
path: "/v1/invitations",
|
|
101
|
+
body: {
|
|
102
|
+
email_address: input.email,
|
|
103
|
+
notify: true,
|
|
104
|
+
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
109
|
+
const { path, body } = clerkInviteRequest(input);
|
|
110
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
113
|
+
body: JSON.stringify(body)
|
|
114
|
+
});
|
|
115
|
+
return { ok: res.ok, status: res.status };
|
|
116
|
+
}
|
|
117
|
+
function clerkUserRequest(input) {
|
|
118
|
+
return {
|
|
119
|
+
path: "/v1/users",
|
|
120
|
+
body: {
|
|
121
|
+
email_address: [input.email],
|
|
122
|
+
skip_password_requirement: true,
|
|
123
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
124
|
+
...input.lastName ? { last_name: input.lastName } : {}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
129
|
+
const { path, body } = clerkUserRequest(input);
|
|
130
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
133
|
+
body: JSON.stringify(body)
|
|
134
|
+
});
|
|
135
|
+
return { ok: res.ok, status: res.status };
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
// src/member.ts
|
|
98
139
|
async function submitApplication(db, chapter, fields, opts) {
|
|
99
140
|
const app = chapter.application;
|
|
@@ -147,21 +188,32 @@ function sharedPersonInput(person) {
|
|
|
147
188
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
148
189
|
return input;
|
|
149
190
|
}
|
|
150
|
-
async function
|
|
151
|
-
const email =
|
|
152
|
-
const input = sharedPersonInput(person);
|
|
191
|
+
async function upsertPerson(deps, opts) {
|
|
192
|
+
const email = opts.email.toLowerCase();
|
|
153
193
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
154
|
-
const { crm_record } = await deps.db.query({
|
|
155
|
-
crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } }
|
|
156
|
-
});
|
|
194
|
+
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
157
195
|
const existing = crm_record?.[0];
|
|
158
196
|
if (existing && typeof existing.id === "string") {
|
|
159
|
-
await updateRecord(crmDeps, { id: existing.id, input });
|
|
197
|
+
await updateRecord(crmDeps, { id: existing.id, input: opts.input });
|
|
160
198
|
return { recordId: existing.id };
|
|
161
199
|
}
|
|
162
|
-
const created = await createRecord(crmDeps, { type: "person", input, mutationId:
|
|
200
|
+
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
163
201
|
return { recordId: created.id };
|
|
164
202
|
}
|
|
203
|
+
async function projectSharedRecord(deps, person) {
|
|
204
|
+
return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
|
|
205
|
+
}
|
|
206
|
+
async function projectApplicant(deps, applicant) {
|
|
207
|
+
const input = sharedPersonInput({
|
|
208
|
+
email: applicant.email,
|
|
209
|
+
firstName: applicant.firstName,
|
|
210
|
+
lastName: applicant.lastName,
|
|
211
|
+
phone: applicant.phone,
|
|
212
|
+
linkedin: applicant.linkedin,
|
|
213
|
+
hubRecordId: applicant.applicationId
|
|
214
|
+
});
|
|
215
|
+
return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
|
|
216
|
+
}
|
|
165
217
|
|
|
166
218
|
// src/scheduling.ts
|
|
167
219
|
var SCHEDULING_DEFAULTS = {
|
|
@@ -209,10 +261,6 @@ function resolveScheduling(config) {
|
|
|
209
261
|
if (typeof c.summaryTemplate !== "string") fail2("summaryTemplate must be a string");
|
|
210
262
|
return { ...c, days };
|
|
211
263
|
}
|
|
212
|
-
var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
213
|
-
function canBookFrom(status) {
|
|
214
|
-
return BOOKABLE_STATUSES.includes(status);
|
|
215
|
-
}
|
|
216
264
|
function slotWindow(now, windowDays) {
|
|
217
265
|
return { from: now, to: now + windowDays * 864e5 };
|
|
218
266
|
}
|
|
@@ -291,7 +339,146 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
291
339
|
return { ...summary, meetingAt, meetUrl, timezone };
|
|
292
340
|
}
|
|
293
341
|
|
|
342
|
+
// src/email.ts
|
|
343
|
+
function render(template, vars) {
|
|
344
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
345
|
+
}
|
|
346
|
+
function groupVars(group, vars) {
|
|
347
|
+
return {
|
|
348
|
+
...vars,
|
|
349
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
350
|
+
commitmentText: group.commitmentText ?? "",
|
|
351
|
+
normsText: group.normsText ?? ""
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function isAlreadySent(priorRows) {
|
|
355
|
+
return priorRows.some((row) => !row.error);
|
|
356
|
+
}
|
|
357
|
+
function planDelivery(input) {
|
|
358
|
+
const tpl = input.group.emailTemplates?.[input.template];
|
|
359
|
+
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
360
|
+
if (tpl.enabled === false && !input.force) return { deliver: false, reason: "disabled" };
|
|
361
|
+
const vars = groupVars(input.group, input.vars);
|
|
362
|
+
const isProd = input.envName === "prod";
|
|
363
|
+
const redirect = !isProd && !!input.group.debugEmail;
|
|
364
|
+
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
365
|
+
const to = redirect ? input.group.debugEmail : input.to;
|
|
366
|
+
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
367
|
+
const text = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
368
|
+
|
|
369
|
+
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
370
|
+
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/notify.ts
|
|
374
|
+
async function sendTemplated(deps, input) {
|
|
375
|
+
const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
376
|
+
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
377
|
+
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
378
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
379
|
+
const decision = planDelivery({
|
|
380
|
+
envName: deps.envName,
|
|
381
|
+
group: input.group,
|
|
382
|
+
template: input.template,
|
|
383
|
+
to: input.to,
|
|
384
|
+
vars: input.vars,
|
|
385
|
+
cloudflareReady,
|
|
386
|
+
force: input.force
|
|
387
|
+
});
|
|
388
|
+
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
389
|
+
let error;
|
|
390
|
+
let messageId;
|
|
391
|
+
if (decision.transport === "cloudflare" && deps.sender && deps.from) {
|
|
392
|
+
try {
|
|
393
|
+
const res = await deps.sender.send({
|
|
394
|
+
from: deps.from,
|
|
395
|
+
to: [decision.to],
|
|
396
|
+
subject: decision.subject,
|
|
397
|
+
text: decision.text,
|
|
398
|
+
replyTo: input.group.replyTo
|
|
399
|
+
});
|
|
400
|
+
messageId = res.messageId;
|
|
401
|
+
} catch (e) {
|
|
402
|
+
error = e instanceof Error ? e.message : String(e);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const id = deps.newId();
|
|
406
|
+
const row = {
|
|
407
|
+
id,
|
|
408
|
+
groupId: input.group.id,
|
|
409
|
+
to: decision.to,
|
|
410
|
+
template: input.template,
|
|
411
|
+
subject: decision.subject,
|
|
412
|
+
body: decision.text,
|
|
413
|
+
transport: decision.transport,
|
|
414
|
+
redirected: decision.redirected,
|
|
415
|
+
dedupeKey: input.dedupeKey,
|
|
416
|
+
sentAt: deps.now(),
|
|
417
|
+
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
418
|
+
...messageId ? { messageId } : {},
|
|
419
|
+
...error ? { error } : {}
|
|
420
|
+
};
|
|
421
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
422
|
+
return error ? { sent: false, reason: error } : { sent: true };
|
|
423
|
+
}
|
|
424
|
+
function emailGroupFrom(row) {
|
|
425
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
426
|
+
const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
|
|
427
|
+
return {
|
|
428
|
+
id: String(row.id),
|
|
429
|
+
name: String(row.name ?? ""),
|
|
430
|
+
replyTo: str(row.replyTo) ?? "",
|
|
431
|
+
debugEmail: str(row.debugEmail),
|
|
432
|
+
refundPolicyText: str(row.refundPolicyText),
|
|
433
|
+
commitmentText: str(row.commitmentText),
|
|
434
|
+
normsText: str(row.normsText),
|
|
435
|
+
emailTemplates: templates
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
294
439
|
// src/worker-routes.ts
|
|
440
|
+
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
441
|
+
const email = typeof fields.email === "string" ? fields.email : "";
|
|
442
|
+
if (!email) return;
|
|
443
|
+
const s = (v) => typeof v === "string" ? v : void 0;
|
|
444
|
+
try {
|
|
445
|
+
await projectApplicant(
|
|
446
|
+
{ crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
447
|
+
{ applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin) }
|
|
448
|
+
);
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
if (chapter.account !== "none") {
|
|
452
|
+
try {
|
|
453
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
454
|
+
if (secret) {
|
|
455
|
+
if (chapter.account === "create") await createClerkUser(secret, { email, firstName: s(fields.firstName), lastName: s(fields.lastName) });
|
|
456
|
+
else await createClerkInvitation(secret, { email });
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
463
|
+
try {
|
|
464
|
+
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
465
|
+
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
466
|
+
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
467
|
+
const s = (v) => typeof v === "string" ? v : "";
|
|
468
|
+
await sendTemplated(
|
|
469
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
470
|
+
{
|
|
471
|
+
group: emailGroupFrom(group),
|
|
472
|
+
template: "adminNotification",
|
|
473
|
+
to: group.notificationEmail,
|
|
474
|
+
vars: { firstName: s(fields.firstName), lastName: s(fields.lastName), email: s(fields.email), phone: s(fields.phone), state: s(fields.state) },
|
|
475
|
+
dedupeKey: `apply:${applicationId}:admin`,
|
|
476
|
+
applicationId
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
295
482
|
async function memberSessionApplication(db, chapterId, email) {
|
|
296
483
|
const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
|
|
297
484
|
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
@@ -303,6 +490,7 @@ async function memberSessionApplication(db, chapterId, email) {
|
|
|
303
490
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
304
491
|
return memberApplication(app, meeting, timezone);
|
|
305
492
|
}
|
|
493
|
+
var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
|
|
306
494
|
var handleConfig = async (_req, url, env, ctx) => {
|
|
307
495
|
if (url.pathname !== "/api/config") return null;
|
|
308
496
|
try {
|
|
@@ -392,13 +580,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
392
580
|
return json({ error: "invalid JSON body" }, 400);
|
|
393
581
|
}
|
|
394
582
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
395
|
-
const
|
|
583
|
+
const db = ctx.makeDb(env);
|
|
584
|
+
const result = await submitApplication(db, chapter, parsed, {
|
|
396
585
|
submissionId,
|
|
397
586
|
groupId: chapter.id,
|
|
398
587
|
now: Date.now(),
|
|
399
588
|
newId: () => crypto.randomUUID()
|
|
400
589
|
});
|
|
401
590
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
591
|
+
if (!result.duplicate) {
|
|
592
|
+
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
593
|
+
await provisionApplicant(db, chapter, result.id, parsed);
|
|
594
|
+
}
|
|
402
595
|
return json({ id: result.id, duplicate: result.duplicate, status: result.status });
|
|
403
596
|
}
|
|
404
597
|
return null;
|
|
@@ -406,6 +599,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
406
599
|
|
|
407
600
|
// src/worker-routes-schedule.ts
|
|
408
601
|
import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
|
|
602
|
+
|
|
603
|
+
// src/pipeline.ts
|
|
604
|
+
function canBook(status, p) {
|
|
605
|
+
return p.bookableFrom.includes(status);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/worker-routes-schedule.ts
|
|
409
609
|
function errCode(err) {
|
|
410
610
|
if (err && typeof err === "object") {
|
|
411
611
|
const code = err.code;
|
|
@@ -447,7 +647,7 @@ async function bookSlot(req, env, ctx) {
|
|
|
447
647
|
const app = await firstRow(db, "applications", { where: { id: applicationId }, limit: 1 });
|
|
448
648
|
if (!app) return json({ error: "not found" }, 404);
|
|
449
649
|
const status = String(app.status ?? "");
|
|
450
|
-
if (!
|
|
650
|
+
if (!canBook(status, ctx.chapter.pipeline)) return json({ error: `cannot book from status "${status}"` }, 409);
|
|
451
651
|
const group = await firstRow(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
|
|
452
652
|
if (!group) return json({ error: "group not found" }, 500);
|
|
453
653
|
const cfg = resolveScheduling(group.schedulingJson);
|
|
@@ -509,6 +709,12 @@ async function bookSlot(req, env, ctx) {
|
|
|
509
709
|
}
|
|
510
710
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };
|
|
511
711
|
await db.transact([meetingOp, appOp]);
|
|
712
|
+
if (typeof app.email === "string" && app.email) {
|
|
713
|
+
await sendTemplated(
|
|
714
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
715
|
+
{ group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
|
|
716
|
+
).catch(() => void 0);
|
|
717
|
+
}
|
|
512
718
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
513
719
|
}
|
|
514
720
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -744,6 +950,25 @@ async function findApplication(db, event) {
|
|
|
744
950
|
}
|
|
745
951
|
return void 0;
|
|
746
952
|
}
|
|
953
|
+
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
954
|
+
try {
|
|
955
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
956
|
+
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
957
|
+
if (!group) return;
|
|
958
|
+
await sendTemplated(
|
|
959
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
960
|
+
{
|
|
961
|
+
group: emailGroupFrom(group),
|
|
962
|
+
template: "paymentConfirmation",
|
|
963
|
+
to: app.email,
|
|
964
|
+
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
965
|
+
dedupeKey: `${eventId}:confirm`,
|
|
966
|
+
applicationId: String(app.id)
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
747
972
|
function webhookPatch(event, status) {
|
|
748
973
|
switch (event.kind) {
|
|
749
974
|
case "first_payment":
|
|
@@ -815,6 +1040,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
815
1040
|
if (Object.keys(patch).length) {
|
|
816
1041
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
|
|
817
1042
|
}
|
|
1043
|
+
if (event.kind === "first_payment") await notifyPaymentConfirmed(db, env, eventId, app);
|
|
818
1044
|
return json({ ok: true });
|
|
819
1045
|
}
|
|
820
1046
|
async function refundApplication(req, url, env, ctx) {
|
|
@@ -851,14 +1077,139 @@ var handlePayments = async (req, url, env, ctx) => {
|
|
|
851
1077
|
return null;
|
|
852
1078
|
};
|
|
853
1079
|
|
|
1080
|
+
// src/worker-routes-admin.ts
|
|
1081
|
+
import { initCalendar as initCalendar2 } from "@odla-ai/calendar";
|
|
1082
|
+
|
|
1083
|
+
// src/reconcile.ts
|
|
1084
|
+
function isReconcilable(meeting, now) {
|
|
1085
|
+
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
1086
|
+
}
|
|
1087
|
+
function reconcileMeetings(meetings, events, now) {
|
|
1088
|
+
const byEvent = new Map(events.map((e) => [e.eventId, e]));
|
|
1089
|
+
const decisions = [];
|
|
1090
|
+
for (const m of meetings) {
|
|
1091
|
+
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
1092
|
+
const g = byEvent.get(m.googleEventId);
|
|
1093
|
+
if (!g || g.status === "cancelled") {
|
|
1094
|
+
decisions.push({
|
|
1095
|
+
meetingId: m.id,
|
|
1096
|
+
applicationId: m.applicationId,
|
|
1097
|
+
kind: "cancelled",
|
|
1098
|
+
meetingPatch: { status: "cancelled", drift: "none", adoptedFromGoogleAt: now },
|
|
1099
|
+
applicationPatch: { meetingAt: 0, meetingLink: "" }
|
|
1100
|
+
});
|
|
1101
|
+
} else if (g.startAt !== void 0 && g.startAt !== m.startAt) {
|
|
1102
|
+
const duration = (m.endAt ?? 0) - (m.startAt ?? 0);
|
|
1103
|
+
decisions.push({
|
|
1104
|
+
meetingId: m.id,
|
|
1105
|
+
applicationId: m.applicationId,
|
|
1106
|
+
kind: "moved",
|
|
1107
|
+
meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: "none", adoptedFromGoogleAt: now },
|
|
1108
|
+
applicationPatch: { meetingAt: g.startAt }
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return decisions;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/worker-routes-admin.ts
|
|
1116
|
+
async function adminGroup(req, env, ctx, url) {
|
|
1117
|
+
const rawDb = ctx.makeDb(env);
|
|
1118
|
+
const u = await ctx.verifyUser(req, env);
|
|
1119
|
+
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1120
|
+
const db = rawDb;
|
|
1121
|
+
const groupId = url.searchParams.get("group") ?? ctx.chapter.id;
|
|
1122
|
+
const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];
|
|
1123
|
+
if (!group) return json({ error: "not found" }, 404);
|
|
1124
|
+
return { db, group };
|
|
1125
|
+
}
|
|
1126
|
+
var handleAdminScheduling = async (req, url, env, ctx) => {
|
|
1127
|
+
if (url.pathname !== "/api/admin/scheduling" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
1128
|
+
const got = await adminGroup(req, env, ctx, url);
|
|
1129
|
+
if (got instanceof Response) return got;
|
|
1130
|
+
const { db, group } = got;
|
|
1131
|
+
if (req.method === "GET") {
|
|
1132
|
+
return json({ scheduling: resolveScheduling(group.schedulingJson) });
|
|
1133
|
+
}
|
|
1134
|
+
let body;
|
|
1135
|
+
try {
|
|
1136
|
+
body = JSON.parse(await req.text());
|
|
1137
|
+
} catch {
|
|
1138
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
1139
|
+
}
|
|
1140
|
+
let resolved;
|
|
1141
|
+
try {
|
|
1142
|
+
resolved = resolveScheduling(body);
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
return json({ error: e instanceof Error ? e.message : "invalid scheduling config" }, 400);
|
|
1145
|
+
}
|
|
1146
|
+
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: resolved } }]);
|
|
1147
|
+
return json({ scheduling: resolved });
|
|
1148
|
+
};
|
|
1149
|
+
async function upcomingEvents(env) {
|
|
1150
|
+
const cal = initCalendar2({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
|
|
1151
|
+
const res = await cal.availability.upcoming();
|
|
1152
|
+
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
1153
|
+
}
|
|
1154
|
+
function toReconcile(rows) {
|
|
1155
|
+
return rows.map((m) => ({
|
|
1156
|
+
id: String(m.id),
|
|
1157
|
+
applicationId: String(m.applicationId),
|
|
1158
|
+
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
1159
|
+
status: String(m.status ?? ""),
|
|
1160
|
+
startAt: typeof m.startAt === "number" ? m.startAt : null,
|
|
1161
|
+
endAt: typeof m.endAt === "number" ? m.endAt : null
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
1165
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/meetings") return null;
|
|
1166
|
+
const rawDb = ctx.makeDb(env);
|
|
1167
|
+
const u = await ctx.verifyUser(req, env);
|
|
1168
|
+
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1169
|
+
const db = rawDb;
|
|
1170
|
+
const query = await db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } });
|
|
1171
|
+
const rows = Array.isArray(query.meetings) ? query.meetings : [];
|
|
1172
|
+
let decisions = [];
|
|
1173
|
+
try {
|
|
1174
|
+
decisions = reconcileMeetings(toReconcile(rows), await upcomingEvents(env), Date.now());
|
|
1175
|
+
} catch {
|
|
1176
|
+
}
|
|
1177
|
+
const patched = /* @__PURE__ */ new Map();
|
|
1178
|
+
for (const d of decisions) {
|
|
1179
|
+
const ops = [
|
|
1180
|
+
{ t: "update", ns: "meetings", id: d.meetingId, attrs: d.meetingPatch },
|
|
1181
|
+
{ t: "update", ns: "applications", id: d.applicationId, attrs: d.applicationPatch }
|
|
1182
|
+
];
|
|
1183
|
+
try {
|
|
1184
|
+
await db.transact(ops);
|
|
1185
|
+
patched.set(d.meetingId, d.meetingPatch);
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const meetings = rows.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {} })).filter((m) => m.status === "scheduled");
|
|
1190
|
+
return json({ meetings, adopted: decisions.length });
|
|
1191
|
+
};
|
|
1192
|
+
|
|
854
1193
|
// src/worker.ts
|
|
855
|
-
var
|
|
1194
|
+
var BUILTIN_ROUTES = [
|
|
1195
|
+
handleHealth,
|
|
1196
|
+
handleConfig,
|
|
1197
|
+
handleMe,
|
|
1198
|
+
handleCrm,
|
|
1199
|
+
handleNetworkShared,
|
|
1200
|
+
handleMember,
|
|
1201
|
+
handleSchedule,
|
|
1202
|
+
handlePayments,
|
|
1203
|
+
handleAdminMeetings,
|
|
1204
|
+
handleAdminScheduling
|
|
1205
|
+
];
|
|
856
1206
|
function chapterWorker(options) {
|
|
857
1207
|
const ctx = createWorkerContext(options);
|
|
1208
|
+
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
858
1209
|
return {
|
|
859
1210
|
async fetch(req, env) {
|
|
860
1211
|
const url = new URL(req.url);
|
|
861
|
-
for (const route of
|
|
1212
|
+
for (const route of routes) {
|
|
862
1213
|
const res = await route(req, url, env, ctx);
|
|
863
1214
|
if (res) return res;
|
|
864
1215
|
}
|
|
@@ -867,6 +1218,7 @@ function chapterWorker(options) {
|
|
|
867
1218
|
};
|
|
868
1219
|
}
|
|
869
1220
|
export {
|
|
870
|
-
chapterWorker
|
|
1221
|
+
chapterWorker,
|
|
1222
|
+
createWorkerContext
|
|
871
1223
|
};
|
|
872
1224
|
//# sourceMappingURL=index.js.map
|