@odla-ai/chapter 0.4.0 → 0.7.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 +63 -28
- package/dist/index.cjs +169 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -8
- package/dist/index.d.ts +136 -8
- package/dist/index.js +169 -14
- package/dist/index.js.map +1 -1
- package/dist/ui/index.d.ts +54 -3
- package/dist/ui/index.js +188 -93
- package/dist/ui/index.js.map +1 -1
- package/dist/worker/index.cjs +344 -17
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +60 -6
- package/dist/worker/index.d.ts +60 -6
- package/dist/worker/index.js +344 -17
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
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,27 @@ 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
|
+
|
|
123
145
|
// src/member.ts
|
|
124
146
|
async function submitApplication(db, chapter, fields, opts) {
|
|
125
147
|
const app = chapter.application;
|
|
@@ -173,21 +195,32 @@ function sharedPersonInput(person) {
|
|
|
173
195
|
if (person.linkedin) input.linkedin = person.linkedin;
|
|
174
196
|
return input;
|
|
175
197
|
}
|
|
176
|
-
async function
|
|
177
|
-
const email =
|
|
178
|
-
const input = sharedPersonInput(person);
|
|
198
|
+
async function upsertPerson(deps, opts) {
|
|
199
|
+
const email = opts.email.toLowerCase();
|
|
179
200
|
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
|
-
});
|
|
201
|
+
const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
|
|
183
202
|
const existing = crm_record?.[0];
|
|
184
203
|
if (existing && typeof existing.id === "string") {
|
|
185
|
-
await (0, import_crm.updateRecord)(crmDeps, { id: existing.id, input });
|
|
204
|
+
await (0, import_crm.updateRecord)(crmDeps, { id: existing.id, input: opts.input });
|
|
186
205
|
return { recordId: existing.id };
|
|
187
206
|
}
|
|
188
|
-
const created = await (0, import_crm.createRecord)(crmDeps, { type: "person", input, mutationId:
|
|
207
|
+
const created = await (0, import_crm.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
189
208
|
return { recordId: created.id };
|
|
190
209
|
}
|
|
210
|
+
async function projectSharedRecord(deps, person) {
|
|
211
|
+
return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
|
|
212
|
+
}
|
|
213
|
+
async function projectApplicant(deps, applicant) {
|
|
214
|
+
const input = sharedPersonInput({
|
|
215
|
+
email: applicant.email,
|
|
216
|
+
firstName: applicant.firstName,
|
|
217
|
+
lastName: applicant.lastName,
|
|
218
|
+
phone: applicant.phone,
|
|
219
|
+
linkedin: applicant.linkedin,
|
|
220
|
+
hubRecordId: applicant.applicationId
|
|
221
|
+
});
|
|
222
|
+
return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
|
|
223
|
+
}
|
|
191
224
|
|
|
192
225
|
// src/scheduling.ts
|
|
193
226
|
var SCHEDULING_DEFAULTS = {
|
|
@@ -235,10 +268,6 @@ function resolveScheduling(config) {
|
|
|
235
268
|
if (typeof c.summaryTemplate !== "string") fail2("summaryTemplate must be a string");
|
|
236
269
|
return { ...c, days };
|
|
237
270
|
}
|
|
238
|
-
var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
239
|
-
function canBookFrom(status) {
|
|
240
|
-
return BOOKABLE_STATUSES.includes(status);
|
|
241
|
-
}
|
|
242
271
|
function slotWindow(now, windowDays) {
|
|
243
272
|
return { from: now, to: now + windowDays * 864e5 };
|
|
244
273
|
}
|
|
@@ -317,7 +346,141 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
317
346
|
return { ...summary, meetingAt, meetUrl, timezone };
|
|
318
347
|
}
|
|
319
348
|
|
|
349
|
+
// src/email.ts
|
|
350
|
+
function render(template, vars) {
|
|
351
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
352
|
+
}
|
|
353
|
+
function groupVars(group, vars) {
|
|
354
|
+
return {
|
|
355
|
+
...vars,
|
|
356
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
357
|
+
commitmentText: group.commitmentText ?? "",
|
|
358
|
+
normsText: group.normsText ?? ""
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function isAlreadySent(priorRows) {
|
|
362
|
+
return priorRows.some((row) => !row.error);
|
|
363
|
+
}
|
|
364
|
+
function planDelivery(input) {
|
|
365
|
+
const tpl = input.group.emailTemplates?.[input.template];
|
|
366
|
+
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
367
|
+
if (tpl.enabled === false && !input.force) return { deliver: false, reason: "disabled" };
|
|
368
|
+
const vars = groupVars(input.group, input.vars);
|
|
369
|
+
const isProd = input.envName === "prod";
|
|
370
|
+
const redirect = !isProd && !!input.group.debugEmail;
|
|
371
|
+
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
372
|
+
const to = redirect ? input.group.debugEmail : input.to;
|
|
373
|
+
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
374
|
+
const text = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
375
|
+
|
|
376
|
+
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
377
|
+
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/notify.ts
|
|
381
|
+
async function sendTemplated(deps, input) {
|
|
382
|
+
const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
|
|
383
|
+
const prior = Array.isArray(emailLog) ? emailLog : [];
|
|
384
|
+
if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
|
|
385
|
+
const cloudflareReady = Boolean(deps.sender && deps.from);
|
|
386
|
+
const decision = planDelivery({
|
|
387
|
+
envName: deps.envName,
|
|
388
|
+
group: input.group,
|
|
389
|
+
template: input.template,
|
|
390
|
+
to: input.to,
|
|
391
|
+
vars: input.vars,
|
|
392
|
+
cloudflareReady,
|
|
393
|
+
force: input.force
|
|
394
|
+
});
|
|
395
|
+
if (!decision.deliver) return { sent: false, reason: decision.reason };
|
|
396
|
+
let error;
|
|
397
|
+
let messageId;
|
|
398
|
+
if (decision.transport === "cloudflare" && deps.sender && deps.from) {
|
|
399
|
+
try {
|
|
400
|
+
const res = await deps.sender.send({
|
|
401
|
+
from: deps.from,
|
|
402
|
+
to: [decision.to],
|
|
403
|
+
subject: decision.subject,
|
|
404
|
+
text: decision.text,
|
|
405
|
+
replyTo: input.group.replyTo
|
|
406
|
+
});
|
|
407
|
+
messageId = res.messageId;
|
|
408
|
+
} catch (e) {
|
|
409
|
+
error = e instanceof Error ? e.message : String(e);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const id = deps.newId();
|
|
413
|
+
const row = {
|
|
414
|
+
id,
|
|
415
|
+
groupId: input.group.id,
|
|
416
|
+
to: decision.to,
|
|
417
|
+
template: input.template,
|
|
418
|
+
subject: decision.subject,
|
|
419
|
+
body: decision.text,
|
|
420
|
+
transport: decision.transport,
|
|
421
|
+
redirected: decision.redirected,
|
|
422
|
+
dedupeKey: input.dedupeKey,
|
|
423
|
+
sentAt: deps.now(),
|
|
424
|
+
...input.applicationId ? { applicationId: input.applicationId } : {},
|
|
425
|
+
...messageId ? { messageId } : {},
|
|
426
|
+
...error ? { error } : {}
|
|
427
|
+
};
|
|
428
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
429
|
+
return error ? { sent: false, reason: error } : { sent: true };
|
|
430
|
+
}
|
|
431
|
+
function emailGroupFrom(row) {
|
|
432
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
433
|
+
const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
|
|
434
|
+
return {
|
|
435
|
+
id: String(row.id),
|
|
436
|
+
name: String(row.name ?? ""),
|
|
437
|
+
replyTo: str(row.replyTo) ?? "",
|
|
438
|
+
debugEmail: str(row.debugEmail),
|
|
439
|
+
refundPolicyText: str(row.refundPolicyText),
|
|
440
|
+
commitmentText: str(row.commitmentText),
|
|
441
|
+
normsText: str(row.normsText),
|
|
442
|
+
emailTemplates: templates
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
320
446
|
// src/worker-routes.ts
|
|
447
|
+
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
448
|
+
const email = typeof fields.email === "string" ? fields.email : "";
|
|
449
|
+
if (!email) return;
|
|
450
|
+
const s = (v) => typeof v === "string" ? v : void 0;
|
|
451
|
+
try {
|
|
452
|
+
await projectApplicant(
|
|
453
|
+
{ crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
454
|
+
{ applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin) }
|
|
455
|
+
);
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
460
|
+
if (secret) await createClerkInvitation(secret, { email });
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
465
|
+
try {
|
|
466
|
+
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
467
|
+
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
468
|
+
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
469
|
+
const s = (v) => typeof v === "string" ? v : "";
|
|
470
|
+
await sendTemplated(
|
|
471
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
472
|
+
{
|
|
473
|
+
group: emailGroupFrom(group),
|
|
474
|
+
template: "adminNotification",
|
|
475
|
+
to: group.notificationEmail,
|
|
476
|
+
vars: { firstName: s(fields.firstName), lastName: s(fields.lastName), email: s(fields.email), phone: s(fields.phone), state: s(fields.state) },
|
|
477
|
+
dedupeKey: `apply:${applicationId}:admin`,
|
|
478
|
+
applicationId
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
}
|
|
321
484
|
async function memberSessionApplication(db, chapterId, email) {
|
|
322
485
|
const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
|
|
323
486
|
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
@@ -329,6 +492,7 @@ async function memberSessionApplication(db, chapterId, email) {
|
|
|
329
492
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
330
493
|
return memberApplication(app, meeting, timezone);
|
|
331
494
|
}
|
|
495
|
+
var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
|
|
332
496
|
var handleConfig = async (_req, url, env, ctx) => {
|
|
333
497
|
if (url.pathname !== "/api/config") return null;
|
|
334
498
|
try {
|
|
@@ -418,13 +582,18 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
418
582
|
return json({ error: "invalid JSON body" }, 400);
|
|
419
583
|
}
|
|
420
584
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
421
|
-
const
|
|
585
|
+
const db = ctx.makeDb(env);
|
|
586
|
+
const result = await submitApplication(db, chapter, parsed, {
|
|
422
587
|
submissionId,
|
|
423
588
|
groupId: chapter.id,
|
|
424
589
|
now: Date.now(),
|
|
425
590
|
newId: () => crypto.randomUUID()
|
|
426
591
|
});
|
|
427
592
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
593
|
+
if (!result.duplicate) {
|
|
594
|
+
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
595
|
+
await provisionApplicant(db, chapter, result.id, parsed);
|
|
596
|
+
}
|
|
428
597
|
return json({ id: result.id, duplicate: result.duplicate, status: result.status });
|
|
429
598
|
}
|
|
430
599
|
return null;
|
|
@@ -432,6 +601,13 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
432
601
|
|
|
433
602
|
// src/worker-routes-schedule.ts
|
|
434
603
|
var import_calendar = require("@odla-ai/calendar");
|
|
604
|
+
|
|
605
|
+
// src/pipeline.ts
|
|
606
|
+
function canBook(status, p) {
|
|
607
|
+
return p.bookableFrom.includes(status);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/worker-routes-schedule.ts
|
|
435
611
|
function errCode(err) {
|
|
436
612
|
if (err && typeof err === "object") {
|
|
437
613
|
const code = err.code;
|
|
@@ -473,7 +649,7 @@ async function bookSlot(req, env, ctx) {
|
|
|
473
649
|
const app = await firstRow(db, "applications", { where: { id: applicationId }, limit: 1 });
|
|
474
650
|
if (!app) return json({ error: "not found" }, 404);
|
|
475
651
|
const status = String(app.status ?? "");
|
|
476
|
-
if (!
|
|
652
|
+
if (!canBook(status, ctx.chapter.pipeline)) return json({ error: `cannot book from status "${status}"` }, 409);
|
|
477
653
|
const group = await firstRow(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
|
|
478
654
|
if (!group) return json({ error: "group not found" }, 500);
|
|
479
655
|
const cfg = resolveScheduling(group.schedulingJson);
|
|
@@ -535,6 +711,12 @@ async function bookSlot(req, env, ctx) {
|
|
|
535
711
|
}
|
|
536
712
|
const appOp = { t: "update", ns: "applications", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };
|
|
537
713
|
await db.transact([meetingOp, appOp]);
|
|
714
|
+
if (typeof app.email === "string" && app.email) {
|
|
715
|
+
await sendTemplated(
|
|
716
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
717
|
+
{ group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
|
|
718
|
+
).catch(() => void 0);
|
|
719
|
+
}
|
|
538
720
|
return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
|
|
539
721
|
}
|
|
540
722
|
var handleSchedule = async (req, url, env, ctx) => {
|
|
@@ -770,6 +952,25 @@ async function findApplication(db, event) {
|
|
|
770
952
|
}
|
|
771
953
|
return void 0;
|
|
772
954
|
}
|
|
955
|
+
async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
956
|
+
try {
|
|
957
|
+
if (typeof app.email !== "string" || !app.email) return;
|
|
958
|
+
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
959
|
+
if (!group) return;
|
|
960
|
+
await sendTemplated(
|
|
961
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
962
|
+
{
|
|
963
|
+
group: emailGroupFrom(group),
|
|
964
|
+
template: "paymentConfirmation",
|
|
965
|
+
to: app.email,
|
|
966
|
+
vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
|
|
967
|
+
dedupeKey: `${eventId}:confirm`,
|
|
968
|
+
applicationId: String(app.id)
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
}
|
|
773
974
|
function webhookPatch(event, status) {
|
|
774
975
|
switch (event.kind) {
|
|
775
976
|
case "first_payment":
|
|
@@ -841,6 +1042,7 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
841
1042
|
if (Object.keys(patch).length) {
|
|
842
1043
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
|
|
843
1044
|
}
|
|
1045
|
+
if (event.kind === "first_payment") await notifyPaymentConfirmed(db, env, eventId, app);
|
|
844
1046
|
return json({ ok: true });
|
|
845
1047
|
}
|
|
846
1048
|
async function refundApplication(req, url, env, ctx) {
|
|
@@ -877,14 +1079,139 @@ var handlePayments = async (req, url, env, ctx) => {
|
|
|
877
1079
|
return null;
|
|
878
1080
|
};
|
|
879
1081
|
|
|
1082
|
+
// src/worker-routes-admin.ts
|
|
1083
|
+
var import_calendar2 = require("@odla-ai/calendar");
|
|
1084
|
+
|
|
1085
|
+
// src/reconcile.ts
|
|
1086
|
+
function isReconcilable(meeting, now) {
|
|
1087
|
+
return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
|
|
1088
|
+
}
|
|
1089
|
+
function reconcileMeetings(meetings, events, now) {
|
|
1090
|
+
const byEvent = new Map(events.map((e) => [e.eventId, e]));
|
|
1091
|
+
const decisions = [];
|
|
1092
|
+
for (const m of meetings) {
|
|
1093
|
+
if (!isReconcilable(m, now) || !m.googleEventId) continue;
|
|
1094
|
+
const g = byEvent.get(m.googleEventId);
|
|
1095
|
+
if (!g || g.status === "cancelled") {
|
|
1096
|
+
decisions.push({
|
|
1097
|
+
meetingId: m.id,
|
|
1098
|
+
applicationId: m.applicationId,
|
|
1099
|
+
kind: "cancelled",
|
|
1100
|
+
meetingPatch: { status: "cancelled", drift: "none", adoptedFromGoogleAt: now },
|
|
1101
|
+
applicationPatch: { meetingAt: 0, meetingLink: "" }
|
|
1102
|
+
});
|
|
1103
|
+
} else if (g.startAt !== void 0 && g.startAt !== m.startAt) {
|
|
1104
|
+
const duration = (m.endAt ?? 0) - (m.startAt ?? 0);
|
|
1105
|
+
decisions.push({
|
|
1106
|
+
meetingId: m.id,
|
|
1107
|
+
applicationId: m.applicationId,
|
|
1108
|
+
kind: "moved",
|
|
1109
|
+
meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: "none", adoptedFromGoogleAt: now },
|
|
1110
|
+
applicationPatch: { meetingAt: g.startAt }
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return decisions;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/worker-routes-admin.ts
|
|
1118
|
+
async function adminGroup(req, env, ctx, url) {
|
|
1119
|
+
const rawDb = ctx.makeDb(env);
|
|
1120
|
+
const u = await ctx.verifyUser(req, env);
|
|
1121
|
+
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1122
|
+
const db = rawDb;
|
|
1123
|
+
const groupId = url.searchParams.get("group") ?? ctx.chapter.id;
|
|
1124
|
+
const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];
|
|
1125
|
+
if (!group) return json({ error: "not found" }, 404);
|
|
1126
|
+
return { db, group };
|
|
1127
|
+
}
|
|
1128
|
+
var handleAdminScheduling = async (req, url, env, ctx) => {
|
|
1129
|
+
if (url.pathname !== "/api/admin/scheduling" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
1130
|
+
const got = await adminGroup(req, env, ctx, url);
|
|
1131
|
+
if (got instanceof Response) return got;
|
|
1132
|
+
const { db, group } = got;
|
|
1133
|
+
if (req.method === "GET") {
|
|
1134
|
+
return json({ scheduling: resolveScheduling(group.schedulingJson) });
|
|
1135
|
+
}
|
|
1136
|
+
let body;
|
|
1137
|
+
try {
|
|
1138
|
+
body = JSON.parse(await req.text());
|
|
1139
|
+
} catch {
|
|
1140
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
1141
|
+
}
|
|
1142
|
+
let resolved;
|
|
1143
|
+
try {
|
|
1144
|
+
resolved = resolveScheduling(body);
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
return json({ error: e instanceof Error ? e.message : "invalid scheduling config" }, 400);
|
|
1147
|
+
}
|
|
1148
|
+
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: resolved } }]);
|
|
1149
|
+
return json({ scheduling: resolved });
|
|
1150
|
+
};
|
|
1151
|
+
async function upcomingEvents(env) {
|
|
1152
|
+
const cal = (0, import_calendar2.initCalendar)({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
|
|
1153
|
+
const res = await cal.availability.upcoming();
|
|
1154
|
+
return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
|
|
1155
|
+
}
|
|
1156
|
+
function toReconcile(rows) {
|
|
1157
|
+
return rows.map((m) => ({
|
|
1158
|
+
id: String(m.id),
|
|
1159
|
+
applicationId: String(m.applicationId),
|
|
1160
|
+
googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
|
|
1161
|
+
status: String(m.status ?? ""),
|
|
1162
|
+
startAt: typeof m.startAt === "number" ? m.startAt : null,
|
|
1163
|
+
endAt: typeof m.endAt === "number" ? m.endAt : null
|
|
1164
|
+
}));
|
|
1165
|
+
}
|
|
1166
|
+
var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
1167
|
+
if (req.method !== "GET" || url.pathname !== "/api/admin/meetings") return null;
|
|
1168
|
+
const rawDb = ctx.makeDb(env);
|
|
1169
|
+
const u = await ctx.verifyUser(req, env);
|
|
1170
|
+
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1171
|
+
const db = rawDb;
|
|
1172
|
+
const query = await db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } });
|
|
1173
|
+
const rows = Array.isArray(query.meetings) ? query.meetings : [];
|
|
1174
|
+
let decisions = [];
|
|
1175
|
+
try {
|
|
1176
|
+
decisions = reconcileMeetings(toReconcile(rows), await upcomingEvents(env), Date.now());
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
const patched = /* @__PURE__ */ new Map();
|
|
1180
|
+
for (const d of decisions) {
|
|
1181
|
+
const ops = [
|
|
1182
|
+
{ t: "update", ns: "meetings", id: d.meetingId, attrs: d.meetingPatch },
|
|
1183
|
+
{ t: "update", ns: "applications", id: d.applicationId, attrs: d.applicationPatch }
|
|
1184
|
+
];
|
|
1185
|
+
try {
|
|
1186
|
+
await db.transact(ops);
|
|
1187
|
+
patched.set(d.meetingId, d.meetingPatch);
|
|
1188
|
+
} catch {
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const meetings = rows.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {} })).filter((m) => m.status === "scheduled");
|
|
1192
|
+
return json({ meetings, adopted: decisions.length });
|
|
1193
|
+
};
|
|
1194
|
+
|
|
880
1195
|
// src/worker.ts
|
|
881
|
-
var
|
|
1196
|
+
var BUILTIN_ROUTES = [
|
|
1197
|
+
handleHealth,
|
|
1198
|
+
handleConfig,
|
|
1199
|
+
handleMe,
|
|
1200
|
+
handleCrm,
|
|
1201
|
+
handleNetworkShared,
|
|
1202
|
+
handleMember,
|
|
1203
|
+
handleSchedule,
|
|
1204
|
+
handlePayments,
|
|
1205
|
+
handleAdminMeetings,
|
|
1206
|
+
handleAdminScheduling
|
|
1207
|
+
];
|
|
882
1208
|
function chapterWorker(options) {
|
|
883
1209
|
const ctx = createWorkerContext(options);
|
|
1210
|
+
const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
|
|
884
1211
|
return {
|
|
885
1212
|
async fetch(req, env) {
|
|
886
1213
|
const url = new URL(req.url);
|
|
887
|
-
for (const route of
|
|
1214
|
+
for (const route of routes) {
|
|
888
1215
|
const res = await route(req, url, env, ctx);
|
|
889
1216
|
if (res) return res;
|
|
890
1217
|
}
|