@odla-ai/chapter 0.0.2 → 0.4.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/dist/index.cjs +532 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +616 -5
- package/dist/index.d.ts +616 -5
- package/dist/index.js +532 -4
- package/dist/index.js.map +1 -1
- package/dist/ui/index.d.ts +121 -1
- package/dist/ui/index.js +503 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/worker/index.cjs +817 -43
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +88 -4
- package/dist/worker/index.d.ts +88 -4
- package/dist/worker/index.js +816 -44
- package/dist/worker/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -20,11 +20,57 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BOOKABLE_STATUSES: () => BOOKABLE_STATUSES,
|
|
24
|
+
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
25
|
+
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
26
|
+
applicationSummary: () => applicationSummary,
|
|
27
|
+
bookingDecision: () => bookingDecision,
|
|
23
28
|
buildGroupSeed: () => buildGroupSeed,
|
|
29
|
+
canApprove: () => canApprove,
|
|
30
|
+
canBook: () => canBook,
|
|
31
|
+
canBookFrom: () => canBookFrom,
|
|
32
|
+
canChangeRole: () => canChangeRole,
|
|
33
|
+
canTransition: () => canTransition,
|
|
34
|
+
canceledPatch: () => canceledPatch,
|
|
24
35
|
chapterDb: () => chapterDb,
|
|
25
36
|
createChapterIntegration: () => createChapterIntegration,
|
|
26
37
|
defaultCrm: () => defaultCrm,
|
|
27
|
-
defineChapter: () => defineChapter
|
|
38
|
+
defineChapter: () => defineChapter,
|
|
39
|
+
endForSlot: () => endForSlot,
|
|
40
|
+
findApplicationRef: () => findApplicationRef,
|
|
41
|
+
firstPaymentPatch: () => firstPaymentPatch,
|
|
42
|
+
getVaultSecret: () => getVaultSecret,
|
|
43
|
+
introIdempotencyKey: () => introIdempotencyKey,
|
|
44
|
+
isAdminRole: () => isAdminRole,
|
|
45
|
+
isAlreadySent: () => isAlreadySent,
|
|
46
|
+
isSlotAvailable: () => isSlotAvailable,
|
|
47
|
+
joinConfig: () => joinConfig,
|
|
48
|
+
meetingCreateRow: () => meetingCreateRow,
|
|
49
|
+
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
50
|
+
memberApplication: () => memberApplication,
|
|
51
|
+
memberSession: () => memberSession,
|
|
52
|
+
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
53
|
+
paymentsReady: () => paymentsReady,
|
|
54
|
+
planDelivery: () => planDelivery,
|
|
55
|
+
projectSharedRecord: () => projectSharedRecord,
|
|
56
|
+
refundedPatch: () => refundedPatch,
|
|
57
|
+
render: () => render,
|
|
58
|
+
renderSummary: () => renderSummary,
|
|
59
|
+
renderTemplateBody: () => renderTemplateBody,
|
|
60
|
+
renewalPatch: () => renewalPatch,
|
|
61
|
+
resolveApplication: () => resolveApplication,
|
|
62
|
+
resolveAuth: () => resolveAuth,
|
|
63
|
+
resolvePipeline: () => resolvePipeline,
|
|
64
|
+
resolveScheduling: () => resolveScheduling,
|
|
65
|
+
roleFromClaim: () => roleFromClaim,
|
|
66
|
+
sharedPersonInput: () => sharedPersonInput,
|
|
67
|
+
slotWindow: () => slotWindow,
|
|
68
|
+
stageIndex: () => stageIndex,
|
|
69
|
+
stripeForm: () => stripeForm,
|
|
70
|
+
submitApplication: () => submitApplication,
|
|
71
|
+
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
72
|
+
verifyStripeSignature: () => verifyStripeSignature,
|
|
73
|
+
webhookMutationId: () => webhookMutationId
|
|
28
74
|
});
|
|
29
75
|
module.exports = __toCommonJS(index_exports);
|
|
30
76
|
|
|
@@ -49,6 +95,14 @@ var admins = {
|
|
|
49
95
|
note: attr("string", { optional: true })
|
|
50
96
|
}
|
|
51
97
|
};
|
|
98
|
+
var superAdmins = {
|
|
99
|
+
attrs: {
|
|
100
|
+
id: id(),
|
|
101
|
+
email: attr("string", { unique: true, indexed: true }),
|
|
102
|
+
note: attr("string", { optional: true }),
|
|
103
|
+
createdAt: attr("number", { indexed: true })
|
|
104
|
+
}
|
|
105
|
+
};
|
|
52
106
|
var applications = {
|
|
53
107
|
attrs: {
|
|
54
108
|
id: id(),
|
|
@@ -136,8 +190,16 @@ var emailLog = {
|
|
|
136
190
|
sentAt: attr("number", { indexed: true })
|
|
137
191
|
}
|
|
138
192
|
};
|
|
139
|
-
function chapterDb(mode) {
|
|
140
|
-
const entities =
|
|
193
|
+
function chapterDb(mode, auth) {
|
|
194
|
+
const entities = {};
|
|
195
|
+
if (mode === "chapter") {
|
|
196
|
+
entities.applications = applications;
|
|
197
|
+
entities.groups = groups;
|
|
198
|
+
entities.meetings = meetings;
|
|
199
|
+
entities.emailLog = emailLog;
|
|
200
|
+
}
|
|
201
|
+
if (auth.source === "table") entities.admins = admins;
|
|
202
|
+
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
141
203
|
const schema = { entities, links: {} };
|
|
142
204
|
const rules = {};
|
|
143
205
|
for (const ns of Object.keys(entities)) {
|
|
@@ -294,6 +356,168 @@ function buildGroupSeed(config) {
|
|
|
294
356
|
return row;
|
|
295
357
|
}
|
|
296
358
|
|
|
359
|
+
// src/auth.ts
|
|
360
|
+
function resolveAuth(mode, auth) {
|
|
361
|
+
const a = auth ?? {};
|
|
362
|
+
const source = a.source ?? (mode === "hub" ? "table" : "claim");
|
|
363
|
+
if (source !== "claim" && source !== "table") {
|
|
364
|
+
throw new Error(`defineChapter.auth.source: must be "claim" or "table" \u2014 got ${JSON.stringify(a.source)}`);
|
|
365
|
+
}
|
|
366
|
+
const claim = a.claim ?? "role";
|
|
367
|
+
if (typeof claim !== "string" || claim === "") {
|
|
368
|
+
throw new Error("defineChapter.auth.claim: must be a non-empty string");
|
|
369
|
+
}
|
|
370
|
+
const ladder = a.ladder ?? ["provisional", "member", "admin"];
|
|
371
|
+
if (!Array.isArray(ladder) || ladder.length === 0 || !ladder.every((r) => typeof r === "string" && r !== "")) {
|
|
372
|
+
throw new Error("defineChapter.auth.ladder: must be a non-empty array of role strings");
|
|
373
|
+
}
|
|
374
|
+
const adminRole = ladder[ladder.length - 1];
|
|
375
|
+
const superAdmins2 = a.superAdmins ?? source === "claim";
|
|
376
|
+
return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
|
|
377
|
+
}
|
|
378
|
+
function roleFromClaim(payload, auth) {
|
|
379
|
+
const raw = payload[auth.claim];
|
|
380
|
+
return typeof raw === "string" && auth.ladder.includes(raw) ? raw : auth.ladder[0];
|
|
381
|
+
}
|
|
382
|
+
function isAdminRole(role, auth) {
|
|
383
|
+
return role === auth.adminRole;
|
|
384
|
+
}
|
|
385
|
+
function canChangeRole(ctx) {
|
|
386
|
+
const { auth } = ctx;
|
|
387
|
+
if (!auth.ladder.includes(ctx.newRole)) {
|
|
388
|
+
return { ok: false, status: 400, error: `role must be one of: ${auth.ladder.join(", ")}` };
|
|
389
|
+
}
|
|
390
|
+
if (ctx.actorId === ctx.targetId) {
|
|
391
|
+
return { ok: false, status: 400, error: "you cannot change your own role" };
|
|
392
|
+
}
|
|
393
|
+
if (ctx.targetIsSuper && !ctx.actorIsSuper) {
|
|
394
|
+
return { ok: false, status: 403, error: "this person is a super-admin; their access is managed in odla Studio" };
|
|
395
|
+
}
|
|
396
|
+
const touchesAdmin = ctx.newRole === auth.adminRole || ctx.targetCurrentRole === auth.adminRole;
|
|
397
|
+
if (auth.superAdmins && touchesAdmin && !ctx.actorIsSuper) {
|
|
398
|
+
return { ok: false, status: 403, error: `only super-admins can create or change an ${auth.adminRole}` };
|
|
399
|
+
}
|
|
400
|
+
return { ok: true };
|
|
401
|
+
}
|
|
402
|
+
async function getVaultSecret(db, name) {
|
|
403
|
+
try {
|
|
404
|
+
const value = await db.secrets.get(name);
|
|
405
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
406
|
+
} catch {
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/pipeline.ts
|
|
412
|
+
var DEFAULT_STAGES = [
|
|
413
|
+
"submitted",
|
|
414
|
+
"paid_pending_vetting",
|
|
415
|
+
"call_scheduled",
|
|
416
|
+
"interviewed",
|
|
417
|
+
"approved",
|
|
418
|
+
"declined",
|
|
419
|
+
"refunded"
|
|
420
|
+
];
|
|
421
|
+
var DEFAULT_BOOKABLE = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
422
|
+
var DEFAULT_APPROVABLE = ["paid_pending_vetting", "call_scheduled", "interviewed"];
|
|
423
|
+
function resolvePipeline(p) {
|
|
424
|
+
const usingDefaults = !p?.stages;
|
|
425
|
+
const stages = p?.stages ?? [...DEFAULT_STAGES];
|
|
426
|
+
if (!Array.isArray(stages) || stages.length === 0 || !stages.every((s) => typeof s === "string" && s !== "")) {
|
|
427
|
+
throw new Error("defineChapter.pipeline.stages: must be a non-empty array of status strings");
|
|
428
|
+
}
|
|
429
|
+
if (new Set(stages).size !== stages.length) {
|
|
430
|
+
throw new Error("defineChapter.pipeline.stages: statuses must be unique");
|
|
431
|
+
}
|
|
432
|
+
const initial = p?.initial ?? stages[0];
|
|
433
|
+
if (!stages.includes(initial)) {
|
|
434
|
+
throw new Error(`defineChapter.pipeline.initial: "${initial}" is not one of the stages`);
|
|
435
|
+
}
|
|
436
|
+
const bookableFrom = p?.bookableFrom ?? (usingDefaults ? [...DEFAULT_BOOKABLE] : []);
|
|
437
|
+
const approvableFrom = p?.approvableFrom ?? (usingDefaults ? [...DEFAULT_APPROVABLE] : []);
|
|
438
|
+
for (const [name, subset] of [
|
|
439
|
+
["bookableFrom", bookableFrom],
|
|
440
|
+
["approvableFrom", approvableFrom]
|
|
441
|
+
]) {
|
|
442
|
+
for (const s of subset) {
|
|
443
|
+
if (!stages.includes(s)) throw new Error(`defineChapter.pipeline.${name}: "${s}" is not one of the stages`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return { stages, bookableFrom, approvableFrom, initial };
|
|
447
|
+
}
|
|
448
|
+
function stageIndex(status, p) {
|
|
449
|
+
return p.stages.indexOf(status);
|
|
450
|
+
}
|
|
451
|
+
function canTransition(from, to, p) {
|
|
452
|
+
const fi = p.stages.indexOf(from);
|
|
453
|
+
const ti = p.stages.indexOf(to);
|
|
454
|
+
return fi >= 0 && ti >= 0 && ti >= fi;
|
|
455
|
+
}
|
|
456
|
+
function canBook(status, p) {
|
|
457
|
+
return p.bookableFrom.includes(status);
|
|
458
|
+
}
|
|
459
|
+
function canApprove(status, p) {
|
|
460
|
+
return p.approvableFrom.includes(status);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/member.ts
|
|
464
|
+
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
465
|
+
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
466
|
+
function resolveApplication(a) {
|
|
467
|
+
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
468
|
+
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
469
|
+
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
470
|
+
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
471
|
+
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
required,
|
|
476
|
+
optional,
|
|
477
|
+
maxLen: a?.maxLen ?? {},
|
|
478
|
+
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
479
|
+
bodyCap: a?.bodyCap ?? 32768
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
async function submitApplication(db, chapter, fields, opts) {
|
|
483
|
+
const app = chapter.application;
|
|
484
|
+
for (const f of app.required) {
|
|
485
|
+
const v = fields[f];
|
|
486
|
+
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
487
|
+
}
|
|
488
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
489
|
+
const v = fields[f];
|
|
490
|
+
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
491
|
+
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
492
|
+
}
|
|
493
|
+
const id2 = opts.newId();
|
|
494
|
+
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
495
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
496
|
+
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
497
|
+
}
|
|
498
|
+
if (fields.focus !== void 0) row.focus = fields.focus;
|
|
499
|
+
if (opts.groupId) row.groupId = opts.groupId;
|
|
500
|
+
const { duplicate } = await db.transact(
|
|
501
|
+
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
502
|
+
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
503
|
+
);
|
|
504
|
+
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial };
|
|
505
|
+
}
|
|
506
|
+
function joinConfig(group, paymentsReady2) {
|
|
507
|
+
return {
|
|
508
|
+
id: group.id,
|
|
509
|
+
name: group.name,
|
|
510
|
+
standardPriceCents: group.standardPriceCents ?? 0,
|
|
511
|
+
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
512
|
+
disclaimerText: group.disclaimerText ?? "",
|
|
513
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
514
|
+
trustCopy: group.trustCopy ?? "",
|
|
515
|
+
commitmentText: group.commitmentText ?? "",
|
|
516
|
+
normsText: group.normsText ?? "",
|
|
517
|
+
paymentsReady: paymentsReady2
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
297
521
|
// src/config.ts
|
|
298
522
|
var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
299
523
|
function isResolvedCrm(x) {
|
|
@@ -317,7 +541,10 @@ function defineChapter(config) {
|
|
|
317
541
|
throw new Error("defineChapter.prices.standardCents: required in chapter mode");
|
|
318
542
|
}
|
|
319
543
|
}
|
|
320
|
-
const
|
|
544
|
+
const auth = resolveAuth(mode, config.auth);
|
|
545
|
+
const pipeline = resolvePipeline(config.pipeline);
|
|
546
|
+
const application = resolveApplication(config.application);
|
|
547
|
+
const { schema, rules } = chapterDb(mode, auth);
|
|
321
548
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
322
549
|
const chapter = {
|
|
323
550
|
config,
|
|
@@ -325,6 +552,9 @@ function defineChapter(config) {
|
|
|
325
552
|
name,
|
|
326
553
|
mode,
|
|
327
554
|
crm,
|
|
555
|
+
auth,
|
|
556
|
+
pipeline,
|
|
557
|
+
application,
|
|
328
558
|
schema,
|
|
329
559
|
rules,
|
|
330
560
|
services,
|
|
@@ -365,4 +595,302 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
365
595
|
probes: [...crmDesc.probes ?? []]
|
|
366
596
|
};
|
|
367
597
|
}
|
|
598
|
+
|
|
599
|
+
// src/email.ts
|
|
600
|
+
function render(template, vars) {
|
|
601
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
602
|
+
}
|
|
603
|
+
function groupVars(group, vars) {
|
|
604
|
+
return {
|
|
605
|
+
...vars,
|
|
606
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
607
|
+
commitmentText: group.commitmentText ?? "",
|
|
608
|
+
normsText: group.normsText ?? ""
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function renderTemplateBody(group, template, vars) {
|
|
612
|
+
const tpl = group.emailTemplates?.[template];
|
|
613
|
+
if (!tpl) return null;
|
|
614
|
+
return render(tpl.text, groupVars(group, vars));
|
|
615
|
+
}
|
|
616
|
+
function isAlreadySent(priorRows) {
|
|
617
|
+
return priorRows.some((row) => !row.error);
|
|
618
|
+
}
|
|
619
|
+
function planDelivery(input) {
|
|
620
|
+
const tpl = input.group.emailTemplates?.[input.template];
|
|
621
|
+
if (!tpl) return { deliver: false, reason: "template-missing" };
|
|
622
|
+
if (tpl.enabled === false && !input.force) return { deliver: false, reason: "disabled" };
|
|
623
|
+
const vars = groupVars(input.group, input.vars);
|
|
624
|
+
const isProd = input.envName === "prod";
|
|
625
|
+
const redirect = !isProd && !!input.group.debugEmail;
|
|
626
|
+
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
627
|
+
const to = redirect ? input.group.debugEmail : input.to;
|
|
628
|
+
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
629
|
+
const text = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
630
|
+
|
|
631
|
+
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
632
|
+
return { deliver: true, transport, to, subject, text, redirected: redirect };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/payments.ts
|
|
636
|
+
function parseSigHeader(header) {
|
|
637
|
+
const parts = {};
|
|
638
|
+
for (const p of header.split(",")) {
|
|
639
|
+
const [k, v] = p.split("=", 2);
|
|
640
|
+
if (k && v !== void 0) parts[k] = v;
|
|
641
|
+
}
|
|
642
|
+
return { t: parts.t, v1: parts.v1 };
|
|
643
|
+
}
|
|
644
|
+
function toHex(buf) {
|
|
645
|
+
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
646
|
+
}
|
|
647
|
+
function timingSafeEqual(a, b) {
|
|
648
|
+
if (a.length !== b.length) return false;
|
|
649
|
+
let diff = 0;
|
|
650
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
651
|
+
return diff === 0;
|
|
652
|
+
}
|
|
653
|
+
async function verifyStripeSignature(payload, header, secret, opts = {}) {
|
|
654
|
+
const { t, v1 } = parseSigHeader(header);
|
|
655
|
+
if (!t || !v1) return false;
|
|
656
|
+
const ts = Number(t);
|
|
657
|
+
if (!Number.isFinite(ts)) return false;
|
|
658
|
+
const nowSec = (opts.now ?? Date.now()) / 1e3;
|
|
659
|
+
const tolerance = opts.toleranceSec ?? 300;
|
|
660
|
+
if (Math.abs(nowSec - ts) > tolerance) return false;
|
|
661
|
+
const enc = new TextEncoder();
|
|
662
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
663
|
+
const mac = await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${payload}`));
|
|
664
|
+
return timingSafeEqual(toHex(mac), v1);
|
|
665
|
+
}
|
|
666
|
+
function paymentsReady(group, hasSecretKey) {
|
|
667
|
+
return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
|
|
668
|
+
}
|
|
669
|
+
function stripeForm(params) {
|
|
670
|
+
const out = new URLSearchParams();
|
|
671
|
+
for (const [k, v] of Object.entries(params)) {
|
|
672
|
+
if (v === void 0 || v === null) continue;
|
|
673
|
+
if (typeof v === "object") {
|
|
674
|
+
for (const [k2, v2] of Object.entries(v)) {
|
|
675
|
+
if (v2 !== void 0 && v2 !== null) out.append(`${k}[${k2}]`, String(v2));
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
out.append(k, String(v));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return out.toString();
|
|
682
|
+
}
|
|
683
|
+
function subscriptionIdempotencyKey(applicationId) {
|
|
684
|
+
return `sub:${applicationId}`;
|
|
685
|
+
}
|
|
686
|
+
function webhookMutationId(eventId) {
|
|
687
|
+
return `stripe:${eventId}`;
|
|
688
|
+
}
|
|
689
|
+
function findApplicationRef(obj) {
|
|
690
|
+
const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
|
|
691
|
+
const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
|
|
692
|
+
const applicationId = pick(metaOf(obj)) ?? pick(metaOf(obj.subscription_details)) ?? pick(metaOf(obj.parent?.subscription_details));
|
|
693
|
+
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
694
|
+
return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
|
|
695
|
+
}
|
|
696
|
+
function normalizeWebhookEvent(event) {
|
|
697
|
+
const obj = event.data?.object ?? {};
|
|
698
|
+
const ref = findApplicationRef(obj);
|
|
699
|
+
switch (event.type) {
|
|
700
|
+
case "invoice.paid": {
|
|
701
|
+
const lines = obj.lines?.data ?? [];
|
|
702
|
+
const periodEnd = lines[0]?.period?.end;
|
|
703
|
+
const renewalAt = typeof periodEnd === "number" ? periodEnd * 1e3 : void 0;
|
|
704
|
+
const kind = obj.billing_reason === "subscription_create" ? "first_payment" : "renewal";
|
|
705
|
+
return { kind, ...ref, ...renewalAt !== void 0 ? { renewalAt } : {} };
|
|
706
|
+
}
|
|
707
|
+
case "charge.refunded":
|
|
708
|
+
return { kind: "refunded", ...ref };
|
|
709
|
+
case "customer.subscription.deleted":
|
|
710
|
+
return { kind: "canceled", ...ref };
|
|
711
|
+
default:
|
|
712
|
+
return { kind: "ignored", type: event.type };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function firstPaymentPatch(currentStatus, renewalAt) {
|
|
716
|
+
return {
|
|
717
|
+
...currentStatus === "submitted" ? { status: "paid_pending_vetting" } : {},
|
|
718
|
+
...renewalAt !== void 0 ? { renewalAt } : {}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function renewalPatch(renewalAt) {
|
|
722
|
+
return { renewalAt };
|
|
723
|
+
}
|
|
724
|
+
function refundedPatch() {
|
|
725
|
+
return { status: "refunded" };
|
|
726
|
+
}
|
|
727
|
+
function canceledPatch() {
|
|
728
|
+
return { canceled: true };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/network.ts
|
|
732
|
+
var import_crm3 = require("@odla-ai/crm");
|
|
733
|
+
function sharedPersonInput(person) {
|
|
734
|
+
const email = person.email.toLowerCase();
|
|
735
|
+
const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
|
|
736
|
+
const input = { name: person.name ?? fullName ?? email, email };
|
|
737
|
+
if (input.name === "") input.name = email;
|
|
738
|
+
if (person.firstName) input.firstName = person.firstName;
|
|
739
|
+
if (person.lastName) input.lastName = person.lastName;
|
|
740
|
+
if (person.phone) input.phone = person.phone;
|
|
741
|
+
if (person.linkedin) input.linkedin = person.linkedin;
|
|
742
|
+
return input;
|
|
743
|
+
}
|
|
744
|
+
async function projectSharedRecord(deps, person) {
|
|
745
|
+
const email = person.email.toLowerCase();
|
|
746
|
+
const input = sharedPersonInput(person);
|
|
747
|
+
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
748
|
+
const { crm_record } = await deps.db.query({
|
|
749
|
+
crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } }
|
|
750
|
+
});
|
|
751
|
+
const existing = crm_record?.[0];
|
|
752
|
+
if (existing && typeof existing.id === "string") {
|
|
753
|
+
await (0, import_crm3.updateRecord)(crmDeps, { id: existing.id, input });
|
|
754
|
+
return { recordId: existing.id };
|
|
755
|
+
}
|
|
756
|
+
const created = await (0, import_crm3.createRecord)(crmDeps, { type: "person", input, mutationId: `share:${person.hubRecordId}` });
|
|
757
|
+
return { recordId: created.id };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/session.ts
|
|
761
|
+
function applicationSummary(app) {
|
|
762
|
+
return {
|
|
763
|
+
id: app.id,
|
|
764
|
+
firstName: app.firstName ?? null,
|
|
765
|
+
lastName: app.lastName ?? null,
|
|
766
|
+
email: app.email ?? null,
|
|
767
|
+
status: app.status,
|
|
768
|
+
createdAt: app.createdAt ?? null,
|
|
769
|
+
meetingLink: app.meetingLink ?? null,
|
|
770
|
+
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
771
|
+
renewalAt: app.renewalAt ?? null,
|
|
772
|
+
canceled: app.canceled === true
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function memberApplication(app, meeting, defaultTimezone) {
|
|
776
|
+
const summary = applicationSummary(app);
|
|
777
|
+
let meetingAt = app.meetingAt ?? null;
|
|
778
|
+
let meetUrl = null;
|
|
779
|
+
let timezone = defaultTimezone;
|
|
780
|
+
if (meeting) {
|
|
781
|
+
timezone = meeting.timezone ?? timezone;
|
|
782
|
+
if (meeting.status === "scheduled") {
|
|
783
|
+
meetingAt = meeting.startAt ?? null;
|
|
784
|
+
meetUrl = meeting.meetUrl ?? null;
|
|
785
|
+
} else {
|
|
786
|
+
meetingAt = null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return { ...summary, meetingAt, meetUrl, timezone };
|
|
790
|
+
}
|
|
791
|
+
function memberSession(user, opts) {
|
|
792
|
+
return {
|
|
793
|
+
userId: user.userId,
|
|
794
|
+
email: user.email ?? null,
|
|
795
|
+
role: user.role,
|
|
796
|
+
superAdmin: opts.superAdmin,
|
|
797
|
+
application: opts.application
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/scheduling.ts
|
|
802
|
+
var SCHEDULING_DEFAULTS = {
|
|
803
|
+
slotMinutes: 45,
|
|
804
|
+
days: [1, 2, 3, 4, 5],
|
|
805
|
+
startHour: 9,
|
|
806
|
+
endHour: 17,
|
|
807
|
+
timezone: "America/Los_Angeles",
|
|
808
|
+
minNoticeHours: 24,
|
|
809
|
+
windowDays: 14,
|
|
810
|
+
summaryTemplate: "Introduction call with {{firstName}} {{lastName}}"
|
|
811
|
+
};
|
|
812
|
+
function isValidTimeZone(tz) {
|
|
813
|
+
try {
|
|
814
|
+
new Intl.DateTimeFormat(void 0, { timeZone: tz });
|
|
815
|
+
return true;
|
|
816
|
+
} catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function resolveScheduling(config) {
|
|
821
|
+
const d = config ?? {};
|
|
822
|
+
const c = {
|
|
823
|
+
slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
|
|
824
|
+
days: d.days ?? SCHEDULING_DEFAULTS.days,
|
|
825
|
+
startHour: d.startHour ?? SCHEDULING_DEFAULTS.startHour,
|
|
826
|
+
endHour: d.endHour ?? SCHEDULING_DEFAULTS.endHour,
|
|
827
|
+
timezone: d.timezone ?? SCHEDULING_DEFAULTS.timezone,
|
|
828
|
+
minNoticeHours: d.minNoticeHours ?? SCHEDULING_DEFAULTS.minNoticeHours,
|
|
829
|
+
windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
|
|
830
|
+
summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
|
|
831
|
+
};
|
|
832
|
+
const fail = (msg) => {
|
|
833
|
+
throw new Error(`scheduling: ${msg}`);
|
|
834
|
+
};
|
|
835
|
+
if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail("slotMinutes must be 15\u2013240");
|
|
836
|
+
if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail("windowDays must be 1\u201362 (FreeBusy caps at 62)");
|
|
837
|
+
if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail("minNoticeHours must be 0\u2013336");
|
|
838
|
+
if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail("require 0 \u2264 startHour < endHour \u2264 24");
|
|
839
|
+
const days = [...c.days];
|
|
840
|
+
if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
|
|
841
|
+
fail("days must be a non-empty list of weekday integers 0\u20136");
|
|
842
|
+
}
|
|
843
|
+
if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone "${c.timezone}"`);
|
|
844
|
+
if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
|
|
845
|
+
return { ...c, days };
|
|
846
|
+
}
|
|
847
|
+
var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
|
|
848
|
+
function canBookFrom(status) {
|
|
849
|
+
return BOOKABLE_STATUSES.includes(status);
|
|
850
|
+
}
|
|
851
|
+
function slotWindow(now, windowDays) {
|
|
852
|
+
return { from: now, to: now + windowDays * 864e5 };
|
|
853
|
+
}
|
|
854
|
+
function endForSlot(startAt, slotMinutes) {
|
|
855
|
+
return startAt + slotMinutes * 6e4;
|
|
856
|
+
}
|
|
857
|
+
function isSlotAvailable(slots, startAt) {
|
|
858
|
+
return slots.some((s) => s.startAt === startAt);
|
|
859
|
+
}
|
|
860
|
+
function renderSummary(template, app) {
|
|
861
|
+
return template.replace("{{firstName}}", app.firstName ?? "").replace("{{lastName}}", app.lastName ?? "");
|
|
862
|
+
}
|
|
863
|
+
function bookingDecision(existing) {
|
|
864
|
+
const eventId = existing?.googleEventId ?? null;
|
|
865
|
+
return { reschedule: Boolean(eventId), eventId };
|
|
866
|
+
}
|
|
867
|
+
function introIdempotencyKey(applicationId) {
|
|
868
|
+
return `application:${applicationId}:intro`;
|
|
869
|
+
}
|
|
870
|
+
function meetingCreateRow(i) {
|
|
871
|
+
return {
|
|
872
|
+
id: i.meetingId,
|
|
873
|
+
applicationId: i.applicationId,
|
|
874
|
+
groupId: i.groupId,
|
|
875
|
+
startAt: i.startAt,
|
|
876
|
+
endAt: i.endAt,
|
|
877
|
+
timezone: i.timezone,
|
|
878
|
+
status: "scheduled",
|
|
879
|
+
googleEventId: i.googleEventId,
|
|
880
|
+
...i.meetUrl ? { meetUrl: i.meetUrl } : {},
|
|
881
|
+
...i.htmlLink ? { htmlLink: i.htmlLink } : {},
|
|
882
|
+
drift: "none",
|
|
883
|
+
createdAt: i.createdAt
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function meetingRescheduleUpdate(startAt, endAt) {
|
|
887
|
+
return { startAt, endAt, drift: "none" };
|
|
888
|
+
}
|
|
889
|
+
function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
890
|
+
return {
|
|
891
|
+
meetingAt: startAt,
|
|
892
|
+
...htmlLink ? { meetingLink: htmlLink } : {},
|
|
893
|
+
...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
368
896
|
//# sourceMappingURL=index.cjs.map
|