@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 CHANGED
@@ -386,6 +386,12 @@ var ProspectingBuildTemplateSchema = DisplayMetadataSchema.extend({
386
386
  id: ModelIdSchema,
387
387
  steps: z.array(ProspectingBuildTemplateStepSchema).min(1)
388
388
  });
389
+ z.object({
390
+ id: ModelIdSchema,
391
+ label: z.string(),
392
+ description: z.string(),
393
+ resourceId: ModelIdSchema
394
+ });
389
395
  var PROSPECTING_STEPS = {
390
396
  localServices: {
391
397
  sourceCompanies: {
@@ -1124,8 +1130,12 @@ var LEGACY_FEATURE_ALIASES = /* @__PURE__ */ new Map([
1124
1130
  function hasFeature(featuresById, featureId) {
1125
1131
  return featuresById.has(featureId) || featuresById.has(LEGACY_FEATURE_ALIASES.get(featureId) ?? "");
1126
1132
  }
1133
+ function defaultFeaturePathFor(id) {
1134
+ return `/${id.replaceAll(".", "/")}`;
1135
+ }
1127
1136
  var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
1128
1137
  const featuresById = collectIds(model.features, ctx, ["features"], "Feature");
1138
+ const featureIdsByEffectivePath = /* @__PURE__ */ new Map();
1129
1139
  model.features.forEach((feature, featureIndex) => {
1130
1140
  const segments = feature.id.split(".");
1131
1141
  if (segments.length > 1) {
@@ -1141,6 +1151,20 @@ var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ct
1141
1151
  const hasChildren = model.features.some(
1142
1152
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.id !== feature.id
1143
1153
  );
1154
+ const contributesRoutePath = feature.path !== void 0 || !hasChildren;
1155
+ if (contributesRoutePath) {
1156
+ const effectivePath = feature.path ?? defaultFeaturePathFor(feature.id);
1157
+ const existingFeatureId = featureIdsByEffectivePath.get(effectivePath);
1158
+ if (existingFeatureId !== void 0) {
1159
+ addIssue(
1160
+ ctx,
1161
+ ["features", featureIndex, feature.path === void 0 ? "id" : "path"],
1162
+ `Feature "${feature.id}" effective path "${effectivePath}" duplicates feature "${existingFeatureId}"`
1163
+ );
1164
+ } else {
1165
+ featureIdsByEffectivePath.set(effectivePath, feature.id);
1166
+ }
1167
+ }
1144
1168
  if (hasChildren && feature.enabled) {
1145
1169
  const hasEnabledDescendant = model.features.some(
1146
1170
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.enabled
@@ -1154,6 +1178,43 @@ var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ct
1154
1178
  }
1155
1179
  }
1156
1180
  });
1181
+ const surfacesById = collectIds(model.navigation.surfaces, ctx, ["navigation", "surfaces"], "Navigation surface");
1182
+ if (model.navigation.defaultSurfaceId !== void 0 && !surfacesById.has(model.navigation.defaultSurfaceId)) {
1183
+ addIssue(
1184
+ ctx,
1185
+ ["navigation", "defaultSurfaceId"],
1186
+ `Navigation defaultSurfaceId references unknown surface "${model.navigation.defaultSurfaceId}"`
1187
+ );
1188
+ }
1189
+ model.navigation.groups.forEach((group, groupIndex) => {
1190
+ group.surfaceIds.forEach((surfaceId, surfaceIndex) => {
1191
+ if (!surfacesById.has(surfaceId)) {
1192
+ addIssue(
1193
+ ctx,
1194
+ ["navigation", "groups", groupIndex, "surfaceIds", surfaceIndex],
1195
+ `Navigation group "${group.id}" references unknown surface "${surfaceId}"`
1196
+ );
1197
+ }
1198
+ });
1199
+ });
1200
+ model.navigation.surfaces.forEach((surface, surfaceIndex) => {
1201
+ if (surface.featureId !== void 0 && !hasFeature(featuresById, surface.featureId)) {
1202
+ addIssue(
1203
+ ctx,
1204
+ ["navigation", "surfaces", surfaceIndex, "featureId"],
1205
+ `Navigation surface "${surface.id}" references unknown feature "${surface.featureId}"`
1206
+ );
1207
+ }
1208
+ surface.featureIds.forEach((featureId, featureIndex) => {
1209
+ if (!hasFeature(featuresById, featureId)) {
1210
+ addIssue(
1211
+ ctx,
1212
+ ["navigation", "surfaces", surfaceIndex, "featureIds", featureIndex],
1213
+ `Navigation surface "${surface.id}" references unknown feature "${featureId}"`
1214
+ );
1215
+ }
1216
+ });
1217
+ });
1157
1218
  const segmentsById = new Map(model.customers.segments.map((seg) => [seg.id, seg]));
1158
1219
  model.offerings.products.forEach((product, productIndex) => {
1159
1220
  product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
@@ -1199,6 +1260,7 @@ var OrganizationGraphNodeKindSchema = z.enum([
1199
1260
  "surface",
1200
1261
  "entity",
1201
1262
  "capability",
1263
+ "stage",
1202
1264
  "resource",
1203
1265
  "knowledge"
1204
1266
  ]);
@@ -1216,7 +1216,7 @@ declare const OrganizationModelSchema: z.ZodObject<{
1216
1216
 
1217
1217
  type OrganizationModel = z.infer<typeof OrganizationModelSchema>;
1218
1218
 
1219
- type OrganizationGraphNodeKind = 'organization' | 'feature' | 'surface' | 'entity' | 'capability' | 'resource' | 'knowledge';
1219
+ type OrganizationGraphNodeKind = 'organization' | 'feature' | 'surface' | 'entity' | 'capability' | 'stage' | 'resource' | 'knowledge';
1220
1220
  type OrganizationGraphEdgeKind = 'contains' | 'references' | 'exposes' | 'maps_to' | 'operates-on' | 'uses' | 'governs';
1221
1221
 
1222
1222
  interface OrganizationGraphNode {
@@ -386,6 +386,12 @@ var ProspectingBuildTemplateSchema = DisplayMetadataSchema.extend({
386
386
  id: ModelIdSchema,
387
387
  steps: z.array(ProspectingBuildTemplateStepSchema).min(1)
388
388
  });
389
+ z.object({
390
+ id: ModelIdSchema,
391
+ label: z.string(),
392
+ description: z.string(),
393
+ resourceId: ModelIdSchema
394
+ });
389
395
  var PROSPECTING_STEPS = {
390
396
  localServices: {
391
397
  sourceCompanies: {
@@ -1124,8 +1130,12 @@ var LEGACY_FEATURE_ALIASES = /* @__PURE__ */ new Map([
1124
1130
  function hasFeature(featuresById, featureId) {
1125
1131
  return featuresById.has(featureId) || featuresById.has(LEGACY_FEATURE_ALIASES.get(featureId) ?? "");
1126
1132
  }
1133
+ function defaultFeaturePathFor(id) {
1134
+ return `/${id.replaceAll(".", "/")}`;
1135
+ }
1127
1136
  var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
1128
1137
  const featuresById = collectIds(model.features, ctx, ["features"], "Feature");
1138
+ const featureIdsByEffectivePath = /* @__PURE__ */ new Map();
1129
1139
  model.features.forEach((feature, featureIndex) => {
1130
1140
  const segments = feature.id.split(".");
1131
1141
  if (segments.length > 1) {
@@ -1141,6 +1151,20 @@ var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ct
1141
1151
  const hasChildren = model.features.some(
1142
1152
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.id !== feature.id
1143
1153
  );
1154
+ const contributesRoutePath = feature.path !== void 0 || !hasChildren;
1155
+ if (contributesRoutePath) {
1156
+ const effectivePath = feature.path ?? defaultFeaturePathFor(feature.id);
1157
+ const existingFeatureId = featureIdsByEffectivePath.get(effectivePath);
1158
+ if (existingFeatureId !== void 0) {
1159
+ addIssue(
1160
+ ctx,
1161
+ ["features", featureIndex, feature.path === void 0 ? "id" : "path"],
1162
+ `Feature "${feature.id}" effective path "${effectivePath}" duplicates feature "${existingFeatureId}"`
1163
+ );
1164
+ } else {
1165
+ featureIdsByEffectivePath.set(effectivePath, feature.id);
1166
+ }
1167
+ }
1144
1168
  if (hasChildren && feature.enabled) {
1145
1169
  const hasEnabledDescendant = model.features.some(
1146
1170
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.enabled
@@ -1154,6 +1178,43 @@ var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ct
1154
1178
  }
1155
1179
  }
1156
1180
  });
1181
+ const surfacesById = collectIds(model.navigation.surfaces, ctx, ["navigation", "surfaces"], "Navigation surface");
1182
+ if (model.navigation.defaultSurfaceId !== void 0 && !surfacesById.has(model.navigation.defaultSurfaceId)) {
1183
+ addIssue(
1184
+ ctx,
1185
+ ["navigation", "defaultSurfaceId"],
1186
+ `Navigation defaultSurfaceId references unknown surface "${model.navigation.defaultSurfaceId}"`
1187
+ );
1188
+ }
1189
+ model.navigation.groups.forEach((group, groupIndex) => {
1190
+ group.surfaceIds.forEach((surfaceId, surfaceIndex) => {
1191
+ if (!surfacesById.has(surfaceId)) {
1192
+ addIssue(
1193
+ ctx,
1194
+ ["navigation", "groups", groupIndex, "surfaceIds", surfaceIndex],
1195
+ `Navigation group "${group.id}" references unknown surface "${surfaceId}"`
1196
+ );
1197
+ }
1198
+ });
1199
+ });
1200
+ model.navigation.surfaces.forEach((surface, surfaceIndex) => {
1201
+ if (surface.featureId !== void 0 && !hasFeature(featuresById, surface.featureId)) {
1202
+ addIssue(
1203
+ ctx,
1204
+ ["navigation", "surfaces", surfaceIndex, "featureId"],
1205
+ `Navigation surface "${surface.id}" references unknown feature "${surface.featureId}"`
1206
+ );
1207
+ }
1208
+ surface.featureIds.forEach((featureId, featureIndex) => {
1209
+ if (!hasFeature(featuresById, featureId)) {
1210
+ addIssue(
1211
+ ctx,
1212
+ ["navigation", "surfaces", surfaceIndex, "featureIds", featureIndex],
1213
+ `Navigation surface "${surface.id}" references unknown feature "${featureId}"`
1214
+ );
1215
+ }
1216
+ });
1217
+ });
1157
1218
  const segmentsById = new Map(model.customers.segments.map((seg) => [seg.id, seg]));
1158
1219
  model.offerings.products.forEach((product, productIndex) => {
1159
1220
  product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
@@ -1199,6 +1260,7 @@ var OrganizationGraphNodeKindSchema = z.enum([
1199
1260
  "surface",
1200
1261
  "entity",
1201
1262
  "capability",
1263
+ "stage",
1202
1264
  "resource",
1203
1265
  "knowledge"
1204
1266
  ]);
@@ -19801,6 +19801,12 @@ var ProspectingBuildTemplateSchema = DisplayMetadataSchema.extend({
19801
19801
  id: ModelIdSchema,
19802
19802
  steps: z.array(ProspectingBuildTemplateStepSchema).min(1)
19803
19803
  });
19804
+ z.object({
19805
+ id: ModelIdSchema,
19806
+ label: z.string(),
19807
+ description: z.string(),
19808
+ resourceId: ModelIdSchema
19809
+ });
19804
19810
  var PROSPECTING_STEPS = {
19805
19811
  localServices: {
19806
19812
  sourceCompanies: {
@@ -20521,8 +20527,12 @@ var LEGACY_FEATURE_ALIASES = /* @__PURE__ */ new Map([
20521
20527
  function hasFeature(featuresById, featureId) {
20522
20528
  return featuresById.has(featureId) || featuresById.has(LEGACY_FEATURE_ALIASES.get(featureId) ?? "");
20523
20529
  }
20530
+ function defaultFeaturePathFor(id) {
20531
+ return `/${id.replaceAll(".", "/")}`;
20532
+ }
20524
20533
  var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
20525
20534
  const featuresById = collectIds(model.features, ctx, ["features"], "Feature");
20535
+ const featureIdsByEffectivePath = /* @__PURE__ */ new Map();
20526
20536
  model.features.forEach((feature, featureIndex) => {
20527
20537
  const segments = feature.id.split(".");
20528
20538
  if (segments.length > 1) {
@@ -20538,6 +20548,20 @@ var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ct
20538
20548
  const hasChildren = model.features.some(
20539
20549
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.id !== feature.id
20540
20550
  );
20551
+ const contributesRoutePath = feature.path !== void 0 || !hasChildren;
20552
+ if (contributesRoutePath) {
20553
+ const effectivePath = feature.path ?? defaultFeaturePathFor(feature.id);
20554
+ const existingFeatureId = featureIdsByEffectivePath.get(effectivePath);
20555
+ if (existingFeatureId !== void 0) {
20556
+ addIssue(
20557
+ ctx,
20558
+ ["features", featureIndex, feature.path === void 0 ? "id" : "path"],
20559
+ `Feature "${feature.id}" effective path "${effectivePath}" duplicates feature "${existingFeatureId}"`
20560
+ );
20561
+ } else {
20562
+ featureIdsByEffectivePath.set(effectivePath, feature.id);
20563
+ }
20564
+ }
20541
20565
  if (hasChildren && feature.enabled) {
20542
20566
  const hasEnabledDescendant = model.features.some(
20543
20567
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.enabled
@@ -20551,6 +20575,43 @@ var OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ct
20551
20575
  }
20552
20576
  }
20553
20577
  });
20578
+ const surfacesById = collectIds(model.navigation.surfaces, ctx, ["navigation", "surfaces"], "Navigation surface");
20579
+ if (model.navigation.defaultSurfaceId !== void 0 && !surfacesById.has(model.navigation.defaultSurfaceId)) {
20580
+ addIssue(
20581
+ ctx,
20582
+ ["navigation", "defaultSurfaceId"],
20583
+ `Navigation defaultSurfaceId references unknown surface "${model.navigation.defaultSurfaceId}"`
20584
+ );
20585
+ }
20586
+ model.navigation.groups.forEach((group, groupIndex) => {
20587
+ group.surfaceIds.forEach((surfaceId, surfaceIndex) => {
20588
+ if (!surfacesById.has(surfaceId)) {
20589
+ addIssue(
20590
+ ctx,
20591
+ ["navigation", "groups", groupIndex, "surfaceIds", surfaceIndex],
20592
+ `Navigation group "${group.id}" references unknown surface "${surfaceId}"`
20593
+ );
20594
+ }
20595
+ });
20596
+ });
20597
+ model.navigation.surfaces.forEach((surface, surfaceIndex) => {
20598
+ if (surface.featureId !== void 0 && !hasFeature(featuresById, surface.featureId)) {
20599
+ addIssue(
20600
+ ctx,
20601
+ ["navigation", "surfaces", surfaceIndex, "featureId"],
20602
+ `Navigation surface "${surface.id}" references unknown feature "${surface.featureId}"`
20603
+ );
20604
+ }
20605
+ surface.featureIds.forEach((featureId, featureIndex) => {
20606
+ if (!hasFeature(featuresById, featureId)) {
20607
+ addIssue(
20608
+ ctx,
20609
+ ["navigation", "surfaces", surfaceIndex, "featureIds", featureIndex],
20610
+ `Navigation surface "${surface.id}" references unknown feature "${featureId}"`
20611
+ );
20612
+ }
20613
+ });
20614
+ });
20554
20615
  const segmentsById = new Map(model.customers.segments.map((seg) => [seg.id, seg]));
20555
20616
  model.offerings.products.forEach((product, productIndex) => {
20556
20617
  product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -41,8 +41,8 @@
41
41
  "rollup-plugin-dts": "^6.3.0",
42
42
  "tsup": "^8.0.0",
43
43
  "typescript": "5.9.2",
44
- "@repo/eslint-config": "0.0.0",
45
- "@repo/typescript-config": "0.0.0"
44
+ "@repo/typescript-config": "0.0.0",
45
+ "@repo/eslint-config": "0.0.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "@anthropic-ai/sdk": "^0.62.0",
@@ -1123,7 +1123,7 @@ export const DEFAULT_CRM_PRIORITY_RULE_CONFIG: CrmPriorityRuleConfig = {
1123
1123
  ### `DealStageSchema`
1124
1124
 
1125
1125
  ```typescript
1126
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
1126
+ export const DealStageSchema = CrmStageKeySchema
1127
1127
  ```
1128
1128
 
1129
1129
  ### `AcqDealTaskKindSchema`
@@ -1215,7 +1215,7 @@ export const TransitionItemRequestSchema = z
1215
1215
  .object({
1216
1216
  pipelineKey: z.string().min(1),
1217
1217
  stageKey: z.string().min(1),
1218
- stateKey: z.string().nullable().optional(),
1218
+ stateKey: z.string().min(1).nullable().optional(),
1219
1219
  reason: z.string().optional(),
1220
1220
  expectedUpdatedAt: z.string().datetime().optional()
1221
1221
  })
@@ -1409,6 +1409,11 @@ export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
1409
1409
 
1410
1410
  ```typescript
1411
1411
  export const DealSchemas = {
1412
+ // Primitives
1413
+ CrmStageKey: CrmStageKeySchema,
1414
+ CrmStateKey: CrmStateKeySchema,
1415
+ DealStage: DealStageSchema,
1416
+
1412
1417
  // Params
1413
1418
  DealIdParams: DealIdParamsSchema,
1414
1419
  DealTaskIdParams: DealTaskIdParamsSchema,
@@ -1421,7 +1426,7 @@ export const DealSchemas = {
1421
1426
  // Request bodies
1422
1427
  CreateDealNoteRequest: CreateDealNoteRequestSchema,
1423
1428
  CreateDealTaskRequest: CreateDealTaskRequestSchema,
1424
- TransitionItemRequest: TransitionItemRequestSchema,
1429
+ TransitionItemRequest: CrmTransitionItemRequestSchema,
1425
1430
  TransitionDealStateRequest: TransitionDealStateRequestSchema,
1426
1431
  ExecuteActionParams: ExecuteActionParamsSchema,
1427
1432
  ExecuteActionRequest: ExecuteActionRequestSchema,
@@ -2552,7 +2557,7 @@ export const AcqSubstrateSchemas = {
2552
2557
  ListCompanyIdParams: ListCompanyIdParamsSchema,
2553
2558
  AcqListCompanyResponse: AcqListCompanyResponseSchema,
2554
2559
 
2555
- // Transition (shared with deals — TransitionItemRequestSchema)
2560
+ // Transition (generic stateful substrate)
2556
2561
  TransitionItemRequest: TransitionItemRequestSchema
2557
2562
  }
2558
2563
  ```
@@ -2620,6 +2625,8 @@ export interface StatefulStageDefinition {
2620
2625
  /** Matches stage_key values written by workflow steps. */
2621
2626
  stageKey: string
2622
2627
  label: string
2628
+ /** UI color token. Consumers may map this to their design system. */
2629
+ color?: string
2623
2630
  states: StatefulStateDefinition[]
2624
2631
  }
2625
2632
  ```
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import {
3
+ CRM_PIPELINE_DEFINITION,
3
4
  DEFAULT_CRM_PRIORITY_RULE_CONFIG,
4
5
  LEAD_GEN_PIPELINE_DEFINITIONS,
5
6
  LEAD_GEN_STAGE_CATALOG
@@ -19,6 +20,9 @@ import {
19
20
  CreateContactRequestSchema,
20
21
  CreateDealNoteRequestSchema,
21
22
  CreateDealTaskRequestSchema,
23
+ CrmStageKeySchema,
24
+ CrmStateKeySchema,
25
+ CrmTransitionItemRequestSchema,
22
26
  CreateListRequestSchema,
23
27
  DealDetailResponseSchema,
24
28
  DealListItemSchema,
@@ -37,6 +41,7 @@ import {
37
41
  ListStatusSchema,
38
42
  PipelineStageSchema,
39
43
  ScrapingConfigSchema,
44
+ TransitionDealStateRequestSchema,
40
45
  TransitionItemRequestSchema,
41
46
  UpdateCompanyRequestSchema,
42
47
  UpdateContactRequestSchema,
@@ -67,17 +72,31 @@ const PRIORITY = {
67
72
  // ---------------------------------------------------------------------------
68
73
 
69
74
  describe('DealStageSchema', () => {
70
- it.each(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])(
71
- 'accepts canonical stage "%s"',
72
- (stage) => {
73
- expect(DealStageSchema.safeParse(stage).success).toBe(true)
74
- }
75
- )
75
+ const crmStageKeys = CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)
76
+ const crmStateKeys = CRM_PIPELINE_DEFINITION.stages.flatMap((stage) => stage.states.map((state) => state.stateKey))
77
+
78
+ it('derives CRM stage keys from CRM_PIPELINE_DEFINITION', () => {
79
+ expect(CrmStageKeySchema.options).toEqual(crmStageKeys)
80
+ expect(DealStageSchema.options).toEqual(crmStageKeys)
81
+ })
82
+
83
+ it('derives CRM state keys from CRM_PIPELINE_DEFINITION', () => {
84
+ expect(CrmStateKeySchema.options).toEqual(crmStateKeys)
85
+ })
86
+
87
+ it.each(crmStageKeys)('accepts canonical stage "%s"', (stage) => {
88
+ expect(DealStageSchema.safeParse(stage).success).toBe(true)
89
+ })
76
90
 
77
91
  it('rejects an unknown stage value', () => {
78
92
  expect(DealStageSchema.safeParse('open').success).toBe(false)
79
93
  expect(DealStageSchema.safeParse('').success).toBe(false)
80
94
  })
95
+
96
+ it('rejects unknown CRM state values', () => {
97
+ expect(CrmStateKeySchema.safeParse('custom_state').success).toBe(false)
98
+ expect(CrmStateKeySchema.safeParse('').success).toBe(false)
99
+ })
81
100
  })
82
101
 
83
102
  // ---------------------------------------------------------------------------
@@ -86,7 +105,7 @@ describe('DealStageSchema', () => {
86
105
 
87
106
  describe('TransitionItemRequestSchema', () => {
88
107
  const valid = {
89
- pipelineKey: 'default',
108
+ pipelineKey: 'lead-gen',
90
109
  stageKey: 'interested',
91
110
  stateKey: null
92
111
  }
@@ -122,11 +141,16 @@ describe('TransitionItemRequestSchema', () => {
122
141
  })
123
142
 
124
143
  it('accepts all canonical CRM deal stages', () => {
125
- const stages = ['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing']
126
- for (const stageKey of stages) {
127
- expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'default', stageKey, stateKey: null }).success).toBe(
128
- true
129
- )
144
+ for (const stageKey of CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)) {
145
+ expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'crm', stageKey, stateKey: null }).success).toBe(true)
146
+ }
147
+ })
148
+
149
+ it('accepts catalog-derived CRM state keys', () => {
150
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
151
+ stage.states.map((state) => state.stateKey)
152
+ )) {
153
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stateKey }).success).toBe(true)
130
154
  }
131
155
  })
132
156
 
@@ -158,6 +182,11 @@ describe('TransitionItemRequestSchema', () => {
158
182
  expect(result.success).toBe(false)
159
183
  })
160
184
 
185
+ it('accepts unknown non-empty stage and state keys for generic substrate transitions', () => {
186
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'custom_stage' }).success).toBe(true)
187
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'custom_state' }).success).toBe(true)
188
+ })
189
+
161
190
  it('rejects unknown top-level fields (strict mode)', () => {
162
191
  const result = TransitionItemRequestSchema.safeParse({ ...valid, unknownField: 'x' })
163
192
  expect(result.success).toBe(false)
@@ -176,6 +205,64 @@ describe('TransitionItemRequestSchema', () => {
176
205
  })
177
206
  })
178
207
 
208
+ // ---------------------------------------------------------------------------
209
+ // CrmTransitionItemRequestSchema
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe('CrmTransitionItemRequestSchema', () => {
213
+ const valid = {
214
+ pipelineKey: 'crm',
215
+ stageKey: 'interested',
216
+ stateKey: null
217
+ }
218
+
219
+ it('accepts catalog-derived CRM stage and state keys', () => {
220
+ for (const stageKey of CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)) {
221
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey }).success).toBe(true)
222
+ }
223
+
224
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
225
+ stage.states.map((state) => state.stateKey)
226
+ )) {
227
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey }).success).toBe(true)
228
+ }
229
+ })
230
+
231
+ it('rejects non-CRM pipeline keys and unknown CRM keys', () => {
232
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: 'lead-gen' }).success).toBe(false)
233
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'unknown_stage' }).success).toBe(false)
234
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'unknown_state' }).success).toBe(false)
235
+ })
236
+ })
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // TransitionDealStateRequestSchema
240
+ // ---------------------------------------------------------------------------
241
+
242
+ describe('TransitionDealStateRequestSchema', () => {
243
+ it('accepts catalog-derived CRM state keys', () => {
244
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
245
+ stage.states.map((state) => state.stateKey)
246
+ )) {
247
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey }).success).toBe(true)
248
+ }
249
+ })
250
+
251
+ it('rejects unknown state values', () => {
252
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey: 'unknown_state' }).success).toBe(false)
253
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey: '' }).success).toBe(false)
254
+ })
255
+
256
+ it('preserves strict request schema behavior', () => {
257
+ expect(
258
+ TransitionDealStateRequestSchema.safeParse({
259
+ stateKey: CRM_PIPELINE_DEFINITION.stages[0]?.states[0]?.stateKey,
260
+ extra: 'x'
261
+ }).success
262
+ ).toBe(false)
263
+ })
264
+ })
265
+
179
266
  // ---------------------------------------------------------------------------
180
267
  // ExecuteActionRequestSchema
181
268
  // ---------------------------------------------------------------------------
@@ -698,7 +785,7 @@ describe('DealDetailResponseSchema (forward-compat)', () => {
698
785
  organization_id: VALID_UUID,
699
786
  contact_id: null,
700
787
  contact_email: 'test@example.com',
701
- pipeline_key: 'default',
788
+ pipeline_key: 'crm',
702
789
  stage_key: null,
703
790
  state_key: null,
704
791
  activity_log: [],
@@ -1585,5 +1672,4 @@ describe('AcqContactResponseSchema (forward-compat)', () => {
1585
1672
  it('rejects an invalid emailValid value', () => {
1586
1673
  expect(AcqContactResponseSchema.safeParse({ ...baseContact, emailValid: 'BAD' }).success).toBe(false)
1587
1674
  })
1588
-
1589
1675
  })
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
3
- import { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
3
+ import { CRM_PIPELINE_DEFINITION, LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
4
  import { CAPABILITY_REGISTRY } from '../../organization-model/domains/prospecting'
5
5
  import { isProspectingBuildTemplateId } from './build-templates'
6
6
  export { CrmPriorityBucketKeySchema, CrmPriorityBucketOverrideSchema, CrmPriorityOverrideSchema } from './crm-priority'
@@ -16,10 +16,19 @@ export const LeadGenStageKeySchema = z
16
16
 
17
17
  export const LeadGenCapabilityKeySchema = z
18
18
  .string()
19
- .refine((value) => Object.prototype.hasOwnProperty.call(CAPABILITY_REGISTRY, value), {
19
+ .refine((value) => CAPABILITY_REGISTRY.some((c) => c.id === value), {
20
20
  message: 'capabilityKey must match CAPABILITY_REGISTRY'
21
21
  })
22
22
 
23
+ const crmStageKeys = CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey) as [string, ...string[]]
24
+ const crmStateKeys = CRM_PIPELINE_DEFINITION.stages.flatMap((stage) => stage.states.map((state) => state.stateKey)) as [
25
+ string,
26
+ ...string[]
27
+ ]
28
+
29
+ export const CrmStageKeySchema = z.enum(crmStageKeys)
30
+ export const CrmStateKeySchema = z.enum(crmStateKeys)
31
+
23
32
  export const ProcessingStateEntrySchema = z
24
33
  .object({
25
34
  status: ProcessingStageStatusSchema,
@@ -47,7 +56,7 @@ export const ContactProcessingStateSchema = ProcessingStateSchema
47
56
  // Enum literals (must match DB CHECK constraints exactly)
48
57
  // ---------------------------------------------------------------------------
49
58
 
50
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
59
+ export const DealStageSchema = CrmStageKeySchema
51
60
 
52
61
  export const AcqDealTaskKindSchema = z.enum(['call', 'email', 'meeting', 'other'])
53
62
 
@@ -115,7 +124,17 @@ export const TransitionItemRequestSchema = z
115
124
  .object({
116
125
  pipelineKey: z.string().min(1),
117
126
  stageKey: z.string().min(1),
118
- stateKey: z.string().nullable().optional(),
127
+ stateKey: z.string().min(1).nullable().optional(),
128
+ reason: z.string().optional(),
129
+ expectedUpdatedAt: z.string().datetime().optional()
130
+ })
131
+ .strict()
132
+
133
+ export const CrmTransitionItemRequestSchema = z
134
+ .object({
135
+ pipelineKey: z.literal(CRM_PIPELINE_DEFINITION.pipelineKey),
136
+ stageKey: CrmStageKeySchema,
137
+ stateKey: CrmStateKeySchema.nullable().optional(),
119
138
  reason: z.string().optional(),
120
139
  expectedUpdatedAt: z.string().datetime().optional()
121
140
  })
@@ -123,7 +142,7 @@ export const TransitionItemRequestSchema = z
123
142
 
124
143
  export const TransitionDealStateRequestSchema = z
125
144
  .object({
126
- stateKey: z.string().min(1),
145
+ stateKey: CrmStateKeySchema,
127
146
  reason: z.string().optional(),
128
147
  expectedUpdatedAt: z.string().datetime().optional()
129
148
  })
@@ -331,6 +350,11 @@ export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
331
350
  // ---------------------------------------------------------------------------
332
351
 
333
352
  export const DealSchemas = {
353
+ // Primitives
354
+ CrmStageKey: CrmStageKeySchema,
355
+ CrmStateKey: CrmStateKeySchema,
356
+ DealStage: DealStageSchema,
357
+
334
358
  // Params
335
359
  DealIdParams: DealIdParamsSchema,
336
360
  DealTaskIdParams: DealTaskIdParamsSchema,
@@ -343,7 +367,7 @@ export const DealSchemas = {
343
367
  // Request bodies
344
368
  CreateDealNoteRequest: CreateDealNoteRequestSchema,
345
369
  CreateDealTaskRequest: CreateDealTaskRequestSchema,
346
- TransitionItemRequest: TransitionItemRequestSchema,
370
+ TransitionItemRequest: CrmTransitionItemRequestSchema,
347
371
  TransitionDealStateRequest: TransitionDealStateRequestSchema,
348
372
  ExecuteActionParams: ExecuteActionParamsSchema,
349
373
  ExecuteActionRequest: ExecuteActionRequestSchema,
@@ -366,6 +390,8 @@ export const DealSchemas = {
366
390
  // ---------------------------------------------------------------------------
367
391
 
368
392
  export type DealStage = z.infer<typeof DealStageSchema>
393
+ export type CrmStageKey = z.infer<typeof CrmStageKeySchema>
394
+ export type CrmStateKey = z.infer<typeof CrmStateKeySchema>
369
395
  export type AcqDealTaskKind = z.infer<typeof AcqDealTaskKindSchema>
370
396
  export type DealIdParams = z.infer<typeof DealIdParamsSchema>
371
397
  export type DealTaskIdParams = z.infer<typeof DealTaskIdParamsSchema>
@@ -375,6 +401,7 @@ export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
375
401
  export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
376
402
  export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
377
403
  export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
404
+ export type CrmTransitionItemRequest = z.infer<typeof CrmTransitionItemRequestSchema>
378
405
  export type TransitionDealStateRequest = z.infer<typeof TransitionDealStateRequestSchema>
379
406
  export type ExecuteActionParams = z.infer<typeof ExecuteActionParamsSchema>
380
407
  export type ExecuteActionRequest = z.infer<typeof ExecuteActionRequestSchema>
@@ -1090,8 +1117,8 @@ export const AcqListCompanyResponseSchema = z.object({
1090
1117
  })
1091
1118
 
1092
1119
  // ---------------------------------------------------------------------------
1093
- // Track B: Transition Request (shared by list, list-member, list-company)
1094
- // TransitionItemRequestSchema already exists above (for deals) — reuse it.
1120
+ // Track B: Transition request for list, list-member, and list-company substrate routes.
1121
+ // CRM deals use DealSchemas.TransitionItemRequest, which is catalog-backed.
1095
1122
  // ---------------------------------------------------------------------------
1096
1123
 
1097
1124
  export const AcqCompanySchemas = {
@@ -1197,7 +1224,7 @@ export const AcqSubstrateSchemas = {
1197
1224
  ListCompanyIdParams: ListCompanyIdParamsSchema,
1198
1225
  AcqListCompanyResponse: AcqListCompanyResponseSchema,
1199
1226
 
1200
- // Transition (shared with deals — TransitionItemRequestSchema)
1227
+ // Transition (generic stateful substrate)
1201
1228
  TransitionItemRequest: TransitionItemRequestSchema
1202
1229
  }
1203
1230