@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.
- package/dist/index.d.ts +162 -105
- package/dist/index.js +280 -174
- package/dist/knowledge/index.d.ts +43 -43
- package/dist/organization-model/index.d.ts +162 -105
- package/dist/organization-model/index.js +280 -174
- package/dist/test-utils/index.d.ts +20 -20
- package/dist/test-utils/index.js +184 -126
- package/package.json +3 -3
- 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__/define-domain-record.test.ts +289 -0
- package/src/organization-model/__tests__/om-spine-doc-contract.test.ts +56 -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/actions.ts +13 -0
- package/src/organization-model/domains/branding.ts +6 -6
- package/src/organization-model/domains/customers.ts +95 -78
- package/src/organization-model/domains/entities.ts +157 -144
- package/src/organization-model/domains/goals.ts +100 -83
- package/src/organization-model/domains/knowledge.ts +106 -93
- package/src/organization-model/domains/offerings.ts +88 -71
- package/src/organization-model/domains/policies.ts +115 -102
- package/src/organization-model/domains/roles.ts +109 -96
- package/src/organization-model/domains/sales.test.ts +104 -218
- package/src/organization-model/domains/sales.ts +212 -375
- package/src/organization-model/domains/statuses.ts +351 -339
- package/src/organization-model/domains/systems.ts +176 -164
- package/src/organization-model/helpers.ts +331 -306
- package/src/organization-model/index.ts +43 -0
- package/src/organization-model/published.ts +27 -2
- package/src/organization-model/schema-refinements.ts +667 -0
- package/src/organization-model/schema.ts +8 -715
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +1000 -1087
|
@@ -1,153 +1,208 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { ActivityEventSchema } from './activity-events'
|
|
7
|
-
import { DealStageSchema, TransitionItemRequestSchema } from './api-schemas'
|
|
8
|
-
import { deriveActions } from './derive-actions'
|
|
9
|
-
import type { DealStage } from './types'
|
|
10
|
-
|
|
11
|
-
const DEAL_STAGES = [
|
|
12
|
-
'interested',
|
|
13
|
-
'proposal',
|
|
14
|
-
'closing',
|
|
15
|
-
'closed_won',
|
|
16
|
-
'closed_lost',
|
|
17
|
-
'nurturing'
|
|
18
|
-
] as const satisfies readonly DealStage[]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
])
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
LEAD_GEN_PIPELINE_DEFINITIONS,
|
|
4
|
+
type StatefulPipelineDefinition
|
|
5
|
+
} from '../../organization-model/domains/sales'
|
|
6
|
+
import { ActivityEventSchema } from './activity-events'
|
|
7
|
+
import { DealStageSchema, TransitionItemRequestSchema } from './api-schemas'
|
|
8
|
+
import { deriveActions } from './derive-actions'
|
|
9
|
+
import type { DealStage } from './types'
|
|
10
|
+
|
|
11
|
+
const DEAL_STAGES = [
|
|
12
|
+
'interested',
|
|
13
|
+
'proposal',
|
|
14
|
+
'closing',
|
|
15
|
+
'closed_won',
|
|
16
|
+
'closed_lost',
|
|
17
|
+
'nurturing'
|
|
18
|
+
] as const satisfies readonly DealStage[]
|
|
19
|
+
|
|
20
|
+
const CRM_PIPELINE_FIXTURE: StatefulPipelineDefinition = {
|
|
21
|
+
pipelineKey: 'crm',
|
|
22
|
+
label: 'CRM',
|
|
23
|
+
entityKey: 'crm.deal',
|
|
24
|
+
stages: DEAL_STAGES.map((stageKey, index) => ({
|
|
25
|
+
stageKey,
|
|
26
|
+
label: stageKey,
|
|
27
|
+
color: 'gray',
|
|
28
|
+
states:
|
|
29
|
+
index === 0
|
|
30
|
+
? [
|
|
31
|
+
{ stateKey: 'discovery_replied', label: 'Discovery Replied' },
|
|
32
|
+
{ stateKey: 'discovery_link_sent', label: 'Discovery Link Sent' },
|
|
33
|
+
{ stateKey: 'discovery_nudging', label: 'Discovery Nudging' },
|
|
34
|
+
{ stateKey: 'discovery_booking_cancelled', label: 'Discovery Booking Cancelled' }
|
|
35
|
+
]
|
|
36
|
+
: []
|
|
37
|
+
}))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CRM_ACTION_FIXTURE = [
|
|
41
|
+
{
|
|
42
|
+
key: 'move_to_proposal',
|
|
43
|
+
label: 'Move to Proposal',
|
|
44
|
+
workflowId: 'move_to_proposal-workflow',
|
|
45
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) => deal.stage_key === 'interested'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'move_to_closing',
|
|
49
|
+
label: 'Move to Closing',
|
|
50
|
+
workflowId: 'move_to_closing-workflow',
|
|
51
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) => deal.stage_key === 'proposal'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'move_to_closed_won',
|
|
55
|
+
label: 'Close Won',
|
|
56
|
+
workflowId: 'move_to_closed_won-workflow',
|
|
57
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) => deal.stage_key === 'closing'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'move_to_closed_lost',
|
|
61
|
+
label: 'Close Lost',
|
|
62
|
+
workflowId: 'move_to_closed_lost-workflow',
|
|
63
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) =>
|
|
64
|
+
deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: 'move_to_nurturing',
|
|
68
|
+
label: 'Move to Nurturing',
|
|
69
|
+
workflowId: 'move_to_nurturing-workflow',
|
|
70
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) =>
|
|
71
|
+
deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'send_link',
|
|
75
|
+
label: 'Send Booking Link',
|
|
76
|
+
workflowId: 'crm-send-booking-link-workflow',
|
|
77
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) =>
|
|
78
|
+
deal.stage_key === 'interested' && deal.state_key === 'discovery_replied'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'send_nudge',
|
|
82
|
+
label: 'Send Nudge',
|
|
83
|
+
workflowId: 'crm-send-nudge-workflow',
|
|
84
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) =>
|
|
85
|
+
deal.stage_key === 'interested' &&
|
|
86
|
+
(deal.state_key === 'discovery_link_sent' || deal.state_key === 'discovery_nudging')
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'mark_no_show',
|
|
90
|
+
label: 'Mark No-Show',
|
|
91
|
+
workflowId: 'mark_no_show-workflow',
|
|
92
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) =>
|
|
93
|
+
deal.stage_key === 'interested' && deal.state_key === 'discovery_nudging'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'rebook',
|
|
97
|
+
label: 'Rebook',
|
|
98
|
+
workflowId: 'crm-rebook-workflow',
|
|
99
|
+
isAvailableFor: (deal: Parameters<typeof deriveActions>[0]) =>
|
|
100
|
+
deal.stage_key === 'interested' && deal.state_key === 'discovery_booking_cancelled'
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
function deal(stageKey: string | null, stateKey: string | null = null): Parameters<typeof deriveActions>[0] {
|
|
105
|
+
return { stage_key: stageKey, state_key: stateKey } as Parameters<typeof deriveActions>[0]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('CRM deal action derivation', () => {
|
|
109
|
+
it('derives the exact base actions for interested deals from caller-supplied actions', () => {
|
|
110
|
+
expect(deriveActions(deal('interested'), CRM_ACTION_FIXTURE)).toEqual([
|
|
111
|
+
{ key: 'move_to_proposal', label: 'Move to Proposal' },
|
|
112
|
+
{ key: 'move_to_closed_lost', label: 'Close Lost' },
|
|
113
|
+
{ key: 'move_to_nurturing', label: 'Move to Nurturing' }
|
|
114
|
+
])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('derives no actions unless the caller supplies an action catalog', () => {
|
|
118
|
+
expect(deriveActions(deal('interested'))).toEqual([])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it.each([
|
|
122
|
+
['discovery_replied', { key: 'send_link', label: 'Send Booking Link' }],
|
|
123
|
+
['discovery_link_sent', { key: 'send_nudge', label: 'Send Nudge' }],
|
|
124
|
+
['discovery_booking_cancelled', { key: 'rebook', label: 'Rebook' }]
|
|
125
|
+
] as const)('adds the expected workflow action for interested/%s', (stateKey, expectedAction) => {
|
|
126
|
+
expect(deriveActions(deal('interested', stateKey), CRM_ACTION_FIXTURE)).toContainEqual(expectedAction)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('adds nudge and no-show actions for interested/discovery_nudging', () => {
|
|
130
|
+
expect(deriveActions(deal('interested', 'discovery_nudging'), CRM_ACTION_FIXTURE)).toEqual([
|
|
131
|
+
{ key: 'move_to_proposal', label: 'Move to Proposal' },
|
|
132
|
+
{ key: 'move_to_closed_lost', label: 'Close Lost' },
|
|
133
|
+
{ key: 'move_to_nurturing', label: 'Move to Nurturing' },
|
|
134
|
+
{ key: 'send_nudge', label: 'Send Nudge' },
|
|
135
|
+
{ key: 'mark_no_show', label: 'Mark No-Show' }
|
|
136
|
+
])
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('ActivityEventSchema', () => {
|
|
141
|
+
const timestamp = '2026-04-27T12:34:56.000Z'
|
|
142
|
+
|
|
143
|
+
it.each([
|
|
144
|
+
[{ type: 'stage_change', timestamp, stageBefore: 'interested', stageAfter: 'proposal', reason: 'qualified' }],
|
|
145
|
+
[{ type: 'state_change', timestamp, stateBefore: null, stateAfter: 'discovery_replied' }],
|
|
146
|
+
[{ type: 'action_taken', timestamp, actionKey: 'send_link', payload: { channel: 'email' } }],
|
|
147
|
+
[{ type: 'approval_created', timestamp, commandId: 'cmd_123', dealStageBefore: 'proposal' }],
|
|
148
|
+
[{ type: 'approval_resolved', timestamp, commandId: 'cmd_123', resolution: 'superseded' }],
|
|
149
|
+
[{ type: 'approval_stale', timestamp, commandId: 'cmd_123', dealStageAfter: 'closing' }],
|
|
150
|
+
[{ type: 'task_created', timestamp, taskId: 'task_123' }],
|
|
151
|
+
[{ type: 'deal_created', timestamp }]
|
|
152
|
+
])('accepts expected %s events', (event) => {
|
|
153
|
+
expect(ActivityEventSchema.safeParse(event).success).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it.each([
|
|
157
|
+
[{ type: 'deal_created', timestamp: 'not-a-date' }],
|
|
158
|
+
[{ type: 'unknown_event', timestamp }],
|
|
159
|
+
[{ type: 'stage_change', timestamp, stageBefore: 'interested' }],
|
|
160
|
+
[{ type: 'approval_resolved', timestamp, commandId: 'cmd_123', resolution: 'approved' }],
|
|
161
|
+
[{ type: 'booking_nudge_sent', timestamp, followupDay: '2' }],
|
|
162
|
+
[{ type: 'action_taken', timestamp }]
|
|
163
|
+
])('rejects malformed event payload %o', (event) => {
|
|
164
|
+
expect(ActivityEventSchema.safeParse(event).success).toBe(false)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('CRM stage and transition vocabulary contracts', () => {
|
|
169
|
+
it('keeps DealStage and DealStageSchema aligned with a caller-owned CRM fixture', () => {
|
|
170
|
+
const crmStageKeys = CRM_PIPELINE_FIXTURE.stages.map((stage) => stage.stageKey)
|
|
171
|
+
|
|
172
|
+
expect(crmStageKeys).toEqual([...DEAL_STAGES])
|
|
173
|
+
for (const stageKey of crmStageKeys) {
|
|
174
|
+
expect(DealStageSchema.safeParse(stageKey).success).toBe(true)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('accepts every fixture CRM deal stage in transition requests', () => {
|
|
179
|
+
for (const stageKey of DEAL_STAGES) {
|
|
180
|
+
expect(
|
|
181
|
+
TransitionItemRequestSchema.safeParse({
|
|
182
|
+
pipelineKey: CRM_PIPELINE_FIXTURE.pipelineKey,
|
|
183
|
+
stageKey,
|
|
184
|
+
stateKey: null,
|
|
185
|
+
expectedUpdatedAt: '2026-04-27T12:34:56.000Z'
|
|
186
|
+
}).success
|
|
187
|
+
).toBe(true)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('accepts canonical lead-gen stage/state pairs in transition requests', () => {
|
|
192
|
+
for (const pipelineDefinitions of Object.values(LEAD_GEN_PIPELINE_DEFINITIONS)) {
|
|
193
|
+
for (const pipeline of pipelineDefinitions) {
|
|
194
|
+
for (const stage of pipeline.stages) {
|
|
195
|
+
for (const state of stage.states) {
|
|
196
|
+
expect(
|
|
197
|
+
TransitionItemRequestSchema.safeParse({
|
|
198
|
+
pipelineKey: pipeline.pipelineKey,
|
|
199
|
+
stageKey: stage.stageKey,
|
|
200
|
+
stateKey: state.stateKey
|
|
201
|
+
}).success
|
|
202
|
+
).toBe(true)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { AcqDealRow } from './types'
|
|
3
3
|
import type { Action, ActionDef } from './derive-actions'
|
|
4
|
-
import {
|
|
4
|
+
import { SendReplyActionPayloadSchema, deriveActions as deriveActionsCore } from './derive-actions'
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Fixture builder
|
|
@@ -12,7 +12,7 @@ import { DEFAULT_CRM_ACTIONS, SendReplyActionPayloadSchema, deriveActions } from
|
|
|
12
12
|
* Only stage_key and state_key affect availability logic; all other required
|
|
13
13
|
* columns are filled with safe, inert defaults.
|
|
14
14
|
*/
|
|
15
|
-
function makeDeal(
|
|
15
|
+
function makeDeal(
|
|
16
16
|
overrides: Partial<AcqDealRow> & { ownership?: 'us' | 'them' | null; nextAction?: string | null } = {}
|
|
17
17
|
): AcqDealRow & { ownership?: 'us' | 'them' | null; nextAction?: string | null } {
|
|
18
18
|
return {
|
|
@@ -51,7 +51,85 @@ function makeDeal(
|
|
|
51
51
|
updated_at: '2026-01-01T00:00:00.000Z',
|
|
52
52
|
...overrides
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_CRM_ACTIONS: ActionDef[] = [
|
|
57
|
+
{
|
|
58
|
+
key: 'move_to_proposal',
|
|
59
|
+
label: 'Move to Proposal',
|
|
60
|
+
workflowId: 'move_to_proposal-workflow',
|
|
61
|
+
isAvailableFor: (deal) => deal.stage_key === 'interested'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'move_to_closing',
|
|
65
|
+
label: 'Move to Closing',
|
|
66
|
+
workflowId: 'move_to_closing-workflow',
|
|
67
|
+
isAvailableFor: (deal) => deal.stage_key === 'proposal'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: 'move_to_closed_won',
|
|
71
|
+
label: 'Close Won',
|
|
72
|
+
workflowId: 'move_to_closed_won-workflow',
|
|
73
|
+
isAvailableFor: (deal) => deal.stage_key === 'closing'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: 'move_to_closed_lost',
|
|
77
|
+
label: 'Close Lost',
|
|
78
|
+
workflowId: 'move_to_closed_lost-workflow',
|
|
79
|
+
isAvailableFor: (deal) =>
|
|
80
|
+
deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: 'move_to_nurturing',
|
|
84
|
+
label: 'Move to Nurturing',
|
|
85
|
+
workflowId: 'move_to_nurturing-workflow',
|
|
86
|
+
isAvailableFor: (deal) =>
|
|
87
|
+
deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: 'send_reply',
|
|
91
|
+
label: 'Send Reply',
|
|
92
|
+
workflowId: 'crm-send-reply-workflow',
|
|
93
|
+
payloadSchema: SendReplyActionPayloadSchema,
|
|
94
|
+
isAvailableFor: (deal) =>
|
|
95
|
+
deal.stage_key === 'interested' &&
|
|
96
|
+
deal.nextAction === 'send_reply' &&
|
|
97
|
+
(deal.state_key === 'discovery_replied' ||
|
|
98
|
+
deal.state_key === 'discovery_link_sent' ||
|
|
99
|
+
deal.state_key === 'discovery_nudging')
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'send_link',
|
|
103
|
+
label: 'Send Booking Link',
|
|
104
|
+
workflowId: 'crm-send-booking-link-workflow',
|
|
105
|
+
isAvailableFor: (deal) => deal.stage_key === 'interested' && deal.state_key === 'discovery_replied'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'send_nudge',
|
|
109
|
+
label: 'Send Nudge',
|
|
110
|
+
workflowId: 'crm-send-nudge-workflow',
|
|
111
|
+
isAvailableFor: (deal) =>
|
|
112
|
+
deal.stage_key === 'interested' &&
|
|
113
|
+
(deal.state_key === 'discovery_link_sent' || deal.state_key === 'discovery_nudging')
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: 'mark_no_show',
|
|
117
|
+
label: 'Mark No-Show',
|
|
118
|
+
workflowId: 'mark_no_show-workflow',
|
|
119
|
+
isAvailableFor: (deal) => deal.stage_key === 'interested' && deal.state_key === 'discovery_nudging'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'rebook',
|
|
123
|
+
label: 'Rebook',
|
|
124
|
+
workflowId: 'crm-rebook-workflow',
|
|
125
|
+
isAvailableFor: (deal) =>
|
|
126
|
+
deal.stage_key === 'interested' && deal.state_key === 'discovery_booking_cancelled'
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
function deriveActions(deal: Parameters<typeof deriveActionsCore>[0], actions: ActionDef[] = DEFAULT_CRM_ACTIONS): Action[] {
|
|
131
|
+
return deriveActionsCore(deal, actions)
|
|
132
|
+
}
|
|
55
133
|
|
|
56
134
|
// ---------------------------------------------------------------------------
|
|
57
135
|
// DEFAULT_CRM_ACTIONS static contract
|
|
@@ -321,11 +399,9 @@ describe('deriveActions()', () => {
|
|
|
321
399
|
expect(deriveActions(makeDeal({ stage_key: 'interested' }), [])).toEqual([])
|
|
322
400
|
})
|
|
323
401
|
|
|
324
|
-
it('
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
expect(withDefaults).toEqual(withExplicit)
|
|
328
|
-
})
|
|
402
|
+
it('core deriveActions returns [] when no action catalog is provided', () => {
|
|
403
|
+
expect(deriveActionsCore(makeDeal({ stage_key: 'proposal' }))).toEqual([])
|
|
404
|
+
})
|
|
329
405
|
})
|
|
330
406
|
|
|
331
407
|
// ---------------------------------------------------------------------------
|
|
@@ -514,11 +590,12 @@ describe('deriveActions() — multi-state discovery path coverage', () => {
|
|
|
514
590
|
describe('deriveActions() - ownership-gated reply action', () => {
|
|
515
591
|
it('lead asks us to respond: send_reply is available and ownership is our move', () => {
|
|
516
592
|
const actions = deriveActions(
|
|
517
|
-
makeDeal({
|
|
518
|
-
stage_key: 'interested',
|
|
519
|
-
state_key: 'discovery_replied',
|
|
520
|
-
|
|
521
|
-
|
|
593
|
+
makeDeal({
|
|
594
|
+
stage_key: 'interested',
|
|
595
|
+
state_key: 'discovery_replied',
|
|
596
|
+
nextAction: 'send_reply',
|
|
597
|
+
activity_log: [{ type: 'reply_received', timestamp: '2026-01-10T10:00:00Z' }]
|
|
598
|
+
})
|
|
522
599
|
)
|
|
523
600
|
|
|
524
601
|
expect(actions).toContainEqual({ key: 'send_reply', label: 'Send Reply', payloadSchema: SendReplyActionPayloadSchema })
|