@elevasis/core 0.18.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.
Files changed (54) hide show
  1. package/dist/index.d.ts +82 -1
  2. package/dist/index.js +353 -171
  3. package/dist/knowledge/index.d.ts +44 -1
  4. package/dist/organization-model/index.d.ts +82 -1
  5. package/dist/organization-model/index.js +353 -171
  6. package/dist/test-utils/index.d.ts +41 -12
  7. package/dist/test-utils/index.js +352 -171
  8. package/package.json +4 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +89 -69
  10. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
  11. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
  12. package/src/business/acquisition/api-schemas.test.ts +199 -15
  13. package/src/business/acquisition/api-schemas.ts +116 -51
  14. package/src/business/acquisition/build-templates.test.ts +212 -0
  15. package/src/business/acquisition/derive-actions.test.ts +1 -1
  16. package/src/business/acquisition/types.ts +21 -38
  17. package/src/business/deals/api-schemas.ts +2 -2
  18. package/src/execution/engine/index.ts +436 -434
  19. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
  20. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
  21. package/src/execution/engine/tools/lead-service-types.ts +51 -9
  22. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
  23. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
  24. package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
  25. package/src/execution/engine/tools/registry.ts +700 -698
  26. package/src/execution/engine/tools/tool-maps.ts +10 -0
  27. package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
  28. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
  29. package/src/integrations/oauth/provider-registry.ts +74 -61
  30. package/src/integrations/oauth/server/credentials.ts +43 -39
  31. package/src/knowledge/__tests__/queries.test.ts +89 -0
  32. package/src/organization-model/__tests__/graph.test.ts +108 -2
  33. package/src/organization-model/__tests__/icons.test.ts +61 -0
  34. package/src/organization-model/__tests__/knowledge.test.ts +118 -1
  35. package/src/organization-model/__tests__/prospecting-ssot.test.ts +91 -0
  36. package/src/organization-model/__tests__/schema.test.ts +122 -0
  37. package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
  38. package/src/organization-model/defaults.ts +8 -0
  39. package/src/organization-model/domains/knowledge.ts +9 -0
  40. package/src/organization-model/domains/prospecting.ts +347 -226
  41. package/src/organization-model/domains/sales.ts +40 -30
  42. package/src/organization-model/graph/build.ts +74 -0
  43. package/src/organization-model/graph/schema.ts +1 -0
  44. package/src/organization-model/graph/types.ts +1 -0
  45. package/src/organization-model/icons.ts +3 -0
  46. package/src/organization-model/schema.ts +63 -0
  47. package/src/organization-model/surface-projection.ts +218 -0
  48. package/src/organization-model/types.ts +9 -1
  49. package/src/platform/constants/versions.ts +1 -1
  50. package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
  51. package/src/platform/utils/validation.ts +425 -425
  52. package/src/reference/_generated/contracts.md +89 -69
  53. package/src/server.ts +6 -0
  54. package/src/supabase/database.types.ts +6 -12
@@ -1,10 +1,45 @@
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
+ import { CAPABILITY_REGISTRY } from '../../organization-model/domains/prospecting'
4
5
  import { isProspectingBuildTemplateId } from './build-templates'
5
6
  export { CrmPriorityBucketKeySchema, CrmPriorityBucketOverrideSchema, CrmPriorityOverrideSchema } from './crm-priority'
6
7
  export type { CrmPriorityBucketOverride, CrmPriorityOverride, ResolvedCrmPriorityRuleConfig } from './crm-priority'
7
8
 
