@elevasis/core 0.19.0 → 0.20.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.js +62 -0
- package/dist/knowledge/index.d.ts +1 -1
- package/dist/organization-model/index.js +62 -0
- package/dist/test-utils/index.js +61 -0
- package/package.json +3 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +11 -4
- package/src/business/acquisition/api-schemas.test.ts +100 -14
- package/src/business/acquisition/api-schemas.ts +36 -9
- package/src/business/acquisition/derive-actions.test.ts +1 -1
- package/src/business/acquisition/types.ts +2 -2
- package/src/business/deals/api-schemas.ts +2 -2
- package/src/organization-model/__tests__/graph.test.ts +108 -2
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +6 -9
- package/src/organization-model/__tests__/schema.test.ts +122 -0
- package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
- package/src/organization-model/domains/prospecting.ts +91 -16
- package/src/organization-model/domains/sales.ts +8 -5
- package/src/organization-model/graph/build.ts +74 -0
- package/src/organization-model/graph/schema.ts +1 -0
- package/src/organization-model/graph/types.ts +1 -0
- package/src/organization-model/schema.ts +63 -0
- package/src/organization-model/surface-projection.ts +218 -0
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +11 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from '../../supabase/database.types'
|
|
2
|
-
import type {
|
|
2
|
+
import type { Capability } from '../../organization-model/domains/prospecting'
|
|
3
3
|
import type { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
|
|
4
4
|
import type { PipelineStage, ProcessingStageStatus } from './api-schemas'
|
|
5
5
|
|
|
@@ -52,7 +52,7 @@ export interface WebPost {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export type LeadGenStageKey = (typeof LEAD_GEN_STAGE_CATALOG)[keyof typeof LEAD_GEN_STAGE_CATALOG]['key']
|
|
55
|
-
export type LeadGenCapabilityKey =
|
|
55
|
+
export type LeadGenCapabilityKey = Capability['id']
|
|
56
56
|
|
|
57
57
|
export interface ProcessingStateEntry {
|
|
58
58
|
status: ProcessingStageStatus
|
|
@@ -27,7 +27,7 @@ export {
|
|
|
27
27
|
// Request body schemas
|
|
28
28
|
CreateDealNoteRequestSchema,
|
|
29
29
|
CreateDealTaskRequestSchema,
|
|
30
|
-
TransitionItemRequestSchema,
|
|
30
|
+
CrmTransitionItemRequestSchema as TransitionItemRequestSchema,
|
|
31
31
|
TransitionDealStateRequestSchema,
|
|
32
32
|
ExecuteActionParamsSchema,
|
|
33
33
|
ExecuteActionRequestSchema,
|
|
@@ -61,7 +61,7 @@ export {
|
|
|
61
61
|
type ListDealTasksDueQuery,
|
|
62
62
|
type CreateDealNoteRequest,
|
|
63
63
|
type CreateDealTaskRequest,
|
|
64
|
-
type TransitionItemRequest,
|
|
64
|
+
type CrmTransitionItemRequest as TransitionItemRequest,
|
|
65
65
|
type TransitionDealStateRequest,
|
|
66
66
|
type ExecuteActionParams,
|
|
67
67
|
type ExecuteActionRequest,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { buildOrganizationGraph } from '../graph/build'
|
|
3
|
+
import { CAPABILITY_REGISTRY } from '../domains/prospecting'
|
|
3
4
|
import { resolveOrganizationModel } from '../resolve'
|
|
4
5
|
|
|
5
6
|
describe('organization graph', () => {
|
|
@@ -11,7 +12,9 @@ describe('organization graph', () => {
|
|
|
11
12
|
sourceId: 'sales.crm',
|
|
12
13
|
label: 'CRM'
|
|
13
14
|
})
|
|
14
|
-
expect(
|
|
15
|
+
expect(
|
|
16
|
+
graph.edges.find((edge) => edge.sourceId === 'feature:sales' && edge.targetId === 'feature:sales.crm')
|
|
17
|
+
).toMatchObject({
|
|
15
18
|
kind: 'contains'
|
|
16
19
|
})
|
|
17
20
|
})
|
|
@@ -74,9 +77,112 @@ describe('organization graph', () => {
|
|
|
74
77
|
kind: 'resource',
|
|
75
78
|
label: 'missing-resource'
|
|
76
79
|
})
|
|
77
|
-
expect(graph.edges.find((edge) => edge.
|
|
80
|
+
expect(graph.edges.find((edge) => edge.kind === 'references' && edge.relationshipType === 'uses')).toMatchObject({
|
|
78
81
|
kind: 'references',
|
|
79
82
|
relationshipType: 'uses'
|
|
80
83
|
})
|
|
81
84
|
})
|
|
85
|
+
|
|
86
|
+
it('projects stage nodes from prospecting lifecycle stages', () => {
|
|
87
|
+
const graph = buildOrganizationGraph({
|
|
88
|
+
organizationModel: resolveOrganizationModel({
|
|
89
|
+
features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const stageNodes = graph.nodes.filter((node) => node.kind === 'stage')
|
|
94
|
+
expect(stageNodes.length).toBeGreaterThan(0)
|
|
95
|
+
|
|
96
|
+
const populated = graph.nodes.find((node) => node.id === 'stage:populated')
|
|
97
|
+
expect(populated).toMatchObject({ kind: 'stage', sourceId: 'populated' })
|
|
98
|
+
expect(populated?.label).toBeTruthy()
|
|
99
|
+
|
|
100
|
+
expect(graph.edges.find((edge) => edge.kind === 'contains' && edge.targetId === 'stage:populated')).toMatchObject({
|
|
101
|
+
sourceId: 'organization-model'
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('projects capability nodes from CAPABILITY_REGISTRY with maps_to edges to resources', () => {
|
|
106
|
+
const graph = buildOrganizationGraph({
|
|
107
|
+
organizationModel: resolveOrganizationModel({
|
|
108
|
+
features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const capabilityNodes = graph.nodes.filter((node) => node.kind === 'capability')
|
|
113
|
+
expect(capabilityNodes).toHaveLength(CAPABILITY_REGISTRY.length)
|
|
114
|
+
|
|
115
|
+
const sample = CAPABILITY_REGISTRY[0]
|
|
116
|
+
const node = graph.nodes.find((n) => n.id === `capability:${sample.id}`)
|
|
117
|
+
expect(node).toMatchObject({
|
|
118
|
+
kind: 'capability',
|
|
119
|
+
sourceId: sample.id,
|
|
120
|
+
label: sample.label,
|
|
121
|
+
description: sample.description
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
graph.edges.find(
|
|
126
|
+
(edge) =>
|
|
127
|
+
edge.kind === 'maps_to' &&
|
|
128
|
+
edge.sourceId === `capability:${sample.id}` &&
|
|
129
|
+
edge.targetId === `resource:${sample.resourceId}`
|
|
130
|
+
)
|
|
131
|
+
).toBeDefined()
|
|
132
|
+
|
|
133
|
+
expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('emits uses edges from stages to capabilities for every prospecting build-template step', () => {
|
|
137
|
+
const model = resolveOrganizationModel({
|
|
138
|
+
features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
|
|
139
|
+
})
|
|
140
|
+
const graph = buildOrganizationGraph({ organizationModel: model })
|
|
141
|
+
|
|
142
|
+
for (const template of model.prospecting.buildTemplates) {
|
|
143
|
+
for (const step of template.steps) {
|
|
144
|
+
const stageNodeId = `stage:${step.stageKey}`
|
|
145
|
+
const capNodeId = `capability:${step.capabilityKey}`
|
|
146
|
+
expect(
|
|
147
|
+
graph.edges.find(
|
|
148
|
+
(edge) =>
|
|
149
|
+
edge.kind === 'uses' &&
|
|
150
|
+
edge.sourceId === stageNodeId &&
|
|
151
|
+
edge.targetId === capNodeId &&
|
|
152
|
+
edge.id.includes(step.id)
|
|
153
|
+
),
|
|
154
|
+
`${template.id}.${step.id}: ${stageNodeId} uses ${capNodeId}`
|
|
155
|
+
).toBeDefined()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('emits references edges between stages for every step dependsOn link', () => {
|
|
161
|
+
const model = resolveOrganizationModel({
|
|
162
|
+
features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
|
|
163
|
+
})
|
|
164
|
+
const graph = buildOrganizationGraph({ organizationModel: model })
|
|
165
|
+
|
|
166
|
+
for (const template of model.prospecting.buildTemplates) {
|
|
167
|
+
const stepById = new Map(template.steps.map((s) => [s.id, s]))
|
|
168
|
+
for (const step of template.steps) {
|
|
169
|
+
for (const depId of step.dependsOn ?? []) {
|
|
170
|
+
const depStep = stepById.get(depId)
|
|
171
|
+
if (!depStep) continue
|
|
172
|
+
const stageNodeId = `stage:${step.stageKey}`
|
|
173
|
+
const depStageNodeId = `stage:${depStep.stageKey}`
|
|
174
|
+
expect(
|
|
175
|
+
graph.edges.find(
|
|
176
|
+
(edge) =>
|
|
177
|
+
edge.kind === 'references' &&
|
|
178
|
+
edge.sourceId === stageNodeId &&
|
|
179
|
+
edge.targetId === depStageNodeId &&
|
|
180
|
+
edge.id.includes(step.id)
|
|
181
|
+
),
|
|
182
|
+
`${template.id}.${step.id} dependsOn ${depId}`
|
|
183
|
+
).toBeDefined()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})
|
|
82
188
|
})
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { LEAD_GEN_STAGE_CATALOG } from '../domains/sales'
|
|
3
|
-
import {
|
|
4
|
-
CAPABILITY_REGISTRY,
|
|
5
|
-
DEFAULT_ORGANIZATION_MODEL_PROSPECTING,
|
|
6
|
-
PROSPECTING_STEPS
|
|
7
|
-
} from '../domains/prospecting'
|
|
3
|
+
import { CAPABILITY_REGISTRY, DEFAULT_ORGANIZATION_MODEL_PROSPECTING, PROSPECTING_STEPS } from '../domains/prospecting'
|
|
8
4
|
|
|
9
|
-
const
|
|
5
|
+
const EXPECTED_CAPABILITY_RESOURCE_BY_ID: Record<string, string> = {
|
|
10
6
|
'lead-gen.company.source': 'lgn-import-workflow',
|
|
11
7
|
'lead-gen.company.apollo-import': 'lgn-01c-apollo-import-workflow',
|
|
12
8
|
'lead-gen.contact.discover': 'lgn-04-email-discovery-workflow',
|
|
@@ -56,7 +52,7 @@ describe('prospecting organization-model SSOT', () => {
|
|
|
56
52
|
})
|
|
57
53
|
|
|
58
54
|
it('uses registered capabilities for every prospecting step', () => {
|
|
59
|
-
const capabilityKeys = new Set(
|
|
55
|
+
const capabilityKeys = new Set(CAPABILITY_REGISTRY.map((c) => c.id))
|
|
60
56
|
|
|
61
57
|
for (const templateSteps of Object.values(PROSPECTING_STEPS)) {
|
|
62
58
|
for (const step of Object.values(templateSteps)) {
|
|
@@ -88,7 +84,8 @@ describe('prospecting organization-model SSOT', () => {
|
|
|
88
84
|
})
|
|
89
85
|
|
|
90
86
|
it('covers all known lead-gen capability registry entries', () => {
|
|
91
|
-
expect(CAPABILITY_REGISTRY).
|
|
92
|
-
|
|
87
|
+
expect(CAPABILITY_REGISTRY).toHaveLength(12)
|
|
88
|
+
const actualResourceById = Object.fromEntries(CAPABILITY_REGISTRY.map((c) => [c.id, c.resourceId]))
|
|
89
|
+
expect(actualResourceById).toEqual(EXPECTED_CAPABILITY_RESOURCE_BY_ID)
|
|
93
90
|
})
|
|
94
91
|
})
|
|
@@ -14,6 +14,20 @@ function makeFeature(id: string, path = `/${id.replaceAll('.', '/')}`) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function makeSurface(id: string, featureId?: string, featureIds: string[] = []) {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
label: id,
|
|
21
|
+
path: `/${id.replaceAll('.', '/')}`,
|
|
22
|
+
surfaceType: 'page' as const,
|
|
23
|
+
featureId,
|
|
24
|
+
featureIds,
|
|
25
|
+
entityIds: [],
|
|
26
|
+
resourceIds: [],
|
|
27
|
+
capabilityIds: []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
function makeMinimalModel(features: unknown[] = []) {
|
|
18
32
|
return {
|
|
19
33
|
version: 1 as const,
|
|
@@ -48,6 +62,22 @@ describe('flat feature tree validation', () => {
|
|
|
48
62
|
expect(messages.some((message) => message.includes('Feature id "sales" must be unique'))).toBe(true)
|
|
49
63
|
})
|
|
50
64
|
|
|
65
|
+
it('rejects duplicate effective feature paths when explicit and default paths collide', () => {
|
|
66
|
+
const messages = getIssueMessages(
|
|
67
|
+
makeMinimalModel([
|
|
68
|
+
{ id: 'sales', label: 'Sales', enabled: true },
|
|
69
|
+
makeFeature('sales.crm', '/sales/lead-gen'),
|
|
70
|
+
{ id: 'sales.lead-gen', label: 'Lead Gen', enabled: true }
|
|
71
|
+
])
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
messages.some((message) =>
|
|
76
|
+
message.includes('Feature "sales.lead-gen" effective path "/sales/lead-gen" duplicates feature "sales.crm"')
|
|
77
|
+
)
|
|
78
|
+
).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
51
81
|
it('rejects a dotted child when its immediate parent is missing', () => {
|
|
52
82
|
const messages = getIssueMessages(makeMinimalModel([makeFeature('sales.crm.pipeline')]))
|
|
53
83
|
|
|
@@ -80,4 +110,96 @@ describe('flat feature tree validation', () => {
|
|
|
80
110
|
expect(model.navigation.groups).toEqual([])
|
|
81
111
|
expect('resourceMappings' in model).toBe(false)
|
|
82
112
|
})
|
|
113
|
+
|
|
114
|
+
it('rejects an unknown navigation default surface when surfaces are explicitly empty', () => {
|
|
115
|
+
const messages = getIssueMessages({
|
|
116
|
+
...makeMinimalModel([makeFeature('dashboard', '/')]),
|
|
117
|
+
navigation: {
|
|
118
|
+
defaultSurfaceId: 'missing.surface',
|
|
119
|
+
surfaces: [],
|
|
120
|
+
groups: []
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
messages.some((message) =>
|
|
126
|
+
message.includes('Navigation defaultSurfaceId references unknown surface "missing.surface"')
|
|
127
|
+
)
|
|
128
|
+
).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('rejects navigation group surfaceIds that reference missing surfaces', () => {
|
|
132
|
+
const messages = getIssueMessages({
|
|
133
|
+
...makeMinimalModel([makeFeature('dashboard', '/')]),
|
|
134
|
+
navigation: {
|
|
135
|
+
surfaces: [makeSurface('dashboard.home', 'dashboard')],
|
|
136
|
+
groups: [
|
|
137
|
+
{
|
|
138
|
+
id: 'primary',
|
|
139
|
+
label: 'Primary',
|
|
140
|
+
placement: 'primary',
|
|
141
|
+
surfaceIds: ['missing.surface']
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(
|
|
148
|
+
messages.some((message) =>
|
|
149
|
+
message.includes('Navigation group "primary" references unknown surface "missing.surface"')
|
|
150
|
+
)
|
|
151
|
+
).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('rejects navigation surface featureId references to missing features', () => {
|
|
155
|
+
const messages = getIssueMessages({
|
|
156
|
+
...makeMinimalModel([makeFeature('dashboard', '/')]),
|
|
157
|
+
navigation: {
|
|
158
|
+
surfaces: [makeSurface('dashboard.home', 'missing.feature')],
|
|
159
|
+
groups: []
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(
|
|
164
|
+
messages.some((message) =>
|
|
165
|
+
message.includes('Navigation surface "dashboard.home" references unknown feature "missing.feature"')
|
|
166
|
+
)
|
|
167
|
+
).toBe(true)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('rejects navigation surface featureIds references to missing features', () => {
|
|
171
|
+
const messages = getIssueMessages({
|
|
172
|
+
...makeMinimalModel([makeFeature('dashboard', '/')]),
|
|
173
|
+
navigation: {
|
|
174
|
+
surfaces: [makeSurface('dashboard.home', 'dashboard', ['dashboard', 'missing.feature'])],
|
|
175
|
+
groups: []
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
messages.some((message) =>
|
|
181
|
+
message.includes('Navigation surface "dashboard.home" references unknown feature "missing.feature"')
|
|
182
|
+
)
|
|
183
|
+
).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('allows legacy navigation feature aliases when canonical features exist', () => {
|
|
187
|
+
const result = OrganizationModelSchema.safeParse({
|
|
188
|
+
...makeMinimalModel([
|
|
189
|
+
{ id: 'sales', label: 'Sales', enabled: true },
|
|
190
|
+
makeFeature('sales.crm', '/sales/crm/pipeline'),
|
|
191
|
+
makeFeature('sales.lead-gen', '/lead-gen/lists')
|
|
192
|
+
]),
|
|
193
|
+
navigation: {
|
|
194
|
+
defaultSurfaceId: 'crm.pipeline',
|
|
195
|
+
surfaces: [
|
|
196
|
+
makeSurface('crm.pipeline', 'crm', ['crm']),
|
|
197
|
+
makeSurface('lead-gen.lists', 'lead-gen', ['lead-gen'])
|
|
198
|
+
],
|
|
199
|
+
groups: []
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(true)
|
|
204
|
+
})
|
|
83
205
|
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
|
|
3
|
+
import {
|
|
4
|
+
projectOrganizationSurfaces,
|
|
5
|
+
validateOrganizationSurfaceProjection
|
|
6
|
+
} from '../surface-projection'
|
|
7
|
+
import type { OrganizationModel, OrganizationModelFeature, OrganizationModelSurface } from '../types'
|
|
8
|
+
|
|
9
|
+
function makeFeature(
|
|
10
|
+
id: string,
|
|
11
|
+
overrides: Partial<OrganizationModelFeature> = {}
|
|
12
|
+
): OrganizationModelFeature {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
label: id,
|
|
16
|
+
enabled: true,
|
|
17
|
+
...overrides
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeSurface(
|
|
22
|
+
id: string,
|
|
23
|
+
overrides: Partial<OrganizationModelSurface> = {}
|
|
24
|
+
): OrganizationModelSurface {
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
label: id,
|
|
28
|
+
path: `/${id.replaceAll('.', '/')}`,
|
|
29
|
+
surfaceType: 'page',
|
|
30
|
+
enabled: true,
|
|
31
|
+
featureIds: [],
|
|
32
|
+
entityIds: [],
|
|
33
|
+
resourceIds: [],
|
|
34
|
+
capabilityIds: [],
|
|
35
|
+
...overrides
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeModel(
|
|
40
|
+
features: OrganizationModelFeature[],
|
|
41
|
+
surfaces: OrganizationModelSurface[],
|
|
42
|
+
navigation: Partial<OrganizationModel['navigation']> = {}
|
|
43
|
+
): OrganizationModel {
|
|
44
|
+
return {
|
|
45
|
+
...DEFAULT_ORGANIZATION_MODEL,
|
|
46
|
+
features,
|
|
47
|
+
navigation: {
|
|
48
|
+
surfaces,
|
|
49
|
+
groups: [],
|
|
50
|
+
...navigation
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('projectOrganizationSurfaces', () => {
|
|
56
|
+
it('projects navigation surfaces into data-only DTOs with inherited flags', () => {
|
|
57
|
+
const model = makeModel(
|
|
58
|
+
[
|
|
59
|
+
makeFeature('sales', { requiresAdmin: true }),
|
|
60
|
+
makeFeature('sales.crm', { devOnly: true })
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
makeSurface('crm.pipeline', {
|
|
64
|
+
label: 'Pipeline',
|
|
65
|
+
path: '/crm/pipeline',
|
|
66
|
+
surfaceType: 'graph',
|
|
67
|
+
featureId: 'sales.crm',
|
|
68
|
+
featureIds: ['sales.crm'],
|
|
69
|
+
entityIds: ['crm.deal'],
|
|
70
|
+
resourceIds: ['workflow.crm-sync'],
|
|
71
|
+
capabilityIds: ['crm.pipeline.manage']
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(projectOrganizationSurfaces(model)).toEqual([
|
|
77
|
+
{
|
|
78
|
+
id: 'crm.pipeline',
|
|
79
|
+
label: 'Pipeline',
|
|
80
|
+
path: '/crm/pipeline',
|
|
81
|
+
surfaceType: 'graph',
|
|
82
|
+
featureId: 'sales.crm',
|
|
83
|
+
featureIds: ['sales.crm'],
|
|
84
|
+
entityIds: ['crm.deal'],
|
|
85
|
+
resourceIds: ['workflow.crm-sync'],
|
|
86
|
+
capabilityIds: ['crm.pipeline.manage'],
|
|
87
|
+
enabled: true,
|
|
88
|
+
devOnly: true,
|
|
89
|
+
requiresAdmin: true
|
|
90
|
+
}
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('canonicalizes legacy feature aliases', () => {
|
|
95
|
+
const model = makeModel(
|
|
96
|
+
[
|
|
97
|
+
makeFeature('sales'),
|
|
98
|
+
makeFeature('sales.crm'),
|
|
99
|
+
makeFeature('sales.lead-gen'),
|
|
100
|
+
makeFeature('monitoring'),
|
|
101
|
+
makeFeature('monitoring.submitted-requests')
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
makeSurface('legacy.sales', {
|
|
105
|
+
featureId: 'crm',
|
|
106
|
+
featureIds: ['crm', 'lead-gen', 'submitted-requests']
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(projectOrganizationSurfaces(model)[0]).toMatchObject({
|
|
112
|
+
featureId: 'sales.crm',
|
|
113
|
+
featureIds: ['sales.crm', 'sales.lead-gen', 'monitoring.submitted-requests']
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('disables projected surfaces when feature lineage is disabled', () => {
|
|
118
|
+
const model = makeModel(
|
|
119
|
+
[
|
|
120
|
+
makeFeature('sales', { enabled: false }),
|
|
121
|
+
makeFeature('sales.crm')
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
makeSurface('crm.pipeline', {
|
|
125
|
+
enabled: true,
|
|
126
|
+
featureId: 'sales.crm',
|
|
127
|
+
featureIds: ['sales.crm']
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
expect(projectOrganizationSurfaces(model)[0].enabled).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('validateOrganizationSurfaceProjection', () => {
|
|
137
|
+
it('returns issue codes for duplicate and unknown surface references', () => {
|
|
138
|
+
const model = makeModel(
|
|
139
|
+
[makeFeature('sales'), makeFeature('sales.crm')],
|
|
140
|
+
[
|
|
141
|
+
makeSurface('crm.pipeline', {
|
|
142
|
+
path: '/crm/pipeline',
|
|
143
|
+
featureId: 'sales.crm',
|
|
144
|
+
featureIds: ['sales.crm']
|
|
145
|
+
}),
|
|
146
|
+
makeSurface('crm.pipeline', {
|
|
147
|
+
path: '/crm/pipeline/',
|
|
148
|
+
featureId: 'missing.primary',
|
|
149
|
+
featureIds: ['missing.related']
|
|
150
|
+
})
|
|
151
|
+
],
|
|
152
|
+
{
|
|
153
|
+
defaultSurfaceId: 'missing.default',
|
|
154
|
+
groups: [
|
|
155
|
+
{
|
|
156
|
+
id: 'primary',
|
|
157
|
+
label: 'Primary',
|
|
158
|
+
placement: 'primary',
|
|
159
|
+
surfaceIds: ['missing.group']
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
expect(validateOrganizationSurfaceProjection(model).map((issue) => issue.code)).toEqual([
|
|
166
|
+
'duplicate-surface-id',
|
|
167
|
+
'duplicate-surface-path',
|
|
168
|
+
'unknown-surface-feature',
|
|
169
|
+
'unknown-surface-feature-reference',
|
|
170
|
+
'unknown-default-surface',
|
|
171
|
+
'unknown-group-surface'
|
|
172
|
+
])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -30,22 +30,95 @@ export const ProspectingBuildTemplateSchema = DisplayMetadataSchema.extend({
|
|
|
30
30
|
export type ListBuilderStep = z.infer<typeof ProspectingBuildTemplateStepSchema>
|
|
31
31
|
export type TemplateName = 'localServices' | 'dtcApolloClickup'
|
|
32
32
|
export type StepName = string
|
|
33
|
-
export type CapabilityRegistry = Record<string, string>
|
|
34
33
|
|
|
35
|
-
export const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
export const CapabilitySchema = z.object({
|
|
35
|
+
id: ModelIdSchema,
|
|
36
|
+
label: z.string(),
|
|
37
|
+
description: z.string(),
|
|
38
|
+
resourceId: ModelIdSchema
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export type Capability = z.infer<typeof CapabilitySchema>
|
|
42
|
+
export type CapabilityRegistry = Capability[]
|
|
43
|
+
|
|
44
|
+
export const CAPABILITY_REGISTRY: CapabilityRegistry = [
|
|
45
|
+
{
|
|
46
|
+
id: 'lead-gen.company.source',
|
|
47
|
+
label: 'Source companies',
|
|
48
|
+
description: 'Import source companies from a list provider.',
|
|
49
|
+
resourceId: 'lgn-import-workflow'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'lead-gen.company.apollo-import',
|
|
53
|
+
label: 'Import from Apollo',
|
|
54
|
+
description: 'Pull companies and seed contact data from an Apollo search or list.',
|
|
55
|
+
resourceId: 'lgn-01c-apollo-import-workflow'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'lead-gen.contact.discover',
|
|
59
|
+
label: 'Discover contact emails',
|
|
60
|
+
description: 'Find email addresses for contacts at qualified companies.',
|
|
61
|
+
resourceId: 'lgn-04-email-discovery-workflow'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'lead-gen.contact.verify-email',
|
|
65
|
+
label: 'Verify emails',
|
|
66
|
+
description: 'Check email deliverability before outreach.',
|
|
67
|
+
resourceId: 'lgn-05-email-verification-workflow'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'lead-gen.company.website-extract',
|
|
71
|
+
label: 'Extract website signals',
|
|
72
|
+
description: 'Scrape and analyze company websites for qualification signals.',
|
|
73
|
+
resourceId: 'lgn-02-website-extract-workflow'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'lead-gen.company.qualify',
|
|
77
|
+
label: 'Qualify companies',
|
|
78
|
+
description: 'Score and filter companies against the ICP rubric.',
|
|
79
|
+
resourceId: 'lgn-03-company-qualification-workflow'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'lead-gen.company.dtc-subscription-qualify',
|
|
83
|
+
label: 'Qualify DTC subscription fit',
|
|
84
|
+
description: 'Classify subscription potential and consumable-product fit for DTC brands.',
|
|
85
|
+
resourceId: 'lgn-03b-dtc-subscription-score-workflow'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'lead-gen.contact.apollo-decision-maker-enrich',
|
|
89
|
+
label: 'Enrich decision-makers',
|
|
90
|
+
description: 'Find and enrich qualified contacts at qualified companies via Apollo.',
|
|
91
|
+
resourceId: 'lgn-04b-apollo-decision-maker-enrich-workflow'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'lead-gen.contact.personalize',
|
|
95
|
+
label: 'Personalize outreach',
|
|
96
|
+
description: 'Generate personalized opening lines for each contact.',
|
|
97
|
+
resourceId: 'ist-personalization-workflow'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'lead-gen.review.outreach-ready',
|
|
101
|
+
label: 'Upload to outreach',
|
|
102
|
+
description: 'Upload approved contacts to the outreach sequence after QC review.',
|
|
103
|
+
resourceId: 'ist-upload-contacts-workflow'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'lead-gen.export.list',
|
|
107
|
+
label: 'Export lead list',
|
|
108
|
+
description: 'Export approved leads as a downloadable lead list.',
|
|
109
|
+
resourceId: 'lgn-06-export-list-workflow'
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'lead-gen.company.cleanup',
|
|
113
|
+
label: 'Clean up companies',
|
|
114
|
+
description: 'Remove disqualified or duplicate companies from the list.',
|
|
115
|
+
resourceId: 'lgn-company-cleanup-workflow'
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
export function findCapabilityById(id: string): Capability | undefined {
|
|
120
|
+
return CAPABILITY_REGISTRY.find((c) => c.id === id)
|
|
121
|
+
}
|
|
49
122
|
|
|
50
123
|
export const PROSPECTING_STEPS = {
|
|
51
124
|
localServices: {
|
|
@@ -234,7 +307,9 @@ function toProspectingLifecycleStage(stage: LeadGenStageCatalogEntry): z.infer<t
|
|
|
234
307
|
}
|
|
235
308
|
}
|
|
236
309
|
|
|
237
|
-
function leadGenStagesForEntity(
|
|
310
|
+
function leadGenStagesForEntity(
|
|
311
|
+
entity: LeadGenStageCatalogEntry['entity']
|
|
312
|
+
): z.infer<typeof ProspectingLifecycleStageSchema>[] {
|
|
238
313
|
return Object.values(LEAD_GEN_STAGE_CATALOG)
|
|
239
314
|
.filter((stage) => stage.entity === entity)
|
|
240
315
|
.sort((a, b) => a.order - b.order)
|
|
@@ -135,6 +135,8 @@ export interface StatefulStageDefinition {
|
|
|
135
135
|
/** Matches stage_key values written by workflow steps. */
|
|
136
136
|
stageKey: string
|
|
137
137
|
label: string
|
|
138
|
+
/** UI color token. Consumers may map this to their design system. */
|
|
139
|
+
color?: string
|
|
138
140
|
states: StatefulStateDefinition[]
|
|
139
141
|
}
|
|
140
142
|
|
|
@@ -251,6 +253,7 @@ export const CRM_PIPELINE_DEFINITION: StatefulPipelineDefinition = {
|
|
|
251
253
|
{
|
|
252
254
|
stageKey: 'interested',
|
|
253
255
|
label: 'Interested',
|
|
256
|
+
color: 'blue',
|
|
254
257
|
states: [
|
|
255
258
|
CRM_DISCOVERY_REPLIED_STATE,
|
|
256
259
|
CRM_DISCOVERY_LINK_SENT_STATE,
|
|
@@ -262,11 +265,11 @@ export const CRM_PIPELINE_DEFINITION: StatefulPipelineDefinition = {
|
|
|
262
265
|
CRM_FOLLOWUP_3_SENT_STATE
|
|
263
266
|
]
|
|
264
267
|
},
|
|
265
|
-
{ stageKey: 'proposal', label: 'Proposal', states: [] },
|
|
266
|
-
{ stageKey: 'closing', label: 'Closing', states: [] },
|
|
267
|
-
{ stageKey: 'closed_won', label: 'Closed Won', states: [] },
|
|
268
|
-
{ stageKey: 'closed_lost', label: 'Closed Lost', states: [] },
|
|
269
|
-
{ stageKey: 'nurturing', label: 'Nurturing', states: [] }
|
|
268
|
+
{ stageKey: 'proposal', label: 'Proposal', color: 'yellow', states: [] },
|
|
269
|
+
{ stageKey: 'closing', label: 'Closing', color: 'orange', states: [] },
|
|
270
|
+
{ stageKey: 'closed_won', label: 'Closed Won', color: 'green', states: [] },
|
|
271
|
+
{ stageKey: 'closed_lost', label: 'Closed Lost', color: 'red', states: [] },
|
|
272
|
+
{ stageKey: 'nurturing', label: 'Nurturing', color: 'grape', states: [] }
|
|
270
273
|
]
|
|
271
274
|
}
|
|
272
275
|
|