@elevasis/core 0.26.0 → 0.28.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.
Files changed (49) hide show
  1. package/dist/index.d.ts +162 -105
  2. package/dist/index.js +280 -174
  3. package/dist/knowledge/index.d.ts +43 -43
  4. package/dist/organization-model/index.d.ts +162 -105
  5. package/dist/organization-model/index.js +280 -174
  6. package/dist/test-utils/index.d.ts +20 -20
  7. package/dist/test-utils/index.js +184 -126
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +976 -1063
  10. package/src/business/acquisition/api-schemas.test.ts +1962 -1841
  11. package/src/business/acquisition/api-schemas.ts +1461 -1464
  12. package/src/business/acquisition/crm-next-action.test.ts +45 -25
  13. package/src/business/acquisition/crm-next-action.ts +227 -220
  14. package/src/business/acquisition/crm-priority.test.ts +41 -8
  15. package/src/business/acquisition/crm-priority.ts +365 -349
  16. package/src/business/acquisition/crm-state-actions.test.ts +208 -153
  17. package/src/business/acquisition/derive-actions.test.ts +90 -13
  18. package/src/business/acquisition/derive-actions.ts +8 -139
  19. package/src/business/acquisition/ontology-validation.ts +72 -158
  20. package/src/business/pdf/sections/investment.ts +1 -1
  21. package/src/business/pdf/sections/summary-investment.ts +1 -1
  22. package/src/execution/engine/tools/tool-maps.ts +872 -831
  23. package/src/organization-model/__tests__/cross-ref.test.ts +167 -0
  24. package/src/organization-model/__tests__/define-domain-record.test.ts +289 -0
  25. package/src/organization-model/__tests__/om-spine-doc-contract.test.ts +56 -0
  26. package/src/organization-model/__tests__/published-zero-leak.test.ts +60 -1
  27. package/src/organization-model/__tests__/resolve.test.ts +1 -1
  28. package/src/organization-model/__tests__/schema-refinements.test.ts +72 -0
  29. package/src/organization-model/cross-ref.ts +175 -0
  30. package/src/organization-model/domains/actions.ts +13 -0
  31. package/src/organization-model/domains/branding.ts +6 -6
  32. package/src/organization-model/domains/customers.ts +95 -78
  33. package/src/organization-model/domains/entities.ts +157 -144
  34. package/src/organization-model/domains/goals.ts +100 -83
  35. package/src/organization-model/domains/knowledge.ts +106 -93
  36. package/src/organization-model/domains/offerings.ts +88 -71
  37. package/src/organization-model/domains/policies.ts +115 -102
  38. package/src/organization-model/domains/roles.ts +109 -96
  39. package/src/organization-model/domains/sales.test.ts +104 -218
  40. package/src/organization-model/domains/sales.ts +212 -375
  41. package/src/organization-model/domains/statuses.ts +351 -339
  42. package/src/organization-model/domains/systems.ts +176 -164
  43. package/src/organization-model/helpers.ts +331 -306
  44. package/src/organization-model/index.ts +43 -0
  45. package/src/organization-model/published.ts +27 -2
  46. package/src/organization-model/schema-refinements.ts +667 -0
  47. package/src/organization-model/schema.ts +8 -715
  48. package/src/platform/constants/versions.ts +1 -1
  49. package/src/reference/_generated/contracts.md +1000 -1087
