@odla-ai/chapter 0.9.0 → 0.10.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 +36 -14
- package/dist/index.cjs +55 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +55 -13
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +99 -27
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +17 -0
- package/dist/worker/index.d.ts +17 -0
- package/dist/worker/index.js +99 -27
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -5
package/dist/worker/index.cjs
CHANGED
|
@@ -155,6 +155,20 @@ function clerkUserRequest(input) {
|
|
|
155
155
|
}
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
+
async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
|
|
159
|
+
const auth = { authorization: `Bearer ${secretKey}` };
|
|
160
|
+
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
161
|
+
if (!found.ok) return false;
|
|
162
|
+
const users = await found.json().catch(() => null);
|
|
163
|
+
const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
164
|
+
if (!id) return false;
|
|
165
|
+
const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id}/metadata`, {
|
|
166
|
+
method: "PATCH",
|
|
167
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
168
|
+
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
169
|
+
});
|
|
170
|
+
return patched.ok;
|
|
171
|
+
}
|
|
158
172
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
159
173
|
const { path, body } = clerkUserRequest(input);
|
|
160
174
|
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
@@ -162,7 +176,11 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
162
176
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
163
177
|
body: JSON.stringify(body)
|
|
164
178
|
});
|
|
165
|
-
|
|
179
|
+
if (res.ok) return { ok: true, status: res.status };
|
|
180
|
+
const healed = heal(res.status);
|
|
181
|
+
if (!healed.existed || !input.publicMetadata) return healed;
|
|
182
|
+
const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
|
|
183
|
+
return { ...healed, refreshed };
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
// src/member.ts
|
|
@@ -265,6 +283,12 @@ function isValidTimeZone(tz) {
|
|
|
265
283
|
}
|
|
266
284
|
}
|
|
267
285
|
function resolveScheduling(config) {
|
|
286
|
+
const result = validateScheduling(config);
|
|
287
|
+
if (result.ok) return result.value;
|
|
288
|
+
const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
|
|
289
|
+
throw new Error(`scheduling: ${detail}`);
|
|
290
|
+
}
|
|
291
|
+
function validateScheduling(config) {
|
|
268
292
|
const d = config ?? {};
|
|
269
293
|
const c = {
|
|
270
294
|
slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
|
|
@@ -276,20 +300,29 @@ function resolveScheduling(config) {
|
|
|
276
300
|
windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
|
|
277
301
|
summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
|
|
278
302
|
};
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (!(c.windowDays >= 1 && c.windowDays <= 62))
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
const errors = {};
|
|
304
|
+
if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
|
|
305
|
+
errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
|
|
306
|
+
}
|
|
307
|
+
if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
|
|
308
|
+
errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
|
|
309
|
+
}
|
|
310
|
+
if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
|
|
311
|
+
errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
|
|
312
|
+
}
|
|
313
|
+
if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
|
|
314
|
+
errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
|
|
315
|
+
}
|
|
286
316
|
const days = [...c.days];
|
|
287
|
-
if (!days.length
|
|
288
|
-
|
|
317
|
+
if (!days.length) errors.days = "Pick at least one day.";
|
|
318
|
+
else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
|
|
319
|
+
errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
|
|
289
320
|
}
|
|
290
|
-
if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone))
|
|
291
|
-
|
|
292
|
-
|
|
321
|
+
if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
|
|
322
|
+
errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
|
|
323
|
+
}
|
|
324
|
+
if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
|
|
325
|
+
return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
|
|
293
326
|
}
|
|
294
327
|
function slotWindow(now, windowDays) {
|
|
295
328
|
return { from: now, to: now + windowDays * 864e5 };
|
|
@@ -619,7 +652,9 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
619
652
|
});
|
|
620
653
|
if (!result.ok) return json({ error: result.error }, 400);
|
|
621
654
|
if (!result.duplicate) {
|
|
622
|
-
|
|
655
|
+
if (chapter.sends.adminNotification === "submit") {
|
|
656
|
+
await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
|
|
657
|
+
}
|
|
623
658
|
await provisionApplicant(db, chapter, result.id, parsed);
|
|
624
659
|
}
|
|
625
660
|
return json({ id: result.id, duplicate: result.duplicate, status: result.status });
|
|
@@ -999,6 +1034,25 @@ async function notifyPaymentConfirmed(db, env, eventId, app) {
|
|
|
999
1034
|
} catch {
|
|
1000
1035
|
}
|
|
1001
1036
|
}
|
|
1037
|
+
async function notifyAdminOfPayment(db, env, eventId, app) {
|
|
1038
|
+
try {
|
|
1039
|
+
const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
|
|
1040
|
+
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
1041
|
+
const s = (v) => typeof v === "string" ? v : "";
|
|
1042
|
+
await sendTemplated(
|
|
1043
|
+
{ db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
1044
|
+
{
|
|
1045
|
+
group: emailGroupFrom(group),
|
|
1046
|
+
template: "adminNotification",
|
|
1047
|
+
to: group.notificationEmail,
|
|
1048
|
+
vars: { firstName: s(app.firstName), lastName: s(app.lastName), email: s(app.email), phone: s(app.phone), state: s(app.state) },
|
|
1049
|
+
dedupeKey: `${eventId}:admin`,
|
|
1050
|
+
applicationId: String(app.id)
|
|
1051
|
+
}
|
|
1052
|
+
);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1002
1056
|
function webhookPatch(event, status) {
|
|
1003
1057
|
switch (event.kind) {
|
|
1004
1058
|
case "first_payment":
|
|
@@ -1070,7 +1124,10 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
1070
1124
|
if (Object.keys(patch).length) {
|
|
1071
1125
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
|
|
1072
1126
|
}
|
|
1073
|
-
if (event.kind === "first_payment")
|
|
1127
|
+
if (event.kind === "first_payment") {
|
|
1128
|
+
await notifyPaymentConfirmed(db, env, eventId, app);
|
|
1129
|
+
if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app);
|
|
1130
|
+
}
|
|
1074
1131
|
return json({ ok: true });
|
|
1075
1132
|
}
|
|
1076
1133
|
async function refundApplication(req, url, env, ctx) {
|
|
@@ -1167,14 +1224,10 @@ var handleAdminScheduling = async (req, url, env, ctx) => {
|
|
|
1167
1224
|
} catch {
|
|
1168
1225
|
return json({ error: "invalid JSON body" }, 400);
|
|
1169
1226
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
return json({ error: e instanceof Error ? e.message : "invalid scheduling config" }, 400);
|
|
1175
|
-
}
|
|
1176
|
-
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: resolved } }]);
|
|
1177
|
-
return json({ scheduling: resolved });
|
|
1227
|
+
const checked = validateScheduling(body);
|
|
1228
|
+
if (!checked.ok) return json({ error: "invalid scheduling config", errors: checked.errors }, 400);
|
|
1229
|
+
await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: checked.value } }]);
|
|
1230
|
+
return json({ scheduling: checked.value });
|
|
1178
1231
|
};
|
|
1179
1232
|
async function upcomingEvents(env) {
|
|
1180
1233
|
const cal = (0, import_calendar2.initCalendar)({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
|
|
@@ -1197,8 +1250,15 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
1197
1250
|
const u = await ctx.verifyUser(req, env);
|
|
1198
1251
|
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1199
1252
|
const db = rawDb;
|
|
1200
|
-
const
|
|
1201
|
-
const
|
|
1253
|
+
const all = url.searchParams.get("all") === "1";
|
|
1254
|
+
const from = Number(url.searchParams.get("from"));
|
|
1255
|
+
const to = Number(url.searchParams.get("to"));
|
|
1256
|
+
const query = await db.query({
|
|
1257
|
+
meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
|
|
1258
|
+
});
|
|
1259
|
+
let rows = Array.isArray(query.meetings) ? query.meetings : [];
|
|
1260
|
+
if (Number.isFinite(from)) rows = rows.filter((m) => Number(m.startAt ?? 0) >= from);
|
|
1261
|
+
if (Number.isFinite(to)) rows = rows.filter((m) => Number(m.startAt ?? 0) <= to);
|
|
1202
1262
|
let decisions = [];
|
|
1203
1263
|
try {
|
|
1204
1264
|
decisions = reconcileMeetings(toReconcile(rows), await upcomingEvents(env), Date.now());
|
|
@@ -1216,8 +1276,20 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
1216
1276
|
} catch {
|
|
1217
1277
|
}
|
|
1218
1278
|
}
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1279
|
+
const appQuery = await db.query({ applications: { $: { limit: 1e3 } } });
|
|
1280
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1281
|
+
for (const a of Array.isArray(appQuery.applications) ? appQuery.applications : []) {
|
|
1282
|
+
byId.set(String(a.id), a);
|
|
1283
|
+
}
|
|
1284
|
+
const applicantOf = (m) => {
|
|
1285
|
+
const a = byId.get(String(m.applicationId));
|
|
1286
|
+
if (!a) return null;
|
|
1287
|
+
return { id: a.id, firstName: a.firstName, lastName: a.lastName, email: a.email, status: a.status };
|
|
1288
|
+
};
|
|
1289
|
+
const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
|
|
1290
|
+
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
1291
|
+
const meetings = rows.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
|
|
1292
|
+
return json({ meetings, adopted: decisions.length, timezone });
|
|
1221
1293
|
};
|
|
1222
1294
|
|
|
1223
1295
|
// src/worker.ts
|