@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/organization-model/index.d.ts +1 -1
- package/dist/organization-model/index.js +9 -2
- package/dist/test-utils/index.d.ts +463 -377
- package/dist/test-utils/index.js +9 -2
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2336 -0
- package/src/business/acquisition/activity-events.test.ts +250 -0
- package/src/business/acquisition/activity-events.ts +7 -65
- package/src/business/acquisition/api-schemas.test.ts +1180 -0
- package/src/business/acquisition/api-schemas.ts +317 -73
- package/src/business/acquisition/crm-state-actions.test.ts +160 -0
- package/src/business/acquisition/derive-actions.test.ts +518 -0
- package/src/business/acquisition/derive-actions.ts +101 -78
- package/src/business/acquisition/index.ts +51 -9
- package/src/business/acquisition/stateful.ts +30 -0
- package/src/business/acquisition/types.ts +48 -80
- package/src/execution/engine/index.ts +437 -434
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -360
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
- package/src/execution/engine/tools/integration/service.test.ts +34 -9
- package/src/execution/engine/tools/integration/service.ts +6 -3
- package/src/execution/engine/tools/lead-service-types.ts +934 -874
- package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
- package/src/execution/engine/tools/registry.ts +701 -699
- package/src/execution/engine/tools/tool-maps.ts +30 -2
- package/src/execution/engine/workflow/types.ts +11 -0
- package/src/organization-model/contracts.ts +4 -4
- package/src/organization-model/domains/navigation.ts +62 -62
- package/src/organization-model/domains/sales.test.ts +189 -0
- package/src/organization-model/domains/sales.ts +456 -94
- package/src/organization-model/published.ts +21 -21
- package/src/organization-model/resolve.ts +21 -8
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +2336 -0
- 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
|
|
345
|
+
// Primitives — list status enum + jsonb config schemas
|
|
317
346
|
// ---------------------------------------------------------------------------
|
|
318
347
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
*
|
|
364
|
-
*
|
|
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
|
|
367
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
(
|
|
436
|
-
|
|
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
|
-
*
|
|
447
|
-
*
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
})
|