@@ -1,96 +1,109 @@
1
- import { z } from 'zod'
2
- import { ResourceIdSchema } from './resources'
3
- import { ModelIdSchema } from './shared'
4
- import { SystemIdSchema } from './systems'
5
-
6
- // ---------------------------------------------------------------------------
7
- // Role schema - one entry per distinct role in the organization's chart.
8
- // Inspired by the EOS Accountability Chart but uses plain-language field names
9
- // throughout. No EOS jargon: "title" (not seatTitle), "responsibilities"
10
- // (not accountabilities), "reportsToId", "heldBy".
11
- //
12
- // Cross-reference enforcement lives in `OrganizationModelSchema.superRefine()`:
13
- // `reportsToId` must resolve to another Role, `responsibleFor` entries must
14
- // resolve to Systems, and agent holders must resolve to Agent Resources.
15
- // Role hierarchy cycle detection is also enforced at the model level.
16
- // ---------------------------------------------------------------------------
17
-
18
- export const RoleIdSchema = ModelIdSchema
19
-
20
- export const HumanRoleHolderSchema = z.object({
21
- kind: z.literal('human'),
22
- userId: z.string().trim().min(1).max(200)
23
- })
24
-
25
- export const AgentRoleHolderSchema = z.object({
26
- kind: z.literal('agent'),
27
- agentId: ResourceIdSchema.meta({ ref: 'resource' })
28
- })
29
-
30
- export const TeamRoleHolderSchema = z.object({
31
- kind: z.literal('team'),
32
- memberIds: z.array(z.string().trim().min(1).max(200)).min(1)
33
- })
34
-
35
- export const RoleHolderSchema = z.discriminatedUnion('kind', [
36
- HumanRoleHolderSchema,
37
- AgentRoleHolderSchema,
38
- TeamRoleHolderSchema
39
- ])
40
-
41
- export const RoleHoldersSchema = z.union([RoleHolderSchema, z.array(RoleHolderSchema).min(1)])
42
-
43
- export const RoleSchema = z.object({
44
- /** Stable unique identifier for the role (e.g. "role-ceo", "role-head-of-sales"). */
45
- id: RoleIdSchema,
46
- /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
47
- order: z.number(),
48
- /** Human-readable title shown to agents and in UI (e.g. "CEO", "Head of Sales"). */
49
- title: z.string().trim().min(1).max(200),
50
- /**
51
- * List of responsibilities this role owns - plain-language descriptions of
52
- * what the person in this role is accountable for delivering.
53
- * Defaults to empty array so minimal role definitions stay concise.
54
- */
55
- responsibilities: z.array(z.string().trim().max(500)).default([]),
56
- /**
57
- * Optional: ID of another role this role reports to.
58
- * When present, must reference another `roles[].id` in the same organization.
59
- */
60
- reportsToId: RoleIdSchema.meta({ ref: 'role' }).optional(),
61
- /**
62
- * Optional: human, agent, or team holder currently filling this role.
63
- * Agent holders reference OM Resource IDs and are validated at the model level.
64
- */
65
- heldBy: RoleHoldersSchema.optional(),
66
- /**
67
- * Optional Systems this role is accountable for.
68
- * Cross-reference enforced in `OrganizationModelSchema.superRefine()`.
69
- */
70
- responsibleFor: z.array(SystemIdSchema.meta({ ref: 'system' })).optional()
71
- })
72
-
73
- // ---------------------------------------------------------------------------
74
- // Roles domain schema - a collection of roles.
75
- // ---------------------------------------------------------------------------
76
-
77
- export const RolesDomainSchema = z
78
- .record(z.string(), RoleSchema)
79
- .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
80
- message: 'Each role entry id must match its map key'
81
- })
82
- .default({})
83
-
84
- // ---------------------------------------------------------------------------
85
- // Seed - empty by default; adapters populate with real role definitions.
86
- // ---------------------------------------------------------------------------
87
-
88
- export const DEFAULT_ORGANIZATION_MODEL_ROLES: z.infer<typeof RolesDomainSchema> = {}
89
-
90
- export type RoleId = z.infer<typeof RoleIdSchema>
91
- export type HumanRoleHolder = z.infer<typeof HumanRoleHolderSchema>
92
- export type AgentRoleHolder = z.infer<typeof AgentRoleHolderSchema>
93
- export type TeamRoleHolder = z.infer<typeof TeamRoleHolderSchema>
94
- export type RoleHolder = z.infer<typeof RoleHolderSchema>
95
- export type Role = z.infer<typeof RoleSchema>
96
- export type RolesDomain = z.infer<typeof RolesDomainSchema>
1
+ import { z } from 'zod'
2
+ import { ResourceIdSchema } from './resources'
3
+ import { ModelIdSchema } from './shared'
4
+ import { SystemIdSchema } from './systems'
5
+ import { defineDomainRecord } from '../helpers'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Role schema - one entry per distinct role in the organization's chart.
9
+ // Inspired by the EOS Accountability Chart but uses plain-language field names
10
+ // throughout. No EOS jargon: "title" (not seatTitle), "responsibilities"
11
+ // (not accountabilities), "reportsToId", "heldBy".
12
+ //
13
+ // Cross-reference enforcement lives in `OrganizationModelSchema.superRefine()`:
14
+ // `reportsToId` must resolve to another Role, `responsibleFor` entries must
15
+ // resolve to Systems, and agent holders must resolve to Agent Resources.
16
+ // Role hierarchy cycle detection is also enforced at the model level.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const RoleIdSchema = ModelIdSchema
20
+
21
+ export const HumanRoleHolderSchema = z.object({
22
+ kind: z.literal('human'),
23
+ userId: z.string().trim().min(1).max(200)
24
+ })
25
+
26
+ export const AgentRoleHolderSchema = z.object({
27
+ kind: z.literal('agent'),
28
+ agentId: ResourceIdSchema.meta({ ref: 'resource' })
29
+ })
30
+
31
+ export const TeamRoleHolderSchema = z.object({
32
+ kind: z.literal('team'),
33
+ memberIds: z.array(z.string().trim().min(1).max(200)).min(1)
34
+ })
35
+
36
+ export const RoleHolderSchema = z.discriminatedUnion('kind', [
37
+ HumanRoleHolderSchema,
38
+ AgentRoleHolderSchema,
39
+ TeamRoleHolderSchema
40
+ ])
41
+
42
+ export const RoleHoldersSchema = z.union([RoleHolderSchema, z.array(RoleHolderSchema).min(1)])
43
+
44
+ export const RoleSchema = z.object({
45
+ /** Stable unique identifier for the role (e.g. "role-ceo", "role-head-of-sales"). */
46
+ id: RoleIdSchema,
47
+ /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
48
+ order: z.number(),
49
+ /** Human-readable title shown to agents and in UI (e.g. "CEO", "Head of Sales"). */
50
+ title: z.string().trim().min(1).max(200),
51
+ /**
52
+ * List of responsibilities this role owns - plain-language descriptions of
53
+ * what the person in this role is accountable for delivering.
54
+ * Defaults to empty array so minimal role definitions stay concise.
55
+ */
56
+ responsibilities: z.array(z.string().trim().max(500)).default([]),
57
+ /**
58
+ * Optional: ID of another role this role reports to.
59
+ * When present, must reference another `roles[].id` in the same organization.
60
+ */
61
+ reportsToId: RoleIdSchema.meta({ ref: 'role' }).optional(),
62
+ /**
63
+ * Optional: human, agent, or team holder currently filling this role.
64
+ * Agent holders reference OM Resource IDs and are validated at the model level.
65
+ */
66
+ heldBy: RoleHoldersSchema.optional(),
67
+ /**
68
+ * Optional Systems this role is accountable for.
69
+ * Cross-reference enforced in `OrganizationModelSchema.superRefine()`.
70
+ */
71
+ responsibleFor: z.array(SystemIdSchema.meta({ ref: 'system' })).optional()
72
+ })
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Roles domain schema - a collection of roles.
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export const RolesDomainSchema = z
79
+ .record(z.string(), RoleSchema)
80
+ .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
81
+ message: 'Each role entry id must match its map key'
82
+ })
83
+ .default({})
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Seed - empty by default; adapters populate with real role definitions.
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export const DEFAULT_ORGANIZATION_MODEL_ROLES: z.infer<typeof RolesDomainSchema> = {}
90
+
91
+ /** Validate and return a single role entry. */
92
+ export function defineRole(entry: z.input<typeof RoleSchema>): z.infer<typeof RoleSchema> {
93
+ return RoleSchema.parse(entry)
94
+ }
95
+
96
+ /** Validate and return an id-keyed map of role entries. */
97
+ export function defineRoles(
98
+ entries: readonly z.input<typeof RoleSchema>[]
99
+ ): Record<string, z.infer<typeof RoleSchema>> {
100
+ return defineDomainRecord(RoleSchema, entries)
101
+ }
102
+
103
+ export type RoleId = z.infer<typeof RoleIdSchema>
104
+ export type HumanRoleHolder = z.infer<typeof HumanRoleHolderSchema>
105
+ export type AgentRoleHolder = z.infer<typeof AgentRoleHolderSchema>
106
+ export type TeamRoleHolder = z.infer<typeof TeamRoleHolderSchema>
107
+ export type RoleHolder = z.infer<typeof RoleHolderSchema>
108
+ export type Role = z.infer<typeof RoleSchema>
109
+ export type RolesDomain = z.infer<typeof RolesDomainSchema>
@@ -1,218 +1,104 @@
1
- import { describe, it, expect } from 'vitest'
2
- import {
3
- CRM_PIPELINE_DEFINITION,
4
- CRM_DISCOVERY_REPLIED_STATE,
5
- CRM_DISCOVERY_LINK_SENT_STATE,
6
- CRM_DISCOVERY_NUDGING_STATE,
7
- CRM_DISCOVERY_BOOKING_CANCELLED_STATE,
8
- CRM_REPLY_SENT_STATE,
9
- CRM_FOLLOWUP_1_SENT_STATE,
10
- CRM_FOLLOWUP_2_SENT_STATE,
11
- CRM_FOLLOWUP_3_SENT_STATE,
12
- CRM_PRIORITY_BUCKETS,
13
- DEFAULT_CRM_PRIORITY_RULE_CONFIG,
14
- getValidStatesForStage,
15
- findPipeline,
16
- ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE,
17
- ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE,
18
- LEAD_GEN_PIPELINE_DEFINITIONS
19
- } from './sales'
20
-
21
- describe('CRM_PIPELINE_DEFINITION', () => {
22
- it('has pipelineKey "crm"', () => {
23
- expect(CRM_PIPELINE_DEFINITION.pipelineKey).toBe('crm')
24
- })
25
-
26
- it('has entityKey "crm.deal"', () => {
27
- expect(CRM_PIPELINE_DEFINITION.entityKey).toBe('crm.deal')
28
- })
29
-
30
- it('has exactly 6 stages', () => {
31
- expect(CRM_PIPELINE_DEFINITION.stages).toHaveLength(6)
32
- })
33
-
34
- it('stage keys are interested, proposal, closing, closed_won, closed_lost, nurturing in order', () => {
35
- const keys = CRM_PIPELINE_DEFINITION.stages.map((s) => s.stageKey)
36
- expect(keys).toEqual(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
37
- })
38
-
39
- it('interested stage has exactly 8 states', () => {
40
- const interested = CRM_PIPELINE_DEFINITION.stages.find((s) => s.stageKey === 'interested')
41
- expect(interested?.states).toHaveLength(8)
42
- })
43
-
44
- it('interested stage contains all 8 expected state constants', () => {
45
- const interested = CRM_PIPELINE_DEFINITION.stages.find((s) => s.stageKey === 'interested')!
46
- const stateKeys = interested.states.map((s) => s.stateKey)
47
- expect(stateKeys).toContain(CRM_DISCOVERY_REPLIED_STATE.stateKey)
48
- expect(stateKeys).toContain(CRM_DISCOVERY_LINK_SENT_STATE.stateKey)
49
- expect(stateKeys).toContain(CRM_DISCOVERY_NUDGING_STATE.stateKey)
50
- expect(stateKeys).toContain(CRM_DISCOVERY_BOOKING_CANCELLED_STATE.stateKey)
51
- expect(stateKeys).toContain(CRM_REPLY_SENT_STATE.stateKey)
52
- expect(stateKeys).toContain(CRM_FOLLOWUP_1_SENT_STATE.stateKey)
53
- expect(stateKeys).toContain(CRM_FOLLOWUP_2_SENT_STATE.stateKey)
54
- expect(stateKeys).toContain(CRM_FOLLOWUP_3_SENT_STATE.stateKey)
55
- })
56
-
57
- it('interested stage states reference the exported constants by identity', () => {
58
- const interested = CRM_PIPELINE_DEFINITION.stages.find((s) => s.stageKey === 'interested')!
59
- expect(interested.states).toContain(CRM_DISCOVERY_REPLIED_STATE)
60
- expect(interested.states).toContain(CRM_FOLLOWUP_3_SENT_STATE)
61
- })
62
-
63
- it.each(['proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])(
64
- '%s stage has an empty states array',
65
- (stageKey) => {
66
- const stage = CRM_PIPELINE_DEFINITION.stages.find((s) => s.stageKey === stageKey)
67
- expect(stage).toBeDefined()
68
- expect(stage?.states).toHaveLength(0)
69
- }
70
- )
71
-
72
- it('closed_won stage has empty states array', () => {
73
- const stage = CRM_PIPELINE_DEFINITION.stages.find((s) => s.stageKey === 'closed_won')
74
- expect(stage?.states).toEqual([])
75
- })
76
-
77
- it('closed_lost stage has empty states array', () => {
78
- const stage = CRM_PIPELINE_DEFINITION.stages.find((s) => s.stageKey === 'closed_lost')
79
- expect(stage?.states).toEqual([])
80
- })
81
- })
82
-
83
- describe('CRM state constant shapes', () => {
84
- it('CRM_DISCOVERY_REPLIED_STATE has correct stateKey and label', () => {
85
- expect(CRM_DISCOVERY_REPLIED_STATE).toEqual({ stateKey: 'discovery_replied', label: 'Discovery Replied' })
86
- })
87
-
88
- it('CRM_FOLLOWUP_1_SENT_STATE has correct stateKey and label', () => {
89
- expect(CRM_FOLLOWUP_1_SENT_STATE).toEqual({ stateKey: 'followup_1_sent', label: 'Follow-up 1 Sent' })
90
- })
91
-
92
- it('CRM_FOLLOWUP_2_SENT_STATE has correct stateKey and label', () => {
93
- expect(CRM_FOLLOWUP_2_SENT_STATE).toEqual({ stateKey: 'followup_2_sent', label: 'Follow-up 2 Sent' })
94
- })
95
-
96
- it('CRM_FOLLOWUP_3_SENT_STATE has correct stateKey and label', () => {
97
- expect(CRM_FOLLOWUP_3_SENT_STATE).toEqual({ stateKey: 'followup_3_sent', label: 'Follow-up 3 Sent' })
98
- })
99
- })
100
-
101
- describe('DEFAULT_CRM_PRIORITY_RULE_CONFIG', () => {
102
- it('defines the canonical CRM priority buckets in sort order', () => {
103
- expect(CRM_PRIORITY_BUCKETS.map((bucket) => bucket.bucketKey)).toEqual([
104
- 'needs_response',
105
- 'follow_up_due',
106
- 'waiting',
107
- 'stale',
108
- 'closed_low'
109
- ])
110
- expect(CRM_PRIORITY_BUCKETS.map((bucket) => bucket.rank)).toEqual([10, 20, 30, 40, 50])
111
- })
112
-
113
- it('includes display metadata for each bucket', () => {
114
- for (const bucket of CRM_PRIORITY_BUCKETS) {
115
- expect(bucket.label).toEqual(expect.any(String))
116
- expect(bucket.rank).toEqual(expect.any(Number))
117
- expect(bucket.color).toEqual(expect.any(String))
118
- }
119
- })
120
-
121
- it('maps CRM states and closed stages into priority rules', () => {
122
- expect(DEFAULT_CRM_PRIORITY_RULE_CONFIG.closedStageKeys).toEqual(['closed_won', 'closed_lost'])
123
- expect(DEFAULT_CRM_PRIORITY_RULE_CONFIG.followUpAfterDaysByStateKey.discovery_link_sent).toBe(3)
124
- expect(DEFAULT_CRM_PRIORITY_RULE_CONFIG.staleAfterDays).toBeGreaterThan(0)
125
- })
126
- })
127
-
128
- describe('getValidStatesForStage', () => {
129
- it('returns all 8 states for the interested stage', () => {
130
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'interested')
131
- expect(states).toHaveLength(8)
132
- expect(states.map((s) => s.stateKey)).toContain('discovery_replied')
133
- expect(states.map((s) => s.stateKey)).toContain('followup_3_sent')
134
- })
135
-
136
- it('returns empty array for the proposal stage (no states defined)', () => {
137
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'proposal')
138
- expect(states).toEqual([])
139
- })
140
-
141
- it('returns empty array for the closing stage (no states defined)', () => {
142
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'closing')
143
- expect(states).toEqual([])
144
- })
145
-
146
- it('returns empty array for closed_won stage', () => {
147
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'closed_won')
148
- expect(states).toEqual([])
149
- })
150
-
151
- it('returns empty array for closed_lost stage', () => {
152
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'closed_lost')
153
- expect(states).toEqual([])
154
- })
155
-
156
- it('returns empty array for nurturing stage (no states defined)', () => {
157
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'nurturing')
158
- expect(states).toEqual([])
159
- })
160
-
161
- it('returns empty array for an unknown stage key', () => {
162
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'unknown_stage')
163
- expect(states).toEqual([])
164
- })
165
-
166
- it('returns empty array for an empty string stage key', () => {
167
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, '')
168
- expect(states).toEqual([])
169
- })
170
-
171
- it('is case-sensitive — "Interested" does not match "interested"', () => {
172
- const states = getValidStatesForStage(CRM_PIPELINE_DEFINITION, 'Interested')
173
- expect(states).toEqual([])
174
- })
175
-
176
- it('works with the lead-gen member pipeline for known stage', () => {
177
- const states = getValidStatesForStage(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE, 'outreach')
178
- expect(states.length).toBeGreaterThan(0)
179
- expect(states.map((s) => s.stateKey)).toContain('personalized')
180
- })
181
-
182
- it('works with the lead-gen member pipeline for unknown stage', () => {
183
- const states = getValidStatesForStage(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE, 'nonexistent')
184
- expect(states).toEqual([])
185
- })
186
- })
187
-
188
- describe('findPipeline', () => {
189
- it('finds a pipeline by pipelineKey when present', () => {
190
- const found = findPipeline(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-member'], 'lead-gen')
191
- expect(found).toBe(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE)
192
- })
193
-
194
- it('returns undefined for an unknown pipelineKey', () => {
195
- const found = findPipeline(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-member'], 'crm')
196
- expect(found).toBeUndefined()
197
- })
198
-
199
- it('returns undefined for an empty array', () => {
200
- const found = findPipeline([], 'lead-gen')
201
- expect(found).toBeUndefined()
202
- })
203
- })
204
-
205
- describe('LEAD_GEN_PIPELINE_DEFINITIONS', () => {
206
- it('has entries for acq.list-member and acq.list-company', () => {
207
- expect(LEAD_GEN_PIPELINE_DEFINITIONS).toHaveProperty('acq.list-member')
208
- expect(LEAD_GEN_PIPELINE_DEFINITIONS).toHaveProperty('acq.list-company')
209
- })
210
-
211
- it('acq.list-member entry contains the ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE', () => {
212
- expect(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-member']).toContain(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE)
213
- })
214
-
215
- it('acq.list-company entry contains the ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE', () => {
216
- expect(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-company']).toContain(ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE)
217
- })
218
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE,
4
+ ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE,
5
+ LEAD_GEN_PIPELINE_DEFINITIONS,
6
+ findPipeline,
7
+ getValidStatesForStage,
8
+ type StatefulPipelineDefinition
9
+ } from './sales'
10
+
11
+ const TEST_CRM_PIPELINE: StatefulPipelineDefinition = {
12
+ pipelineKey: 'crm',
13
+ label: 'CRM',
14
+ entityKey: 'crm.deal',
15
+ stages: [
16
+ {
17
+ stageKey: 'interested',
18
+ label: 'Interested',
19
+ color: 'blue',
20
+ states: [
21
+ { stateKey: 'discovery_replied', label: 'Discovery Replied' },
22
+ { stateKey: 'discovery_link_sent', label: 'Discovery Link Sent' }
23
+ ]
24
+ },
25
+ { stageKey: 'proposal', label: 'Proposal', color: 'yellow', states: [] }
26
+ ]
27
+ }
28
+
29
+ describe('sales domain published surface', () => {
30
+ it('does not export Elevasis CRM runtime constants from the generic core sales domain', async () => {
31
+ const sales = await import('./sales')
32
+
33
+ expect(Object.keys(sales)).not.toEqual(
34
+ expect.arrayContaining([
35
+ 'CRM_PIPELINE_DEFINITION',
36
+ 'CRM_DISCOVERY_REPLIED_STATE',
37
+ 'CRM_PRIORITY_BUCKETS',
38
+ 'DEFAULT_CRM_PRIORITY_RULE_CONFIG',
39
+ 'DEFAULT_CRM_NEXT_ACTION_RULE_CONFIG'
40
+ ])
41
+ )
42
+ })
43
+ })
44
+
45
+ describe('getValidStatesForStage', () => {
46
+ it('returns states for a known stage from a caller-supplied pipeline definition', () => {
47
+ const states = getValidStatesForStage(TEST_CRM_PIPELINE, 'interested')
48
+
49
+ expect(states).toEqual([
50
+ { stateKey: 'discovery_replied', label: 'Discovery Replied' },
51
+ { stateKey: 'discovery_link_sent', label: 'Discovery Link Sent' }
52
+ ])
53
+ })
54
+
55
+ it('returns empty array for a stage with no states', () => {
56
+ expect(getValidStatesForStage(TEST_CRM_PIPELINE, 'proposal')).toEqual([])
57
+ })
58
+
59
+ it('returns empty array for an unknown stage key', () => {
60
+ expect(getValidStatesForStage(TEST_CRM_PIPELINE, 'unknown_stage')).toEqual([])
61
+ })
62
+
63
+ it('is case-sensitive', () => {
64
+ expect(getValidStatesForStage(TEST_CRM_PIPELINE, 'Interested')).toEqual([])
65
+ })
66
+
67
+ it('works with the lead-gen member pipeline for known stage', () => {
68
+ const states = getValidStatesForStage(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE, 'outreach')
69
+ expect(states.length).toBeGreaterThan(0)
70
+ expect(states.map((s) => s.stateKey)).toContain('personalized')
71
+ })
72
+ })
73
+
74
+ describe('findPipeline', () => {
75
+ it('finds a pipeline by pipelineKey when present', () => {
76
+ const found = findPipeline(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-member'], 'lead-gen')
77
+ expect(found).toBe(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE)
78
+ })
79
+
80
+ it('returns undefined for an unknown pipelineKey', () => {
81
+ const found = findPipeline(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-member'], 'crm')
82
+ expect(found).toBeUndefined()
83
+ })
84
+
85
+ it('returns undefined for an empty array', () => {
86
+ const found = findPipeline([], 'lead-gen')
87
+ expect(found).toBeUndefined()
88
+ })
89
+ })
90
+
91
+ describe('LEAD_GEN_PIPELINE_DEFINITIONS', () => {
92
+ it('has entries for acq.list-member and acq.list-company', () => {
93
+ expect(LEAD_GEN_PIPELINE_DEFINITIONS).toHaveProperty('acq.list-member')
94
+ expect(LEAD_GEN_PIPELINE_DEFINITIONS).toHaveProperty('acq.list-company')
95
+ })
96
+
97
+ it('acq.list-member entry contains the ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE', () => {
98
+ expect(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-member']).toContain(ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE)
99
+ })
100
+
101
+ it('acq.list-company entry contains the ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE', () => {
102
+ expect(LEAD_GEN_PIPELINE_DEFINITIONS['acq.list-company']).toContain(ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE)
103
+ })
104
+ })