@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/index.d.cts CHANGED
@@ -1,4 +1,103 @@
1
- import { CrmConfig, Crm, TypeDef, FieldDef, TypeSummary, CrmRecord } from '@odla-ai/crm';
1
+ import { CrmConfig, Crm, FieldConditions, TypeDef, FieldDef, TypeSummary, CrmRecord } from '@odla-ai/crm';
2
+
3
+ /** One membership tier a chapter offers. `priceCents: 0` means free. */
4
+ interface ChapterTier {
5
+ /** Stable slug, referenced by `applications.tierId`. Never renumbered. */
6
+ id: string;
7
+ /** The group this tier belongs to. */
8
+ groupId: string;
9
+ /** Display name, e.g. "Founding". */
10
+ name: string;
11
+ /** Price in minor units. `0` is free and needs no Stripe price. */
12
+ priceCents: number;
13
+ /** Stripe price to subscribe. Required only when `priceCents > 0`. */
14
+ stripePriceId?: string | null;
15
+ /** Short description shown beside the tier on the join page. */
16
+ blurb?: string;
17
+ /** Ascending display order; ties fall back to `id` for a stable sort. */
18
+ sortOrder: number;
19
+ /** An inactive tier is retained for existing members but never offered. */
20
+ active: boolean;
21
+ }
22
+ /**
23
+ * A tier as declared in `defineChapter`, before it becomes a row.
24
+ *
25
+ * `groupId` is absent here because the chapter owns exactly one group at seed
26
+ * time; the seeder fills it in.
27
+ */
28
+ interface ChapterTierConfig {
29
+ id: string;
30
+ name: string;
31
+ priceCents: number;
32
+ stripePriceId?: string;
33
+ blurb?: string;
34
+ /** Defaults to declaration order when omitted. */
35
+ sortOrder?: number;
36
+ /** Defaults to true; set false to retire a tier without deleting it. */
37
+ active?: boolean;
38
+ }
39
+ /** Build the seed rows for a chapter's declared tiers. */
40
+ declare function buildTierSeeds(groupId: string, configured?: readonly ChapterTierConfig[], now?: number): Array<Record<string, unknown>>;
41
+ /** The `groups`-row fields tier payability depends on. */
42
+ interface TierGroup {
43
+ id?: string;
44
+ stripePublishableKey?: string | null;
45
+ /** Legacy single price, used only to synthesize an implicit tier. */
46
+ stripePriceId?: string | null;
47
+ standardPriceCents?: number;
48
+ foundingDiscountCents?: number;
49
+ }
50
+ /**
51
+ * Whether this tier costs nothing. A free tier never touches Stripe.
52
+ *
53
+ * Free means both no price AND no Stripe price to charge against. The second
54
+ * half matters for chapters that predate tiers: a group can carry a real
55
+ * `stripePriceId` while `standardPriceCents` is 0, because the cents field is
56
+ * display copy and the amount actually lives in Stripe. Reading such a group as
57
+ * free would silently stop charging an existing paying chapter.
58
+ */
59
+ declare function tierIsFree(tier: Pick<ChapterTier, "priceCents"> & {
60
+ stripePriceId?: string | null;
61
+ }): boolean;
62
+ /**
63
+ * Whether a tier can actually be joined right now.
64
+ *
65
+ * A free tier is always joinable — there is nothing to charge, so Stripe being
66
+ * unconfigured is irrelevant to it. A paid tier needs the group's publishable
67
+ * key, its own Stripe price, and the vault secret; missing any of those drops
68
+ * the tier rather than half-charging for it.
69
+ */
70
+ declare function tierPayable(tier: Pick<ChapterTier, "priceCents" | "stripePriceId">, group: TierGroup, hasSecretKey: boolean): boolean;
71
+ /**
72
+ * The tiers a chapter offers, given the stored rows.
73
+ *
74
+ * A chapter that predates tiers has no rows, and must keep working untouched:
75
+ * one implicit `"standard"` tier is synthesized from the group's legacy
76
+ * `standardPriceCents`/`stripePriceId` columns. That is why there is no
77
+ * migration — an existing chapter reads exactly as it did before, and writing
78
+ * its first real tier row takes over.
79
+ */
80
+ declare function resolveTiers(group: TierGroup, rows?: readonly ChapterTier[]): ChapterTier[];
81
+ /** The tiers to show on the join page: active, and joinable as configured. */
82
+ declare function offerableTiers(tiers: readonly ChapterTier[], group: TierGroup, hasSecretKey: boolean): ChapterTier[];
83
+ /**
84
+ * Resolve the tier an application chose.
85
+ *
86
+ * An absent `tierId` is the pre-tier case and selects the only offered tier
87
+ * when exactly one exists; with several it is genuinely ambiguous and returns
88
+ * `null`, because guessing would charge someone the wrong price.
89
+ */
90
+ declare function findTier(tiers: readonly ChapterTier[], tierId: string | null | undefined): ChapterTier | null;
91
+ /** The public shape of a tier on the join config. Never exposes Stripe ids. */
92
+ interface JoinConfigTier {
93
+ id: string;
94
+ name: string;
95
+ priceCents: number;
96
+ blurb: string;
97
+ free: boolean;
98
+ }
99
+ /** Project tiers for the public join config, dropping provider identifiers. */
100
+ declare function joinConfigTiers(tiers: readonly ChapterTier[]): JoinConfigTier[];
2
101
 