9
+ export const ProcessingStageStatusSchema = z.enum(['success', 'no_result', 'skipped', 'error'])
10
+
11
+ export const LeadGenStageKeySchema = z
12
+ .string()
13
+ .refine((value) => Object.prototype.hasOwnProperty.call(LEAD_GEN_STAGE_CATALOG, value), {
14
+ message: 'processing state key must match LEAD_GEN_STAGE_CATALOG'
15
+ })
16
+
17
+ export const LeadGenCapabilityKeySchema = z
18
+ .string()
19
+ .refine((value) => CAPABILITY_REGISTRY.some((c) => c.id === value), {
20
+ message: 'capabilityKey must match CAPABILITY_REGISTRY'
21
+ })
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
+
32
+ export const ProcessingStateEntrySchema = z
33
+ .object({
34
+ status: ProcessingStageStatusSchema,
35
+ data: z.unknown().optional()
36
+ })
37
+ .passthrough()
38
+
39
+ export const ProcessingStateSchema = z.record(LeadGenStageKeySchema, ProcessingStateEntrySchema)
40
+ export const CompanyProcessingStateSchema = ProcessingStateSchema
41
+ export const ContactProcessingStateSchema = ProcessingStateSchema
42
+
8
43
  /**
9
44
  * Deal Management API Schemas
10
45
  *
@@ -21,7 +56,7 @@ export type { CrmPriorityBucketOverride, CrmPriorityOverride, ResolvedCrmPriorit
21
56
  // Enum literals (must match DB CHECK constraints exactly)
22
57
  // ---------------------------------------------------------------------------
23
58
 
24
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
59
+ export const DealStageSchema = CrmStageKeySchema
25
60
 
26
61
  export const AcqDealTaskKindSchema = z.enum(['call', 'email', 'meeting', 'other'])
27
62
 
@@ -89,7 +124,17 @@ export const TransitionItemRequestSchema = z
89
124
  .object({
90
125
  pipelineKey: z.string().min(1),
91
126
  stageKey: z.string().min(1),
92
- 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(),
93
138
  reason: z.string().optional(),
94
139
  expectedUpdatedAt: z.string().datetime().optional()
95
140
  })
@@ -97,7 +142,7 @@ export const TransitionItemRequestSchema = z
97
142
 
98
143
  export const TransitionDealStateRequestSchema = z
99
144
  .object({
100
- stateKey: z.string().min(1),
145
+ stateKey: CrmStateKeySchema,
101
146
  reason: z.string().optional(),
102
147
  expectedUpdatedAt: z.string().datetime().optional()
103
148
  })
@@ -132,7 +177,7 @@ export const DealContactSummarySchema = z.object({
132
177
  title: z.string().nullable(),
133
178
  headline: z.string().nullable(),
134
179
  linkedin_url: z.string().nullable(),
135
- pipeline_status: z.record(z.string(), z.unknown()).nullable(),
180
+ processing_state: ProcessingStateSchema.nullable(),
136
181
  enrichment_data: z.record(z.string(), z.unknown()).nullable(),
137
182
  company: z
138
183
  .object({
@@ -305,6 +350,11 @@ export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
305
350
  // ---------------------------------------------------------------------------
306
351
 
307
352
  export const DealSchemas = {
353
+ // Primitives
354
+ CrmStageKey: CrmStageKeySchema,
355
+ CrmStateKey: CrmStateKeySchema,
356
+ DealStage: DealStageSchema,
357
+
308
358
  // Params
309
359
  DealIdParams: DealIdParamsSchema,
310
360
  DealTaskIdParams: DealTaskIdParamsSchema,
@@ -317,7 +367,7 @@ export const DealSchemas = {
317
367
  // Request bodies
318
368
  CreateDealNoteRequest: CreateDealNoteRequestSchema,
319
369
  CreateDealTaskRequest: CreateDealTaskRequestSchema,
320
- TransitionItemRequest: TransitionItemRequestSchema,
370
+ TransitionItemRequest: CrmTransitionItemRequestSchema,
321
371
  TransitionDealStateRequest: TransitionDealStateRequestSchema,
322
372
  ExecuteActionParams: ExecuteActionParamsSchema,
323
373
  ExecuteActionRequest: ExecuteActionRequestSchema,
@@ -340,6 +390,8 @@ export const DealSchemas = {
340
390
  // ---------------------------------------------------------------------------
341
391
 
342
392
  export type DealStage = z.infer<typeof DealStageSchema>
393
+ export type CrmStageKey = z.infer<typeof CrmStageKeySchema>
394
+ export type CrmStateKey = z.infer<typeof CrmStateKeySchema>
343
395
  export type AcqDealTaskKind = z.infer<typeof AcqDealTaskKindSchema>
344
396
  export type DealIdParams = z.infer<typeof DealIdParamsSchema>
345
397
  export type DealTaskIdParams = z.infer<typeof DealTaskIdParamsSchema>
@@ -349,6 +401,7 @@ export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
349
401
  export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
350
402
  export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
351
403
  export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
404
+ export type CrmTransitionItemRequest = z.infer<typeof CrmTransitionItemRequestSchema>
352
405
  export type TransitionDealStateRequest = z.infer<typeof TransitionDealStateRequestSchema>
353
406
  export type ExecuteActionParams = z.infer<typeof ExecuteActionParamsSchema>
354
407
  export type ExecuteActionRequest = z.infer<typeof ExecuteActionRequestSchema>
@@ -442,10 +495,10 @@ export const BuildPlanSnapshotStepSchema = z
442
495
  description: z.string().trim().min(1).max(2000).optional(),
443
496
  primaryEntity: z.enum(['company', 'contact']),
444
497
  outputs: z.array(z.enum(['company', 'contact', 'export'])).min(1),
445
- stageKey: z.string().trim().min(1).max(100),
498
+ stageKey: LeadGenStageKeySchema,
446
499
  dependsOn: z.array(z.string().trim().min(1).max(100)).optional(),
447
500
  dependencyMode: z.literal('per-record-eligibility'),
448
- capabilityKey: z.string().trim().min(1).max(100),
501
+ capabilityKey: LeadGenCapabilityKeySchema,
449
502
  defaultBatchSize: z.number().int().positive(),
450
503
  maxBatchSize: z.number().int().positive()
451
504
  })
@@ -454,11 +507,38 @@ export const BuildPlanSnapshotStepSchema = z
454
507
  path: ['defaultBatchSize']
455
508
  })
456
509
 
457
- export const BuildPlanSnapshotSchema = z.object({
458
- templateId: z.string().trim().min(1).max(100),
459
- templateLabel: z.string().trim().min(1).max(120),
460
- steps: z.array(BuildPlanSnapshotStepSchema).min(1)
461
- })
510
+ export const BuildPlanSnapshotSchema = z
511
+ .object({
512
+ templateId: z.string().trim().min(1).max(100),
513
+ templateLabel: z.string().trim().min(1).max(120),
514
+ steps: z.array(BuildPlanSnapshotStepSchema).min(1)
515
+ })
516
+ .superRefine((snapshot, ctx) => {
517
+ const stepIds = new Set<string>()
518
+
519
+ snapshot.steps.forEach((step, index) => {
520
+ if (stepIds.has(step.id)) {
521
+ ctx.addIssue({
522
+ code: z.ZodIssueCode.custom,
523
+ message: `duplicate build-plan step id "${step.id}"`,
524
+ path: ['steps', index, 'id']
525
+ })
526
+ }
527
+ stepIds.add(step.id)
528
+ })
529
+
530
+ snapshot.steps.forEach((step, index) => {
531
+ for (const dependencyId of step.dependsOn ?? []) {
532
+ if (!stepIds.has(dependencyId)) {
533
+ ctx.addIssue({
534
+ code: z.ZodIssueCode.custom,
535
+ message: `dependsOn references unknown build-plan step "${dependencyId}"`,
536
+ path: ['steps', index, 'dependsOn']
537
+ })
538
+ }
539
+ }
540
+ })
541
+ })
462
542
 
463
543
  export const AcqListMetadataSchema = z
464
544
  .object({
@@ -638,13 +718,6 @@ export const ListTelemetryResponseSchema = ListTelemetrySchema
638
718
 
639
719
  export const ListTelemetryListResponseSchema = z.array(ListTelemetrySchema)
640
720
 
641
- /**
642
- * Terminal row-level status for one lead-gen processing stage.
643
- * Missing key still means not attempted; legacy boolean `true` is normalized
644
- * to `success` by the API reader during rollout.
645
- */
646
- export const ProcessingStageStatusSchema = z.enum(['success', 'no_result', 'skipped', 'error'])
647
-
648
721
  /**
649
722
  * Per-stage progress aggregate for a single pipeline stage.
650
723
  * `attempted` counts terminal statuses, including success, no-result, skipped,
@@ -704,29 +777,6 @@ export const AcqCompanyStatusSchema = z.enum(['active', 'invalid'])
704
777
  export const AcqContactStatusSchema = z.enum(['active', 'invalid'])
705
778
  export const AcqEmailValidSchema = z.enum(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])
706
779
 
707
- /**
708
- * Zod schema mirroring the CompanyPipelineStatus interface from types.ts.
709
- * Used by operations-layer CompanyRecord schemas to validate pipelineStatus
710
- * against the canonical shape instead of using open passthrough().
711
- */
712
- export const CompanyPipelineStatusSchema = z
713
- .object({
714
- acquired: z.boolean().optional(),
715
- enrichment: z
716
- .record(
717
- z.string(),
718
- z
719
- .object({
720
- status: z.enum(['pending', 'complete', 'failed', 'skipped']),
721
- completedAt: z.string().optional(),
722
- error: z.string().optional()
723
- })
724
- .passthrough()
725
- )
726
- .optional()
727
- })
728
- .passthrough()
729
-
730
780
  export const CompanyIdParamsSchema = z.object({
731
781
  companyId: UuidSchema
732
782
  })
