@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.js
CHANGED
|
@@ -167,204 +167,10 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
167
167
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// src/
|
|
171
|
-
|
|
172
|
-
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
173
|
-
|
|
174
|
-
// src/signup-control-canonical.ts
|
|
175
|
-
var encoder = new TextEncoder();
|
|
176
|
-
function canonicalValue(value) {
|
|
177
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
178
|
-
return JSON.stringify(value);
|
|
179
|
-
}
|
|
180
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
181
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
182
|
-
const record3 = value;
|
|
183
|
-
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
184
|
-
const item = record3[key];
|
|
185
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
186
|
-
});
|
|
187
|
-
return `{${entries.join(",")}}`;
|
|
188
|
-
}
|
|
189
|
-
function canonicalSignupControl(value) {
|
|
190
|
-
return canonicalValue(value);
|
|
191
|
-
}
|
|
192
|
-
async function signupControlDigest(document) {
|
|
193
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
194
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// src/admission-grants.ts
|
|
198
|
-
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
199
|
-
var MAX_GRANTS = 2e3;
|
|
200
|
-
var MAX_NOTE = 2e3;
|
|
201
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
202
|
-
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
203
|
-
function normalizeAdmissionEmail(email) {
|
|
170
|
+
// src/email-identity.ts
|
|
171
|
+
function normalizeChapterEmail(email) {
|
|
204
172
|
return email.trim().toLowerCase();
|
|
205
173
|
}
|
|
206
|
-
function validateAdmissionGrantDocument(input) {
|
|
207
|
-
const errors = [];
|
|
208
|
-
const root = input && typeof input === "object" ? input : {};
|
|
209
|
-
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
210
|
-
const revision = Number(root.revision);
|
|
211
|
-
const generatedAt = Number(root.generatedAt);
|
|
212
|
-
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
213
|
-
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
214
|
-
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
215
|
-
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
216
|
-
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
217
|
-
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
218
|
-
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
219
|
-
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
220
|
-
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
221
|
-
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
222
|
-
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
223
|
-
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
224
|
-
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
225
|
-
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
226
|
-
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
227
|
-
const grants = [];
|
|
228
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
229
|
-
const seenEmails = /* @__PURE__ */ new Set();
|
|
230
|
-
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
231
|
-
const grant = raw && typeof raw === "object" ? raw : {};
|
|
232
|
-
const id = typeof grant.id === "string" ? grant.id : "";
|
|
233
|
-
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
234
|
-
const normalized = normalizeAdmissionEmail(email);
|
|
235
|
-
const decision = grant.decision;
|
|
236
|
-
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
237
|
-
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
238
|
-
if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
239
|
-
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
240
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
241
|
-
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
242
|
-
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
243
|
-
errors.push(`document.grants[${index}].decision is invalid`);
|
|
244
|
-
}
|
|
245
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
246
|
-
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
247
|
-
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
248
|
-
}
|
|
249
|
-
seenIds.add(id);
|
|
250
|
-
seenEmails.add(normalized);
|
|
251
|
-
grants.push({
|
|
252
|
-
id,
|
|
253
|
-
email,
|
|
254
|
-
decision,
|
|
255
|
-
...note ? { note } : {},
|
|
256
|
-
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
if (errors.length) return { ok: false, errors };
|
|
260
|
-
return {
|
|
261
|
-
ok: true,
|
|
262
|
-
value: {
|
|
263
|
-
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
264
|
-
revision,
|
|
265
|
-
generatedAt,
|
|
266
|
-
source: { appId: sourceId, actor: sourceActor },
|
|
267
|
-
target: { chapterId, environment },
|
|
268
|
-
grants
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
var rows = (value) => Array.isArray(value) ? value : [];
|
|
273
|
-
function asGrant(row) {
|
|
274
|
-
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
275
|
-
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
276
|
-
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
277
|
-
return row;
|
|
278
|
-
}
|
|
279
|
-
async function findAdmissionGrant(db, targetId, email, now) {
|
|
280
|
-
const emailNormalized = normalizeAdmissionEmail(email);
|
|
281
|
-
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
282
|
-
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
283
|
-
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
284
|
-
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
285
|
-
)).sort((a, b) => {
|
|
286
|
-
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
287
|
-
return scope || b.createdAt - a.createdAt;
|
|
288
|
-
});
|
|
289
|
-
return active[0] ?? null;
|
|
290
|
-
}
|
|
291
|
-
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
292
|
-
const where = { sourceId, targetId, environment };
|
|
293
|
-
if (db.aggregate) {
|
|
294
|
-
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
295
|
-
}
|
|
296
|
-
const found = await db.query({ admissionGrantRevisions: {} });
|
|
297
|
-
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
298
|
-
}
|
|
299
|
-
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
300
|
-
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
301
|
-
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
302
|
-
where: { sourceId, targetId, environment },
|
|
303
|
-
order: { revision: "desc" },
|
|
304
|
-
limit: 1
|
|
305
|
-
} } });
|
|
306
|
-
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
307
|
-
return row ? row : null;
|
|
308
|
-
}
|
|
309
|
-
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
310
|
-
const { document, digest } = envelope;
|
|
311
|
-
const current = rows((await db.query({ admissionGrants: { $: {
|
|
312
|
-
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
313
|
-
limit: MAX_GRANTS
|
|
314
|
-
} } })).admissionGrants);
|
|
315
|
-
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
316
|
-
const ops = [];
|
|
317
|
-
for (const row of current) {
|
|
318
|
-
const id = String(row.id ?? "");
|
|
319
|
-
if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
320
|
-
active: false,
|
|
321
|
-
revokedAt: receivedAt,
|
|
322
|
-
revokedBy: document.source.actor,
|
|
323
|
-
updatedAt: receivedAt,
|
|
324
|
-
updatedBy: document.source.actor
|
|
325
|
-
} });
|
|
326
|
-
}
|
|
327
|
-
for (const grant of document.grants) {
|
|
328
|
-
const id = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
329
|
-
const existing = current.find((row) => row.id === id);
|
|
330
|
-
ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
331
|
-
id,
|
|
332
|
-
sourceGrantId: grant.id,
|
|
333
|
-
sourceScope: "parent",
|
|
334
|
-
sourceId: document.source.appId,
|
|
335
|
-
targetId: document.target.chapterId,
|
|
336
|
-
email: grant.email,
|
|
337
|
-
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
338
|
-
decision: grant.decision,
|
|
339
|
-
...grant.note ? { note: grant.note } : {},
|
|
340
|
-
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
341
|
-
active: true,
|
|
342
|
-
sourceRevision: document.revision,
|
|
343
|
-
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
344
|
-
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
345
|
-
updatedAt: receivedAt,
|
|
346
|
-
updatedBy: document.source.actor
|
|
347
|
-
} });
|
|
348
|
-
const retract = [
|
|
349
|
-
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
350
|
-
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
351
|
-
];
|
|
352
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
|
|
353
|
-
}
|
|
354
|
-
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
355
|
-
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
356
|
-
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
357
|
-
id: revisionId,
|
|
358
|
-
sourceId: document.source.appId,
|
|
359
|
-
targetId: document.target.chapterId,
|
|
360
|
-
environment: document.target.environment,
|
|
361
|
-
revision: document.revision,
|
|
362
|
-
digest,
|
|
363
|
-
active: true,
|
|
364
|
-
receivedAt
|
|
365
|
-
} });
|
|
366
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
367
|
-
}
|
|
368
174
|
|
|
369
175
|
// src/member.ts
|
|
370
176
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
@@ -417,9 +223,9 @@ function resolveApplication(a) {
|
|
|
417
223
|
validateEmail: a?.validateEmail ?? true
|
|
418
224
|
};
|
|
419
225
|
}
|
|
420
|
-
var
|
|
226
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
421
227
|
function isValidEmail(value) {
|
|
422
|
-
return typeof value === "string" &&
|
|
228
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
423
229
|
}
|
|
424
230
|
function clampArray(value, max) {
|
|
425
231
|
if (!Array.isArray(value)) return value;
|
|
@@ -454,7 +260,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
454
260
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
455
261
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
456
262
|
}
|
|
457
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
263
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
|
|
458
264
|
return { ok: false, error: "email must be a valid email address" };
|
|
459
265
|
}
|
|
460
266
|
const acked = hasDisclaimerAck(fields);
|
|
@@ -470,7 +276,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
470
276
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
471
277
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
472
278
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
473
|
-
row.emailNormalized =
|
|
279
|
+
row.emailNormalized = normalizeChapterEmail(fields.email);
|
|
474
280
|
}
|
|
475
281
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
476
282
|
row.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -698,6 +504,281 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
698
504
|
};
|
|
699
505
|
}
|
|
700
506
|
|
|
507
|
+
// src/admission-grants-contract.ts
|
|
508
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
509
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
510
|
+
|
|
511
|
+
// src/signup-control-canonical.ts
|
|
512
|
+
var encoder = new TextEncoder();
|
|
513
|
+
function canonicalValue(value) {
|
|
514
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
515
|
+
return JSON.stringify(value);
|
|
516
|
+
}
|
|
517
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
518
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
519
|
+
const record3 = value;
|
|
520
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
521
|
+
const item = record3[key];
|
|
522
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
523
|
+
});
|
|
524
|
+
return `{${entries.join(",")}}`;
|
|
525
|
+
}
|
|
526
|
+
function canonicalSignupControl(value) {
|
|
527
|
+
return canonicalValue(value);
|
|
528
|
+
}
|
|
529
|
+
async function signupControlDigest(document) {
|
|
530
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
531
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/admission-grants.ts
|
|
535
|
+
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
536
|
+
var MAX_GRANTS = 2e3;
|
|
537
|
+
var MAX_NOTE = 2e3;
|
|
538
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
539
|
+
var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
|
|
540
|
+
function normalizeAdmissionEmail(email) {
|
|
541
|
+
return normalizeChapterEmail(email);
|
|
542
|
+
}
|
|
543
|
+
function validateAdmissionGrantDocument(input) {
|
|
544
|
+
const errors = [];
|
|
545
|
+
const root = input && typeof input === "object" ? input : {};
|
|
546
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
547
|
+
const revision = Number(root.revision);
|
|
548
|
+
const generatedAt = Number(root.generatedAt);
|
|
549
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
550
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
551
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
552
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
553
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
554
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
555
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
556
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
557
|
+
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
558
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
559
|
+
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
560
|
+
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
561
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
562
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
563
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
564
|
+
const grants = [];
|
|
565
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
566
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
567
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
568
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
569
|
+
const id = typeof grant.id === "string" ? grant.id : "";
|
|
570
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
571
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
572
|
+
const decision = grant.decision;
|
|
573
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
574
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
575
|
+
if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
576
|
+
if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
|
|
577
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
578
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
579
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
580
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
581
|
+
}
|
|
582
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
583
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
584
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
585
|
+
}
|
|
586
|
+
seenIds.add(id);
|
|
587
|
+
seenEmails.add(normalized);
|
|
588
|
+
grants.push({
|
|
589
|
+
id,
|
|
590
|
+
email,
|
|
591
|
+
decision,
|
|
592
|
+
...note ? { note } : {},
|
|
593
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (errors.length) return { ok: false, errors };
|
|
597
|
+
return {
|
|
598
|
+
ok: true,
|
|
599
|
+
value: {
|
|
600
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
601
|
+
revision,
|
|
602
|
+
generatedAt,
|
|
603
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
604
|
+
target: { chapterId, environment },
|
|
605
|
+
grants
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
610
|
+
function asGrant(row) {
|
|
611
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
612
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
613
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
614
|
+
return row;
|
|
615
|
+
}
|
|
616
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
617
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
618
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
619
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
620
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
621
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
622
|
+
)).sort((a, b) => {
|
|
623
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
624
|
+
return scope || b.createdAt - a.createdAt;
|
|
625
|
+
});
|
|
626
|
+
return active[0] ?? null;
|
|
627
|
+
}
|
|
628
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
629
|
+
const where = { sourceId, targetId, environment };
|
|
630
|
+
if (db.aggregate) {
|
|
631
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
632
|
+
}
|
|
633
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
634
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
635
|
+
}
|
|
636
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
637
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
638
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
639
|
+
where: { sourceId, targetId, environment },
|
|
640
|
+
order: { revision: "desc" },
|
|
641
|
+
limit: 1
|
|
642
|
+
} } });
|
|
643
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
644
|
+
return row ? row : null;
|
|
645
|
+
}
|
|
646
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
647
|
+
const { document, digest } = envelope;
|
|
648
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
649
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
650
|
+
limit: MAX_GRANTS
|
|
651
|
+
} } })).admissionGrants);
|
|
652
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
653
|
+
const ops = [];
|
|
654
|
+
for (const row of current) {
|
|
655
|
+
const id = String(row.id ?? "");
|
|
656
|
+
if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
657
|
+
active: false,
|
|
658
|
+
revokedAt: receivedAt,
|
|
659
|
+
revokedBy: document.source.actor,
|
|
660
|
+
updatedAt: receivedAt,
|
|
661
|
+
updatedBy: document.source.actor
|
|
662
|
+
} });
|
|
663
|
+
}
|
|
664
|
+
for (const grant of document.grants) {
|
|
665
|
+
const id = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
666
|
+
const existing = current.find((row) => row.id === id);
|
|
667
|
+
ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
|
|
668
|
+
id,
|
|
669
|
+
sourceGrantId: grant.id,
|
|
670
|
+
sourceScope: "parent",
|
|
671
|
+
sourceId: document.source.appId,
|
|
672
|
+
targetId: document.target.chapterId,
|
|
673
|
+
email: grant.email,
|
|
674
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
675
|
+
decision: grant.decision,
|
|
676
|
+
...grant.note ? { note: grant.note } : {},
|
|
677
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
678
|
+
active: true,
|
|
679
|
+
sourceRevision: document.revision,
|
|
680
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
681
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
682
|
+
updatedAt: receivedAt,
|
|
683
|
+
updatedBy: document.source.actor
|
|
684
|
+
} });
|
|
685
|
+
const retract = [
|
|
686
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
687
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
688
|
+
];
|
|
689
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
|
|
690
|
+
}
|
|
691
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
692
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
693
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
694
|
+
id: revisionId,
|
|
695
|
+
sourceId: document.source.appId,
|
|
696
|
+
targetId: document.target.chapterId,
|
|
697
|
+
environment: document.target.environment,
|
|
698
|
+
revision: document.revision,
|
|
699
|
+
digest,
|
|
700
|
+
active: true,
|
|
701
|
+
receivedAt
|
|
702
|
+
} });
|
|
703
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/flow-fields.ts
|
|
707
|
+
var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
|
|
708
|
+
var APPLICATION_LABELS = {
|
|
709
|
+
firstName: "First name",
|
|
710
|
+
lastName: "Last name",
|
|
711
|
+
email: "Email",
|
|
712
|
+
referral: "Referral source",
|
|
713
|
+
referralName: "Referral name",
|
|
714
|
+
whoYouAre: "Who you are",
|
|
715
|
+
linkedin: "LinkedIn",
|
|
716
|
+
message: "Intro message",
|
|
717
|
+
phone: "Phone",
|
|
718
|
+
state: "State"
|
|
719
|
+
};
|
|
720
|
+
function humanize(id) {
|
|
721
|
+
const text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
722
|
+
return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
|
|
723
|
+
}
|
|
724
|
+
function applicationDefs(chapter) {
|
|
725
|
+
try {
|
|
726
|
+
return chapter.crm.type("person").fields;
|
|
727
|
+
} catch {
|
|
728
|
+
return {};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function applicationType(id, def) {
|
|
732
|
+
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
733
|
+
return id === "email" ? "email" : id === "linkedin" ? "url" : "string";
|
|
734
|
+
}
|
|
735
|
+
function applicationFlowFields(chapter, application = chapter.application) {
|
|
736
|
+
const defs = applicationDefs(chapter);
|
|
737
|
+
return [...application.required, ...application.optional].map((id, order) => {
|
|
738
|
+
const def = defs[id];
|
|
739
|
+
const conditions = application.conditions[id];
|
|
740
|
+
const type = applicationType(id, def);
|
|
741
|
+
return {
|
|
742
|
+
id,
|
|
743
|
+
label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
|
|
744
|
+
type,
|
|
745
|
+
required: application.required.includes(id),
|
|
746
|
+
order,
|
|
747
|
+
maxLength: application.maxLen[id] ?? application.defaultMaxLen,
|
|
748
|
+
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
749
|
+
...conditions ? { conditions: { ...conditions } } : {},
|
|
750
|
+
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
function resolveFormationFlowFields(crm, formation) {
|
|
755
|
+
if (!formation.enabled) return [];
|
|
756
|
+
const def = crm.type(formation.type);
|
|
757
|
+
return [...formation.required, ...formation.optional].map((id, order) => {
|
|
758
|
+
const field = def.fields[id];
|
|
759
|
+
return {
|
|
760
|
+
id,
|
|
761
|
+
label: field.label ?? humanize(id),
|
|
762
|
+
type: field.type,
|
|
763
|
+
required: formation.required.includes(id),
|
|
764
|
+
order,
|
|
765
|
+
maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
|
|
766
|
+
...field.options ? { options: [...field.options] } : {},
|
|
767
|
+
...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function formationFlowFields(chapter, formation = chapter.formation) {
|
|
772
|
+
return resolveFormationFlowFields(chapter.crm, formation);
|
|
773
|
+
}
|
|
774
|
+
function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
|
|
775
|
+
return {
|
|
776
|
+
schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
777
|
+
flow,
|
|
778
|
+
fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
701
782
|
// src/gift-offers.ts
|
|
702
783
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
703
784
|
var MAX_GIFT_OFFERS = 20;
|
|
@@ -1117,10 +1198,10 @@ async function syncApplicationToCrm(deps, opts) {
|
|
|
1117
1198
|
return recordId;
|
|
1118
1199
|
}
|
|
1119
1200
|
async function backfillCrm(deps) {
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1201
|
+
const data = await deps.db.query({
|
|
1202
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
|
|
1203
|
+
$users: { $: { limit: 1e3 } }
|
|
1204
|
+
});
|
|
1124
1205
|
const seen = /* @__PURE__ */ new Set();
|
|
1125
1206
|
let synced = 0;
|
|
1126
1207
|
const errors = [];
|
|
@@ -1135,8 +1216,8 @@ async function backfillCrm(deps) {
|
|
|
1135
1216
|
errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
|
|
1136
1217
|
}
|
|
1137
1218
|
};
|
|
1138
|
-
for (const a of
|
|
1139
|
-
for (const u of
|
|
1219
|
+
for (const a of data.applications ?? []) await run(a, str(a.status));
|
|
1220
|
+
for (const u of data.$users ?? []) {
|
|
1140
1221
|
if (u.deleted === true) continue;
|
|
1141
1222
|
await run({ email: u.email, firstName: str(u.name) });
|
|
1142
1223
|
}
|
|
@@ -1708,7 +1789,7 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
|
|
|
1708
1789
|
// src/worker-routes.ts
|
|
1709
1790
|
async function memberSessionApplication(db, chapterId, email) {
|
|
1710
1791
|
let apps = (await db.query({ applications: { $: {
|
|
1711
|
-
where: { emailNormalized:
|
|
1792
|
+
where: { emailNormalized: normalizeChapterEmail(email) },
|
|
1712
1793
|
limit: 20
|
|
1713
1794
|
} } })).applications;
|
|
1714
1795
|
if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
|
|
@@ -1807,6 +1888,7 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
1807
1888
|
const application = controlledApplication(chapter.application, activeControl?.document);
|
|
1808
1889
|
return json({
|
|
1809
1890
|
...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
|
|
1891
|
+
fieldContract: chapterFlowFieldContract(chapter, "join", application),
|
|
1810
1892
|
copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
|
|
1811
1893
|
...activeControl ? { signupControl: {
|
|
1812
1894
|
revision: activeControl.revision,
|
|
@@ -3050,7 +3132,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3050
3132
|
return json({ error: "invalid JSON body" }, 400);
|
|
3051
3133
|
}
|
|
3052
3134
|
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
3053
|
-
const emailNormalized =
|
|
3135
|
+
const emailNormalized = normalizeChapterEmail(email);
|
|
3054
3136
|
if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
|
|
3055
3137
|
const decision = body.decision ?? "waive_interview";
|
|
3056
3138
|
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
@@ -3552,7 +3634,7 @@ async function rotateGiftClaim(db, claim, now) {
|
|
|
3552
3634
|
async function redeemGiftClaim(db, claim, beneficiary, now) {
|
|
3553
3635
|
if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
|
|
3554
3636
|
const chapterId = String(claim.chapterId);
|
|
3555
|
-
const emailNormalized =
|
|
3637
|
+
const emailNormalized = normalizeChapterEmail(beneficiary.email);
|
|
3556
3638
|
const target = claim.target;
|
|
3557
3639
|
const ops = [];
|
|
3558
3640
|
let result;
|
|
@@ -3849,7 +3931,7 @@ async function startSubscription(req, env, ctx) {
|
|
|
3849
3931
|
offer,
|
|
3850
3932
|
purchaserType: "application",
|
|
3851
3933
|
purchaserEmail: String(app.email ?? ""),
|
|
3852
|
-
purchaserEmailNormalized:
|
|
3934
|
+
purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
|
|
3853
3935
|
purchaserApplicationId: applicationId,
|
|
3854
3936
|
stripeSecret: secretKey,
|
|
3855
3937
|
publishableKey: group.stripePublishableKey,
|
|
@@ -3998,7 +4080,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
3998
4080
|
if (!user) return json({ error: "unauthorized" }, 401);
|
|
3999
4081
|
const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
|
|
4000
4082
|
const found = await db.query({ giftClaims: { $: { where: {
|
|
4001
|
-
purchaserEmailNormalized:
|
|
4083
|
+
purchaserEmailNormalized: normalizeChapterEmail(user.email),
|
|
4002
4084
|
chapterId: ctx.chapter.id
|
|
4003
4085
|
}, limit: 200 } } });
|
|
4004
4086
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
@@ -4025,7 +4107,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4025
4107
|
const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
|
|
4026
4108
|
const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
|
|
4027
4109
|
if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
|
|
4028
|
-
const emailNormalized =
|
|
4110
|
+
const emailNormalized = normalizeChapterEmail(user.email);
|
|
4029
4111
|
const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
|
|
4030
4112
|
const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
4031
4113
|
try {
|
|
@@ -4067,7 +4149,7 @@ var handleGifts = async (req, url, env, ctx) => {
|
|
|
4067
4149
|
return invalid();
|
|
4068
4150
|
}
|
|
4069
4151
|
}
|
|
4070
|
-
const owns = user ? claim.purchaserEmailNormalized ===
|
|
4152
|
+
const owns = user ? claim.purchaserEmailNormalized === normalizeChapterEmail(user.email) : claim.purchaserApplicationId === applicationId;
|
|
4071
4153
|
if (!owns) return invalid();
|
|
4072
4154
|
const rotated = await rotateGiftClaim(db, claim, Date.now());
|
|
4073
4155
|
return rotated ? json(rotated) : invalid();
|
|
@@ -4260,14 +4342,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
4260
4342
|
if (gate9 instanceof Response) return gate9;
|
|
4261
4343
|
const { db } = gate9;
|
|
4262
4344
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
4263
|
-
const [
|
|
4264
|
-
db.query({
|
|
4265
|
-
|
|
4345
|
+
const [data, roleList] = await Promise.all([
|
|
4346
|
+
db.query({
|
|
4347
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
|
|
4348
|
+
$users: { $: { limit: 200 } }
|
|
4349
|
+
}),
|
|
4266
4350
|
sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
|
|
4267
4351
|
]);
|
|
4268
4352
|
const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
|
|
4269
4353
|
const people = /* @__PURE__ */ new Map();
|
|
4270
|
-
for (const u of
|
|
4354
|
+
for (const u of data.$users ?? []) {
|
|
4271
4355
|
if (u.deleted === true) continue;
|
|
4272
4356
|
const email = typeof u.email === "string" ? u.email : "";
|
|
4273
4357
|
if (!email) continue;
|
|
@@ -4282,7 +4366,7 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
4282
4366
|
application: null
|
|
4283
4367
|
});
|
|
4284
4368
|
}
|
|
4285
|
-
for (const a of
|
|
4369
|
+
for (const a of data.applications ?? []) {
|
|
4286
4370
|
const key = String(a.email ?? "").toLowerCase();
|
|
4287
4371
|
if (!key) continue;
|
|
4288
4372
|
const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
|
|
@@ -4487,13 +4571,19 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4487
4571
|
const now = Date.now();
|
|
4488
4572
|
const d7 = now - 7 * 864e5;
|
|
4489
4573
|
const d30 = now - 30 * 864e5;
|
|
4490
|
-
const
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4574
|
+
const data = await db.query({
|
|
4575
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
|
|
4576
|
+
meetings: {
|
|
4577
|
+
$: {
|
|
4578
|
+
where: { status: "scheduled" },
|
|
4579
|
+
order: { startAt: "asc" },
|
|
4580
|
+
limit: 500
|
|
4581
|
+
}
|
|
4582
|
+
},
|
|
4583
|
+
crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
|
|
4584
|
+
groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
|
|
4585
|
+
});
|
|
4586
|
+
const apps = rows3(data.applications);
|
|
4497
4587
|
const applications = {
|
|
4498
4588
|
total: apps.length,
|
|
4499
4589
|
last7: apps.filter((a) => a.createdAt >= d7).length,
|
|
@@ -4505,7 +4595,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4505
4595
|
pipeline[s] = 0;
|
|
4506
4596
|
pipelineDelta[s] = 0;
|
|
4507
4597
|
}
|
|
4508
|
-
for (const r of rows3(
|
|
4598
|
+
for (const r of rows3(data.crm_record)) {
|
|
4509
4599
|
const s = r.stage;
|
|
4510
4600
|
if (s in pipeline) {
|
|
4511
4601
|
pipeline[s] = (pipeline[s] ?? 0) + 1;
|
|
@@ -4513,7 +4603,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4513
4603
|
if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
|
|
4514
4604
|
}
|
|
4515
4605
|
}
|
|
4516
|
-
const meetings = rows3(
|
|
4606
|
+
const meetings = rows3(data.meetings);
|
|
4517
4607
|
const appById = new Map(apps.map((a) => [a.id, a]));
|
|
4518
4608
|
const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
|
|
4519
4609
|
const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
|
|
@@ -4529,7 +4619,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4529
4619
|
email: a?.email ?? null
|
|
4530
4620
|
};
|
|
4531
4621
|
});
|
|
4532
|
-
const group =
|
|
4622
|
+
const group = data.groups?.[0];
|
|
4533
4623
|
const timezone = resolveScheduling(group?.schedulingJson).timezone;
|
|
4534
4624
|
let revenue = { billingReady: false };
|
|
4535
4625
|
let revenueSeries = null;
|
|
@@ -4563,16 +4653,18 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
4563
4653
|
const db = got;
|
|
4564
4654
|
const sk = await getVaultSecret(db, "stripe_secret_key");
|
|
4565
4655
|
if (!sk) return json({ billingReady: false, rows: [], summary: null });
|
|
4566
|
-
const [
|
|
4567
|
-
db.query({
|
|
4568
|
-
|
|
4569
|
-
|
|
4656
|
+
const [data, subsRes] = await Promise.all([
|
|
4657
|
+
db.query({
|
|
4658
|
+
applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
|
|
4659
|
+
groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
|
|
4660
|
+
}),
|
|
4661
|
+
stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" })
|
|
4570
4662
|
]);
|
|
4571
|
-
const testMode = String(
|
|
4663
|
+
const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
|
|
4572
4664
|
if (!subsRes.ok) return json({ error: "billing lookup failed upstream" }, 502);
|
|
4573
4665
|
const subById = new Map(rows3(subsRes.body.data).map((s) => [s.id, s]));
|
|
4574
4666
|
const billingRows = [];
|
|
4575
|
-
for (const a of rows3(
|
|
4667
|
+
for (const a of rows3(data.applications)) {
|
|
4576
4668
|
if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
|
|
4577
4669
|
const sub = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
|
|
4578
4670
|
const items = sub?.items?.data ?? [];
|
|
@@ -4912,21 +5004,27 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
4912
5004
|
if (got instanceof Response) return got;
|
|
4913
5005
|
const db = got;
|
|
4914
5006
|
const appId = m[1];
|
|
4915
|
-
const
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5007
|
+
const data = await db.query({
|
|
5008
|
+
emailLog: {
|
|
5009
|
+
$: {
|
|
5010
|
+
where: { applicationId: appId },
|
|
5011
|
+
order: { sentAt: "desc" },
|
|
5012
|
+
limit: 100
|
|
5013
|
+
}
|
|
5014
|
+
},
|
|
5015
|
+
meetings: { $: { where: { applicationId: appId } } },
|
|
5016
|
+
applications: { $: { where: { id: appId }, limit: 1 } }
|
|
5017
|
+
});
|
|
5018
|
+
const app = data.applications?.[0] ?? null;
|
|
4921
5019
|
const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
|
|
4922
5020
|
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;
|
|
4923
|
-
const emails = (
|
|
5021
|
+
const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
|
|
4924
5022
|
let mailBody = typeof r.body === "string" ? r.body : null;
|
|
4925
5023
|
if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
|
|
4926
5024
|
const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
|
|
4927
5025
|
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 };
|
|
4928
5026
|
});
|
|
4929
|
-
const calendar = (
|
|
5027
|
+
const calendar = (data.meetings ?? []).map((m2) => ({
|
|
4930
5028
|
kind: "calendar",
|
|
4931
5029
|
channel: "Google Calendar",
|
|
4932
5030
|
label: m2.status === "cancelled" ? "Invitation (call later cancelled)" : "Meeting invitation",
|
|
@@ -5595,31 +5693,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5595
5693
|
|
|
5596
5694
|
// src/worker-routes-formation.ts
|
|
5597
5695
|
import { createRecord as createRecord3 } from "@odla-ai/crm";
|
|
5598
|
-
|
|
5599
|
-
// src/formation.ts
|
|
5600
|
-
function formationFields(crm, formation) {
|
|
5601
|
-
const def = crm.type(formation.type);
|
|
5602
|
-
return [...formation.required, ...formation.optional].map((id) => {
|
|
5603
|
-
const field = def.fields[id];
|
|
5604
|
-
return {
|
|
5605
|
-
id,
|
|
5606
|
-
label: field.label ?? id,
|
|
5607
|
-
type: field.type,
|
|
5608
|
-
required: formation.required.includes(id),
|
|
5609
|
-
...field.options ? { options: field.options } : {}
|
|
5610
|
-
};
|
|
5611
|
-
});
|
|
5612
|
-
}
|
|
5613
|
-
|
|
5614
|
-
// src/worker-routes-formation.ts
|
|
5615
5696
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
5616
5697
|
var handleFormation = async (req, url, env, ctx) => {
|
|
5617
5698
|
const formation = ctx.chapter.formation;
|
|
5618
5699
|
if (!formation.enabled) return null;
|
|
5619
5700
|
if (req.method === "GET" && url.pathname === "/api/formation/config") {
|
|
5701
|
+
const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
|
|
5620
5702
|
return json({
|
|
5621
5703
|
type: formation.type,
|
|
5622
|
-
|
|
5704
|
+
fieldContract,
|
|
5705
|
+
// Backward compatibility for hosts that consumed the original array.
|
|
5706
|
+
fields: fieldContract.fields
|
|
5623
5707
|
});
|
|
5624
5708
|
}
|
|
5625
5709
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|