@elevasis/core 0.13.0 → 0.15.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 (42) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +9 -2
  3. package/dist/organization-model/index.d.ts +1 -1
  4. package/dist/organization-model/index.js +9 -2
  5. package/dist/test-utils/index.d.ts +463 -377
  6. package/dist/test-utils/index.js +9 -2
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2336 -0
  9. package/src/business/acquisition/activity-events.test.ts +250 -0
  10. package/src/business/acquisition/activity-events.ts +7 -65
  11. package/src/business/acquisition/api-schemas.test.ts +1180 -0
  12. package/src/business/acquisition/api-schemas.ts +317 -73
  13. package/src/business/acquisition/crm-state-actions.test.ts +160 -0
  14. package/src/business/acquisition/derive-actions.test.ts +518 -0
  15. package/src/business/acquisition/derive-actions.ts +101 -78
  16. package/src/business/acquisition/index.ts +51 -9
  17. package/src/business/acquisition/stateful.ts +30 -0
  18. package/src/business/acquisition/types.ts +48 -80
  19. package/src/execution/engine/index.ts +437 -434
  20. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -360
  21. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
  22. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
  23. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
  24. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
  25. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
  26. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
  27. package/src/execution/engine/tools/integration/service.test.ts +34 -9
  28. package/src/execution/engine/tools/integration/service.ts +6 -3
  29. package/src/execution/engine/tools/lead-service-types.ts +934 -874
  30. package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
  31. package/src/execution/engine/tools/registry.ts +701 -699
  32. package/src/execution/engine/tools/tool-maps.ts +30 -2
  33. package/src/execution/engine/workflow/types.ts +11 -0
  34. package/src/organization-model/contracts.ts +4 -4
  35. package/src/organization-model/domains/navigation.ts +62 -62
  36. package/src/organization-model/domains/sales.test.ts +189 -0
  37. package/src/organization-model/domains/sales.ts +456 -94
  38. package/src/organization-model/published.ts +21 -21
  39. package/src/organization-model/resolve.ts +21 -8
  40. package/src/platform/constants/versions.ts +1 -1
  41. package/src/reference/_generated/contracts.md +2336 -0
  42. package/src/supabase/database.types.ts +2958 -2886
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
- import { UuidSchema } from '../../platform/utils/validation'
2
+ import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
3
+ import { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
3
4
 
4
5
  /**
5
6
  * Deal Management API Schemas
@@ -91,6 +92,27 @@ export const TransitionItemRequestSchema = z
91
92
  })
92
93
  .strict()
93
94
 
95
+ export const TransitionDealStateRequestSchema = z
96
+ .object({
97
+ stateKey: z.string().min(1),
98
+ reason: z.string().optional(),
99
+ expectedUpdatedAt: z.string().datetime().optional()
100
+ })
101
+ .strict()
102
+
103
+ export const ExecuteActionParamsSchema = z
104
+ .object({
105
+ dealId: UuidSchema,
106
+ actionKey: NonEmptyStringSchema
107
+ })
108
+ .strict()
109
+
110
+ export const ExecuteActionRequestSchema = z
111
+ .object({
112
+ payload: z.record(z.string(), z.unknown()).optional()
113
+ })
114
+ .strict()
115
+
94
116
  // ---------------------------------------------------------------------------
95
117
  // Response schemas (no .strict() — allows forward-compatible additions)
96
118
  // ---------------------------------------------------------------------------
@@ -166,6 +188,7 @@ export const DealListResponseSchema = z.object({
166
188
  export const DealStageSummarySchema = z.object({
167
189
  stage: z.string(),
168
190
  count: z.number().int(),
191
+ totalValue: z.number(),
169
192
  oldestUpdatedAt: z.string().nullable(),
170
193
  newestUpdatedAt: z.string().nullable()
171
194
  })
@@ -263,6 +286,9 @@ export const DealSchemas = {
263
286
  CreateDealNoteRequest: CreateDealNoteRequestSchema,
264
287
  CreateDealTaskRequest: CreateDealTaskRequestSchema,
265
288
  TransitionItemRequest: TransitionItemRequestSchema,
289
+ TransitionDealStateRequest: TransitionDealStateRequestSchema,
290
+ ExecuteActionParams: ExecuteActionParamsSchema,
291
+ ExecuteActionRequest: ExecuteActionRequestSchema,
266
292
 
267
293
  // Responses
268
294
  DealListResponse: DealListResponseSchema,
@@ -289,6 +315,9 @@ export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
289
315
  export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
290
316
  export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
291
317
  export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
318
+ export type TransitionDealStateRequest = z.infer<typeof TransitionDealStateRequestSchema>
319
+ export type ExecuteActionParams = z.infer<typeof ExecuteActionParamsSchema>
320
+ export type ExecuteActionRequest = z.infer<typeof ExecuteActionRequestSchema>
292
321
  export type DealListResponse = z.infer<typeof DealListResponseSchema>
293
322
  export type DealSummaryResponse = z.infer<typeof DealSummaryResponseSchema>
294
323
  export type DealLookupItem = z.infer<typeof DealLookupItemSchema>
@@ -313,61 +342,61 @@ export type DealTaskListResponse = z.infer<typeof DealTaskListResponseSchema>
313
342
  // ---------------------------------------------------------------------------
314
343
 
315
344
  // ---------------------------------------------------------------------------
316
- // Primitives — list config subtrees
345
+ // Primitives — list status enum + jsonb config schemas
317
346
  // ---------------------------------------------------------------------------
318
347
 
319
- export const ListQualificationSchema = z.object({
320
- targetDescription: z.string(),
321
- minReviewCount: z.number().int().min(0),
322
- minRating: z.number().min(0).max(5),
323
- excludeFranchises: z.boolean(),
324
- customRules: z.string()
325
- })
326
-
327
- export const ListEnrichmentSchema = z.object({
328
- emailDiscovery: z
329
- .object({
330
- primary: z.enum(['tomba', 'anymailfinder']),
331
- credentialName: z.string().optional()
332
- })
333
- .optional(),
334
- emailVerification: z
335
- .object({
336
- provider: z.literal('millionverifier'),
337
- threshold: z.enum(['ok', 'ok+catch_all']).optional()
338
- })
339
- .optional()
340
- })
348
+ /**
349
+ * Lifecycle status enum for `acq_lists.status` (mirrors DB CHECK constraint
350
+ * from migration 20260428000003_lead_gen_acq_lists_status_and_config.sql).
351
+ */
352
+ export const ListStatusSchema = z.enum(['draft', 'enriching', 'launched', 'closing', 'archived'])
341
353
 
342
- export const ListPersonalizationSchema = z.object({
343
- industryContext: z.string().optional(),
344
- emailBody: z.string().optional(),
345
- creativeDirection: z.string().optional(),
346
- exclusionRules: z.array(z.string()).optional()
354
+ /**
355
+ * Scraping criteria stored in `acq_lists.scraping_config` jsonb.
356
+ * Edited via the UI; consumed by lgn-01 prospecting workflows (Apify input shape,
357
+ * geography, vertical, size). All fields are optional — empty config is valid.
358
+ */
359
+ export const ScrapingConfigSchema = z.object({
360
+ vertical: z.string().trim().max(255).optional(),
361
+ geography: z.string().trim().max(500).optional(),
362
+ size: z.string().trim().max(255).optional(),
363
+ apifyInput: z.record(z.string(), z.unknown()).optional()
347
364
  })
348
365
 
349
- export const PipelineStepSchema = z.object({
350
- key: z.string(),
351
- label: z.string(),
352
- resourceId: z.string(),
353
- inputTemplate: z.record(z.string(), z.unknown()),
354
- enabled: z.boolean(),
355
- order: z.number().int()
366
+ /**
367
+ * ICP / qualification rubric stored in `acq_lists.icp` jsonb.
368
+ * Replaces the legacy `config.qualification` blob. Consumed by the
369
+ * company-qualification workflow.
370
+ */
371
+ export const IcpRubricSchema = z.object({
372
+ qualificationRubricKey: z.string().trim().max(255).nullish(),
373
+ targetDescription: z.string().optional(),
374
+ minReviewCount: z.number().int().min(0).optional(),
375
+ minRating: z.number().min(0).max(5).optional(),
376
+ excludeFranchises: z.boolean().optional(),
377
+ customRules: z.string().optional()
356
378
  })
357
379
 
358
- export const ListPipelineSchema = z.object({
359
- steps: z.array(PipelineStepSchema)
380
+ /**
381
+ * One stage entry in a list's `pipeline_config.stages[]`. The `key` is
382
+ * validated against `LEAD_GEN_STAGE_CATALOG` so list pipeline definitions
383
+ * stay aligned with the org-os semantic layer.
384
+ */
385
+ export const PipelineStageSchema = z.object({
386
+ key: z.string().refine((value) => Object.prototype.hasOwnProperty.call(LEAD_GEN_STAGE_CATALOG, value), {
387
+ message: 'pipeline stage key must match LEAD_GEN_STAGE_CATALOG'
388
+ }),
389
+ label: z.string().optional(),
390
+ enabled: z.boolean().optional(),
391
+ order: z.number().int().optional()
360
392
  })
361
393
 
362
394
  /**
363
- * Full ListConfig shape. `qualification` is required; everything else optional.
364
- * Matches `acq_lists.config` jsonb and ListConfig type in types.ts.
395
+ * Pipeline presentation contract stored in `acq_lists.pipeline_config` jsonb.
396
+ * `stages[].key` validates against the catalog; the rest is presentation only.
365
397
  */
366
- export const ListConfigSchema = z.object({
367
- qualification: ListQualificationSchema,
368
- enrichment: ListEnrichmentSchema.optional(),
369
- personalization: ListPersonalizationSchema.optional(),
370
- pipeline: ListPipelineSchema.optional()
398
+ export const PipelineConfigSchema = z.object({
399
+ stages: z.array(PipelineStageSchema).optional()
371
400
  })
372
401
 
373
402
  // ---------------------------------------------------------------------------
@@ -375,6 +404,8 @@ export const ListConfigSchema = z.object({
375
404
  // ---------------------------------------------------------------------------
376
405
 
377
406
  export const ListStageCountsSchema = z.object({
407
+ // Attempted counts by canonical lead-gen stage. The detailed status
408
+ // distribution lives on ListProgress; telemetry keeps the overview payload small.
378
409
  stageCounts: z.object({
379
410
  populated: z.number().int(),
380
411
  extracted: z.number().int(),
@@ -418,8 +449,10 @@ export const CreateListRequestSchema = z
418
449
  .object({
419
450
  name: z.string().trim().min(1).max(255),
420
451
  description: z.string().trim().nullable().optional(),
421
- type: z.string().default('manual'),
422
- config: ListConfigSchema.optional()
452
+ status: ListStatusSchema.optional(),
453
+ scrapingConfig: ScrapingConfigSchema.optional(),
454
+ icp: IcpRubricSchema.optional(),
455
+ pipelineConfig: PipelineConfigSchema.optional()
423
456
  })
424
457
  .strict()
425
458
 
@@ -427,35 +460,38 @@ export const UpdateListRequestSchema = z
427
460
  .object({
428
461
  name: z.string().trim().min(1).max(255).optional(),
429
462
  description: z.string().trim().nullable().optional(),
430
- status: z.string().optional(),
431
463
  batchIds: z.array(z.string()).optional()
432
464
  })
433
465
  .strict()
434
- .refine(
435
- (data) =>
436
- data.name !== undefined ||
437
- data.description !== undefined ||
438
- data.status !== undefined ||
439
- data.batchIds !== undefined,
440
- {
441
- message: 'At least one field (name, description, status, or batchIds) must be provided'
442
- }
443
- )
466
+ .refine((data) => data.name !== undefined || data.description !== undefined || data.batchIds !== undefined, {
467
+ message: 'At least one field (name, description, or batchIds) must be provided'
468
+ })
444
469
 
445
470
  /**
446
- * Partial patch for list.config — UI sends only the edited tab's subtree.
447
- * Zod v4: use .partial() on each subtree and remake the root schema.
448
- * Since the root ListConfigSchema marks `qualification` as required, we must
449
- * produce a manually-built deep-partial that makes qualification optional too.
471
+ * Status-only PATCH body for `/acquisition/lists/:listId/status`.
472
+ * Replaces the previous `transitionList` flow.
473
+ */
474
+ export const UpdateListStatusRequestSchema = z
475
+ .object({
476
+ status: ListStatusSchema
477
+ })
478
+ .strict()
479
+
480
+ /**
481
+ * Partial patch for the three jsonb config columns. UI sends only the edited
482
+ * subtree; server writes the field as-is (no deep merge — each column is
483
+ * replaced atomically when present in the patch).
450
484
  */
451
485
  export const UpdateListConfigRequestSchema = z
452
486
  .object({
453
- qualification: ListQualificationSchema.partial().optional(),
454
- enrichment: ListEnrichmentSchema.partial().optional(),
455
- personalization: ListPersonalizationSchema.partial().optional(),
456
- pipeline: ListPipelineSchema.partial().optional()
487
+ scrapingConfig: ScrapingConfigSchema.partial().optional(),
488
+ icp: IcpRubricSchema.partial().optional(),
489
+ pipelineConfig: PipelineConfigSchema.partial().optional()
457
490
  })
458
491
  .strict()
492
+ .refine((data) => data.scrapingConfig !== undefined || data.icp !== undefined || data.pipelineConfig !== undefined, {
493
+ message: 'At least one of scrapingConfig, icp, or pipelineConfig must be provided'
494
+ })
459
495
 
460
496
  export const AddCompaniesToListRequestSchema = z
461
497
  .object({
@@ -498,12 +534,18 @@ export const AcqListResponseSchema = z.object({
498
534
  type: z.string(),
499
535
  batchIds: z.array(z.string()),
500
536
  instantlyCampaignId: z.string().nullable(),
501
- status: z.string(),
537
+ /** Lifecycle status (draft | enriching | launched | closing | archived). */
538
+ status: ListStatusSchema,
502
539
  metadata: z.record(z.string(), z.unknown()),
503
540
  launchedAt: z.string().nullable(),
504
541
  completedAt: z.string().nullable(),
505
542
  createdAt: z.string(),
506
- config: ListConfigSchema
543
+ /** Scraping criteria stored as jsonb on the row. */
544
+ scrapingConfig: ScrapingConfigSchema,
545
+ /** ICP / qualification rubric stored as jsonb on the row. */
546
+ icp: IcpRubricSchema,
547
+ /** Pipeline presentation contract stored as jsonb on the row. */
548
+ pipelineConfig: PipelineConfigSchema
507
549
  })
508
550
 
509
551
  export const AcqListListResponseSchema = z.array(AcqListResponseSchema)
@@ -512,6 +554,42 @@ export const ListTelemetryResponseSchema = ListTelemetrySchema
512
554
 
513
555
  export const ListTelemetryListResponseSchema = z.array(ListTelemetrySchema)
514
556
 
557
+ /**
558
+ * Terminal row-level status for one lead-gen processing stage.
559
+ * Missing key still means not attempted; legacy boolean `true` is normalized
560
+ * to `success` by the API reader during rollout.
561
+ */
562
+ export const ProcessingStageStatusSchema = z.enum(['success', 'no_result', 'skipped', 'error'])
563
+
564
+ /**
565
+ * Per-stage progress aggregate for a single pipeline stage.
566
+ * `attempted` counts terminal statuses, including success, no-result, skipped,
567
+ * error, and tolerant-reader `other` values.
568
+ * `total` = total member/company count for the list.
569
+ */
570
+ export const ListStageProgressSchema = z.object({
571
+ total: z.number().int().min(0),
572
+ attempted: z.number().int().min(0),
573
+ success: z.number().int().min(0),
574
+ noResult: z.number().int().min(0),
575
+ skipped: z.number().int().min(0),
576
+ error: z.number().int().min(0),
577
+ other: z.number().int().min(0),
578
+ notAttempted: z.number().int().min(0)
579
+ })
580
+
581
+ /**
582
+ * Progress response for GET /acquisition/lists/:listId/progress.
583
+ * Aggregated on-demand from processing_state status values.
584
+ * Stage keys are discovered from observed processing_state keys.
585
+ */
586
+ export const ListProgressResponseSchema = z.object({
587
+ totalMembers: z.number().int().min(0),
588
+ totalCompanies: z.number().int().min(0),
589
+ byCompanyStage: z.record(z.string(), ListStageProgressSchema),
590
+ byContactStage: z.record(z.string(), ListStageProgressSchema)
591
+ })
592
+
515
593
  /**
516
594
  * Row from acq_list_executions joined with the execution summary,
517
595
  * shaped for the /lists/:id/executions response.
@@ -767,6 +845,115 @@ export const AcqContactListResponseSchema = z.object({
767
845
  offset: z.number().int()
768
846
  })
769
847
 
848
+ // ---------------------------------------------------------------------------
849
+ // Track A: Artifacts API Schemas
850
+ // ---------------------------------------------------------------------------
851
+
852
+ export const AcqArtifactOwnerKindSchema = z.enum(['company', 'contact', 'deal', 'list', 'list_member'])
853
+
854
+ export const ListArtifactsQuerySchema = z
855
+ .object({
856
+ ownerKind: AcqArtifactOwnerKindSchema,
857
+ ownerId: UuidSchema
858
+ })
859
+ .strict()
860
+
861
+ export const CreateArtifactRequestSchema = z
862
+ .object({
863
+ ownerKind: AcqArtifactOwnerKindSchema,
864
+ ownerId: UuidSchema,
865
+ kind: z.string().trim().min(1).max(255),
866
+ content: z.record(z.string(), z.unknown()),
867
+ sourceExecutionId: UuidSchema.optional()
868
+ })
869
+ .strict()
870
+
871
+ export const AcqArtifactResponseSchema = z.object({
872
+ id: z.string(),
873
+ organizationId: z.string(),
874
+ ownerKind: z.string(),
875
+ ownerId: z.string(),
876
+ kind: z.string(),
877
+ content: z.record(z.string(), z.unknown()),
878
+ sourceExecutionId: z.string().nullable(),
879
+ createdBy: z.string().nullable(),
880
+ createdAt: z.string(),
881
+ version: z.number().int()
882
+ })
883
+
884
+ export const AcqArtifactListResponseSchema = z.object({
885
+ artifacts: z.array(AcqArtifactResponseSchema)
886
+ })
887
+
888
+ // ---------------------------------------------------------------------------
889
+ // Track B: List Members API Schemas
890
+ // ---------------------------------------------------------------------------
891
+
892
+ export const ListMembersQuerySchema = z
893
+ .object({
894
+ limit: z.coerce.number().int().min(1).max(500).default(50),
895
+ offset: z.coerce.number().int().min(0).default(0)
896
+ })
897
+ .strict()
898
+
899
+ export const MemberIdParamsSchema = z.object({
900
+ memberId: UuidSchema
901
+ })
902
+
903
+ export const AcqListMemberContactSummarySchema = z.object({
904
+ id: z.string(),
905
+ email: z.string(),
906
+ firstName: z.string().nullable(),
907
+ lastName: z.string().nullable(),
908
+ title: z.string().nullable(),
909
+ linkedinUrl: z.string().nullable(),
910
+ companyId: z.string().nullable()
911
+ })
912
+
913
+ export const AcqListMemberResponseSchema = z.object({
914
+ id: z.string(),
915
+ listId: z.string(),
916
+ contactId: z.string(),
917
+ pipelineKey: z.string(),
918
+ stageKey: z.string(),
919
+ stateKey: z.string(),
920
+ activityLog: z.unknown(),
921
+ addedAt: z.string(),
922
+ addedBy: z.string().nullable(),
923
+ sourceExecutionId: z.string().nullable(),
924
+ contact: AcqListMemberContactSummarySchema.nullable()
925
+ })
926
+
927
+ export const AcqListMembersResponseSchema = z.object({
928
+ members: z.array(AcqListMemberResponseSchema)
929
+ })
930
+
931
+ // ---------------------------------------------------------------------------
932
+ // Track B: List Companies API Schemas
933
+ // ---------------------------------------------------------------------------
934
+
935
+ export const ListCompanyIdParamsSchema = z.object({
936
+ listCompanyId: UuidSchema
937
+ })
938
+
939
+ export const AcqListCompanyResponseSchema = z.object({
940
+ id: z.string(),
941
+ listId: z.string(),
942
+ companyId: z.string(),
943
+ pipelineKey: z.string(),
944
+ stageKey: z.string(),
945
+ stateKey: z.string(),
946
+ activityLog: z.unknown(),
947
+ addedAt: z.string(),
948
+ addedBy: z.string().nullable(),
949
+ sourceExecutionId: z.string().nullable()
950
+ })
951
+
952
+ // ---------------------------------------------------------------------------
953
+ // Track B: Transition Request (shared by list, list-member, list-company)
954
+ // TransitionItemRequestSchema already exists above (for deals) — reuse it.
955
+ // ---------------------------------------------------------------------------
956
+
770
957
  export const AcqCompanySchemas = {
771
958
  CompanyIdParams: CompanyIdParamsSchema,
772
959
  ListCompaniesQuery: ListCompaniesQuerySchema,
@@ -813,14 +1000,19 @@ export const AcqListSchemas = {
813
1000
  ListIdParams: ListIdParamsSchema,
814
1001
 
815
1002
  // Primitives (for UI / tests)
816
- ListConfig: ListConfigSchema,
1003
+ ListStatus: ListStatusSchema,
1004
+ ScrapingConfig: ScrapingConfigSchema,
1005
+ IcpRubric: IcpRubricSchema,
1006
+ PipelineConfig: PipelineConfigSchema,
1007
+ PipelineStage: PipelineStageSchema,
1008
+ ProcessingStageStatus: ProcessingStageStatusSchema,
817
1009
  ListStageCounts: ListStageCountsSchema,
818
1010
  ListTelemetry: ListTelemetrySchema,
819
- PipelineStep: PipelineStepSchema,
820
1011
 
821
1012
  // Requests
822
1013
  CreateListRequest: CreateListRequestSchema,
823
1014
  UpdateListRequest: UpdateListRequestSchema,
1015
+ UpdateListStatusRequest: UpdateListStatusRequestSchema,
824
1016
  UpdateListConfigRequest: UpdateListConfigRequestSchema,
825
1017
  AddCompaniesToListRequest: AddCompaniesToListRequestSchema,
826
1018
  RemoveCompaniesFromListRequest: RemoveCompaniesFromListRequestSchema,
@@ -832,20 +1024,70 @@ export const AcqListSchemas = {
832
1024
  AcqListListResponse: AcqListListResponseSchema,
833
1025
  ListTelemetryResponse: ListTelemetryResponseSchema,
834
1026
  ListTelemetryListResponse: ListTelemetryListResponseSchema,
835
- ListExecutionsResponse: ListExecutionsResponseSchema
1027
+ ListExecutionsResponse: ListExecutionsResponseSchema,
1028
+ ListProgressResponse: ListProgressResponseSchema
1029
+ }
1030
+
1031
+ // ---------------------------------------------------------------------------
1032
+ // Track A/B bundled schemas
1033
+ // ---------------------------------------------------------------------------
1034
+
1035
+ export const AcqSubstrateSchemas = {
1036
+ // Artifacts
1037
+ ListArtifactsQuery: ListArtifactsQuerySchema,
1038
+ CreateArtifactRequest: CreateArtifactRequestSchema,
1039
+ AcqArtifactResponse: AcqArtifactResponseSchema,
1040
+ AcqArtifactListResponse: AcqArtifactListResponseSchema,
1041
+
1042
+ // List members
1043
+ ListMembersQuery: ListMembersQuerySchema,
1044
+ MemberIdParams: MemberIdParamsSchema,
1045
+ AcqListMemberResponse: AcqListMemberResponseSchema,
1046
+ AcqListMembersResponse: AcqListMembersResponseSchema,
1047
+
1048
+ // List companies
1049
+ ListCompanyIdParams: ListCompanyIdParamsSchema,
1050
+ AcqListCompanyResponse: AcqListCompanyResponseSchema,
1051
+
1052
+ // Transition (shared with deals — TransitionItemRequestSchema)
1053
+ TransitionItemRequest: TransitionItemRequestSchema
836
1054
  }
837
1055
 
838
1056
  // ---------------------------------------------------------------------------
839
1057
  // Inferred types
840
1058
  // ---------------------------------------------------------------------------
841
1059
 
842
- export type ListConfigInput = z.infer<typeof ListConfigSchema>
1060
+ // ---------------------------------------------------------------------------
1061
+ // Inferred types — Track A/B substrate
1062
+ // ---------------------------------------------------------------------------
1063
+
1064
+ export type AcqArtifactOwnerKind = z.infer<typeof AcqArtifactOwnerKindSchema>
1065
+ export type ListArtifactsQuery = z.infer<typeof ListArtifactsQuerySchema>
1066
+ export type CreateArtifactRequest = z.infer<typeof CreateArtifactRequestSchema>
1067
+ export type AcqArtifactResponse = z.infer<typeof AcqArtifactResponseSchema>
1068
+ export type AcqArtifactListResponse = z.infer<typeof AcqArtifactListResponseSchema>
1069
+ export type ListMembersQuery = z.infer<typeof ListMembersQuerySchema>
1070
+ export type MemberIdParams = z.infer<typeof MemberIdParamsSchema>
1071
+ export type AcqListMemberContactSummary = z.infer<typeof AcqListMemberContactSummarySchema>
1072
+ export type AcqListMemberResponse = z.infer<typeof AcqListMemberResponseSchema>
1073
+ export type AcqListMembersResponse = z.infer<typeof AcqListMembersResponseSchema>
1074
+ export type ListCompanyIdParams = z.infer<typeof ListCompanyIdParamsSchema>
1075
+ export type AcqListCompanyResponse = z.infer<typeof AcqListCompanyResponseSchema>
1076
+
1077
+ // ---------------------------------------------------------------------------
1078
+
1079
+ export type ListStatus = z.infer<typeof ListStatusSchema>
1080
+ export type ScrapingConfig = z.infer<typeof ScrapingConfigSchema>
1081
+ export type IcpRubric = z.infer<typeof IcpRubricSchema>
1082
+ export type PipelineStage = z.infer<typeof PipelineStageSchema>
1083
+ export type PipelineConfig = z.infer<typeof PipelineConfigSchema>
1084
+ export type ProcessingStageStatus = z.infer<typeof ProcessingStageStatusSchema>
843
1085
  export type ListStageCountsInput = z.infer<typeof ListStageCountsSchema>['stageCounts']
844
1086
  export type ListTelemetryInput = z.infer<typeof ListTelemetrySchema>
845
- export type PipelineStepInput = z.infer<typeof PipelineStepSchema>
846
1087
  export type ListIdParams = z.infer<typeof ListIdParamsSchema>
847
1088
  export type CreateListRequest = z.infer<typeof CreateListRequestSchema>
848
1089
  export type UpdateListRequest = z.infer<typeof UpdateListRequestSchema>
1090
+ export type UpdateListStatusRequest = z.infer<typeof UpdateListStatusRequestSchema>
849
1091
  export type UpdateListConfigRequest = z.infer<typeof UpdateListConfigRequestSchema>
850
1092
  export type AddCompaniesToListRequest = z.infer<typeof AddCompaniesToListRequestSchema>
851
1093
  export type RemoveCompaniesFromListRequest = z.infer<typeof RemoveCompaniesFromListRequestSchema>
@@ -857,3 +1099,5 @@ export type ListTelemetryResponse = z.infer<typeof ListTelemetryResponseSchema>
857
1099
  export type ListTelemetryListResponse = z.infer<typeof ListTelemetryListResponseSchema>
858
1100
  export type ListExecutionSummaryInput = z.infer<typeof ListExecutionSummarySchema>
859
1101
  export type ListExecutionsResponse = z.infer<typeof ListExecutionsResponseSchema>
1102
+ export type ListStageProgress = z.infer<typeof ListStageProgressSchema>
1103
+ export type ListProgress = z.infer<typeof ListProgressResponseSchema>
@@ -0,0 +1,160 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { DEFAULT_ORGANIZATION_MODEL_SALES, LEAD_GEN_PIPELINE_DEFINITIONS } from '../../organization-model/domains/sales'
3
+ import { ActivityEventSchema } from './activity-events'
4
+ import { DealStageSchema, TransitionItemRequestSchema } from './api-schemas'
5
+ import { deriveActions } from './derive-actions'
6
+ import type { DealStage } from './types'
7
+
8
+ const DEAL_STAGES = [
9
+ 'interested',
10
+ 'proposal',
11
+ 'closing',
12
+ 'closed_won',
13
+ 'closed_lost',
14
+ 'nurturing'
15
+ ] as const satisfies readonly DealStage[]
16
+
17
+ function deal(stageKey: string | null, stateKey: string | null = null): Parameters<typeof deriveActions>[0] {
18
+ return { stage_key: stageKey, state_key: stateKey } as Parameters<typeof deriveActions>[0]
19
+ }
20
+
21
+ describe('CRM deal action derivation', () => {
22
+ it('derives the exact base actions for interested deals', () => {
23
+ expect(deriveActions(deal('interested'))).toEqual([
24
+ { key: 'move_to_proposal', label: 'Move to Proposal' },
25
+ { key: 'move_to_closed_lost', label: 'Close Lost' },
26
+ { key: 'move_to_nurturing', label: 'Move to Nurturing' }
27
+ ])
28
+ })
29
+
30
+ it.each([
31
+ ['discovery_replied', { key: 'send_link', label: 'Send Booking Link' }],
32
+ ['discovery_link_sent', { key: 'send_nudge', label: 'Send Nudge' }],
33
+ ['discovery_booking_cancelled', { key: 'rebook', label: 'Rebook' }]
34
+ ] as const)('adds the expected workflow action for interested/%s', (stateKey, expectedAction) => {
35
+ const expected = [
36
+ { key: 'move_to_proposal', label: 'Move to Proposal' },
37
+ { key: 'move_to_closed_lost', label: 'Close Lost' },
38
+ { key: 'move_to_nurturing', label: 'Move to Nurturing' },
39
+ expectedAction
40
+ ]
41
+
42
+ if (stateKey === 'discovery_replied' || stateKey === 'discovery_link_sent') {
43
+ expected.splice(3, 0, {
44
+ key: 'send_reply',
45
+ label: 'Send Reply',
46
+ payloadSchema: expect.any(Object)
47
+ } as never)
48
+ }
49
+
50
+ expect(deriveActions(deal('interested', stateKey))).toEqual(expected)
51
+ })
52
+
53
+ it('adds reply, nudge, and no-show actions for interested/discovery_nudging', () => {
54
+ expect(deriveActions(deal('interested', 'discovery_nudging'))).toEqual([
55
+ { key: 'move_to_proposal', label: 'Move to Proposal' },
56
+ { key: 'move_to_closed_lost', label: 'Close Lost' },
57
+ { key: 'move_to_nurturing', label: 'Move to Nurturing' },
58
+ { key: 'send_reply', label: 'Send Reply', payloadSchema: expect.any(Object) },
59
+ { key: 'send_nudge', label: 'Send Nudge' },
60
+ { key: 'mark_no_show', label: 'Mark No-Show' }
61
+ ])
62
+ })
63
+
64
+ it('derives exact proposal and closing transitions', () => {
65
+ expect(deriveActions(deal('proposal'))).toEqual([
66
+ { key: 'move_to_closing', label: 'Move to Closing' },
67
+ { key: 'move_to_closed_lost', label: 'Close Lost' },
68
+ { key: 'move_to_nurturing', label: 'Move to Nurturing' }
69
+ ])
70
+
71
+ expect(deriveActions(deal('closing'))).toEqual([
72
+ { key: 'move_to_closed_won', label: 'Close Won' },
73
+ { key: 'move_to_closed_lost', label: 'Close Lost' },
74
+ { key: 'move_to_nurturing', label: 'Move to Nurturing' }
75
+ ])
76
+ })
77
+
78
+ it.each(['closed_won', 'closed_lost', 'nurturing', 'legacy_stage', null] as const)(
79
+ 'derives no actions for terminal or non-canonical stage %s',
80
+ (stageKey) => {
81
+ expect(deriveActions(deal(stageKey))).toEqual([])
82
+ }
83
+ )
84
+ })
85
+
86
+ describe('ActivityEventSchema', () => {
87
+ const timestamp = '2026-04-27T12:34:56.000Z'
88
+
89
+ // Platform events only (8 members). Domain events (reply_received, booking_nudge_sent, etc.)
90
+ // live in external/elevasis/operations as CrmDomainActivityEventSchema.
91
+ // See Open Decision #5 in crm-action-system.mdx for rationale.
92
+ it.each([
93
+ [{ type: 'stage_change', timestamp, stageBefore: 'interested', stageAfter: 'proposal', reason: 'qualified' }],
94
+ [{ type: 'state_change', timestamp, stateBefore: null, stateAfter: 'discovery_replied' }],
95
+ [{ type: 'action_taken', timestamp, actionKey: 'send_link', payload: { channel: 'email' } }],
96
+ [{ type: 'approval_created', timestamp, commandId: 'cmd_123', dealStageBefore: 'proposal' }],
97
+ [{ type: 'approval_resolved', timestamp, commandId: 'cmd_123', resolution: 'superseded' }],
98
+ [{ type: 'approval_stale', timestamp, commandId: 'cmd_123', dealStageAfter: 'closing' }],
99
+ [{ type: 'task_created', timestamp, taskId: 'task_123' }],
100
+ [{ type: 'deal_created', timestamp }]
101
+ ])('accepts expected %s events', (event) => {
102
+ expect(ActivityEventSchema.safeParse(event).success).toBe(true)
103
+ })
104
+
105
+ it.each([
106
+ [{ type: 'deal_created', timestamp: 'not-a-date' }],
107
+ [{ type: 'unknown_event', timestamp }],
108
+ [{ type: 'stage_change', timestamp, stageBefore: 'interested' }],
109
+ [{ type: 'approval_resolved', timestamp, commandId: 'cmd_123', resolution: 'approved' }],
110
+ [{ type: 'booking_nudge_sent', timestamp, followupDay: '2' }],
111
+ [{ type: 'action_taken', timestamp }]
112
+ ])('rejects malformed event payload %o', (event) => {
113
+ expect(ActivityEventSchema.safeParse(event).success).toBe(false)
114
+ })
115
+ })
116
+
117
+ describe('CRM stage and transition vocabulary contracts', () => {
118
+ it('keeps DealStage, DealStageSchema, and the default sales pipeline stages aligned', () => {
119
+ const defaultPipeline = DEFAULT_ORGANIZATION_MODEL_SALES.pipelines.find(
120
+ (pipeline) => pipeline.id === DEFAULT_ORGANIZATION_MODEL_SALES.defaultPipelineId
121
+ )
122
+ const defaultSalesStages = defaultPipeline?.stages
123
+ .toSorted((left, right) => left.order - right.order)
124
+ .map((stage) => stage.id)
125
+
126
+ expect(defaultSalesStages).toEqual([...DEAL_STAGES])
127
+ expect(DealStageSchema.options).toEqual([...DEAL_STAGES])
128
+ })
129
+
130
+ it('accepts every canonical CRM deal stage in transition requests', () => {
131
+ for (const stageKey of DEAL_STAGES) {
132
+ expect(
133
+ TransitionItemRequestSchema.safeParse({
134
+ pipelineKey: DEFAULT_ORGANIZATION_MODEL_SALES.defaultPipelineId,
135
+ stageKey,
136
+ stateKey: null,
137
+ expectedUpdatedAt: '2026-04-27T12:34:56.000Z'
138
+ }).success
139
+ ).toBe(true)
140
+ }
141
+ })
142
+
143
+ it('accepts canonical lead-gen stage/state pairs in transition requests', () => {
144
+ for (const pipelineDefinitions of Object.values(LEAD_GEN_PIPELINE_DEFINITIONS)) {
145
+ for (const pipeline of pipelineDefinitions) {
146
+ for (const stage of pipeline.stages) {
147
+ for (const state of stage.states) {
148
+ expect(
149
+ TransitionItemRequestSchema.safeParse({
150
+ pipelineKey: pipeline.pipelineKey,
151
+ stageKey: stage.stageKey,
152
+ stateKey: state.stateKey
153
+ }).success
154
+ ).toBe(true)
155
+ }
156
+ }
157
+ }
158
+ }
159
+ })
160
+ })