@@ -743,6 +793,7 @@ export const ListCompaniesQuerySchema = z
743
793
  website: z.string().trim().min(1).max(2048).optional(),
744
794
  segment: z.string().trim().min(1).max(255).optional(),
745
795
  category: z.string().trim().min(1).max(255).optional(),
796
+ pipelineStatus: z.unknown().optional(),
746
797
  batchId: z.string().trim().min(1).max(255).optional(),
747
798
  status: AcqCompanyStatusSchema.optional(),
748
799
  includeAll: QueryBooleanSchema.optional(),
@@ -776,6 +827,7 @@ export const CreateCompanyRequestSchema = z
776
827
  category: z.string().trim().min(1).max(255).optional(),
777
828
  source: z.string().trim().min(1).max(255).optional(),
778
829
  batchId: z.string().trim().min(1).max(255).optional(),
830
+ pipelineStatus: z.unknown().optional(),
779
831
  verticalResearch: z.string().trim().min(1).max(5000).optional()
780
832
  })
781
833
  .strict()
@@ -792,7 +844,8 @@ export const UpdateCompanyRequestSchema = z
792
844
  locationState: z.string().trim().min(1).max(255).optional(),
793
845
  category: z.string().trim().min(1).max(255).optional(),
794
846
  segment: z.string().trim().min(1).max(255).optional(),
