@odla-ai/chapter 0.29.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 +176 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -25
- package/dist/index.d.ts +174 -25
- package/dist/index.js +158 -6
- 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 ? {
|
|
@@ -2051,18 +2158,22 @@ var clerkIntegration = {
|
|
|
2051
2158
|
],
|
|
2052
2159
|
secrets: [
|
|
2053
2160
|
{
|
|
2054
|
-
|
|
2161
|
+
// odla-db reads this exact name (CLERK_WEBHOOK_SECRET_NAME in the
|
|
2162
|
+
// platform's clerk-webhook.ts); it is also what `odla-ai secrets set`
|
|
2163
|
+
// writes. It read "CLERK_WEBHOOK_SECRET" until 0.30.0, which sent
|
|
2164
|
+
// adopters to store the value under a name nothing looks up.
|
|
2165
|
+
name: "clerk_webhook_secret",
|
|
2055
2166
|
description: "Svix signing secret for Clerk user webhooks \u2014 verifies $users sync events.",
|
|
2056
2167
|
pattern: "whsec_",
|
|
2057
2168
|
mode: "full",
|
|
2058
|
-
|
|
2169
|
+
required: false
|
|
2059
2170
|
},
|
|
2060
2171
|
{
|
|
2061
|
-
|
|
2172
|
+
name: "clerk_secret_key",
|
|
2062
2173
|
description: "Clerk backend key (sk_*) \u2014 powers account creation, the public role claim, and backend-only private profile signals; it also lets odla-db resolve user email/name via the Clerk API.",
|
|
2063
2174
|
pattern: "sk_",
|
|
2064
2175
|
mode: "full",
|
|
2065
|
-
|
|
2176
|
+
required: false
|
|
2066
2177
|
}
|
|
2067
2178
|
],
|
|
2068
2179
|
syncs: [
|
|
@@ -2380,6 +2491,38 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
2380
2491
|
...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
|
|
2381
2492
|
};
|
|
2382
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
|
+
}
|
|
2383
2526
|
export {
|
|
2384
2527
|
DEFAULT_CHAPTER_COPY,
|
|
2385
2528
|
DEFAULT_SHARE_FIELDS,
|
|
@@ -2392,6 +2535,7 @@ export {
|
|
|
2392
2535
|
brandTokens,
|
|
2393
2536
|
bucketSeries,
|
|
2394
2537
|
buildGroupSeed,
|
|
2538
|
+
buildTierSeeds,
|
|
2395
2539
|
canApprove,
|
|
2396
2540
|
canBook,
|
|
2397
2541
|
canChangeRole,
|
|
@@ -2416,6 +2560,7 @@ export {
|
|
|
2416
2560
|
emailGroupFrom,
|
|
2417
2561
|
endForSlot,
|
|
2418
2562
|
findApplicationRef,
|
|
2563
|
+
findTier,
|
|
2419
2564
|
firstPaymentPatch,
|
|
2420
2565
|
formatChapterCopy,
|
|
2421
2566
|
formationFields,
|
|
@@ -2428,7 +2573,10 @@ export {
|
|
|
2428
2573
|
isSlotAvailable,
|
|
2429
2574
|
isValidEmail,
|
|
2430
2575
|
joinConfig,
|
|
2576
|
+
joinConfigTiers,
|
|
2431
2577
|
leaderCrmConfig,
|
|
2578
|
+
loadOfferableTiers,
|
|
2579
|
+
loadTiers,
|
|
2432
2580
|
meetingCreateRow,
|
|
2433
2581
|
meetingRescheduleUpdate,
|
|
2434
2582
|
memberApplication,
|
|
@@ -2436,6 +2584,7 @@ export {
|
|
|
2436
2584
|
networkSourceTag,
|
|
2437
2585
|
normalizeSharedRecord,
|
|
2438
2586
|
normalizeWebhookEvent,
|
|
2587
|
+
offerableTiers,
|
|
2439
2588
|
paymentsReady,
|
|
2440
2589
|
personInputFromApp,
|
|
2441
2590
|
planDelivery,
|
|
@@ -2453,6 +2602,7 @@ export {
|
|
|
2453
2602
|
resolveLeaderFormation,
|
|
2454
2603
|
resolvePipeline,
|
|
2455
2604
|
resolveScheduling,
|
|
2605
|
+
resolveTiers,
|
|
2456
2606
|
roleFromClaim,
|
|
2457
2607
|
sendTemplated,
|
|
2458
2608
|
sharedPersonInput,
|
|
@@ -2465,6 +2615,8 @@ export {
|
|
|
2465
2615
|
submitApplication,
|
|
2466
2616
|
subscriptionIdempotencyKey,
|
|
2467
2617
|
syncApplicationToCrm,
|
|
2618
|
+
tierIsFree,
|
|
2619
|
+
tierPayable,
|
|
2468
2620
|
updateClerkUserMetadata,
|
|
2469
2621
|
updateClerkUserMetadataByEmail,
|
|
2470
2622
|
validateScheduling,
|