@elevasis/core 0.26.0 → 0.27.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.ts +5 -5
- package/dist/index.js +209 -173
- package/dist/knowledge/index.d.ts +21 -21
- package/dist/organization-model/index.d.ts +5 -5
- package/dist/organization-model/index.js +209 -173
- package/dist/test-utils/index.d.ts +2 -2
- package/dist/test-utils/index.js +182 -126
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +976 -1063
- package/src/business/acquisition/api-schemas.test.ts +1962 -1841
- package/src/business/acquisition/api-schemas.ts +1461 -1464
- package/src/business/acquisition/crm-next-action.test.ts +45 -25
- package/src/business/acquisition/crm-next-action.ts +227 -220
- package/src/business/acquisition/crm-priority.test.ts +41 -8
- package/src/business/acquisition/crm-priority.ts +365 -349
- package/src/business/acquisition/crm-state-actions.test.ts +208 -153
- package/src/business/acquisition/derive-actions.test.ts +90 -13
- package/src/business/acquisition/derive-actions.ts +8 -139
- package/src/business/acquisition/ontology-validation.ts +72 -158
- package/src/business/pdf/sections/investment.ts +1 -1
- package/src/business/pdf/sections/summary-investment.ts +1 -1
- package/src/execution/engine/tools/tool-maps.ts +872 -831
- package/src/organization-model/__tests__/cross-ref.test.ts +167 -0
- package/src/organization-model/__tests__/published-zero-leak.test.ts +60 -1
- package/src/organization-model/__tests__/resolve.test.ts +1 -1
- package/src/organization-model/__tests__/schema-refinements.test.ts +72 -0
- package/src/organization-model/cross-ref.ts +175 -0
- package/src/organization-model/domains/branding.ts +6 -6
- package/src/organization-model/domains/sales.test.ts +104 -218
- package/src/organization-model/domains/sales.ts +212 -375
- package/src/organization-model/index.ts +1 -0
- package/src/organization-model/schema-refinements.ts +667 -0
- package/src/organization-model/schema.ts +8 -715
- package/src/reference/_generated/contracts.md +976 -1063
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
CRM_DISCOVERY_BOOKING_CANCELLED_STATE,
|
|
4
|
-
CRM_DISCOVERY_LINK_SENT_STATE,
|
|
5
|
-
CRM_DISCOVERY_NUDGING_STATE,
|
|
6
|
-
CRM_DISCOVERY_REPLIED_STATE
|
|
7
|
-
} from '../../organization-model/domains/sales'
|
|
8
|
-
import { getDealOwnership, type DealOwnership } from './deal-ownership'
|
|
1
|
+
import { z } from 'zod'
|
|
9
2
|
import type { AcqDealRow } from './types'
|
|
10
3
|
|
|
11
4
|
export interface Action {
|
|
@@ -22,10 +15,10 @@ export interface ActionDef {
|
|
|
22
15
|
payloadSchema?: z.ZodTypeAny
|
|
23
16
|
}
|
|
24
17
|
|
|
25
|
-
type DealActionInput = AcqDealRow & {
|
|
26
|
-
ownership?:
|
|
27
|
-
nextAction?: string | null
|
|
28
|
-
}
|
|
18
|
+
type DealActionInput = AcqDealRow & {
|
|
19
|
+
ownership?: 'us' | 'them' | null
|
|
20
|
+
nextAction?: string | null
|
|
21
|
+
}
|
|
29
22
|
|
|
30
23
|
export const SendReplyActionPayloadSchema = z
|
|
31
24
|
.object({
|
|
@@ -33,133 +26,9 @@ export const SendReplyActionPayloadSchema = z
|
|
|
33
26
|
})
|
|
34
27
|
.strict()
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
move_to_proposal: {
|
|
40
|
-
key: 'move_to_proposal',
|
|
41
|
-
label: 'Move to Proposal',
|
|
42
|
-
workflowId: 'move_to_proposal-workflow'
|
|
43
|
-
},
|
|
44
|
-
move_to_closing: {
|
|
45
|
-
key: 'move_to_closing',
|
|
46
|
-
label: 'Move to Closing',
|
|
47
|
-
workflowId: 'move_to_closing-workflow'
|
|
48
|
-
},
|
|
49
|
-
move_to_closed_won: {
|
|
50
|
-
key: 'move_to_closed_won',
|
|
51
|
-
label: 'Close Won',
|
|
52
|
-
workflowId: 'move_to_closed_won-workflow'
|
|
53
|
-
},
|
|
54
|
-
move_to_closed_lost: {
|
|
55
|
-
key: 'move_to_closed_lost',
|
|
56
|
-
label: 'Close Lost',
|
|
57
|
-
workflowId: 'move_to_closed_lost-workflow'
|
|
58
|
-
},
|
|
59
|
-
move_to_nurturing: {
|
|
60
|
-
key: 'move_to_nurturing',
|
|
61
|
-
label: 'Move to Nurturing',
|
|
62
|
-
workflowId: 'move_to_nurturing-workflow'
|
|
63
|
-
},
|
|
64
|
-
send_reply: {
|
|
65
|
-
key: 'send_reply',
|
|
66
|
-
label: 'Send Reply',
|
|
67
|
-
workflowId: 'crm-send-reply-workflow'
|
|
68
|
-
},
|
|
69
|
-
send_link: {
|
|
70
|
-
key: 'send_link',
|
|
71
|
-
label: 'Send Booking Link',
|
|
72
|
-
workflowId: 'crm-send-booking-link-workflow'
|
|
73
|
-
},
|
|
74
|
-
send_nudge: {
|
|
75
|
-
key: 'send_nudge',
|
|
76
|
-
label: 'Send Nudge',
|
|
77
|
-
workflowId: 'crm-send-nudge-workflow'
|
|
78
|
-
},
|
|
79
|
-
rebook: {
|
|
80
|
-
key: 'rebook',
|
|
81
|
-
label: 'Rebook',
|
|
82
|
-
workflowId: 'crm-rebook-workflow'
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function crmAction(actionId: string, runtime: CrmActionRuntimeDef): ActionDef {
|
|
87
|
-
const metadata = CRM_RUNTIME_ACTION_METADATA[actionId]
|
|
88
|
-
if (!metadata?.workflowId) {
|
|
89
|
-
throw new Error(`CRM action metadata for "${actionId}" must define resourceId`)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
key: metadata.key,
|
|
94
|
-
label: metadata.label,
|
|
95
|
-
workflowId: metadata.workflowId,
|
|
96
|
-
...runtime
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export const DEFAULT_CRM_ACTIONS: ActionDef[] = [
|
|
101
|
-
crmAction('move_to_proposal', {
|
|
102
|
-
isAvailableFor: (deal) => deal.stage_key === 'interested'
|
|
103
|
-
}),
|
|
104
|
-
crmAction('move_to_closing', {
|
|
105
|
-
isAvailableFor: (deal) => deal.stage_key === 'proposal'
|
|
106
|
-
}),
|
|
107
|
-
crmAction('move_to_closed_won', {
|
|
108
|
-
isAvailableFor: (deal) => deal.stage_key === 'closing'
|
|
109
|
-
}),
|
|
110
|
-
crmAction('move_to_closed_lost', {
|
|
111
|
-
isAvailableFor: (deal) =>
|
|
112
|
-
deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
|
|
113
|
-
}),
|
|
114
|
-
crmAction('move_to_nurturing', {
|
|
115
|
-
isAvailableFor: (deal) =>
|
|
116
|
-
deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
|
|
117
|
-
}),
|
|
118
|
-
crmAction('send_reply', {
|
|
119
|
-
isAvailableFor: (deal) =>
|
|
120
|
-
deal.stage_key === 'interested' &&
|
|
121
|
-
isOurReplyAction(deal) &&
|
|
122
|
-
(deal.state_key === CRM_DISCOVERY_REPLIED_STATE.stateKey ||
|
|
123
|
-
deal.state_key === CRM_DISCOVERY_LINK_SENT_STATE.stateKey ||
|
|
124
|
-
deal.state_key === CRM_DISCOVERY_NUDGING_STATE.stateKey),
|
|
125
|
-
payloadSchema: SendReplyActionPayloadSchema
|
|
126
|
-
}),
|
|
127
|
-
crmAction('send_link', {
|
|
128
|
-
isAvailableFor: (deal) =>
|
|
129
|
-
deal.stage_key === 'interested' && deal.state_key === CRM_DISCOVERY_REPLIED_STATE.stateKey
|
|
130
|
-
}),
|
|
131
|
-
crmAction('send_nudge', {
|
|
132
|
-
isAvailableFor: (deal) =>
|
|
133
|
-
deal.stage_key === 'interested' &&
|
|
134
|
-
(deal.state_key === CRM_DISCOVERY_LINK_SENT_STATE.stateKey ||
|
|
135
|
-
deal.state_key === CRM_DISCOVERY_NUDGING_STATE.stateKey)
|
|
136
|
-
}),
|
|
137
|
-
{
|
|
138
|
-
key: 'mark_no_show',
|
|
139
|
-
label: 'Mark No-Show',
|
|
140
|
-
isAvailableFor: (deal) =>
|
|
141
|
-
deal.stage_key === 'interested' && deal.state_key === CRM_DISCOVERY_NUDGING_STATE.stateKey,
|
|
142
|
-
// Mirrors the auto-timeout precedent in operations/sales/crm/pipeline/timeout-actions.ts:
|
|
143
|
-
// both manual-click and timeout move the deal to closed_lost. The action_taken activity
|
|
144
|
-
// event captures operator intent and distinguishes the manual variant from the timed one.
|
|
145
|
-
workflowId: 'mark_no_show-workflow'
|
|
146
|
-
},
|
|
147
|
-
crmAction('rebook', {
|
|
148
|
-
isAvailableFor: (deal) =>
|
|
149
|
-
deal.stage_key === 'interested' && deal.state_key === CRM_DISCOVERY_BOOKING_CANCELLED_STATE.stateKey
|
|
150
|
-
})
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
export function deriveActions(deal: DealActionInput, actions: ActionDef[] = DEFAULT_CRM_ACTIONS): Action[] {
|
|
154
|
-
return actions
|
|
155
|
-
.filter((a) => a.isAvailableFor(deal))
|
|
29
|
+
export function deriveActions(deal: DealActionInput, actions: ActionDef[] = []): Action[] {
|
|
30
|
+
return actions
|
|
31
|
+
.filter((a) => a.isAvailableFor(deal))
|
|
156
32
|
.map(({ key, label, payloadSchema }) => ({ key, label, payloadSchema }))
|
|
157
33
|
}
|
|
158
34
|
|
|
159
|
-
function isOurReplyAction(deal: DealActionInput): boolean {
|
|
160
|
-
if (Object.prototype.hasOwnProperty.call(deal, 'nextAction')) {
|
|
161
|
-
return deal.nextAction === 'send_reply'
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return (deal.ownership ?? getDealOwnership(deal)) === 'us'
|
|
165
|
-
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DEFAULT_ORGANIZATION_MODEL } from '../../organization-model/defaults'
|
|
2
1
|
import {
|
|
3
2
|
compileOrganizationOntology,
|
|
4
3
|
formatOntologyId,
|
|
@@ -10,7 +9,6 @@ import {
|
|
|
10
9
|
} from '../../organization-model/ontology'
|
|
11
10
|
import type { OrganizationModel } from '../../organization-model/types'
|
|
12
11
|
import {
|
|
13
|
-
CRM_PIPELINE_DEFINITION,
|
|
14
12
|
type LeadGenStageCatalogEntry,
|
|
15
13
|
type StatefulStateDefinition
|
|
16
14
|
} from '../../organization-model/domains/sales'
|
|
@@ -28,20 +26,6 @@ export const LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID = formatOntologyId({
|
|
|
28
26
|
localId: 'lead-gen.stage-catalog'
|
|
29
27
|
})
|
|
30
28
|
|
|
31
|
-
const CRM_DEAL_OBJECT_ONTOLOGY_ID = formatOntologyId({
|
|
32
|
-
scope: 'sales.crm',
|
|
33
|
-
kind: 'object',
|
|
34
|
-
localId: 'crm.deal'
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
type CrmCatalogStageEntry = {
|
|
38
|
-
key: string
|
|
39
|
-
label: string
|
|
40
|
-
order: number
|
|
41
|
-
color?: string
|
|
42
|
-
states: StatefulStateDefinition[]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
29
|
export type BusinessOntologyValidationIndex = {
|
|
46
30
|
ontology: ResolvedOntologyIndex
|
|
47
31
|
crmPipelineCatalog: OntologyCatalogType
|
|
@@ -49,30 +33,6 @@ export type BusinessOntologyValidationIndex = {
|
|
|
49
33
|
actionTypesByLegacyId: Record<string, OntologyActionType>
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
function createCrmPipelineCatalog(): OntologyCatalogType {
|
|
53
|
-
return {
|
|
54
|
-
id: CRM_PIPELINE_CATALOG_ONTOLOGY_ID,
|
|
55
|
-
label: CRM_PIPELINE_DEFINITION.label,
|
|
56
|
-
ownerSystemId: 'sales.crm',
|
|
57
|
-
kind: 'pipeline',
|
|
58
|
-
appliesTo: CRM_DEAL_OBJECT_ONTOLOGY_ID,
|
|
59
|
-
entries: Object.fromEntries(
|
|
60
|
-
CRM_PIPELINE_DEFINITION.stages.map((stage, index) => [
|
|
61
|
-
stage.stageKey,
|
|
62
|
-
{
|
|
63
|
-
key: stage.stageKey,
|
|
64
|
-
label: stage.label,
|
|
65
|
-
order: (index + 1) * 10,
|
|
66
|
-
...(stage.color !== undefined ? { color: stage.color } : {}),
|
|
67
|
-
states: stage.states.map((state) => ({ ...state }))
|
|
68
|
-
} satisfies CrmCatalogStageEntry
|
|
69
|
-
])
|
|
70
|
-
),
|
|
71
|
-
legacyPipelineKey: CRM_PIPELINE_DEFINITION.pipelineKey,
|
|
72
|
-
legacyEntityKey: CRM_PIPELINE_DEFINITION.entityKey
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
36
|
function createLeadGenStageCatalog(model: OrganizationModel): OntologyCatalogType {
|
|
77
37
|
return {
|
|
78
38
|
id: LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID,
|
|
@@ -99,9 +59,8 @@ function mergeBridgeCatalogs(model: OrganizationModel): OrganizationModel {
|
|
|
99
59
|
const baseCatalogTypes = model.ontology?.catalogTypes ?? {}
|
|
100
60
|
const bridgeCatalogTypes: Record<OntologyId, OntologyCatalogType> = {}
|
|
101
61
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
62
|
+
// CRM pipeline catalog is now authored directly in @repo/elevasis-core canonicalOrganizationModel
|
|
63
|
+
// at 'sales.crm:catalog/crm.pipeline'. No bridge injection needed — the model self-supplies it.
|
|
105
64
|
if (baseCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] === undefined) {
|
|
106
65
|
bridgeCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] = createLeadGenStageCatalog(model)
|
|
107
66
|
}
|
|
@@ -120,9 +79,7 @@ function mergeBridgeCatalogs(model: OrganizationModel): OrganizationModel {
|
|
|
120
79
|
}
|
|
121
80
|
}
|
|
122
81
|
|
|
123
|
-
function compileBusinessOntologyValidationIndex(
|
|
124
|
-
model: OrganizationModel = DEFAULT_ORGANIZATION_MODEL
|
|
125
|
-
): BusinessOntologyValidationIndex {
|
|
82
|
+
export function compileBusinessOntologyValidationIndex(model: OrganizationModel): BusinessOntologyValidationIndex {
|
|
126
83
|
const compilation = compileOrganizationOntology(mergeBridgeCatalogs(model))
|
|
127
84
|
if (compilation.diagnostics.length > 0) {
|
|
128
85
|
const summary = compilation.diagnostics.map((diagnostic) => diagnostic.message).join('; ')
|
|
@@ -156,34 +113,18 @@ function indexActionTypesByLegacyId(
|
|
|
156
113
|
return byLegacyId
|
|
157
114
|
}
|
|
158
115
|
|
|
159
|
-
export const BUSINESS_ONTOLOGY_VALIDATION_INDEX = compileBusinessOntologyValidationIndex()
|
|
160
|
-
|
|
161
116
|
function getCatalogEntries(catalog: OntologyCatalogType): Record<string, unknown> {
|
|
162
117
|
return isPlainRecord(catalog.entries) ? catalog.entries : {}
|
|
163
118
|
}
|
|
164
119
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const color = typeof record.color === 'string' ? record.color : undefined
|
|
170
|
-
const rawStates = Array.isArray(record.states) ? record.states : []
|
|
171
|
-
const states = rawStates.flatMap((state): StatefulStateDefinition[] => {
|
|
172
|
-
if (!isPlainRecord(state) || typeof state.stateKey !== 'string') return []
|
|
173
|
-
return [
|
|
174
|
-
{
|
|
175
|
-
stateKey: state.stateKey,
|
|
176
|
-
label: typeof state.label === 'string' ? state.label : state.stateKey
|
|
177
|
-
}
|
|
178
|
-
]
|
|
179
|
-
})
|
|
120
|
+
function asStatefulStateDefinition(value: unknown): StatefulStateDefinition | undefined {
|
|
121
|
+
if (!isPlainRecord(value) || typeof value.stateKey !== 'string' || typeof value.label !== 'string') {
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
180
124
|
|
|
181
125
|
return {
|
|
182
|
-
|
|
183
|
-
label
|
|
184
|
-
order,
|
|
185
|
-
...(color !== undefined ? { color } : {}),
|
|
186
|
-
states
|
|
126
|
+
stateKey: value.stateKey,
|
|
127
|
+
label: value.label
|
|
187
128
|
}
|
|
188
129
|
}
|
|
189
130
|
|
|
@@ -211,99 +152,72 @@ function asLeadGenStageEntry(key: string, value: unknown): LeadGenStageCatalogEn
|
|
|
211
152
|
}
|
|
212
153
|
}
|
|
213
154
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
export function getCrmStatesForStage(stageKey: string): StatefulStateDefinition[] {
|
|
231
|
-
const entry = getCatalogEntries(BUSINESS_ONTOLOGY_VALIDATION_INDEX.crmPipelineCatalog)[stageKey]
|
|
232
|
-
return entry === undefined ? [] : asCrmStageEntry(stageKey, entry).states
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function isCrmStageKey(stageKey: string): boolean {
|
|
236
|
-
return Object.prototype.hasOwnProperty.call(
|
|
237
|
-
getCatalogEntries(BUSINESS_ONTOLOGY_VALIDATION_INDEX.crmPipelineCatalog),
|
|
238
|
-
stageKey
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function isCrmStateKey(stateKey: string): boolean {
|
|
243
|
-
return CRM_STATE_KEYS_FROM_ONTOLOGY.includes(stateKey)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function getLeadGenStageCatalogFromOntology(): Record<string, LeadGenStageCatalogEntry> {
|
|
247
|
-
return Object.fromEntries(
|
|
248
|
-
Object.entries(getCatalogEntries(BUSINESS_ONTOLOGY_VALIDATION_INDEX.leadGenStageCatalog)).map(([key, value]) => [
|
|
249
|
-
key,
|
|
250
|
-
asLeadGenStageEntry(key, value)
|
|
251
|
-
])
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export function getLeadGenStageEntry(stageKey: string): LeadGenStageCatalogEntry | undefined {
|
|
256
|
-
return getLeadGenStageCatalogFromOntology()[stageKey]
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function isLeadGenStageKey(stageKey: string): boolean {
|
|
260
|
-
return getLeadGenStageEntry(stageKey) !== undefined
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function isLeadGenStageValidForEntity(stageKey: string, entity: 'company' | 'contact'): boolean {
|
|
264
|
-
const stage = getLeadGenStageEntry(stageKey)
|
|
265
|
-
return stage !== undefined && (stage.entity === entity || stage.additionalEntities?.includes(entity) === true)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function isLeadGenRecordStageValidForEntity(stageKey: string, entity: 'company' | 'contact'): boolean {
|
|
269
|
-
const stage = getLeadGenStageEntry(stageKey)
|
|
270
|
-
return (
|
|
271
|
-
stage !== undefined &&
|
|
272
|
-
(stage.entity === entity || stage.additionalEntities?.includes(entity) === true || stage.recordEntity === entity)
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function resolveLeadGenRecordStageKey(stageKey: string, entity: 'company' | 'contact'): string {
|
|
277
|
-
const stage = getLeadGenStageEntry(stageKey)
|
|
278
|
-
return stage?.recordEntity === entity && stage.recordStageKey ? stage.recordStageKey : stageKey
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export type BusinessActionMetadata = {
|
|
282
|
-
id: string
|
|
283
|
-
label: string
|
|
284
|
-
resourceId?: string
|
|
155
|
+
/**
|
|
156
|
+
* Returns the valid states for a given CRM stage key.
|
|
157
|
+
* Resolves against the caller-supplied ontology validation index so generic
|
|
158
|
+
* core does not materialize an Elevasis CRM pipeline singleton.
|
|
159
|
+
*/
|
|
160
|
+
export function getCrmStatesForStage(
|
|
161
|
+
index: BusinessOntologyValidationIndex,
|
|
162
|
+
stageKey: string
|
|
163
|
+
): StatefulStateDefinition[] {
|
|
164
|
+
const stage = getCatalogEntries(index.crmPipelineCatalog)[stageKey]
|
|
165
|
+
const states = isPlainRecord(stage) && Array.isArray(stage.states) ? stage.states : []
|
|
166
|
+
return states.flatMap((state) => {
|
|
167
|
+
const parsed = asStatefulStateDefinition(state)
|
|
168
|
+
return parsed ? [parsed] : []
|
|
169
|
+
})
|
|
285
170
|
}
|
|
286
171
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Lead-gen stage validators bound to a specific resolved validation index.
|
|
174
|
+
*
|
|
175
|
+
* The lead-gen stage catalog is tenant-model-dependent (it derives from the
|
|
176
|
+
* organization model's `sales.lead-gen` stage records). Hosts that own a
|
|
177
|
+
* populated tenant model (e.g. `apps/api` with the Elevasis canonical model)
|
|
178
|
+
* MUST build their own index via `compileBusinessOntologyValidationIndex(model)`
|
|
179
|
+
* and call `createLeadGenStageValidators(index)`. `@repo/core` stays generic —
|
|
180
|
+
* it must never import a tenant model.
|
|
181
|
+
*/
|
|
182
|
+
export type LeadGenStageValidators = {
|
|
183
|
+
getLeadGenStageCatalog(): Record<string, LeadGenStageCatalogEntry>
|
|
184
|
+
getLeadGenStageEntry(stageKey: string): LeadGenStageCatalogEntry | undefined
|
|
185
|
+
isLeadGenStageKey(stageKey: string): boolean
|
|
186
|
+
isLeadGenStageValidForEntity(stageKey: string, entity: 'company' | 'contact'): boolean
|
|
187
|
+
isLeadGenRecordStageValidForEntity(stageKey: string, entity: 'company' | 'contact'): boolean
|
|
188
|
+
resolveLeadGenRecordStageKey(stageKey: string, entity: 'company' | 'contact'): string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createLeadGenStageValidators(index: BusinessOntologyValidationIndex): LeadGenStageValidators {
|
|
192
|
+
const getCatalog = (): Record<string, LeadGenStageCatalogEntry> =>
|
|
193
|
+
Object.fromEntries(
|
|
194
|
+
Object.entries(getCatalogEntries(index.leadGenStageCatalog)).map(([key, value]) => [
|
|
195
|
+
key,
|
|
196
|
+
asLeadGenStageEntry(key, value)
|
|
197
|
+
])
|
|
198
|
+
)
|
|
199
|
+
const getEntry = (stageKey: string): LeadGenStageCatalogEntry | undefined => getCatalog()[stageKey]
|
|
290
200
|
|
|
291
|
-
const resourceId = actionType['resourceId']
|
|
292
201
|
return {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
202
|
+
getLeadGenStageCatalog: getCatalog,
|
|
203
|
+
getLeadGenStageEntry: getEntry,
|
|
204
|
+
isLeadGenStageKey: (stageKey) => getEntry(stageKey) !== undefined,
|
|
205
|
+
isLeadGenStageValidForEntity: (stageKey, entity) => {
|
|
206
|
+
const stage = getEntry(stageKey)
|
|
207
|
+
return stage !== undefined && (stage.entity === entity || stage.additionalEntities?.includes(entity) === true)
|
|
208
|
+
},
|
|
209
|
+
isLeadGenRecordStageValidForEntity: (stageKey, entity) => {
|
|
210
|
+
const stage = getEntry(stageKey)
|
|
211
|
+
return (
|
|
212
|
+
stage !== undefined &&
|
|
213
|
+
(stage.entity === entity ||
|
|
214
|
+
stage.additionalEntities?.includes(entity) === true ||
|
|
215
|
+
stage.recordEntity === entity)
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
resolveLeadGenRecordStageKey: (stageKey, entity) => {
|
|
219
|
+
const stage = getEntry(stageKey)
|
|
220
|
+
return stage?.recordEntity === entity && stage.recordStageKey ? stage.recordStageKey : stageKey
|
|
221
|
+
}
|
|
296
222
|
}
|
|
297
223
|
}
|
|
298
|
-
|
|
299
|
-
export function getCrmActionMetadata(actionKey: string): BusinessActionMetadata | undefined {
|
|
300
|
-
const actionType = BUSINESS_ONTOLOGY_VALIDATION_INDEX.actionTypesByLegacyId[actionKey]
|
|
301
|
-
return actionType?.ownerSystemId === 'sales.crm' ? getActionMetadata(actionKey) : undefined
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function isLeadGenActionKey(actionKey: string): boolean {
|
|
305
|
-
return (
|
|
306
|
-
actionKey.startsWith('lead-gen.') &&
|
|
307
|
-
BUSINESS_ONTOLOGY_VALIDATION_INDEX.actionTypesByLegacyId[actionKey] !== undefined
|
|
308
|
-
)
|
|
309
|
-
}
|
|
@@ -35,7 +35,7 @@ export function investmentSection(config: InvestmentConfig): Page {
|
|
|
35
35
|
tier,
|
|
36
36
|
customTierDescription,
|
|
37
37
|
nextSteps = DEFAULT_NEXT_STEPS,
|
|
38
|
-
contactCta = 'Questions? Reply to this email or
|
|
38
|
+
contactCta = 'Questions? Reply to this email or use the contact link provided by your advisor'
|
|
39
39
|
} = config
|
|
40
40
|
|
|
41
41
|
const tierDescription = tier === 'custom' && customTierDescription
|
|
@@ -42,7 +42,7 @@ export function summaryInvestmentSection(config: SummaryInvestmentConfig): Page
|
|
|
42
42
|
initialFee,
|
|
43
43
|
monthlyFee,
|
|
44
44
|
nextSteps = DEFAULT_NEXT_STEPS,
|
|
45
|
-
contactCta = 'Questions? Reply to this email or
|
|
45
|
+
contactCta = 'Questions? Reply to this email or use the contact link provided by your advisor'
|
|
46
46
|
} = config
|
|
47
47
|
|
|
48
48
|
// Calculate totals
|