795
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
847
+ processingState: CompanyProcessingStateSchema.optional(),
848
+ pipelineStatus: z.unknown().optional(),
796
849
  enrichmentData: z.record(z.string(), z.unknown()).optional(),
797
850
  source: z.string().trim().min(1).max(255).optional(),
798
851
  batchId: z.string().trim().min(1).max(255).optional(),
@@ -812,6 +865,7 @@ export const UpdateCompanyRequestSchema = z
812
865
  data.locationState !== undefined ||
813
866
  data.category !== undefined ||
814
867
  data.segment !== undefined ||
868
+ data.processingState !== undefined ||
815
869
  data.pipelineStatus !== undefined ||
816
870
  data.enrichmentData !== undefined ||
817
871
  data.source !== undefined ||
@@ -833,7 +887,8 @@ export const CreateContactRequestSchema = z
833
887
  title: z.string().trim().min(1).max(255).optional(),
834
888
  source: z.string().trim().min(1).max(255).optional(),
835
889
  sourceId: z.string().trim().min(1).max(255).optional(),
836
- batchId: z.string().trim().min(1).max(255).optional()
890
+ batchId: z.string().trim().min(1).max(255).optional(),
891
+ pipelineStatus: z.unknown().optional()
837
892
  })
838
893
  .strict()
839
894
 
@@ -848,7 +903,8 @@ export const UpdateContactRequestSchema = z
848
903
  headline: z.string().trim().min(1).max(5000).optional(),
849
904
  filterReason: z.string().trim().min(1).max(5000).optional(),
850
905
  openingLine: z.string().trim().min(1).max(5000).optional(),
851
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
906
+ processingState: ContactProcessingStateSchema.optional(),
907
+ pipelineStatus: z.unknown().optional(),
852
908
  enrichmentData: z.record(z.string(), z.unknown()).optional(),
853
909
  status: AcqContactStatusSchema.optional()
854
910
  })
@@ -864,6 +920,7 @@ export const UpdateContactRequestSchema = z
864
920
  data.headline !== undefined ||
865
921
  data.filterReason !== undefined ||
866
922
  data.openingLine !== undefined ||
923
+ data.processingState !== undefined ||
867
924
  data.pipelineStatus !== undefined ||
868
925
  data.enrichmentData !== undefined ||
869
926
  data.status !== undefined,
