@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.cjs
CHANGED
|
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/worker.ts
|
|
21
21
|
var worker_exports = {};
|
|
22
22
|
__export(worker_exports, {
|
|
23
|
-
chapterWorker: () => chapterWorker
|
|
23
|
+
chapterWorker: () => chapterWorker,
|
|
24
|
+
createWorkerContext: () => createWorkerContext
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(worker_exports);
|
|
26
27
|
|
|
@@ -120,6 +121,47 @@ function createWorkerContext(options) {
|
|
|
120
121
|
// src/worker-routes.ts
|
|
121
122
|
var import_crm2 = require("@odla-ai/crm");
|
|
122
123
|
|
|
124
|
+
// src/clerk.ts
|
|
125
|
+
function clerkInviteRequest(input) {
|
|
126
|
+
return {
|
|
127
|
+
path: "/v1/invitations",
|
|
128
|
+
body: {
|
|
129
|
+
email_address: input.email,
|
|
130
|
+
notify: true,
|
|
131
|
+
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
136
|
+
const { path, body } = clerkInviteRequest(input);
|
|
137
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
140
|
+
body: JSON.stringify(body)
|
|
141
|
+
});
|
|
142
|
+
return { ok: res.ok, status: res.status };
|
|
143
|
+
}
|
|
144
|
+
function clerkUserRequest(input) {
|
|
145
|
+
return {
|
|
146
|
+
path: "/v1/users",
|
|
147
|
+
body: {
|
|
148
|
+
email_address: [input.email],
|
|
149
|
+
skip_password_requirement: true,
|
|
150
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
151
|
+
...input.lastName ? { last_name: input.lastName } : {}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
156
|
+
const { path, body } = clerkUserRequest(input);
|
|
157
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
160
|
+
body: JSON.stringify(body)
|
|
161
|
+
});
|
|
162
|
+
return { ok: res.ok, status: res.status };
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
// src/member.ts
|
|
124
166
|
async function submitApplication(db, chapter, fields, opts) {
|
|
125
167
|
const app = chapter.application;
|
|
@@ -173,21 +215,32 @@ function sharedPersonInput(person) {
|
|
|
173
215
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
174
216
|
return input;
|
|
175
217
|
}
|
|
176
|
-
async function
|
|
177
|
-
const email =
|
|
178
|
-
const input = sharedPersonInput(person);
|
|
218
|
+
async function upsertPerson(deps, opts) {
|
|
219
|
+
const email = opts.email.toLowerCase();
|
|
179
220
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
180
|
-
const { crm_record } = await deps.db.query({
|
|
181
|
-
crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } }
|
|
182
|
-
});
|
|
221
|
+
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
183
222
|
const existing = crm_record?.[0];
|
|
184
223
|
if (existing && typeof existing.id === "string") {
|
|
185
|
-
await (0, import_crm.updateRecord)(crmDeps, { id: existing.id, input });
|
|
224
|
+
await (0, import_crm.updateRecord)(crmDeps, { id: existing.id, input: opts.input });
|
|
186
225
|
return { recordId: existing.id };
|
|
187
226
|
}
|
|
188
|
-
const created = await (0, import_crm.createRecord)(crmDeps, { type: "person", input, mutationId:
|
|
227
|
+
const created = await (0, import_crm.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
189
228
|
return { recordId: created.id };
|
|
190
229
|
}
|
|
230
|
+
async function projectSharedRecord(deps, person) {
|
|
231
|
+
return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
|
|
232
|
+
}
|
|
233
|
+
async function projectApplicant(deps, applicant) {
|
|
234
|
+
const input = sharedPersonInput({
|
|
235
|
+
email: applicant.email,
|
|
236
|
+
firstName: applicant.firstName,
|
|
237
|
+
lastName: applicant.lastName,
|
|
238
|
+
phone: applicant.phone,
|
|
239
|
+
linkedin: applicant.linkedin,
|
|
240
|
+
hubRecordId: applicant.applicationId
|
|
241
|
+
});
|
|
242
|
+
return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
|
|
243
|
+
}
|
|
191
244
|
|
|
192
245
|
// src/scheduling.ts
|
|
193
246
|
var SCHEDULING_DEFAULTS = {
|
|
@@ -235,10 +288,6 @@ function resolveScheduling(config) {
|
|
|
235
288
|
if (typeof c.summaryTemplate !== "string") fail2("summaryTemplate must be a string");
|
|
236
289
|
return { ...c, days };
|
|
237
290
|
}
|
|
238
|
-
var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
239
|
-
function canBookFrom(status) {
|
|
240
|
-
return BOOKABLE_STATUSES.includes(status);
|
|
241
|
-
}
|
|
242
291
|
function slotWindow(now, windowDays) {
|
|
243
292
|
return { from: now, to: now + windowDays * 864e5 };
|
|
244
293
|
}
|
|
@@ -317,7 +366,146 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
317
366
|
return { ...summary, meetingAt, meetUrl, timezone };
|
|
318
367
|
}
|
|
319
368
|
|
|
369
|
+
// src/email.ts
|
|
370
|
+
function render(template, vars) {
|
|
371
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
372
|
+
}
|
|
373
|
+
function groupVars(group, vars) {
|
|
374
|
+
return {
|
|
375
|
+
...vars,
|
|
376
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
377
|
+
commitmentText: group.commitmentText ?? "",
|
|
378
|
+
normsText: group.normsText ?? ""
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function isAlreadySent(priorRows) {
|
|
382
|
+
return priorRows.some((row) => !row.error);
|
|
383
|
+
}
|
|
384
|
+
function planDelivery(input) {
|
|
385
|
+
const tpl = input.group.emailTemplates?.[input.template];
|
|
386
|
+
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
387
|
+
if (tpl.enabled === false && !input.force) return { deliver: false, reason: "disabled" };
|
|
388
|
+
const vars = groupVars(input.group, input.vars);
|
|
389
|
+
const isProd = input.envName === "prod";
|
|
390
|
+
const redirect = !isProd && !!input.group.debugEmail;
|
|
391
|
+
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
392
|
+
const to = redirect ? input.group.debugEmail : input.to;
|
|
393
|
+
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
394
|
+
const text = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
395
|
+
|
|
396
|
+
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
397
|
+
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/notify.ts
|
|
401
|
+
async function sendTemplated(deps, input) {
|
|
402
|
+
const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
403
|
+
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
404
|
+
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
405
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
406
|
+
const decision = planDelivery({
|
|
407
|
+
envName: deps.envName,
|
|
408
|
+
group: input.group,
|
|
409
|
+
template: input.template,
|
|
410
|
+
to: input.to,
|
|
411
|
+
vars: input.vars,
|
|
412
|
+
cloudflareReady,
|
|
413
|
+
force: input.force
|
|
414
|
+
});
|
|
415
|
+
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
416
|
+
let error;
|
|
417
|
+
let messageId;
|
|
418
|
+
if (decision.transport === "cloudflare" && deps.sender && deps.from) {
|
|
419
|
+
try {
|
|
420
|
+
const res = await deps.sender.send({
|
|
421
|
+
from: deps.from,
|
|
422
|
+
to: [decision.to],
|
|
423
|
+
subject: decision.subject,
|
|
424
|
+
text: decision.text,
|
|
425
|
+
replyTo: input.group.replyTo
|
|
426
|
+
});
|
|
427
|
+
messageId = res.messageId;
|
|
428
|
+
} catch (e) {
|
|
429
|
+
error = e instanceof Error ? e.message : String(e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const id = deps.newId();
|
|
433
|
+
const row = {
|
|
434
|
+
id,
|
|
435
|
+
groupId: input.group.id,
|
|
436
|
+
to: decision.to,
|
|
437
|
+
template: input.template,
|
|
438
|
+
subject: decision.subject,
|
|
439
|
+
body: decision.text,
|
|
440
|
+
transport: decision.transport,
|
|
441
|
+
redirected: decision.redirected,
|
|
442
|
+
dedupeKey: input.dedupeKey,
|
|
443
|
+
sentAt: deps.now(),
|
|
444
|
+
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
445
|
+
...messageId ? { messageId } : {},
|
|
446
|
+
...error ? { error } : {}
|
|
447
|
+
};
|
|
448
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
449
|
+
return error ? { sent: false, reason: error } : { sent: true };
|
|
450
|
+
}
|
|
451
|
+
function emailGroupFrom(row) {
|
|
452
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
453
|
+
const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
|
|
454
|
+
return {
|
|
455
|
+
id: String(row.id),
|
|
456
|
+
name: String(row.name ?? ""),
|
|
457
|
+
replyTo: str(row.replyTo) ?? "",
|
|
458
|
+
debugEmail: str(row.debugEmail),
|
|
459
|
+
refundPolicyText: str(row.refundPolicyText),
|
|
460
|
+
commitmentText: str(row.commitmentText),
|
|
461
|
+
normsText: str(row.normsText),
|
|
462
|
+
emailTemplates: templates
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
320
466
|
// src/worker-routes.ts
|
|
467
|
+
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
468
|
+
const email = typeof fields.email === "string" ? fields.email : "";
|
|
469
|
+
if (!email) return;
|
|
470
|
+
const s = (v) => typeof v === "string" ? v : void 0;
|
|
471
|
+
try {
|
|
472
|
+
await projectApplicant(
|
|
473
|
+
{ crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
474
|
+
{ applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin) }
|
|
475
|
+
);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
if (chapter.account !== "none") {
|
|
479
|
+
try {
|
|
480
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
481
|
+
if (secret) {
|
|
482
|
+
if (chapter.account === "create") await createClerkUser(secret, { email, firstName: s(fields.firstName), lastName: s(fields.lastName) });
|
|
483
|
+
else await createClerkInvitation(secret, { email });
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
490
|
+
try {
|
|
491
|
+
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
492
|
+
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
493
|
+
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
494
|
+
const s = (v) => typeof v === "string" ? v : "";
|
|
495
|
+
await sendTemplated(
|
|
496
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
497
|
+
{
|
|
498
|
+
group: emailGroupFrom(group),
|
|
499
|
+
template: "adminNotification",
|
|
500
|
+
to: group.notificationEmail,
|
|
501
|
+
vars: { firstName: s(fields.firstName), lastName: s(fields.lastName), email: s(fields.email), phone: s(fields.phone), state: s(fields.state) },
|
|
502
|
+
dedupeKey: `apply:${applicationId}:admin`,
|
|
503
|
+
applicationId
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
}
|
|
321
509
|
async function memberSessionApplication(db, chapterId, email) {
|
|
322
510
|
const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
|
|
323
511
|
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
@@ -329,6 +517,7 @@ async function memberSessionApplication(db, chapterId, email) {
|
|
|
329
517
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
330
518
|
return memberApplication(app, meeting, timezone);
|
|
331
519
|
}
|
|
520
|
+
var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
|
|
332
521
|
var handleConfig = async (_req, url, env, ctx) => {
|
|
333
522
|
if (url.pathname !== "/api/config") return null;
|
|
334
523
|
try {
|
|
@@ -418,13 +607,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
418
607
|
return json({ error: "invalid JSON body" }, 400);
|
|
419
608
|
}
|
|
420
609
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
421
|
-
const
|
|
610
|
+
const db = ctx.makeDb(env);
|
|
611
|
+
const result = await submitApplication(db, chapter, parsed, {
|
|
422
612
|
submissionId,
|
|
423
613
|
groupId: chapter.id,
|
|
424
614
|
now: Date.now(),
|
|
425
615
|
newId: () => crypto.randomUUID()
|
|
426
616
|
});
|
|
427
617
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
618
|
+
if (!result.duplicate) {
|
|
619
|
+
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
620
|
+
await provisionApplicant(db, chapter, result.id, parsed);
|
|
621
|
+
}
|
|
428
622
|
return json({ id: result.id, duplicate: result.duplicate, status: result.status });
|
|
429
623
|
}
|
|
430
624
|
return null;
|
|
@@ -432,6 +626,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
432
626
|
|
|
433
627
|
// src/worker-routes-schedule.ts
|
|
434
628
|
var import_calendar = require("@odla-ai/calendar");
|
|
629
|
+
|
|
630
|
+
// src/pipeline.ts
|
|
631
|
+
function canBook(status, p) {
|
|
632
|
+
return p.bookableFrom.includes(status);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/worker-routes-schedule.ts
|
|
435
636
|
function errCode(err) {
|
|
436
637
|
if (err && typeof err === "object") {
|
|
437
638
|
const code = err.code;
|
|
@@ -473,7 +674,7 @@ async function bookSlot(req, env, ctx) {
|
|
|
473
674
|
const app = await firstRow(db, "applications", { where: { id: applicationId }, limit: 1 });
|
|
474
675
|
if (!app) return json({ error: "not found" }, 404);
|
|
475
676
|
const status = String(app.status ?? "");
|
|
476
|
-
if (!
|
|
677
|
+
if (!canBook(status, ctx.chapter.pipeline)) return json({ error: `cannot book from status "${status}"` }, 409);
|
|
477
678
|
const group = await firstRow(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
|
|
478
679
|
if (!group) return json({ error: "group not found" }, 500);
|
|
479
680
|
const cfg = resolveScheduling(group.schedulingJson);
|
|
@@ -535,6 +736,12 @@ async function bookSlot(req, env, ctx) {
|
|
|
535
736
|
}
|
|
536
737
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };
|
|
537
738
|
await db.transact([meetingOp, appOp]);
|
|
739
|
+
if (typeof app.email === "string" && app.email) {
|
|
740
|
+
await sendTemplated(
|
|
741
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
742
|
+
{ group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
|
|
743
|
+
).catch(() => void 0);
|
|
744
|
+
}
|
|
538
745
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
539
746
|
}
|
|
540
747
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -770,6 +977,25 @@ async function findApplication(db, event) {
|
|
|
770
977
|
}
|
|
771
978
|
return void 0;
|
|
772
979
|
}
|
|
980
|
+
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
981
|
+
try {
|
|
982
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
983
|
+
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
984
|
+
if (!group) return;
|
|
985
|
+
await sendTemplated(
|
|
986
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
987
|
+
{
|
|
988
|
+
group: emailGroupFrom(group),
|
|
989
|
+
template: "paymentConfirmation",
|
|
990
|
+
to: app.email,
|
|
991
|
+
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
992
|
+
dedupeKey: `${eventId}:confirm`,
|
|
993
|
+
applicationId: String(app.id)
|
|
994
|
+
}
|
|
995
|
+
);
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
773
999
|
function webhookPatch(event, status) {
|
|
774
1000
|
switch (event.kind) {
|
|
775
1001
|
case "first_payment":
|
|
@@ -841,6 +1067,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
841
1067
|
if (Object.keys(patch).length) {
|
|
842
1068
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
|
|
843
1069
|
}
|
|
1070
|
+
if (event.kind === "first_payment") await notifyPaymentConfirmed(db, env, eventId, app);
|
|
844
1071
|
return json({ ok: true });
|
|
845
1072
|
}
|
|
846
1073
|
async function refundApplication(req, url, env, ctx) {
|
|
@@ -877,14 +1104,139 @@ var handlePayments = async (req, url, env, ctx) => {
|
|
|
877
1104
|
return null;
|
|
878
1105
|
};
|
|
879
1106
|
|
|
1107
|
+
// src/worker-routes-admin.ts
|
|
1108
|
+
var import_calendar2 = require("@odla-ai/calendar");
|
|
1109
|
+
|
|
1110
|
+
// src/reconcile.ts
|
|
1111
|
+
function isReconcilable(meeting, now) {
|
|
1112
|
+
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
1113
|
+
}
|
|
1114
|
+
function reconcileMeetings(meetings, events, now) {
|
|
1115
|
+
const byEvent = new Map(events.map((e) => [e.eventId, e]));
|
|
1116
|
+
const decisions = [];
|
|
1117
|
+
for (const m of meetings) {
|
|
1118
|
+
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
1119
|
+
const g = byEvent.get(m.googleEventId);
|
|
1120
|
+
if (!g || g.status === "cancelled") {
|
|
1121
|
+
decisions.push({
|
|
1122
|
+
meetingId: m.id,
|
|
1123
|
+
applicationId: m.applicationId,
|
|
1124
|
+
kind: "cancelled",
|
|
1125
|
+
meetingPatch: { status: "cancelled", drift: "none", adoptedFromGoogleAt: now },
|
|
1126
|
+
applicationPatch: { meetingAt: 0, meetingLink: "" }
|
|
1127
|
+
});
|
|
1128
|
+
} else if (g.startAt !== void 0 && g.startAt !== m.startAt) {
|
|
1129
|
+
const duration = (m.endAt ?? 0) - (m.startAt ?? 0);
|
|
1130
|
+
decisions.push({
|
|
1131
|
+
meetingId: m.id,
|
|
1132
|
+
applicationId: m.applicationId,
|
|
1133
|
+
kind: "moved",
|
|
1134
|
+
meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: "none", adoptedFromGoogleAt: now },
|
|
1135
|
+
applicationPatch: { meetingAt: g.startAt }
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return decisions;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/worker-routes-admin.ts
|
|
1143
|
+
async function adminGroup(req, env, ctx, url) {
|
|
1144
|
+
const rawDb = ctx.makeDb(env);
|
|
1145
|
+
const u = await ctx.verifyUser(req, env);
|
|
1146
|
+
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1147
|
+
const db = rawDb;
|
|
1148
|
+
const groupId = url.searchParams.get("group") ?? ctx.chapter.id;
|
|
1149
|
+
const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];
|
|
1150
|
+
if (!group) return json({ error: "not found" }, 404);
|
|
1151
|
+
return { db, group };
|
|
1152
|
+
}
|
|
1153
|
+
var handleAdminScheduling = async (req, url, env, ctx) => {
|
|
1154
|
+
if (url.pathname !== "/api/admin/scheduling" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
1155
|
+
const got = await adminGroup(req, env, ctx, url);
|
|
1156
|
+
if (got instanceof Response) return got;
|
|
1157
|
+
const { db, group } = got;
|
|
1158
|
+
if (req.method === "GET") {
|
|
1159
|
+
return json({ scheduling: resolveScheduling(group.schedulingJson) });
|
|
1160
|
+
}
|
|
1161
|
+
let body;
|
|
1162
|
+
try {
|
|
1163
|
+
body = JSON.parse(await req.text());
|
|
1164
|
+
} catch {
|
|
1165
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
1166
|
+
}
|
|
1167
|
+
let resolved;
|
|
1168
|
+
try {
|
|
1169
|
+
resolved = resolveScheduling(body);
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
return json({ error: e instanceof Error ? e.message : "invalid scheduling config" }, 400);
|
|
1172
|
+
}
|
|
1173
|
+
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: resolved } }]);
|
|
1174
|
+
return json({ scheduling: resolved });
|
|
1175
|
+
};
|
|
1176
|
+
async function upcomingEvents(env) {
|
|
1177
|
+
const cal = (0, import_calendar2.initCalendar)({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
|
|
1178
|
+
const res = await cal.availability.upcoming();
|
|
1179
|
+
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
1180
|
+
}
|
|
1181
|
+
function toReconcile(rows) {
|
|
1182
|
+
return rows.map((m) => ({
|
|
1183
|
+
id: String(m.id),
|
|
1184
|
+
applicationId: String(m.applicationId),
|
|
1185
|
+
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
1186
|
+
status: String(m.status ?? ""),
|
|
1187
|
+
startAt: typeof m.startAt === "number" ? m.startAt : null,
|
|
1188
|
+
endAt: typeof m.endAt === "number" ? m.endAt : null
|
|
1189
|
+
}));
|
|
1190
|
+
}
|
|
1191
|
+
var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
1192
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/meetings") return null;
|
|
1193
|
+
const rawDb = ctx.makeDb(env);
|
|
1194
|
+
const u = await ctx.verifyUser(req, env);
|
|
1195
|
+
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1196
|
+
const db = rawDb;
|
|
1197
|
+
const query = await db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } });
|
|
1198
|
+
const rows = Array.isArray(query.meetings) ? query.meetings : [];
|
|
1199
|
+
let decisions = [];
|
|
1200
|
+
try {
|
|
1201
|
+
decisions = reconcileMeetings(toReconcile(rows), await upcomingEvents(env), Date.now());
|
|
1202
|
+
} catch {
|
|
1203
|
+
}
|
|
1204
|
+
const patched = /* @__PURE__ */ new Map();
|
|
1205
|
+
for (const d of decisions) {
|
|
1206
|
+
const ops = [
|
|
1207
|
+
{ t: "update", ns: "meetings", id: d.meetingId, attrs: d.meetingPatch },
|
|
1208
|
+
{ t: "update", ns: "applications", id: d.applicationId, attrs: d.applicationPatch }
|
|
1209
|
+
];
|
|
1210
|
+
try {
|
|
1211
|
+
await db.transact(ops);
|
|
1212
|
+
patched.set(d.meetingId, d.meetingPatch);
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const meetings = rows.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {} })).filter((m) => m.status === "scheduled");
|
|
1217
|
+
return json({ meetings, adopted: decisions.length });
|
|
1218
|
+
};
|
|
1219
|
+
|
|
880
1220
|
// src/worker.ts
|
|
881
|
-
var
|
|
1221
|
+
var BUILTIN_ROUTES = [
|
|
1222
|
+
handleHealth,
|
|
1223
|
+
handleConfig,
|
|
1224
|
+
handleMe,
|
|
1225
|
+
handleCrm,
|
|
1226
|
+
handleNetworkShared,
|
|
1227
|
+
handleMember,
|
|
1228
|
+
handleSchedule,
|
|
1229
|
+
handlePayments,
|
|
1230
|
+
handleAdminMeetings,
|
|
1231
|
+
handleAdminScheduling
|
|
1232
|
+
];
|
|
882
1233
|
function chapterWorker(options) {
|
|
883
1234
|
const ctx = createWorkerContext(options);
|
|
1235
|
+
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
884
1236
|
return {
|
|
885
1237
|
async fetch(req, env) {
|
|
886
1238
|
const url = new URL(req.url);
|
|
887
|
-
for (const route of
|
|
1239
|
+
for (const route of routes) {
|
|
888
1240
|
const res = await route(req, url, env, ctx);
|
|
889
1241
|
if (res) return res;
|
|
890
1242
|
}
|