3
102
  type TextFields$1<Key extends string> = {
4
103
  [Field in Key]: string;
@@ -153,6 +252,30 @@ interface ResolvedNetwork {
153
252
  readers: readonly ResolvedNetworkReader[];
154
253
  }
155
254
 
255
+ /**
256
+ * Membership pricing for the group row (chapter mode).
257
+ *
258
+ * This is the single-price model that predates tiers. It still seeds the group
259
+ * row and remains the fallback a chapter reads when it declares no tiers, so
260
+ * existing chapters keep working; new work should prefer `tiers`.
261
+ */
262
+ interface ChapterPrices {
263
+ standardCents: number;
264
+ foundingDiscountCents?: number;
265
+ /** ISO currency, default "usd". */
266
+ currency?: string;
267
+ /** Billing interval, default "year". */
268
+ interval?: "year" | "month";
269
+ }
270
+ /** Membership policy + compliance copy stored on the group row. */
271
+ interface ChapterPolicy {
272
+ disclaimerText?: string;
273
+ refundPolicyText?: string;
274
+ trustCopy?: string;
275
+ commitmentText?: string;
276
+ normsText?: string;
277
+ }
278
+
156
279
  /** Which feature profile a site runs. `chapter` is the full public member site
157
280
  * (join, Stripe membership, booking, member area, admin, CRM); `hub` is
158
281
  * admin-only and CRM-focused (a directory/registry over the same CRM). */
@@ -280,23 +403,7 @@ interface ChapterBrand {
280
403
  }>>;
281
404
  logos?: string;
282
405
  }
283
- /** Membership pricing for the group row (chapter mode). */
284
- interface ChapterPrices {
285
- standardCents: number;
286
- foundingDiscountCents?: number;
287
- /** ISO currency, default "usd". */
288
- currency?: string;
289
- /** Billing interval, default "year". */
290
- interval?: "year" | "month";
291
- }
292
- /** Membership policy + compliance copy stored on the group row. */
293
- interface ChapterPolicy {
294
- disclaimerText?: string;
295
- refundPolicyText?: string;
296
- trustCopy?: string;
297
- commitmentText?: string;
298
- normsText?: string;
299
- }
406
+
300
407
  /** One owner-editable transactional email template. */