@@ -886,7 +943,8 @@ export const AcqCompanyResponseSchema = z.object({
886
943
  category: z.string().nullable(),
887
944
  categoryPain: z.string().nullable(),
888
945
  segment: z.string().nullable(),
889
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
946
+ processingState: CompanyProcessingStateSchema.nullable(),
947
+ pipelineStatus: z.unknown().nullable().optional(),
890
948
  enrichmentData: z.record(z.string(), z.unknown()).nullable(),
891
949
  source: z.string().nullable(),
892
950
  batchId: z.string().nullable(),
@@ -936,7 +994,8 @@ export const AcqContactResponseSchema = z.object({
936
994
  openingLine: z.string().nullable(),
937
995
  source: z.string().nullable(),
938
996
  sourceId: z.string().nullable(),
939
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
997
+ processingState: ContactProcessingStateSchema.nullable(),
998
+ pipelineStatus: z.unknown().nullable().optional(),
940
999
  enrichmentData: z.record(z.string(), z.unknown()).nullable(),
941
1000
  attioPersonId: z.string().nullable(),
942
1001
  batchId: z.string().nullable(),
@@ -1058,11 +1117,12 @@ export const AcqListCompanyResponseSchema = z.object({
1058
1117
  })
1059
1118
 
1060
1119
  // ---------------------------------------------------------------------------
1061
- // Track B: Transition Request (shared by list, list-member, list-company)
1062
- // 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.
1063
1122
  // ---------------------------------------------------------------------------
1064
1123
 
1065
1124
  export const AcqCompanySchemas = {
1125
+ CompanyProcessingState: CompanyProcessingStateSchema,
1066
1126
  CompanyIdParams: CompanyIdParamsSchema,
1067
1127
  ListCompaniesQuery: ListCompaniesQuerySchema,
1068
1128
  CreateCompanyRequest: CreateCompanyRequestSchema,
@@ -1073,6 +1133,7 @@ export const AcqCompanySchemas = {
1073
1133
  }
1074
1134
 
1075
1135
  export const AcqContactSchemas = {
1136
+ ContactProcessingState: ContactProcessingStateSchema,
1076
1137
  ContactIdParams: ContactIdParamsSchema,
1077
1138
  ListContactsQuery: ListContactsQuerySchema,
1078
1139
  CreateContactRequest: CreateContactRequestSchema,
@@ -1116,7 +1177,10 @@ export const AcqListSchemas = {
1116
1177
  BuildPlanSnapshot: BuildPlanSnapshotSchema,
1117
1178
  BuildPlanSnapshotStep: BuildPlanSnapshotStepSchema,
1118
1179
  AcqListMetadata: AcqListMetadataSchema,
1180
+ LeadGenStageKey: LeadGenStageKeySchema,
1181
+ LeadGenCapabilityKey: LeadGenCapabilityKeySchema,
1119
1182
  ProcessingStageStatus: ProcessingStageStatusSchema,
1183
+ ProcessingState: ProcessingStateSchema,
1120
1184
  ListStageCounts: ListStageCountsSchema,
1121
1185
  ListTelemetry: ListTelemetrySchema,
1122
1186
 
@@ -1160,7 +1224,7 @@ export const AcqSubstrateSchemas = {
1160
1224
  ListCompanyIdParams: ListCompanyIdParamsSchema,
1161
1225
  AcqListCompanyResponse: AcqListCompanyResponseSchema,
1162
1226
 
1163
- // Transition (shared with deals — TransitionItemRequestSchema)
1227
+ // Transition (generic stateful substrate)
1164
1228
  TransitionItemRequest: TransitionItemRequestSchema
1165
1229
  }
1166
1230
 
@@ -1195,6 +1259,7 @@ export type PipelineConfig = z.infer<typeof PipelineConfigSchema>
1195
1259
  export type BuildPlanSnapshotStep = z.infer<typeof BuildPlanSnapshotStepSchema>
1196
1260
  export type BuildPlanSnapshot = z.infer<typeof BuildPlanSnapshotSchema>
1197
1261
  export type AcqListMetadata = z.infer<typeof AcqListMetadataSchema>
1262
+ export type LeadGenCapabilityKey = z.infer<typeof LeadGenCapabilityKeySchema>
1198
1263
  export type ProcessingStageStatus = z.infer<typeof ProcessingStageStatusSchema>
1199
1264
  export type ListStageCountsInput = z.infer<typeof ListStageCountsSchema>['stageCounts']
1200
1265
  export type ListTelemetryInput = z.infer<typeof ListTelemetrySchema>
@@ -0,0 +1,212 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID,
4
+ PROSPECTING_BUILD_TEMPLATE_OPTIONS,
5
+ createBuildPlanSnapshotFromTemplateId,
6
+ isProspectingBuildTemplateId
7
+ } from './build-templates'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // isProspectingBuildTemplateId
11
+ // ---------------------------------------------------------------------------
12
+
13
+ describe('isProspectingBuildTemplateId', () => {
14
+ it('returns true for every id in PROSPECTING_BUILD_TEMPLATE_OPTIONS', () => {
15
+ for (const option of PROSPECTING_BUILD_TEMPLATE_OPTIONS) {
16
+ expect(isProspectingBuildTemplateId(option.id)).toBe(true)
17
+ }
18
+ })
19
+
20
+ it('returns true for the known "local-services" template id', () => {
21
+ expect(isProspectingBuildTemplateId('local-services')).toBe(true)
22
+ })
23
+
24
+ it('returns true for the known "dtc-subscription-apollo-clickup" template id', () => {
25
+ expect(isProspectingBuildTemplateId('dtc-subscription-apollo-clickup')).toBe(true)
26
+ })
27
+
28
+ it('returns false for an unknown string', () => {
29
+ expect(isProspectingBuildTemplateId('not-a-template')).toBe(false)
30
+ })
31
+
32
+ it('returns false for an empty string', () => {
33
+ expect(isProspectingBuildTemplateId('')).toBe(false)
34
+ })
35
+
36
+ it('returns false for a partial id match', () => {
37
+ expect(isProspectingBuildTemplateId('local')).toBe(false)
38
+ expect(isProspectingBuildTemplateId('dtc-subscription')).toBe(false)
39
+ })
40
+
41
+ it('returns false for a whitespace-padded id', () => {
42
+ expect(isProspectingBuildTemplateId(' local-services ')).toBe(false)
43
+ })
44
+
45
+ it('verifies PROSPECTING_BUILD_TEMPLATE_OPTIONS contains at least two entries', () => {
46
+ expect(PROSPECTING_BUILD_TEMPLATE_OPTIONS.length).toBeGreaterThanOrEqual(2)
47
+ })
48
+
49
+ it('verifies DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID is itself a valid template id', () => {
50
+ expect(isProspectingBuildTemplateId(DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID)).toBe(true)
51
+ })
52
+ })
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // createBuildPlanSnapshotFromTemplateId — unknown id
56
+ // ---------------------------------------------------------------------------
57
+
58
+ describe('createBuildPlanSnapshotFromTemplateId — unknown id', () => {
59
+ it('returns null for an unknown template id', () => {
60
+ expect(createBuildPlanSnapshotFromTemplateId('not-a-template')).toBeNull()
61
+ })
62
+
63
+ it('returns null for an empty string', () => {
64
+ expect(createBuildPlanSnapshotFromTemplateId('')).toBeNull()
65
+ })
66
+ })
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // createBuildPlanSnapshotFromTemplateId — "local-services"
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe('createBuildPlanSnapshotFromTemplateId — "local-services"', () => {
73
+ const snapshot = createBuildPlanSnapshotFromTemplateId('local-services')
74
+
75
+ it('returns a non-null snapshot', () => {
76
+ expect(snapshot).not.toBeNull()
77
+ })
78
+
79
+ it('preserves the template id in the snapshot', () => {
80
+ expect(snapshot?.templateId).toBe('local-services')
81
+ })
82
+
83
+ it('includes a non-empty templateLabel', () => {
84
+ expect(snapshot?.templateLabel).toBeTruthy()
85
+ })
86
+
87
+ it('produces a steps array with length matching the catalog (7 steps)', () => {
88
+ expect(snapshot?.steps).toHaveLength(7)
89
+ })
90
+
91
+ it('every step has required fields: id, label, primaryEntity, outputs, stageKey, dependencyMode, capabilityKey', () => {
92
+ for (const step of snapshot?.steps ?? []) {
93
+ expect(step.id).toBeTruthy()
94
+ expect(step.label).toBeTruthy()
95
+ expect(['company', 'contact']).toContain(step.primaryEntity)
96
+ expect(step.outputs.length).toBeGreaterThanOrEqual(1)
97
+ expect(step.stageKey).toBeTruthy()
98
+ expect(step.dependencyMode).toBe('per-record-eligibility')
99
+ expect(step.capabilityKey).toBeTruthy()
100
+ }
101
+ })
102
+
103
+ it('every step has positive defaultBatchSize and maxBatchSize', () => {
104
+ for (const step of snapshot?.steps ?? []) {
105
+ expect(step.defaultBatchSize).toBeGreaterThan(0)
106
+ expect(step.maxBatchSize).toBeGreaterThanOrEqual(step.defaultBatchSize)
107
+ }
108
+ })
109
+
110
+ it('steps with declared dependsOn in the catalog have dependsOn in the snapshot', () => {
111
+ const withDeps = snapshot?.steps.filter((step) => step.dependsOn !== undefined) ?? []
112
+ expect(withDeps.length).toBeGreaterThan(0)
113
+ for (const step of withDeps) {
114
+ expect(Array.isArray(step.dependsOn)).toBe(true)
115
+ expect((step.dependsOn ?? []).length).toBeGreaterThan(0)
116
+ }
117
+ })
118
+
119
+ it('the first step (source-companies) has no dependsOn', () => {
120
+ const first = snapshot?.steps[0]
121
+ expect(first?.id).toBe('source-companies')
122
+ expect(first?.dependsOn).toBeUndefined()
123
+ })
124
+
125
+ it('outputs arrays are copies (mutation does not affect subsequent calls)', () => {
126
+ const snapshotA = createBuildPlanSnapshotFromTemplateId('local-services')
127
+ const snapshotB = createBuildPlanSnapshotFromTemplateId('local-services')
128
+ snapshotA?.steps[0]?.outputs.push('export' as never)
129
+ expect(snapshotB?.steps[0]?.outputs).not.toContain('export')
130
+ })
131
+ })
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // createBuildPlanSnapshotFromTemplateId — "dtc-subscription-apollo-clickup"
135
+ // ---------------------------------------------------------------------------
136
+
137
+ describe('createBuildPlanSnapshotFromTemplateId — "dtc-subscription-apollo-clickup"', () => {
138
+ const snapshot = createBuildPlanSnapshotFromTemplateId('dtc-subscription-apollo-clickup')
139
+
140
+ it('returns a non-null snapshot', () => {
141
+ expect(snapshot).not.toBeNull()
142
+ })
143
+
144
+ it('preserves the template id in the snapshot', () => {
145
+ expect(snapshot?.templateId).toBe('dtc-subscription-apollo-clickup')
146
+ })
147
+
148
+ it('produces a steps array with length matching the catalog (6 steps)', () => {
149
+ expect(snapshot?.steps).toHaveLength(6)
150
+ })
151
+
152
+ it('every step has required fields: id, label, primaryEntity, outputs, stageKey, dependencyMode, capabilityKey', () => {
153
+ for (const step of snapshot?.steps ?? []) {
154
+ expect(step.id).toBeTruthy()
155
+ expect(step.label).toBeTruthy()
156
+ expect(['company', 'contact']).toContain(step.primaryEntity)
157
+ expect(step.outputs.length).toBeGreaterThanOrEqual(1)
158
+ expect(step.stageKey).toBeTruthy()
159
+ expect(step.dependencyMode).toBe('per-record-eligibility')
160
+ expect(step.capabilityKey).toBeTruthy()
161
+ }
162
+ })
163
+
164
+ it('every step has positive defaultBatchSize and maxBatchSize', () => {
165
+ for (const step of snapshot?.steps ?? []) {
166
+ expect(step.defaultBatchSize).toBeGreaterThan(0)
167
+ expect(step.maxBatchSize).toBeGreaterThanOrEqual(step.defaultBatchSize)
168
+ }
169
+ })
170
+
171
+ it('the first step (import-apollo-search) has no dependsOn', () => {
172
+ const first = snapshot?.steps[0]
173
+ expect(first?.id).toBe('import-apollo-search')
174
+ expect(first?.dependsOn).toBeUndefined()
175
+ })
176
+
177
+ it('the first step outputs both company and contact', () => {
178
+ const first = snapshot?.steps[0]
179
+ expect(first?.outputs).toContain('company')
180
+ expect(first?.outputs).toContain('contact')
181
+ })
182
+
183
+ it('the final step (review-and-export) outputs export', () => {
184
+ const last = snapshot?.steps[snapshot.steps.length - 1]
185
+ expect(last?.id).toBe('review-and-export')
186
+ expect(last?.outputs).toContain('export')
187
+ })
188
+
189
+ it('steps with descriptions in the catalog include description in the snapshot', () => {
190
+ const withDesc = snapshot?.steps.filter((step) => step.description !== undefined) ?? []
191
+ expect(withDesc.length).toBeGreaterThan(0)
192
+ for (const step of withDesc) {
193
+ expect(typeof step.description).toBe('string')
194
+ expect(step.description!.length).toBeGreaterThan(0)
195
+ }
196
+ })
197
+ })
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Snapshot shape contract — applies to every valid template id
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe('createBuildPlanSnapshotFromTemplateId — shape contract for all templates', () => {
204
+ it('returns a snapshot for every id that isProspectingBuildTemplateId reports valid', () => {
205
+ for (const option of PROSPECTING_BUILD_TEMPLATE_OPTIONS) {
206
+ const snapshot = createBuildPlanSnapshotFromTemplateId(option.id)
207
+ expect(snapshot).not.toBeNull()
208
+ expect(snapshot?.templateId).toBe(option.id)
209
+ expect(snapshot?.steps.length).toBeGreaterThan(0)
210
+ }
211
+ })
212
+ })
@@ -20,7 +20,7 @@ function makeDeal(
20
20
  organization_id: 'org-fixture-id',
21
21
  contact_id: null,
22
22
  contact_email: 'fixture@example.com',
23
- pipeline_key: 'default',
23
+ pipeline_key: 'crm',
24
24
  stage_key: null,
25
25
  state_key: null,
26
26
  activity_log: [],
@@ -1,5 +1,7 @@
1
1
  import type { Database } from '../../supabase/database.types'
2
- import type { PipelineStage } from './api-schemas'
2
+ import type { Capability } from '../../organization-model/domains/prospecting'
3
+ import type { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
+ import type { PipelineStage, ProcessingStageStatus } from './api-schemas'
3
5
 
4
6
  // =============================================================================
5
7
  // Supabase-Generated Types (Re-exported from database.types)
@@ -49,43 +51,20 @@ export interface WebPost {
49
51
  aiInsight?: string
50
52
  }
51
53
 
52
- /**
53
- * Tracks pipeline status for a company across all processing stages.
54
- */
55
- export interface CompanyPipelineStatus {
56
- acquired: boolean
57
- enrichment: {
58
- [source: string]: {
59
- status: 'pending' | 'complete' | 'failed' | 'skipped'
60
- completedAt?: string
61
- error?: string
62
- }
63
- }
64
- }
54
+ export type LeadGenStageKey = (typeof LEAD_GEN_STAGE_CATALOG)[keyof typeof LEAD_GEN_STAGE_CATALOG]['key']
55
+ export type LeadGenCapabilityKey = Capability['id']
65
56
 
66
- /**
67
- * Tracks pipeline status for a contact across all processing stages.
68
- */
69
- export interface ContactPipelineStatus {
70
- enrichment: {
71
- [source: string]: {
72
- status: 'pending' | 'complete' | 'failed' | 'skipped'
73
- completedAt?: string
74
- error?: string
75
- }
76
- }
77
- personalization: {
78
- status: 'pending' | 'complete' | 'failed' | 'skipped'
79
- completedAt?: string
80
- }
81
- outreach: {
82
- status: 'pending' | 'sent' | 'replied' | 'bounced' | 'opted-out'
83
- sentAt?: string
84
- channel?: string
85
- campaignId?: string
86
- }
57
+ export interface ProcessingStateEntry {
58
+ status: ProcessingStageStatus
59
+ data?: unknown
87
60
  }
88
61
 
62
+ export type ProcessingState = Partial<Record<LeadGenStageKey, ProcessingStateEntry>>
63
+ export type CompanyProcessingState = ProcessingState
64
+ export type ContactProcessingState = ProcessingState
65
+ /** @deprecated Use `processingState`. Retained only as a compile-time/read-shape bridge for external tenants. */
66
+ export type LegacyPipelineStatus = unknown
67
+
89
68
  /**
90
69
  * Enrichment data collected for a company from various sources.
91
70
  */
@@ -241,7 +220,9 @@ export interface AcqCompany {
241
220
  category: string | null
242
221
  categoryPain: string | null
243
222
  segment: string | null
244
- pipelineStatus: CompanyPipelineStatus | null
223
+ processingState: CompanyProcessingState | null
224
+ /** @deprecated Use `processingState`. This legacy DB column has been removed; responses return null. */
225
+ pipelineStatus?: LegacyPipelineStatus | null
245
226
  enrichmentData: CompanyEnrichmentData | null
246
227
  source: string | null
247
228
  batchId: string | null
@@ -277,7 +258,9 @@ export interface AcqContact {
277
258
  openingLine: string | null
278
259
  source: string | null
279
260
  sourceId: string | null
280
- pipelineStatus: ContactPipelineStatus | null
261
+ processingState: ContactProcessingState | null
262
+ /** @deprecated Use `processingState`. This legacy DB column has been removed; responses return null. */
263
+ pipelineStatus?: LegacyPipelineStatus | null
281
264
  enrichmentData: ContactEnrichmentData | null
282
265
  /** Attio Person record ID - set when contact responds and is added to CRM */
283
266
  attioPersonId: string | null
@@ -318,7 +301,7 @@ export interface DealContact {
318
301
  title: string | null
319
302
  headline: string | null
320
303
  linkedin_url: string | null
321
- pipeline_status: Record<string, unknown> | null
304
+ processing_state: Record<string, unknown> | null
322
305
  enrichment_data: Record<string, unknown> | null
323
306
  company: {
324
307
  id: string