@odla-ai/chapter 0.37.0 → 0.38.1
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 +30 -2
- package/dist/index.cjs +311 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -8
- package/dist/index.d.ts +55 -8
- package/dist/index.js +311 -235
- package/dist/index.js.map +1 -1
- package/dist/ui/member/index.d.ts +35 -1
- package/dist/worker/index.cjs +344 -260
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +344 -260
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.cjs
CHANGED
|
@@ -199,204 +199,10 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
199
199
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// src/
|
|
203
|
-
|
|
204
|
-
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
205
|
-
|
|
206
|
-
// src/signup-control-canonical.ts
|
|
207
|
-
var encoder = new TextEncoder();
|
|
208
|
-
function canonicalValue(value) {
|
|
209
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
210
|
-
return JSON.stringify(value);
|
|
211
|
-
}
|
|
212
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
213
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
214
|
-
const record3 = value;
|
|
215
|
-
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
216
|
-
const item = record3[key];
|
|
217
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
218
|
-
});
|
|
219
|
-
return `{${entries.join(",")}}`;
|
|
220
|
-
}
|
|
221
|
-
function canonicalSignupControl(value) {
|
|
222
|
-
return canonicalValue(value);
|
|
223
|
-
}
|
|
224
|
-
async function signupControlDigest(document) {
|
|
225
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
226
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// src/admission-grants.ts
|
|
230
|
-
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
231
|
-
var MAX_GRANTS = 2e3;
|
|
232
|
-
var MAX_NOTE = 2e3;
|
|
233
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
234
|
-
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
235
|
-
function normalizeAdmissionEmail(email) {
|
|
202
|
+
// src/email-identity.ts
|
|
203
|
+
function normalizeChapterEmail(email) {
|
|
236
204
|
return email.trim().toLowerCase();
|
|
237
205
|
}
|
|
238
|
-
function validateAdmissionGrantDocument(input) {
|
|
239
|
-
const errors = [];
|
|
240
|
-
const root = input && typeof input === "object" ? input : {};
|
|
241
|
-
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
242
|
-
const revision = Number(root.revision);
|
|
243
|
-
const generatedAt = Number(root.generatedAt);
|
|
244
|
-
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
245
|
-
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
246
|
-
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
247
|
-
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
248
|
-
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
249
|
-
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
250
|
-
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
251
|
-
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
252
|
-
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
253
|
-
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
254
|
-
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
255
|
-
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
256
|
-
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
257
|
-
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
258
|
-
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
259
|
-
const grants = [];
|
|
260
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
261
|
-
const seenEmails = /* @__PURE__ */ new Set();
|
|
262
|
-
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
263
|
-
const grant = raw && typeof raw === "object" ? raw : {};
|
|
264
|
-
const id = typeof grant.id === "string" ? grant.id : "";
|
|
265
|
-
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
266
|
-
const normalized = normalizeAdmissionEmail(email);
|
|
267
|
-
const decision = grant.decision;
|
|
268
|
-
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
269
|
-
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
270
|
-
if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
271
|
-
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
272
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
273
|
-
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
274
|
-
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
275
|
-
errors.push(`document.grants[${index}].decision is invalid`);
|
|
276
|
-
}
|
|
277
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
278
|
-
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
279
|
-
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
280
|
-
}
|
|
281
|
-
seenIds.add(id);
|
|
282
|
-
seenEmails.add(normalized);
|
|
283
|
-
grants.push({
|
|
284
|
-
id,
|
|
285
|
-
email,
|
|
286
|
-
decision,
|
|
287
|
-
...note ? { note } : {},
|
|
288
|
-
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
if (errors.length) return { ok: false, errors };
|
|
292
|
-
return {
|
|
293
|
-
ok: true,
|
|
294
|
-
value: {
|
|
295
|
-
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
296
|
-
revision,
|
|
297
|
-
generatedAt,
|
|
298
|
-
source: { appId: sourceId, actor: sourceActor },
|
|
299
|
-
target: { chapterId, environment },
|
|
300
|
-
grants
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
var rows = (value) => Array.isArray(value) ? value : [];
|
|
305
|
-
function asGrant(row) {
|
|
306
|
-
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
307
|
-
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
308
|
-
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
309
|
-
return row;
|
|
310
|
-
}
|
|
311
|
-
async function findAdmissionGrant(db, targetId, email, now) {
|
|
312
|
-
const emailNormalized = normalizeAdmissionEmail(email);
|
|
313
|
-
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
314
|
-
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
315
|
-
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
316
|
-
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
317
|
-
)).sort((a, b) => {
|
|
318
|
-
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
319
|
-
return scope || b.createdAt - a.createdAt;
|
|
320
|
-
});
|
|
321
|
-
return active[0] ?? null;
|
|
322
|
-
}
|
|
323
|
-
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
324
|
-
const where = { sourceId, targetId, environment };
|
|
325
|
-
if (db.aggregate) {
|
|
326
|
-
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
327
|
-
}
|
|
328
|
-
const found = await db.query({ admissionGrantRevisions: {} });
|
|
329
|
-
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
330
|
-
}
|
|
331
|
-
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
332
|
-
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
333
|
-
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
334
|
-
where: { sourceId, targetId, environment },
|
|
335
|
-
order: { revision: "desc" },
|
|
336
|
-
limit: 1
|
|
337
|
-
} } });
|
|
338
|
-
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
339
|
-
return row ? row : null;
|
|
340
|
-
}
|
|
341
|
-
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
342
|
-
const { document, digest } = envelope;
|
|
343
|
-
const current = rows((await db.query({ admissionGrants: { $: {
|
|
344
|
-
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
345
|
-
limit: MAX_GRANTS
|
|
346
|
-
} } })).admissionGrants);
|
|
347
|
-
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
348
|
-
const ops = [];
|
|
349
|
-
for (const row of current) {
|
|
350
|
-
const id = String(row.id ?? "");
|
|
351
|
-
if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
352
|
-
active: false,
|
|
353
|
-
revokedAt: receivedAt,
|
|
354
|
-
revokedBy: document.source.actor,
|
|
355
|
-
updatedAt: receivedAt,
|
|
356
|
-
updatedBy: document.source.actor
|
|
357
|
-
} });
|
|
358
|
-
}
|
|
359
|
-
for (const grant of document.grants) {
|
|
360
|
-
const id = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
361
|
-
const existing = current.find((row) => row.id === id);
|
|
362
|
-
ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
363
|
-
id,
|
|
364
|
-
sourceGrantId: grant.id,
|
|
365
|
-
sourceScope: "parent",
|
|
366
|
-
sourceId: document.source.appId,
|
|
367
|
-
targetId: document.target.chapterId,
|
|
368
|
-
email: grant.email,
|
|
369
|
-
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
370
|
-
decision: grant.decision,
|
|
371
|
-
...grant.note ? { note: grant.note } : {},
|
|
372
|
-
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
373
|
-
active: true,
|
|
374
|
-
sourceRevision: document.revision,
|
|
375
|
-
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
376
|
-
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
377
|
-
updatedAt: receivedAt,
|
|
378
|
-
updatedBy: document.source.actor
|
|
379
|
-
} });
|
|
380
|
-
const retract = [
|
|
381
|
-
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
382
|
-
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
383
|
-
];
|
|
384
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
|
|
385
|
-
}
|
|
386
|
-
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
387
|
-
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
388
|
-
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
389
|
-
id: revisionId,
|
|
390
|
-
sourceId: document.source.appId,
|
|
391
|
-
targetId: document.target.chapterId,
|
|
392
|
-
environment: document.target.environment,
|
|
393
|
-
revision: document.revision,
|
|
394
|
-
digest,
|
|
395
|
-
active: true,
|
|
396
|
-
receivedAt
|
|
397
|
-
} });
|
|
398
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
399
|
-
}
|
|
400
206
|
|
|
401
207
|
// src/member.ts
|
|
402
208
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
@@ -449,9 +255,9 @@ function resolveApplication(a) {
|
|
|
449
255
|
validateEmail: a?.validateEmail ?? true
|
|
450
256
|
};
|
|
451
257
|
}
|
|
452
|
-
var
|
|
258
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
453
259
|
function isValidEmail(value) {
|
|
454
|
-
return typeof value === "string" &&
|
|
260
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
455
261
|
}
|
|
456
262
|
function clampArray(value, max) {
|
|
457
263
|
if (!Array.isArray(value)) return value;
|
|
@@ -486,7 +292,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
486
292
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
487
293
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
488
294
|
}
|
|
489
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
295
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
|
|
490
296
|
return { ok: false, error: "email must be a valid email address" };
|
|
491
297
|
}
|
|
492
298
|
const acked = hasDisclaimerAck(fields);
|
|
@@ -502,7 +308,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
502
308
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
503
309
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
504
310
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
505
|
-
row.emailNormalized =
|
|
311
|
+
row.emailNormalized = normalizeChapterEmail(fields.email);
|
|
506
312
|
}
|
|
507
313
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
508
314
|
row.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -730,6 +536,281 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
730
536
|
};
|
|
731
537
|
}
|
|
732
538
|
|
|
539
|
+
// src/admission-grants-contract.ts
|
|
540
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
541
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
542
|
+
|
|
543
|
+
// src/signup-control-canonical.ts
|
|
544
|
+
var encoder = new TextEncoder();
|
|
545
|
+
function canonicalValue(value) {
|
|
546
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
547
|
+
return JSON.stringify(value);
|
|
548
|
+
}
|
|
549
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
550
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
551
|
+
const record3 = value;
|
|
552
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
553
|
+
const item = record3[key];
|
|
554
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
555
|
+
});
|
|
556
|
+
return `{${entries.join(",")}}`;
|
|
557
|
+
}
|
|
558
|
+
function canonicalSignupControl(value) {
|
|
559
|
+
return canonicalValue(value);
|
|
560
|
+
}
|
|
561
|
+
async function signupControlDigest(document) {
|
|
562
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
563
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/admission-grants.ts
|
|
567
|
+
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
568
|
+
var MAX_GRANTS = 2e3;
|
|
569
|
+
var MAX_NOTE = 2e3;
|
|
570
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
571
|
+
var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
|
|
572
|
+
function normalizeAdmissionEmail(email) {
|
|
573
|
+
return normalizeChapterEmail(email);
|
|
574
|
+
}
|
|
575
|
+
function validateAdmissionGrantDocument(input) {
|
|
576
|
+
const errors = [];
|
|
577
|
+
const root = input && typeof input === "object" ? input : {};
|
|
578
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
579
|
+
const revision = Number(root.revision);
|
|
580
|
+
const generatedAt = Number(root.generatedAt);
|
|
581
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
582
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
583
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
584
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
585
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
586
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
587
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
588
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
589
|
+
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
590
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
591
|
+
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
592
|
+
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
593
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
594
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
595
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
596
|
+
const grants = [];
|
|
597
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
598
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
599
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
600
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
601
|
+
const id = typeof grant.id === "string" ? grant.id : "";
|
|
602
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
603
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
604
|
+
const decision = grant.decision;
|
|
605
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
606
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
607
|
+
if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
608
|
+
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
609
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
610
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
611
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
612
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
613
|
+
}
|
|
614
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
615
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
616
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
617
|
+
}
|
|
618
|
+
seenIds.add(id);
|
|
619
|
+
seenEmails.add(normalized);
|
|
620
|
+
grants.push({
|
|
621
|
+
id,
|
|
622
|
+
email,
|
|
623
|
+
decision,
|
|
624
|
+
...note ? { note } : {},
|
|
625
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (errors.length) return { ok: false, errors };
|
|
629
|
+
return {
|
|
630
|
+
ok: true,
|
|
631
|
+
value: {
|
|
632
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
633
|
+
revision,
|
|
634
|
+
generatedAt,
|
|
635
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
636
|
+
target: { chapterId, environment },
|
|
637
|
+
grants
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
642
|
+
function asGrant(row) {
|
|
643
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
644
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
645
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
646
|
+
return row;
|
|
647
|
+
}
|
|
648
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
649
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
650
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
651
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
652
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
653
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
654
|
+
)).sort((a, b) => {
|
|
655
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
656
|
+
return scope || b.createdAt - a.createdAt;
|
|
657
|
+
});
|
|
658
|
+
return active[0] ?? null;
|
|
659
|
+
}
|
|
660
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
661
|
+
const where = { sourceId, targetId, environment };
|
|
662
|
+
if (db.aggregate) {
|
|
663
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
664
|
+
}
|
|
665
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
666
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
667
|
+
}
|
|
668
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
669
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
670
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
671
|
+
where: { sourceId, targetId, environment },
|
|
672
|
+
order: { revision: "desc" },
|
|
673
|
+
limit: 1
|
|
674
|
+
} } });
|
|
675
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
676
|
+
return row ? row : null;
|
|
677
|
+
}
|
|
678
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
679
|
+
const { document, digest } = envelope;
|
|
680
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
681
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
682
|
+
limit: MAX_GRANTS
|
|
683
|
+
} } })).admissionGrants);
|
|
684
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
685
|
+
const ops = [];
|
|
686
|
+
for (const row of current) {
|
|
687
|
+
const id = String(row.id ?? "");
|
|
688
|
+
if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
689
|
+
active: false,
|
|
690
|
+
revokedAt: receivedAt,
|
|
691
|
+
revokedBy: document.source.actor,
|
|
692
|
+
updatedAt: receivedAt,
|
|
693
|
+
updatedBy: document.source.actor
|
|
694
|
+
} });
|
|
695
|
+
}
|
|
696
|
+
for (const grant of document.grants) {
|
|
697
|
+
const id = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
698
|
+
const existing = current.find((row) => row.id === id);
|
|
699
|
+
ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
700
|
+
id,
|
|
701
|
+
sourceGrantId: grant.id,
|
|
702
|
+
sourceScope: "parent",
|
|
703
|
+
sourceId: document.source.appId,
|
|
704
|
+
targetId: document.target.chapterId,
|
|
705
|
+
email: grant.email,
|
|
706
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
707
|
+
decision: grant.decision,
|
|
708
|
+
...grant.note ? { note: grant.note } : {},
|
|
709
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
710
|
+
active: true,
|
|
711
|
+
sourceRevision: document.revision,
|
|
712
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
713
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
714
|
+
updatedAt: receivedAt,
|
|
715
|
+
updatedBy: document.source.actor
|
|
716
|
+
} });
|
|
717
|
+
const retract = [
|
|
718
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
719
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
720
|
+
];
|
|
721
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
|
|
722
|
+
}
|
|
723
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
724
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
725
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
726
|
+
id: revisionId,
|
|
727
|
+
sourceId: document.source.appId,
|
|
728
|
+
targetId: document.target.chapterId,
|
|
729
|
+
environment: document.target.environment,
|
|
730
|
+
revision: document.revision,
|
|
731
|
+
digest,
|
|
732
|
+
active: true,
|
|
733
|
+
receivedAt
|
|
734
|
+
} });
|
|
735
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/flow-fields.ts
|
|
739
|
+
var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
|
|
740
|
+
var APPLICATION_LABELS = {
|
|
741
|
+
firstName: "First name",
|
|
742
|
+
lastName: "Last name",
|
|
743
|
+
email: "Email",
|
|
744
|
+
referral: "Referral source",
|
|
745
|
+
referralName: "Referral name",
|
|
746
|
+
whoYouAre: "Who you are",
|
|
747
|
+
linkedin: "LinkedIn",
|
|
748
|
+
message: "Intro message",
|
|
749
|
+
phone: "Phone",
|
|
750
|
+
state: "State"
|
|
751
|
+
};
|
|
752
|
+
function humanize(id) {
|
|
753
|
+
const text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
754
|
+
return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
|
|
755
|
+
}
|
|
756
|
+
function applicationDefs(chapter) {
|
|
757
|
+
try {
|
|
758
|
+
return chapter.crm.type("person").fields;
|
|
759
|
+
} catch {
|
|
760
|
+
return {};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function applicationType(id, def) {
|
|
764
|
+
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
765
|
+
return id === "email" ? "email" : id === "linkedin" ? "url" : "string";
|
|
766
|
+
}
|
|
767
|
+
function applicationFlowFields(chapter, application = chapter.application) {
|
|
768
|
+
const defs = applicationDefs(chapter);
|
|
769
|
+
return [...application.required, ...application.optional].map((id, order) => {
|
|
770
|
+
const def = defs[id];
|
|
771
|
+
const conditions = application.conditions[id];
|
|
772
|
+
const type = applicationType(id, def);
|
|
773
|
+
return {
|
|
774
|
+
id,
|
|
775
|
+
label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
|
|
776
|
+
type,
|
|
777
|
+
required: application.required.includes(id),
|
|
778
|
+
order,
|
|
779
|
+
maxLength: application.maxLen[id] ?? application.defaultMaxLen,
|
|
780
|
+
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
781
|
+
...conditions ? { conditions: { ...conditions } } : {},
|
|
782
|
+
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function resolveFormationFlowFields(crm, formation) {
|
|
787
|
+
if (!formation.enabled) return [];
|
|
788
|
+
const def = crm.type(formation.type);
|
|
789
|
+
return [...formation.required, ...formation.optional].map((id, order) => {
|
|
790
|
+
const field = def.fields[id];
|
|
791
|
+
return {
|
|
792
|
+
id,
|
|
793
|
+
label: field.label ?? humanize(id),
|
|
794
|
+
type: field.type,
|
|
795
|
+
required: formation.required.includes(id),
|
|
796
|
+
order,
|
|
797
|
+
maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
|
|
798
|
+
...field.options ? { options: [...field.options] } : {},
|
|
799
|
+
...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
800
|
+
};
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
function formationFlowFields(chapter, formation = chapter.formation) {
|
|
804
|
+
return resolveFormationFlowFields(chapter.crm, formation);
|
|
805
|
+
}
|
|
806
|
+
function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
|
|
807
|
+
return {
|
|
808
|
+
schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
809
|
+
flow,
|
|
810
|
+
fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
733
814
|
// src/gift-offers.ts
|
|
734
815
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
735
816
|
var MAX_GIFT_OFFERS = 20;
|
|
@@ -1144,10 +1225,10 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
1144
1225
|
return recordId;
|
|
1145
1226
|
}
|
|
1146
1227
|
async function backfillCrm(deps) {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1228
|
+
const data = await deps.db.query({
|
|
1229
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
|
|
1230
|
+
$users: { $: { limit: 1e3 } }
|
|
1231
|
+
});
|
|
1151
1232
|
const seen = /* @__PURE__ */ new Set();
|
|
1152
1233
|
let synced = 0;
|
|
1153
1234
|
const errors = [];
|
|
@@ -1162,8 +1243,8 @@ async function backfillCrm(deps) {
|
|
|
1162
1243
|
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
1163
1244
|
}
|
|
1164
1245
|
};
|
|
1165
|
-
for (const a of
|
|
1166
|
-
for (const u of
|
|
1246
|
+
for (const a of data.applications ?? []) await run(a, str(a.status));
|
|
1247
|
+
for (const u of data.$users ?? []) {
|
|
1167
1248
|
if (u.deleted === true) continue;
|
|
1168
1249
|
await run({ email: u.email, firstName: str(u.name) });
|
|
1169
1250
|
}
|
|
@@ -1735,7 +1816,7 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
|
|
|
1735
1816
|
// src/worker-routes.ts
|
|
1736
1817
|
async function memberSessionApplication(db, chapterId, email) {
|
|
1737
1818
|
let apps = (await db.query({ applications: { $: {
|
|
1738
|
-
where: { emailNormalized:
|
|
1819
|
+
where: { emailNormalized: normalizeChapterEmail(email) },
|
|
1739
1820
|
limit: 20
|
|
1740
1821
|
} } })).applications;
|
|
1741
1822
|
if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
@@ -1834,6 +1915,7 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1834
1915
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1835
1916
|
return json({
|
|
1836
1917
|
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1918
|
+
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
1837
1919
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1838
1920
|
...activeControl ? { signupControl: {
|
|
1839
1921
|
revision: activeControl.revision,
|
|
@@ -3077,7 +3159,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3077
3159
|
return json({ error: "invalid JSON body" }, 400);
|
|
3078
3160
|
}
|
|
3079
3161
|
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
3080
|
-
const emailNormalized =
|
|
3162
|
+
const emailNormalized = normalizeChapterEmail(email);
|
|
3081
3163
|
if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
|
|
3082
3164
|
const decision = body.decision ?? "waive_interview";
|
|
3083
3165
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -3579,7 +3661,7 @@ async function rotateGiftClaim(db, claim, now) {
|
|
|
3579
3661
|
async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
3580
3662
|
if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
|
|
3581
3663
|
const chapterId = String(claim.chapterId);
|
|
3582
|
-
const emailNormalized =
|
|
3664
|
+
const emailNormalized = normalizeChapterEmail(beneficiary.email);
|
|
3583
3665
|
const target = claim.target;
|
|
3584
3666
|
const ops = [];
|
|
3585
3667
|
let result;
|
|
@@ -3876,7 +3958,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
3876
3958
|
offer,
|
|
3877
3959
|
purchaserType: "application",
|
|
3878
3960
|
purchaserEmail: String(app.email ?? ""),
|
|
3879
|
-
purchaserEmailNormalized:
|
|
3961
|
+
purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
|
|
3880
3962
|
purchaserApplicationId: applicationId,
|
|
3881
3963
|
stripeSecret: secretKey,
|
|
3882
3964
|
publishableKey: group.stripePublishableKey,
|
|
@@ -4025,7 +4107,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4025
4107
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
4026
4108
|
const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4027
4109
|
const found = await db.query({ giftClaims: { $: { where: {
|
|
4028
|
-
purchaserEmailNormalized:
|
|
4110
|
+
purchaserEmailNormalized: normalizeChapterEmail(user.email),
|
|
4029
4111
|
chapterId: ctx.chapter.id
|
|
4030
4112
|
}, limit: 200 } } });
|
|
4031
4113
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
@@ -4052,7 +4134,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4052
4134
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4053
4135
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
4054
4136
|
if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
|
|
4055
|
-
const emailNormalized =
|
|
4137
|
+
const emailNormalized = normalizeChapterEmail(user.email);
|
|
4056
4138
|
const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
|
|
4057
4139
|
const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
4058
4140
|
try {
|
|
@@ -4094,7 +4176,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4094
4176
|
return invalid();
|
|
4095
4177
|
}
|
|
4096
4178
|
}
|
|
4097
|
-
const owns = user ? claim.purchaserEmailNormalized ===
|
|
4179
|
+
const owns = user ? claim.purchaserEmailNormalized === normalizeChapterEmail(user.email) : claim.purchaserApplicationId === applicationId;
|
|
4098
4180
|
if (!owns) return invalid();
|
|
4099
4181
|
const rotated = await rotateGiftClaim(db, claim, Date.now());
|
|
4100
4182
|
return rotated ? json(rotated) : invalid();
|
|
@@ -4287,14 +4369,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
4287
4369
|
if (gate9 instanceof Response) return gate9;
|
|
4288
4370
|
const { db } = gate9;
|
|
4289
4371
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
4290
|
-
const [
|
|
4291
|
-
db.query({
|
|
4292
|
-
|
|
4372
|
+
const [data, roleList] = await Promise.all([
|
|
4373
|
+
db.query({
|
|
4374
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
|
|
4375
|
+
$users: { $: { limit: 200 } }
|
|
4376
|
+
}),
|
|
4293
4377
|
sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
|
|
4294
4378
|
]);
|
|
4295
4379
|
const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
|
|
4296
4380
|
const people = /* @__PURE__ */ new Map();
|
|
4297
|
-
for (const u of
|
|
4381
|
+
for (const u of data.$users ?? []) {
|
|
4298
4382
|
if (u.deleted === true) continue;
|
|
4299
4383
|
const email = typeof u.email === "string" ? u.email : "";
|
|
4300
4384
|
if (!email) continue;
|
|
@@ -4309,7 +4393,7 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
4309
4393
|
application: null
|
|
4310
4394
|
});
|
|
4311
4395
|
}
|
|
4312
|
-
for (const a of
|
|
4396
|
+
for (const a of data.applications ?? []) {
|
|
4313
4397
|
const key = String(a.email ?? "").toLowerCase();
|
|
4314
4398
|
if (!key) continue;
|
|
4315
4399
|
const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
|
|
@@ -4514,13 +4598,19 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4514
4598
|
const now = Date.now();
|
|
4515
4599
|
const d7 = now - 7 * 864e5;
|
|
4516
4600
|
const d30 = now - 30 * 864e5;
|
|
4517
|
-
const
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4601
|
+
const data = await db.query({
|
|
4602
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
|
|
4603
|
+
meetings: {
|
|
4604
|
+
$: {
|
|
4605
|
+
where: { status: "scheduled" },
|
|
4606
|
+
order: { startAt: "asc" },
|
|
4607
|
+
limit: 500
|
|
4608
|
+
}
|
|
4609
|
+
},
|
|
4610
|
+
crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
|
|
4611
|
+
groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
|
|
4612
|
+
});
|
|
4613
|
+
const apps = rows3(data.applications);
|
|
4524
4614
|
const applications = {
|
|
4525
4615
|
total: apps.length,
|
|
4526
4616
|
last7: apps.filter((a) => a.createdAt >= d7).length,
|
|
@@ -4532,7 +4622,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4532
4622
|
pipeline[s] = 0;
|
|
4533
4623
|
pipelineDelta[s] = 0;
|
|
4534
4624
|
}
|
|
4535
|
-
for (const r of rows3(
|
|
4625
|
+
for (const r of rows3(data.crm_record)) {
|
|
4536
4626
|
const s = r.stage;
|
|
4537
4627
|
if (s in pipeline) {
|
|
4538
4628
|
pipeline[s] = (pipeline[s] ?? 0) + 1;
|
|
@@ -4540,7 +4630,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4540
4630
|
if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
|
|
4541
4631
|
}
|
|
4542
4632
|
}
|
|
4543
|
-
const meetings = rows3(
|
|
4633
|
+
const meetings = rows3(data.meetings);
|
|
4544
4634
|
const appById = new Map(apps.map((a) => [a.id, a]));
|
|
4545
4635
|
const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
|
|
4546
4636
|
const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
|
|
@@ -4556,7 +4646,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4556
4646
|
email: a?.email ?? null
|
|
4557
4647
|
};
|
|
4558
4648
|
});
|
|
4559
|
-
const group =
|
|
4649
|
+
const group = data.groups?.[0];
|
|
4560
4650
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
4561
4651
|
let revenue = { billingReady: false };
|
|
4562
4652
|
let revenueSeries = null;
|
|
@@ -4590,16 +4680,18 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
4590
4680
|
const db = got;
|
|
4591
4681
|
const sk = await getVaultSecret(db, "stripe_secret_key");
|
|
4592
4682
|
if (!sk) return json({ billingReady: false, rows: [], summary: null });
|
|
4593
|
-
const [
|
|
4594
|
-
db.query({
|
|
4595
|
-
|
|
4596
|
-
|
|
4683
|
+
const [data, subsRes] = await Promise.all([
|
|
4684
|
+
db.query({
|
|
4685
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
|
|
4686
|
+
groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
|
|
4687
|
+
}),
|
|
4688
|
+
stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" })
|
|
4597
4689
|
]);
|
|
4598
|
-
const testMode = String(
|
|
4690
|
+
const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
|
|
4599
4691
|
if (!subsRes.ok) return json({ error: "billing lookup failed upstream" }, 502);
|
|
4600
4692
|
const subById = new Map(rows3(subsRes.body.data).map((s) => [s.id, s]));
|
|
4601
4693
|
const billingRows = [];
|
|
4602
|
-
for (const a of rows3(
|
|
4694
|
+
for (const a of rows3(data.applications)) {
|
|
4603
4695
|
if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
|
|
4604
4696
|
const sub = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
|
|
4605
4697
|
const items = sub?.items?.data ?? [];
|
|
@@ -4939,21 +5031,27 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
4939
5031
|
if (got instanceof Response) return got;
|
|
4940
5032
|
const db = got;
|
|
4941
5033
|
const appId = m[1];
|
|
4942
|
-
const
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5034
|
+
const data = await db.query({
|
|
5035
|
+
emailLog: {
|
|
5036
|
+
$: {
|
|
5037
|
+
where: { applicationId: appId },
|
|
5038
|
+
order: { sentAt: "desc" },
|
|
5039
|
+
limit: 100
|
|
5040
|
+
}
|
|
5041
|
+
},
|
|
5042
|
+
meetings: { $: { where: { applicationId: appId } } },
|
|
5043
|
+
applications: { $: { where: { id: appId }, limit: 1 } }
|
|
5044
|
+
});
|
|
5045
|
+
const app = data.applications?.[0] ?? null;
|
|
4948
5046
|
const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
|
|
4949
5047
|
const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl: `${url.origin}/admin/`, membersUrl: `${url.origin}/members/` } : null;
|
|
4950
|
-
const emails = (
|
|
5048
|
+
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
4951
5049
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
4952
5050
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
4953
5051
|
const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
|
|
4954
5052
|
return { kind: "email", channel, label: String(r.template), subject: str2(r.subject), to: r.to ?? null, body: mailBody, at: r.sentAt, error: r.error ?? null };
|
|
4955
5053
|
});
|
|
4956
|
-
const calendar = (
|
|
5054
|
+
const calendar = (data.meetings ?? []).map((m2) => ({
|
|
4957
5055
|
kind: "calendar",
|
|
4958
5056
|
channel: "Google Calendar",
|
|
4959
5057
|
label: m2.status === "cancelled" ? "Invitation (call later cancelled)" : "Meeting invitation",
|
|
@@ -5618,31 +5716,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5618
5716
|
|
|
5619
5717
|
// src/worker-routes-formation.ts
|
|
5620
5718
|
var import_crm9 = require("@odla-ai/crm");
|
|
5621
|
-
|
|
5622
|
-
// src/formation.ts
|
|
5623
|
-
function formationFields(crm, formation) {
|
|
5624
|
-
const def = crm.type(formation.type);
|
|
5625
|
-
return [...formation.required, ...formation.optional].map((id) => {
|
|
5626
|
-
const field = def.fields[id];
|
|
5627
|
-
return {
|
|
5628
|
-
id,
|
|
5629
|
-
label: field.label ?? id,
|
|
5630
|
-
type: field.type,
|
|
5631
|
-
required: formation.required.includes(id),
|
|
5632
|
-
...field.options ? { options: field.options } : {}
|
|
5633
|
-
};
|
|
5634
|
-
});
|
|
5635
|
-
}
|
|
5636
|
-
|
|
5637
|
-
// src/worker-routes-formation.ts
|
|
5638
5719
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
5639
5720
|
var handleFormation = async (req, url, env, ctx) => {
|
|
5640
5721
|
const formation = ctx.chapter.formation;
|
|
5641
5722
|
if (!formation.enabled) return null;
|
|
5642
5723
|
if (req.method === "GET" && url.pathname === "/api/formation/config") {
|
|
5724
|
+
const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
|
|
5643
5725
|
return json({
|
|
5644
5726
|
type: formation.type,
|
|
5645
|
-
|
|
5727
|
+
fieldContract,
|
|
5728
|
+
// Backward compatibility for hosts that consumed the original array.
|
|
5729
|
+
fields: fieldContract.fields
|
|
5646
5730
|
});
|
|
5647
5731
|
}
|
|
5648
5732
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|