301
408
  interface EmailTemplate {
302
409
  subject: string;
@@ -378,6 +485,11 @@ interface ResolvedPipeline {
378
485
  interface ChapterApplication {
379
486
  required?: readonly string[];
380
487
  optional?: readonly string[];
488
+ /** Per-field CEL conditions — show a field, or demand it, based on the
489
+ * answers so far. Enforced server-side as well as rendered, so a hidden
490
+ * field is never demanded and a conditionally-required one cannot be
491
+ * skipped by posting directly. Validated when the chapter is defined. */
492
+ conditions?: Record<string, FieldConditions>;
381
493
  /** Per-field character cap. Fields not listed use `defaultMaxLen`. */
382
494
  maxLen?: Record<string, number>;
383
495
  defaultMaxLen?: number;
@@ -418,6 +530,8 @@ interface ResolvedApplication {
418
530
  crmFields: readonly string[];
419
531
  maxArrayLen: number;
420
532
  validateEmail: boolean;
533
+ /** Resolved per-field conditions. Empty means every field is unconditional. */
534
+ conditions: Record<string, FieldConditions>;
421
535
  }
422
536
  /** The `defineChapter()` config a site fills in. */
423
537
  interface ChapterConfig {
@@ -439,8 +553,10 @@ interface ChapterConfig {
439
553
  /** Enable a public, host-rendered formation application workflow. */
440
554
  formation?: LeaderFormationConfig;
441
555
  thesis?: unknown;
442
- /** Required in `chapter` mode. */
556
+ /** Required in `chapter` mode, and the fallback when `tiers` is empty. */
443
557
  prices?: ChapterPrices;
558
+ /** Membership tiers. A tier priced at 0 is free and never touches Stripe. */
559
+ tiers?: ChapterTierConfig[];
444
560
  policy?: ChapterPolicy;
445
561
  /** `notificationEmail` required in `chapter` mode. */
446
562
  emails?: ChapterEmails;
@@ -1005,6 +1121,9 @@ declare function submitApplication(db: ChapterDb, chapter: Chapter, fields: Reco
1005
1121
  groupId?: string;
1006
1122
  now: number;
1007
1123
  newId: () => string;
1124
+ /** Tier ids this chapter offers. A posted `tierId` outside it is refused;
1125
+ * omit to accept any (pre-tier callers). */
1126
+ tierIds?: readonly string[];
1008
1127
  }): Promise<SubmitResult>;
1009
1128
  /** The `groups`-row fields the join config exposes. */
1010
1129
  interface JoinConfigGroup {
@@ -1023,8 +1142,14 @@ interface JoinConfigGroup {
1023
1142
  * group row plus `paymentsReady`. When payments aren't wired the join flow drops
1024
1143
  * the payment step (C2) — the worker computes `paymentsReady` from the group's
1025
1144
  * Stripe keys + vault secret. Pure.
1145
+ *
1146
+ * `tiers` carries the offerable membership tiers. It is additive: the legacy
1147
+ * `standardPriceCents`/`foundingDiscountCents` fields still describe the first
1148
+ * tier, so a join page written before tiers keeps rendering unchanged. A free
1149
+ * tier appears here with `free: true` even when Stripe is unconfigured, because
1150
+ * nothing needs charging for it.
1026
1151
  */
1027
- declare function joinConfig(group: JoinConfigGroup, paymentsReady: boolean): Record<string, unknown>;
1152
+ declare function joinConfig(group: JoinConfigGroup, paymentsReady: boolean, tiers?: readonly JoinConfigTier[], conditions?: Readonly<Record<string, FieldConditions>>): Record<string, unknown>;
1028
1153
 
1029
1154
  /** The contact data the hub shares for a prospect. `hubRecordId` is the stable
1030
1155
  * idempotency key (the hub's crm_record id). */
@@ -1724,4 +1849,18 @@ type ApplicationBookingPatch = {
1724
1849
  * already there (never backward). */
1725
1850
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
1726
1851
 
1727
- export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkReader, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterRunbookHints, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkMetadataInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type LeaderCrmOptions, type LeaderFormationConfig, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NetworkRecord, type NetworkRecordNote, type NetworkRecordPage, type NetworkRollup, type NetworkRollupTarget, type NetworkSharedNote, type NetworkSnapshot, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedLeaderFormation, type ResolvedNetwork, type ResolvedNetworkReader, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type SharedRecordV1, type SharedRecordV2, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, formatChapterCopy, formationFields, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, leaderCrmConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolveLeaderFormation, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, updateClerkUserMetadata, updateClerkUserMetadataByEmail, validateScheduling, verifyStripeSignature, webhookMutationId };
1852
+ /** A `groups` row as the tier loader needs it. */
1853
+ type TierGroupRow = TierGroup & {
1854
+ id?: string;
1855
+ };
1856
+ /**
1857
+ * Every tier a group offers, ordered, including the implicit tier synthesized
1858
+ * for a chapter that predates tiers. Never throws on a missing namespace: a
1859
+ * deployment whose schema has not been pushed yet reads as "no rows", which
1860
+ * resolves to the legacy single tier.
1861
+ */
1862
+ declare function loadTiers(db: ChapterDb, group: TierGroupRow): Promise<ChapterTier[]>;
1863
+ /** The tiers a join page may show: active, and joinable as configured. */
1864
+ declare function loadOfferableTiers(db: ChapterDb, group: TierGroupRow, hasSecretKey: boolean): Promise<ChapterTier[]>;
1865
+
1866
+ export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkReader, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterRunbookHints, type ChapterScheduling, type ChapterSends, type ChapterTier, type ChapterTierConfig, type ClerkInviteInput, type ClerkMetadataInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type JoinConfigTier, type LeaderCrmOptions, type LeaderFormationConfig, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NetworkRecord, type NetworkRecordNote, type NetworkRecordPage, type NetworkRollup, type NetworkRollupTarget, type NetworkSharedNote, type NetworkSnapshot, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedLeaderFormation, type ResolvedNetwork, type ResolvedNetworkReader, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type SharedRecordV1, type SharedRecordV2, type StripeEvent, type StripeResult, type SubmitResult, type TierGroup, type TierGroupRow, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, buildTierSeeds, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, findTier, firstPaymentPatch, formatChapterCopy, formationFields, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, joinConfigTiers, leaderCrmConfig, loadOfferableTiers, loadTiers, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, offerableTiers, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolveLeaderFormation, resolvePipeline, resolveScheduling, resolveTiers, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, tierIsFree, tierPayable, updateClerkUserMetadata, updateClerkUserMetadataByEmail, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,103 @@
1
- import { CrmConfig, Crm, TypeDef, FieldDef, TypeSummary, CrmRecord } from '@odla-ai/crm';
1
+ import { CrmConfig, Crm, FieldConditions, TypeDef, FieldDef, TypeSummary, CrmRecord } from '@odla-ai/crm';
2
+
3
+ /** One membership tier a chapter offers. `priceCents: 0` means free. */
4
+ interface ChapterTier {
5
+ /** Stable slug, referenced by `applications.tierId`. Never renumbered. */
6
+ id: string;
7
+ /** The group this tier belongs to. */
8
+ groupId: string;
9
+ /** Display name, e.g. "Founding". */
10
+ name: string;
11
+ /** Price in minor units. `0` is free and needs no Stripe price. */
12
+ priceCents: number;
13
+ /** Stripe price to subscribe. Required only when `priceCents > 0`. */
14
+ stripePriceId?: string | null;
15
+ /** Short description shown beside the tier on the join page. */
16
+ blurb?: string;
17
+ /** Ascending display order; ties fall back to `id` for a stable sort. */
18
+ sortOrder: number;
19
+ /** An inactive tier is retained for existing members but never offered. */
20
+ active: boolean;
21
+ }
22
+ /**
23
+ * A tier as declared in `defineChapter`, before it becomes a row.
24
+ *
25
+ * `groupId` is absent here because the chapter owns exactly one group at seed
26
+ * time; the seeder fills it in.
27
+ */
28
+ interface ChapterTierConfig {
29
+ id: string;
30
+ name: string;
31
+ priceCents: number;
32
+ stripePriceId?: string;
33
+ blurb?: string;
34
+ /** Defaults to declaration order when omitted. */
35
+ sortOrder?: number;
36
+ /** Defaults to true; set false to retire a tier without deleting it. */
37
+ active?: boolean;
38
+ }
39
+ /** Build the seed rows for a chapter's declared tiers. */
40
+ declare function buildTierSeeds(groupId: string, configured?: readonly ChapterTierConfig[], now?: number): Array<Record<string, unknown>>;
41
+ /** The `groups`-row fields tier payability depends on. */
42
+ interface TierGroup {
43
+ id?: string;
44
+ stripePublishableKey?: string | null;
45
+ /** Legacy single price, used only to synthesize an implicit tier. */
46
+ stripePriceId?: string | null;
47
+ standardPriceCents?: number;
48
+ foundingDiscountCents?: number;
49
+ }
50
+ /**
51
+ * Whether this tier costs nothing. A free tier never touches Stripe.
52
+ *
53
+ * Free means both no price AND no Stripe price to charge against. The second
54
+ * half matters for chapters that predate tiers: a group can carry a real
55
+ * `stripePriceId` while `standardPriceCents` is 0, because the cents field is
56
+ * display copy and the amount actually lives in Stripe. Reading such a group as
57
+ * free would silently stop charging an existing paying chapter.
58
+ */
59
+ declare function tierIsFree(tier: Pick<ChapterTier, "priceCents"> & {
60
+ stripePriceId?: string | null;
61
+ }): boolean;
62
+ /**
63
+ * Whether a tier can actually be joined right now.
64
+ *
65
+ * A free tier is always joinable — there is nothing to charge, so Stripe being
66
+ * unconfigured is irrelevant to it. A paid tier needs the group's publishable
67
+ * key, its own Stripe price, and the vault secret; missing any of those drops
68
+ * the tier rather than half-charging for it.
69
+ */
70
+ declare function tierPayable(tier: Pick<ChapterTier, "priceCents" | "stripePriceId">, group: TierGroup, hasSecretKey: boolean): boolean;
71
+ /**
72
+ * The tiers a chapter offers, given the stored rows.
73
+ *
74
+ * A chapter that predates tiers has no rows, and must keep working untouched:
75
+ * one implicit `"standard"` tier is synthesized from the group's legacy
76
+ * `standardPriceCents`/`stripePriceId` columns. That is why there is no
77
+ * migration — an existing chapter reads exactly as it did before, and writing
78
+ * its first real tier row takes over.
79
+ */
80
+ declare function resolveTiers(group: TierGroup, rows?: readonly ChapterTier[]): ChapterTier[];
81
+ /** The tiers to show on the join page: active, and joinable as configured. */
82
+ declare function offerableTiers(tiers: readonly ChapterTier[], group: TierGroup, hasSecretKey: boolean): ChapterTier[];
83
+ /**
84
+ * Resolve the tier an application chose.
85
+ *
86
+ * An absent `tierId` is the pre-tier case and selects the only offered tier
87
+ * when exactly one exists; with several it is genuinely ambiguous and returns
88
+ * `null`, because guessing would charge someone the wrong price.
89
+ */
90
+ declare function findTier(tiers: readonly ChapterTier[], tierId: string | null | undefined): ChapterTier | null;
91
+ /** The public shape of a tier on the join config. Never exposes Stripe ids. */
92
+ interface JoinConfigTier {
93
+ id: string;
94
+ name: string;
95
+ priceCents: number;
96
+ blurb: string;
97
+ free: boolean;
98
+ }
99
+ /** Project tiers for the public join config, dropping provider identifiers. */
100
+ declare function joinConfigTiers(tiers: readonly ChapterTier[]): JoinConfigTier[];
2
101
 
3
102
  type TextFields$1<Key extends string> = {
4
103
  [Field in Key]: string;
@@ -153,6 +252,30 @@ interface ResolvedNetwork {
153
252
  readers: readonly ResolvedNetworkReader[];
154
253
  }
155
254
 
255
+ /**
256
+ * Membership pricing for the group row (chapter mode).
257
+ *
258
+ * This is the single-price model that predates tiers. It still seeds the group
259
+ * row and remains the fallback a chapter reads when it declares no tiers, so
260
+ * existing chapters keep working; new work should prefer `tiers`.
261
+ */
262
+ interface ChapterPrices {
263
+ standardCents: number;
264
+ foundingDiscountCents?: number;
265
+ /** ISO currency, default "usd". */
266
+ currency?: string;
267
+ /** Billing interval, default "year". */
268
+ interval?: "year" | "month";
269
+ }
270
+ /** Membership policy + compliance copy stored on the group row. */
271
+ interface ChapterPolicy {
272
+ disclaimerText?: string;
273
+ refundPolicyText?: string;
274
+ trustCopy?: string;
275
+ commitmentText?: string;
276
+ normsText?: string;
277
+ }
278
+
156
279
  /** Which feature profile a site runs. `chapter` is the full public member site
157
280
  * (join, Stripe membership, booking, member area, admin, CRM); `hub` is
158
281
  * admin-only and CRM-focused (a directory/registry over the same CRM). */
@@ -280,23 +403,7 @@ interface ChapterBrand {
280
403
  }>>;
281
404
  logos?: string;
282
405
  }
283
- /** Membership pricing for the group row (chapter mode). */
284
- interface ChapterPrices {
285
- standardCents: number;
286
- foundingDiscountCents?: number;
287
- /** ISO currency, default "usd". */
288
- currency?: string;
289
- /** Billing interval, default "year". */
290
- interval?: "year" | "month";
291
- }
292
- /** Membership policy + compliance copy stored on the group row. */
293
- interface ChapterPolicy {
294
- disclaimerText?: string;
295
- refundPolicyText?: string;
296
- trustCopy?: string;
297
- commitmentText?: string;
298
- normsText?: string;
299
- }
406
+
300
407
  /** One owner-editable transactional email template. */
301
408
  interface EmailTemplate {
302
409
  subject: string;
@@ -378,6 +485,11 @@ interface ResolvedPipeline {
378
485
  interface ChapterApplication {
379
486
  required?: readonly string[];
380
487
  optional?: readonly string[];
488
+ /** Per-field CEL conditions — show a field, or demand it, based on the
489
+ * answers so far. Enforced server-side as well as rendered, so a hidden
490
+ * field is never demanded and a conditionally-required one cannot be
491
+ * skipped by posting directly. Validated when the chapter is defined. */
492
+ conditions?: Record<string, FieldConditions>;
381
493
  /** Per-field character cap. Fields not listed use `defaultMaxLen`. */
382
494
  maxLen?: Record<string, number>;
383
495
  defaultMaxLen?: number;
@@ -418,6 +530,8 @@ interface ResolvedApplication {
418
530
  crmFields: readonly string[];
419
531
  maxArrayLen: number;
420
532
  validateEmail: boolean;
533
+ /** Resolved per-field conditions. Empty means every field is unconditional. */
534
+ conditions: Record<string, FieldConditions>;
421
535
  }
422
536
  /** The `defineChapter()` config a site fills in. */
423
537
  interface ChapterConfig {
@@ -439,8 +553,10 @@ interface ChapterConfig {
439
553
  /** Enable a public, host-rendered formation application workflow. */
440
554
  formation?: LeaderFormationConfig;
441
555
  thesis?: unknown;
442
- /** Required in `chapter` mode. */
556
+ /** Required in `chapter` mode, and the fallback when `tiers` is empty. */
443
557
  prices?: ChapterPrices;
558
+ /** Membership tiers. A tier priced at 0 is free and never touches Stripe. */
559
+ tiers?: ChapterTierConfig[];
444
560
  policy?: ChapterPolicy;
445
561
  /** `notificationEmail` required in `chapter` mode. */
446
562
  emails?: ChapterEmails;
@@ -1005,6 +1121,9 @@ declare function submitApplication(db: ChapterDb, chapter: Chapter, fields: Reco
1005
1121
  groupId?: string;
1006
1122
  now: number;
1007
1123
  newId: () => string;
1124
+ /** Tier ids this chapter offers. A posted `tierId` outside it is refused;
1125
+ * omit to accept any (pre-tier callers). */
1126
+ tierIds?: readonly string[];
1008
1127
  }): Promise<SubmitResult>;
1009
1128
  /** The `groups`-row fields the join config exposes. */
1010
1129
  interface JoinConfigGroup {
@@ -1023,8 +1142,14 @@ interface JoinConfigGroup {
1023
1142
  * group row plus `paymentsReady`. When payments aren't wired the join flow drops
1024
1143
  * the payment step (C2) — the worker computes `paymentsReady` from the group's
1025
1144
  * Stripe keys + vault secret. Pure.
1145
+ *
1146
+ * `tiers` carries the offerable membership tiers. It is additive: the legacy
1147
+ * `standardPriceCents`/`foundingDiscountCents` fields still describe the first
1148
+ * tier, so a join page written before tiers keeps rendering unchanged. A free
1149
+ * tier appears here with `free: true` even when Stripe is unconfigured, because
1150
+ * nothing needs charging for it.
1026
1151
  */
1027
- declare function joinConfig(group: JoinConfigGroup, paymentsReady: boolean): Record<string, unknown>;
1152
+ declare function joinConfig(group: JoinConfigGroup, paymentsReady: boolean, tiers?: readonly JoinConfigTier[], conditions?: Readonly<Record<string, FieldConditions>>): Record<string, unknown>;
1028
1153
 
1029
1154
  /** The contact data the hub shares for a prospect. `hubRecordId` is the stable
1030
1155
  * idempotency key (the hub's crm_record id). */
@@ -1724,4 +1849,18 @@ type ApplicationBookingPatch = {
1724
1849
  * already there (never backward). */
1725
1850
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
1726
1851
 
1727
- export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkReader, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterRunbookHints, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkMetadataInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type LeaderCrmOptions, type LeaderFormationConfig, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NetworkRecord, type NetworkRecordNote, type NetworkRecordPage, type NetworkRollup, type NetworkRollupTarget, type NetworkSharedNote, type NetworkSnapshot, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedLeaderFormation, type ResolvedNetwork, type ResolvedNetworkReader, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type SharedRecordV1, type SharedRecordV2, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, formatChapterCopy, formationFields, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, leaderCrmConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolveLeaderFormation, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, updateClerkUserMetadata, updateClerkUserMetadataByEmail, validateScheduling, verifyStripeSignature, webhookMutationId };
1852
+ /** A `groups` row as the tier loader needs it. */
1853
+ type TierGroupRow = TierGroup & {
1854
+ id?: string;
1855
+ };
1856
+ /**
1857
+ * Every tier a group offers, ordered, including the implicit tier synthesized
1858
+ * for a chapter that predates tiers. Never throws on a missing namespace: a
1859
+ * deployment whose schema has not been pushed yet reads as "no rows", which
1860
+ * resolves to the legacy single tier.
1861
+ */
1862
+ declare function loadTiers(db: ChapterDb, group: TierGroupRow): Promise<ChapterTier[]>;
1863
+ /** The tiers a join page may show: active, and joinable as configured. */
1864
+ declare function loadOfferableTiers(db: ChapterDb, group: TierGroupRow, hasSecretKey: boolean): Promise<ChapterTier[]>;
1865
+
1866
+ export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkReader, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterRunbookHints, type ChapterScheduling, type ChapterSends, type ChapterTier, type ChapterTierConfig, type ClerkInviteInput, type ClerkMetadataInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type JoinConfigTier, type LeaderCrmOptions, type LeaderFormationConfig, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NetworkRecord, type NetworkRecordNote, type NetworkRecordPage, type NetworkRollup, type NetworkRollupTarget, type NetworkSharedNote, type NetworkSnapshot, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedLeaderFormation, type ResolvedNetwork, type ResolvedNetworkReader, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type SharedRecordV1, type SharedRecordV2, type StripeEvent, type StripeResult, type SubmitResult, type TierGroup, type TierGroupRow, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, buildTierSeeds, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, findTier, firstPaymentPatch, formatChapterCopy, formationFields, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, joinConfigTiers, leaderCrmConfig, loadOfferableTiers, loadTiers, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, offerableTiers, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolveLeaderFormation, resolvePipeline, resolveScheduling, resolveTiers, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, tierIsFree, tierPayable, updateClerkUserMetadata, updateClerkUserMetadataByEmail, validateScheduling, verifyStripeSignature, webhookMutationId };