@odla-ai/chapter 0.30.0 → 0.31.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/chunk-IVXVECKR.js +15 -0
- package/dist/chunk-IVXVECKR.js.map +1 -0
- package/dist/{chunk-IOHVFBXH.js → chunk-QWMP6TSM.js} +75 -34
- package/dist/chunk-QWMP6TSM.js.map +1 -0
- package/dist/{copy-context-efEMF_aD.d.ts → copy-context-HAhNTZdl.d.ts} +54 -19
- package/dist/index.cjs +168 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +160 -21
- package/dist/index.d.ts +160 -21
- package/dist/index.js +150 -2
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/conditions/index.d.ts +29 -0
- package/dist/ui/conditions/index.js +32 -0
- package/dist/ui/conditions/index.js.map +1 -0
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +10 -3
- package/dist/ui/member/index.d.ts +62 -3
- package/dist/ui/member/index.js +10 -3
- package/dist/worker/index.cjs +146 -39
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +54 -19
- package/dist/worker/index.d.ts +54 -19
- package/dist/worker/index.js +115 -8
- package/dist/worker/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/chunk-IOHVFBXH.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -64,6 +64,8 @@ var applications = {
|
|
|
64
64
|
phone: attr("string", { optional: true }),
|
|
65
65
|
state: attr("string", { optional: true }),
|
|
66
66
|
groupId: attr("string", { indexed: true, optional: true }),
|
|
67
|
+
// The tier this application joined on. Absent on rows that predate tiers.
|
|
68
|
+
tierId: attr("string", { indexed: true, optional: true }),
|
|
67
69
|
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
68
70
|
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
69
71
|
renewalAt: attr("number", { optional: true }),
|
|
@@ -95,6 +97,19 @@ var groups = {
|
|
|
95
97
|
createdAt: attr("number", { indexed: true })
|
|
96
98
|
}
|
|
97
99
|
};
|
|
100
|
+
var tiers = {
|
|
101
|
+
attrs: {
|
|
102
|
+
id: id(),
|
|
103
|
+
groupId: attr("string", { indexed: true }),
|
|
104
|
+
name: attr("string"),
|
|
105
|
+
priceCents: attr("number"),
|
|
106
|
+
stripePriceId: attr("string", { optional: true }),
|
|
107
|
+
blurb: attr("string", { optional: true }),
|
|
108
|
+
sortOrder: attr("number", { indexed: true }),
|
|
109
|
+
active: attr("boolean", { indexed: true }),
|
|
110
|
+
createdAt: attr("number", { indexed: true })
|
|
111
|
+
}
|
|
112
|
+
};
|
|
98
113
|
var meetings = {
|
|
99
114
|
attrs: {
|
|
100
115
|
id: id(),
|
|
@@ -136,6 +151,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false) {
|
|
|
136
151
|
if (mode === "chapter") {
|
|
137
152
|
entities.applications = applications;
|
|
138
153
|
entities.groups = groups;
|
|
154
|
+
entities.tiers = tiers;
|
|
139
155
|
entities.meetings = meetings;
|
|
140
156
|
entities.emailLog = emailLog;
|
|
141
157
|
}
|
|
@@ -396,6 +412,9 @@ function canApprove(status, p) {
|
|
|
396
412
|
return p.approvableFrom.includes(status);
|
|
397
413
|
}
|
|
398
414
|
|
|
415
|
+
// src/member.ts
|
|
416
|
+
import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
|
|
417
|
+
|
|
399
418
|
// src/application-id.ts
|
|
400
419
|
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
401
420
|
async function applicationIdForSubmission(submissionId) {
|
|
@@ -424,9 +443,21 @@ function resolveApplication(a) {
|
|
|
424
443
|
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
425
444
|
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
426
445
|
}
|
|
446
|
+
const conditions = a?.conditions ?? {};
|
|
447
|
+
for (const [field, declared] of Object.entries(conditions)) {
|
|
448
|
+
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
449
|
+
const expression = declared?.[key];
|
|
450
|
+
if (expression === void 0) continue;
|
|
451
|
+
if (typeof expression !== "string" || !expression.trim()) {
|
|
452
|
+
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
453
|
+
}
|
|
454
|
+
assertFieldCondition(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
427
457
|
return {
|
|
428
458
|
required,
|
|
429
459
|
optional,
|
|
460
|
+
conditions,
|
|
430
461
|
maxLen: a?.maxLen ?? {},
|
|
431
462
|
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
432
463
|
bodyCap: a?.bodyCap ?? 32768,
|
|
@@ -463,7 +494,9 @@ function applicantProfile(chapter, fields) {
|
|
|
463
494
|
}
|
|
464
495
|
async function submitApplication(db, chapter, fields, opts) {
|
|
465
496
|
const app = chapter.application;
|
|
466
|
-
|
|
497
|
+
const fieldStates = resolveFieldStates(app.conditions ?? {}, fields, app.required);
|
|
498
|
+
for (const [f, state] of Object.entries(fieldStates)) {
|
|
499
|
+
if (!state.required) continue;
|
|
467
500
|
const v = fields[f];
|
|
468
501
|
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
469
502
|
}
|
|
@@ -482,10 +515,17 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
482
515
|
const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
483
516
|
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
484
517
|
for (const f of [...app.required, ...app.optional]) {
|
|
518
|
+
if (fieldStates[f]?.visible === false) continue;
|
|
485
519
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
486
520
|
}
|
|
487
521
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
488
522
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
523
|
+
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
524
|
+
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
525
|
+
return { ok: false, error: "tierId is not an offered tier" };
|
|
526
|
+
}
|
|
527
|
+
row.tierId = fields.tierId;
|
|
528
|
+
}
|
|
489
529
|
if (acked) row.disclaimerAckAt = opts.now;
|
|
490
530
|
const { duplicate } = await db.transact(
|
|
491
531
|
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
@@ -493,12 +533,15 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
493
533
|
);
|
|
494
534
|
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
495
535
|
}
|
|
496
|
-
function joinConfig(group, paymentsReady2) {
|
|
536
|
+
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
497
537
|
return {
|
|
498
538
|
id: group.id,
|
|
499
539
|
name: group.name,
|
|
500
540
|
standardPriceCents: group.standardPriceCents ?? 0,
|
|
501
541
|
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
542
|
+
tiers: [...tiers2],
|
|
543
|
+
// The browser evaluates the same conditions the server enforces.
|
|
544
|
+
conditions,
|
|
502
545
|
disclaimerText: group.disclaimerText ?? "",
|
|
503
546
|
refundPolicyText: group.refundPolicyText ?? "",
|
|
504
547
|
trustCopy: group.trustCopy ?? "",
|
|
@@ -1293,6 +1336,62 @@ function leaderCrmConfig(options = {}) {
|
|
|
1293
1336
|
|
|
1294
1337
|
// src/descriptor.ts
|
|
1295
1338
|
import { createCrmIntegration } from "@odla-ai/crm";
|
|
1339
|
+
|
|
1340
|
+
// src/tiers.ts
|
|
1341
|
+
function buildTierSeeds(groupId, configured = [], now = 0) {
|
|
1342
|
+
return configured.map((tier, index) => ({
|
|
1343
|
+
id: tier.id,
|
|
1344
|
+
groupId,
|
|
1345
|
+
name: tier.name,
|
|
1346
|
+
priceCents: tier.priceCents,
|
|
1347
|
+
...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
|
|
1348
|
+
...tier.blurb ? { blurb: tier.blurb } : {},
|
|
1349
|
+
sortOrder: tier.sortOrder ?? index,
|
|
1350
|
+
active: tier.active !== false,
|
|
1351
|
+
createdAt: now
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
function tierIsFree(tier) {
|
|
1355
|
+
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
1356
|
+
}
|
|
1357
|
+
function tierPayable(tier, group, hasSecretKey) {
|
|
1358
|
+
if (tierIsFree(tier)) return true;
|
|
1359
|
+
return Boolean(group.stripePublishableKey && tier.stripePriceId && hasSecretKey);
|
|
1360
|
+
}
|
|
1361
|
+
function byOrder(a, b) {
|
|
1362
|
+
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1363
|
+
}
|
|
1364
|
+
function resolveTiers(group, rows = []) {
|
|
1365
|
+
const stored = rows.filter((tier) => tier.active);
|
|
1366
|
+
if (stored.length) return [...stored].sort(byOrder);
|
|
1367
|
+
return [{
|
|
1368
|
+
id: "standard",
|
|
1369
|
+
groupId: String(group.id ?? ""),
|
|
1370
|
+
name: "Membership",
|
|
1371
|
+
priceCents: group.standardPriceCents ?? 0,
|
|
1372
|
+
...group.stripePriceId ? { stripePriceId: group.stripePriceId } : {},
|
|
1373
|
+
sortOrder: 0,
|
|
1374
|
+
active: true
|
|
1375
|
+
}];
|
|
1376
|
+
}
|
|
1377
|
+
function offerableTiers(tiers2, group, hasSecretKey) {
|
|
1378
|
+
return tiers2.filter((tier) => tier.active && tierPayable(tier, group, hasSecretKey));
|
|
1379
|
+
}
|
|
1380
|
+
function findTier(tiers2, tierId) {
|
|
1381
|
+
if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
|
|
1382
|
+
return tiers2.find((tier) => tier.id === tierId) ?? null;
|
|
1383
|
+
}
|
|
1384
|
+
function joinConfigTiers(tiers2) {
|
|
1385
|
+
return tiers2.map((tier) => ({
|
|
1386
|
+
id: tier.id,
|
|
1387
|
+
name: tier.name,
|
|
1388
|
+
priceCents: tier.priceCents,
|
|
1389
|
+
blurb: tier.blurb ?? "",
|
|
1390
|
+
free: tierIsFree(tier)
|
|
1391
|
+
}));
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/descriptor.ts
|
|
1296
1395
|
function createChapterIntegration(chapter, options = {}) {
|
|
1297
1396
|
const basePath = options.basePath ?? "/api/crm";
|
|
1298
1397
|
const now = options.now ?? Date.now();
|
|
@@ -1308,6 +1407,14 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1308
1407
|
const group = chapter.groupSeed();
|
|
1309
1408
|
if (group) {
|
|
1310
1409
|
seeds.push({ id: "group", ns: "groups", key: { attr: "id", value: chapter.id }, attrs: { ...group, createdAt: now } });
|
|
1410
|
+
for (const tier of buildTierSeeds(chapter.id, chapter.config.tiers, now)) {
|
|
1411
|
+
seeds.push({
|
|
1412
|
+
id: `tier:${String(tier.id)}`,
|
|
1413
|
+
ns: "tiers",
|
|
1414
|
+
key: { attr: "id", value: String(tier.id) },
|
|
1415
|
+
attrs: tier
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1311
1418
|
}
|
|
1312
1419
|
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
1313
1420
|
const runbooks = isLeader ? {
|
|
@@ -2384,6 +2491,38 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
2384
2491
|
...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
|
|
2385
2492
|
};
|
|
2386
2493
|
}
|
|
2494
|
+
|
|
2495
|
+
// src/tiers-store.ts
|
|
2496
|
+
function rowToTier(row, groupId) {
|
|
2497
|
+
return {
|
|
2498
|
+
id: String(row.id ?? ""),
|
|
2499
|
+
groupId: String(row.groupId ?? groupId),
|
|
2500
|
+
name: String(row.name ?? ""),
|
|
2501
|
+
priceCents: typeof row.priceCents === "number" ? row.priceCents : 0,
|
|
2502
|
+
...typeof row.stripePriceId === "string" && row.stripePriceId ? { stripePriceId: row.stripePriceId } : {},
|
|
2503
|
+
...typeof row.blurb === "string" ? { blurb: row.blurb } : {},
|
|
2504
|
+
sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
|
|
2505
|
+
// A row that never set `active` is offered; only an explicit false retires
|
|
2506
|
+
// a tier, so a half-seeded row cannot silently hide a membership.
|
|
2507
|
+
active: row.active !== false
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
async function loadTiers(db, group) {
|
|
2511
|
+
const groupId = String(group.id ?? "");
|
|
2512
|
+
let rows;
|
|
2513
|
+
try {
|
|
2514
|
+
rows = (await db.query({
|
|
2515
|
+
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2516
|
+
})).tiers;
|
|
2517
|
+
} catch {
|
|
2518
|
+
rows = void 0;
|
|
2519
|
+
}
|
|
2520
|
+
const list = Array.isArray(rows) ? rows.map((row) => rowToTier(row, groupId)) : [];
|
|
2521
|
+
return resolveTiers(group, list);
|
|
2522
|
+
}
|
|
2523
|
+
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
2524
|
+
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
2525
|
+
}
|
|
2387
2526
|
export {
|
|
2388
2527
|
DEFAULT_CHAPTER_COPY,
|
|
2389
2528
|
DEFAULT_SHARE_FIELDS,
|
|
@@ -2396,6 +2535,7 @@ export {
|
|
|
2396
2535
|
brandTokens,
|
|
2397
2536
|
bucketSeries,
|
|
2398
2537
|
buildGroupSeed,
|
|
2538
|
+
buildTierSeeds,
|
|
2399
2539
|
canApprove,
|
|
2400
2540
|
canBook,
|
|
2401
2541
|
canChangeRole,
|
|
@@ -2420,6 +2560,7 @@ export {
|
|
|
2420
2560
|
emailGroupFrom,
|
|
2421
2561
|
endForSlot,
|
|
2422
2562
|
findApplicationRef,
|
|
2563
|
+
findTier,
|
|
2423
2564
|
firstPaymentPatch,
|
|
2424
2565
|
formatChapterCopy,
|
|
2425
2566
|
formationFields,
|
|
@@ -2432,7 +2573,10 @@ export {
|
|
|
2432
2573
|
isSlotAvailable,
|
|
2433
2574
|
isValidEmail,
|
|
2434
2575
|
joinConfig,
|
|
2576
|
+
joinConfigTiers,
|
|
2435
2577
|
leaderCrmConfig,
|
|
2578
|
+
loadOfferableTiers,
|
|
2579
|
+
loadTiers,
|
|
2436
2580
|
meetingCreateRow,
|
|
2437
2581
|
meetingRescheduleUpdate,
|
|
2438
2582
|
memberApplication,
|
|
@@ -2440,6 +2584,7 @@ export {
|
|
|
2440
2584
|
networkSourceTag,
|
|
2441
2585
|
normalizeSharedRecord,
|
|
2442
2586
|
normalizeWebhookEvent,
|
|
2587
|
+
offerableTiers,
|
|
2443
2588
|
paymentsReady,
|
|
2444
2589
|
personInputFromApp,
|
|
2445
2590
|
planDelivery,
|
|
@@ -2457,6 +2602,7 @@ export {
|
|
|
2457
2602
|
resolveLeaderFormation,
|
|
2458
2603
|
resolvePipeline,
|
|
2459
2604
|
resolveScheduling,
|
|
2605
|
+
resolveTiers,
|
|
2460
2606
|
roleFromClaim,
|
|
2461
2607
|
sendTemplated,
|
|
2462
2608
|
sharedPersonInput,
|
|
@@ -2469,6 +2615,8 @@ export {
|
|
|
2469
2615
|
submitApplication,
|
|
2470
2616
|
subscriptionIdempotencyKey,
|
|
2471
2617
|
syncApplicationToCrm,
|
|
2618
|
+
tierIsFree,
|
|
2619
|
+
tierPayable,
|
|
2472
2620
|
updateClerkUserMetadata,
|
|
2473
2621
|
updateClerkUserMetadataByEmail,
|
|
2474
2622
|
validateScheduling,
|