@elevasis/core 0.15.0 → 0.16.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 +1718 -19
- package/dist/index.js +369 -25
- package/dist/organization-model/index.d.ts +1718 -19
- package/dist/organization-model/index.js +369 -25
- package/dist/test-utils/index.d.ts +1108 -371
- package/dist/test-utils/index.js +357 -17
- package/package.json +5 -1
- package/src/__tests__/publish.test.ts +14 -13
- package/src/__tests__/template-core-compatibility.test.ts +4 -4
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1109 -882
- package/src/auth/multi-tenancy/index.ts +3 -0
- package/src/auth/multi-tenancy/theme-presets.ts +45 -0
- package/src/auth/multi-tenancy/types.ts +57 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +165 -194
- package/src/business/acquisition/activity-events.ts +13 -4
- package/src/business/acquisition/api-schemas.test.ts +315 -4
- package/src/business/acquisition/api-schemas.ts +122 -8
- package/src/business/acquisition/build-templates.ts +44 -0
- package/src/business/acquisition/crm-next-action.test.ts +262 -0
- package/src/business/acquisition/crm-next-action.ts +220 -0
- package/src/business/acquisition/crm-priority.test.ts +216 -0
- package/src/business/acquisition/crm-priority.ts +349 -0
- package/src/business/acquisition/crm-state-actions.test.ts +151 -160
- package/src/business/acquisition/deal-ownership.test.ts +351 -0
- package/src/business/acquisition/deal-ownership.ts +120 -0
- package/src/business/acquisition/derive-actions.test.ts +101 -37
- package/src/business/acquisition/derive-actions.ts +102 -87
- package/src/business/acquisition/index.ts +10 -0
- package/src/business/acquisition/types.ts +400 -366
- package/src/business/crm/api-schemas.ts +40 -0
- package/src/business/crm/index.ts +1 -0
- package/src/business/deals/api-schemas.ts +79 -0
- package/src/business/deals/index.ts +1 -0
- package/src/business/projects/types.ts +124 -88
- package/src/execution/core/runner-types.ts +61 -80
- package/src/execution/engine/index.ts +4 -3
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -102
- package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -179
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -309
- package/src/execution/engine/tools/integration/tool.ts +255 -253
- package/src/execution/engine/tools/lead-service-types.ts +939 -924
- package/src/execution/engine/tools/messages.ts +43 -0
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -5
- package/src/execution/engine/tools/platform/acquisition/types.ts +5 -2
- package/src/execution/engine/tools/platform/email/types.ts +97 -96
- package/src/execution/engine/tools/registry.ts +4 -3
- package/src/execution/engine/tools/tool-maps.ts +3 -1
- package/src/execution/engine/tools/types.ts +234 -233
- package/src/execution/engine/workflow/types.ts +195 -193
- package/src/execution/external/api-schemas.ts +40 -0
- package/src/execution/external/index.ts +1 -0
- package/src/knowledge/README.md +32 -0
- package/src/knowledge/__tests__/queries.test.ts +504 -0
- package/src/knowledge/format.ts +99 -0
- package/src/knowledge/index.ts +5 -0
- package/src/knowledge/queries.ts +256 -0
- package/src/organization-model/__tests__/defaults.test.ts +172 -172
- package/src/organization-model/__tests__/foundation.test.ts +7 -7
- package/src/organization-model/__tests__/icons.test.ts +27 -0
- package/src/organization-model/__tests__/knowledge.test.ts +214 -0
- package/src/organization-model/contracts.ts +17 -15
- package/src/organization-model/defaults.ts +74 -19
- package/src/organization-model/domains/knowledge.ts +53 -0
- package/src/organization-model/domains/navigation.ts +416 -399
- package/src/organization-model/domains/prospecting.ts +204 -1
- package/src/organization-model/domains/sales.test.ts +29 -0
- package/src/organization-model/domains/sales.ts +102 -0
- package/src/organization-model/domains/shared.ts +6 -5
- package/src/organization-model/foundation.ts +10 -6
- package/src/organization-model/graph/build.ts +209 -182
- package/src/organization-model/graph/schema.ts +37 -34
- package/src/organization-model/graph/types.ts +47 -31
- package/src/organization-model/icons.ts +81 -0
- package/src/organization-model/index.ts +8 -3
- package/src/organization-model/organization-model.mdx +1 -1
- package/src/organization-model/published.ts +103 -86
- package/src/organization-model/schema.ts +90 -85
- package/src/organization-model/types.ts +42 -35
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/index.ts +23 -27
- package/src/platform/registry/index.ts +0 -4
- package/src/platform/registry/resource-registry.ts +0 -77
- package/src/platform/registry/serialized-types.ts +148 -219
- package/src/platform/registry/stats-types.ts +60 -60
- package/src/reference/_generated/contracts.md +829 -595
- package/src/supabase/database.types.ts +2978 -2958
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_CRM_PRIORITY_RULE_CONFIG,
|
|
4
|
+
LEAD_GEN_PIPELINE_DEFINITIONS,
|
|
5
|
+
LEAD_GEN_STAGE_CATALOG
|
|
6
|
+
} from '../../organization-model/domains/sales'
|
|
7
|
+
import { CrmPriorityOverrideSchema, evaluateCrmDealPriority, resolveCrmPriorityRuleConfig } from './crm-priority'
|
|
3
8
|
import {
|
|
4
9
|
AddCompaniesToListRequestSchema,
|
|
5
10
|
AddContactsToListRequestSchema,
|
|
@@ -15,6 +20,7 @@ import {
|
|
|
15
20
|
CreateDealTaskRequestSchema,
|
|
16
21
|
CreateListRequestSchema,
|
|
17
22
|
DealDetailResponseSchema,
|
|
23
|
+
DealListItemSchema,
|
|
18
24
|
DealListResponseSchema,
|
|
19
25
|
DealNoteResponseSchema,
|
|
20
26
|
DealStageSchema,
|
|
@@ -44,6 +50,15 @@ import {
|
|
|
44
50
|
|
|
45
51
|
const VALID_UUID = '00000000-0000-4000-8000-000000000001'
|
|
46
52
|
const ISO_TS = '2026-04-27T12:34:56.000Z'
|
|
53
|
+
const PRIORITY = {
|
|
54
|
+
bucketKey: 'waiting' as const,
|
|
55
|
+
rank: 30,
|
|
56
|
+
label: 'Waiting',
|
|
57
|
+
color: 'blue',
|
|
58
|
+
reason: 'No immediate response or follow-up is due.',
|
|
59
|
+
latestActivityAt: ISO_TS,
|
|
60
|
+
nextActionAt: null
|
|
61
|
+
}
|
|
47
62
|
|
|
48
63
|
// ---------------------------------------------------------------------------
|
|
49
64
|
// DealStageSchema
|
|
@@ -414,10 +429,251 @@ describe('UpdateContactRequestSchema', () => {
|
|
|
414
429
|
})
|
|
415
430
|
})
|
|
416
431
|
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// CRM priority override contract
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
describe('CrmPriorityOverrideSchema', () => {
|
|
437
|
+
it('accepts a valid partial organization override', () => {
|
|
438
|
+
const result = CrmPriorityOverrideSchema.safeParse({
|
|
439
|
+
enabled: true,
|
|
440
|
+
staleAfterDays: 10,
|
|
441
|
+
bucketOrder: ['needs_response', 'follow_up_due', 'stale', 'waiting', 'closed_low'],
|
|
442
|
+
buckets: {
|
|
443
|
+
needs_response: { label: 'Reply Now', color: 'red', rank: 5 },
|
|
444
|
+
waiting: { label: 'Pending' }
|
|
445
|
+
},
|
|
446
|
+
followUpAfterDaysByStateKey: {
|
|
447
|
+
discovery_link_sent: 2,
|
|
448
|
+
custom_state: 4
|
|
449
|
+
},
|
|
450
|
+
closedStageKeys: ['closed_won', 'closed_lost']
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
expect(result.success).toBe(true)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('rejects needsResponseStateKeys and needsResponseActivityTypes (removed fields)', () => {
|
|
457
|
+
expect(CrmPriorityOverrideSchema.safeParse({ needsResponseStateKeys: ['x'] }).success).toBe(false)
|
|
458
|
+
expect(CrmPriorityOverrideSchema.safeParse({ needsResponseActivityTypes: ['x'] }).success).toBe(false)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('accepts sparse partial overrides', () => {
|
|
462
|
+
const result = CrmPriorityOverrideSchema.safeParse({
|
|
463
|
+
staleAfterDays: 21,
|
|
464
|
+
buckets: {
|
|
465
|
+
stale: { color: 'yellow' }
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
expect(result.success).toBe(true)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('rejects invalid unknown input shapes', () => {
|
|
473
|
+
expect(CrmPriorityOverrideSchema.safeParse('bad').success).toBe(false)
|
|
474
|
+
expect(CrmPriorityOverrideSchema.safeParse({ staleAfterDays: -1 }).success).toBe(false)
|
|
475
|
+
expect(CrmPriorityOverrideSchema.safeParse({ buckets: { invalid_bucket: { label: 'Bad' } } }).success).toBe(false)
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
describe('resolveCrmPriorityRuleConfig', () => {
|
|
480
|
+
it('merges valid organization config overrides with default rules', () => {
|
|
481
|
+
const resolved = resolveCrmPriorityRuleConfig({
|
|
482
|
+
crm: {
|
|
483
|
+
priority: {
|
|
484
|
+
staleAfterDays: 7,
|
|
485
|
+
bucketOrder: ['stale', 'needs_response'],
|
|
486
|
+
buckets: {
|
|
487
|
+
stale: { label: 'Dormant', color: 'yellow' },
|
|
488
|
+
needs_response: { rank: 3 }
|
|
489
|
+
},
|
|
490
|
+
followUpAfterDaysByStateKey: { discovery_link_sent: 1 },
|
|
491
|
+
closedStageKeys: ['won', 'lost']
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
expect(resolved.enabled).toBe(true)
|
|
497
|
+
expect(resolved.staleAfterDays).toBe(7)
|
|
498
|
+
expect(resolved.closedStageKeys).toEqual(['won', 'lost'])
|
|
499
|
+
expect(resolved.followUpAfterDaysByStateKey.discovery_link_sent).toBe(1)
|
|
500
|
+
expect(resolved.followUpAfterDaysByStateKey.reply_sent).toBe(
|
|
501
|
+
DEFAULT_CRM_PRIORITY_RULE_CONFIG.followUpAfterDaysByStateKey.reply_sent
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
const staleBucket = resolved.buckets.find((bucket) => bucket.bucketKey === 'stale')
|
|
505
|
+
expect(staleBucket).toMatchObject({ label: 'Dormant', color: 'yellow', rank: 10 })
|
|
506
|
+
|
|
507
|
+
const needsResponseBucket = resolved.buckets.find((bucket) => bucket.bucketKey === 'needs_response')
|
|
508
|
+
expect(needsResponseBucket?.rank).toBe(3)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('falls back to defaults for missing or invalid input', () => {
|
|
512
|
+
expect(resolveCrmPriorityRuleConfig(undefined)).toMatchObject({
|
|
513
|
+
enabled: true,
|
|
514
|
+
staleAfterDays: DEFAULT_CRM_PRIORITY_RULE_CONFIG.staleAfterDays,
|
|
515
|
+
closedStageKeys: DEFAULT_CRM_PRIORITY_RULE_CONFIG.closedStageKeys
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
expect(resolveCrmPriorityRuleConfig({ staleAfterDays: 0 })).toMatchObject({
|
|
519
|
+
enabled: true,
|
|
520
|
+
staleAfterDays: DEFAULT_CRM_PRIORITY_RULE_CONFIG.staleAfterDays,
|
|
521
|
+
closedStageKeys: DEFAULT_CRM_PRIORITY_RULE_CONFIG.closedStageKeys
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
|
|
417
526
|
// ---------------------------------------------------------------------------
|
|
418
527
|
// Response schemas — smoke-level forward-compat checks
|
|
419
528
|
// ---------------------------------------------------------------------------
|
|
420
529
|
|
|
530
|
+
describe('evaluateCrmDealPriority', () => {
|
|
531
|
+
const now = '2026-04-30T12:00:00.000Z'
|
|
532
|
+
const baseInput = {
|
|
533
|
+
stage_key: 'interested',
|
|
534
|
+
state_key: null,
|
|
535
|
+
activity_log: [],
|
|
536
|
+
updated_at: '2026-04-29T12:00:00.000Z',
|
|
537
|
+
created_at: '2026-04-01T12:00:00.000Z'
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
it('returns needs_response when the lead replied last (ownership us)', () => {
|
|
541
|
+
// A reply_received inbound event makes ownership === 'us' → needs_response
|
|
542
|
+
const priority = evaluateCrmDealPriority(
|
|
543
|
+
{
|
|
544
|
+
...baseInput,
|
|
545
|
+
state_key: 'discovery_replied',
|
|
546
|
+
activity_log: [{ type: 'reply_received', timestamp: '2026-04-29T10:00:00.000Z' }]
|
|
547
|
+
},
|
|
548
|
+
{ now }
|
|
549
|
+
)
|
|
550
|
+
expect(priority.bucketKey).toBe('needs_response')
|
|
551
|
+
expect(priority.rank).toBe(10)
|
|
552
|
+
expect(priority.reason).toBe('Lead replied last — we owe the next move.')
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('returns follow_up_due when configured follow-up window has elapsed', () => {
|
|
556
|
+
const priority = evaluateCrmDealPriority(
|
|
557
|
+
{
|
|
558
|
+
...baseInput,
|
|
559
|
+
state_key: 'discovery_link_sent',
|
|
560
|
+
activity_log: [{ type: 'action_taken', timestamp: '2026-04-25T12:00:00.000Z', actionKey: 'send_link' }],
|
|
561
|
+
updated_at: '2026-04-25T12:00:00.000Z'
|
|
562
|
+
},
|
|
563
|
+
{ now }
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
expect(priority.bucketKey).toBe('follow_up_due')
|
|
567
|
+
expect(priority.nextActionAt).toBe('2026-04-28T12:00:00.000Z')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('returns waiting when the next action is still in the future', () => {
|
|
571
|
+
const priority = evaluateCrmDealPriority(
|
|
572
|
+
{
|
|
573
|
+
...baseInput,
|
|
574
|
+
state_key: 'discovery_link_sent',
|
|
575
|
+
activity_log: [{ type: 'action_taken', timestamp: '2026-04-29T12:00:00.000Z', actionKey: 'send_link' }],
|
|
576
|
+
updated_at: '2026-04-29T12:00:00.000Z'
|
|
577
|
+
},
|
|
578
|
+
{ now }
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
expect(priority.bucketKey).toBe('waiting')
|
|
582
|
+
expect(priority.nextActionAt).toBe('2026-05-02T12:00:00.000Z')
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('returns stale when there is no recent meaningful activity', () => {
|
|
586
|
+
const priority = evaluateCrmDealPriority(
|
|
587
|
+
{ ...baseInput, updated_at: '2026-04-01T12:00:00.000Z', created_at: '2026-04-01T12:00:00.000Z' },
|
|
588
|
+
{ now }
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
expect(priority.bucketKey).toBe('stale')
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('returns closed_low for closed stages', () => {
|
|
595
|
+
const priority = evaluateCrmDealPriority({ ...baseInput, stage_key: 'closed_lost' }, { now })
|
|
596
|
+
expect(priority.bucketKey).toBe('closed_low')
|
|
597
|
+
expect(priority.rank).toBe(50)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('ignores malformed activity entries without throwing', () => {
|
|
601
|
+
const priority = evaluateCrmDealPriority(
|
|
602
|
+
{
|
|
603
|
+
...baseInput,
|
|
604
|
+
activity_log: [null, 'bad', { type: 'reply_received', timestamp: 'not-a-date' }, { other: true }],
|
|
605
|
+
updated_at: '2026-04-29T12:00:00.000Z'
|
|
606
|
+
},
|
|
607
|
+
{ now }
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
expect(priority.bucketKey).toBe('waiting')
|
|
611
|
+
expect(priority.latestActivityAt).toBe('2026-04-29T12:00:00.000Z')
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('returns a neutral waiting priority when CRM priority is disabled', () => {
|
|
615
|
+
const config = resolveCrmPriorityRuleConfig({
|
|
616
|
+
enabled: false,
|
|
617
|
+
buckets: {
|
|
618
|
+
waiting: { label: 'Priority Off', color: 'gray', rank: 999 }
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
const priority = evaluateCrmDealPriority(
|
|
622
|
+
{
|
|
623
|
+
...baseInput,
|
|
624
|
+
state_key: 'discovery_replied',
|
|
625
|
+
activity_log: [{ type: 'reply_received', timestamp: '2026-04-30T10:00:00.000Z' }]
|
|
626
|
+
},
|
|
627
|
+
{ config, now }
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
expect(priority).toMatchObject({
|
|
631
|
+
bucketKey: 'waiting',
|
|
632
|
+
label: 'Priority Off',
|
|
633
|
+
color: 'gray',
|
|
634
|
+
rank: 999,
|
|
635
|
+
reason: 'CRM priority evaluation is disabled.',
|
|
636
|
+
nextActionAt: null
|
|
637
|
+
})
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
describe('DealListItemSchema', () => {
|
|
642
|
+
it('accepts a deal with a derived priority object', () => {
|
|
643
|
+
const result = DealListItemSchema.safeParse({
|
|
644
|
+
id: VALID_UUID,
|
|
645
|
+
organization_id: VALID_UUID,
|
|
646
|
+
contact_id: null,
|
|
647
|
+
contact_email: 'test@example.com',
|
|
648
|
+
pipeline_key: 'crm',
|
|
649
|
+
stage_key: 'interested',
|
|
650
|
+
state_key: 'discovery_link_sent',
|
|
651
|
+
activity_log: [],
|
|
652
|
+
discovery_data: null,
|
|
653
|
+
discovery_submitted_at: null,
|
|
654
|
+
discovery_submitted_by: null,
|
|
655
|
+
proposal_data: null,
|
|
656
|
+
proposal_sent_at: null,
|
|
657
|
+
proposal_pdf_url: null,
|
|
658
|
+
signature_envelope_id: null,
|
|
659
|
+
source_list_id: null,
|
|
660
|
+
source_type: null,
|
|
661
|
+
initial_fee: null,
|
|
662
|
+
monthly_fee: null,
|
|
663
|
+
closed_lost_at: null,
|
|
664
|
+
closed_lost_reason: null,
|
|
665
|
+
created_at: ISO_TS,
|
|
666
|
+
updated_at: ISO_TS,
|
|
667
|
+
priority: PRIORITY,
|
|
668
|
+
ownership: null,
|
|
669
|
+
nextAction: null,
|
|
670
|
+
contact: null
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
expect(result.success).toBe(true)
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
|
|
421
677
|
describe('DealDetailResponseSchema (forward-compat)', () => {
|
|
422
678
|
const baseDeal = {
|
|
423
679
|
id: VALID_UUID,
|
|
@@ -443,13 +699,39 @@ describe('DealDetailResponseSchema (forward-compat)', () => {
|
|
|
443
699
|
closed_lost_reason: null,
|
|
444
700
|
created_at: ISO_TS,
|
|
445
701
|
updated_at: ISO_TS,
|
|
446
|
-
|
|
702
|
+
priority: PRIORITY,
|
|
703
|
+
ownership: null,
|
|
704
|
+
nextAction: null,
|
|
705
|
+
contact: null,
|
|
706
|
+
conversation: {
|
|
707
|
+
messages: []
|
|
708
|
+
}
|
|
447
709
|
}
|
|
448
710
|
|
|
449
|
-
it('accepts a deal with null contact', () => {
|
|
711
|
+
it('accepts a deal with null contact and an empty conversation', () => {
|
|
450
712
|
expect(DealDetailResponseSchema.safeParse(baseDeal).success).toBe(true)
|
|
451
713
|
})
|
|
452
714
|
|
|
715
|
+
it('accepts conversation messages with preview-derived bodies', () => {
|
|
716
|
+
const withConversation = {
|
|
717
|
+
...baseDeal,
|
|
718
|
+
conversation: {
|
|
719
|
+
messages: [
|
|
720
|
+
{
|
|
721
|
+
id: 'message-1',
|
|
722
|
+
direction: 'inbound',
|
|
723
|
+
fromEmail: 'lead@example.com',
|
|
724
|
+
toEmail: 'sender@example.com',
|
|
725
|
+
subject: 'Re: quick thought',
|
|
726
|
+
body: 'Sure, send it over.',
|
|
727
|
+
sentAt: ISO_TS
|
|
728
|
+
}
|
|
729
|
+
]
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
expect(DealDetailResponseSchema.safeParse(withConversation).success).toBe(true)
|
|
733
|
+
})
|
|
734
|
+
|
|
453
735
|
it('accepts a deal with nested contact that has null company', () => {
|
|
454
736
|
const withContact = {
|
|
455
737
|
...baseDeal,
|
|
@@ -668,6 +950,19 @@ describe('CreateListRequestSchema', () => {
|
|
|
668
950
|
expect(CreateListRequestSchema.safeParse({ name: 'X', status: 'draft' }).success).toBe(true)
|
|
669
951
|
})
|
|
670
952
|
|
|
953
|
+
it('accepts a known prospecting build template id', () => {
|
|
954
|
+
const result = CreateListRequestSchema.safeParse({
|
|
955
|
+
name: 'DTC Subscription Brands',
|
|
956
|
+
buildTemplateId: 'dtc-subscription-apollo-clickup'
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
expect(result.success).toBe(true)
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
it('rejects an unknown prospecting build template id', () => {
|
|
963
|
+
expect(CreateListRequestSchema.safeParse({ name: 'X', buildTemplateId: 'not-a-template' }).success).toBe(false)
|
|
964
|
+
})
|
|
965
|
+
|
|
671
966
|
it('rejects an invalid status', () => {
|
|
672
967
|
expect(CreateListRequestSchema.safeParse({ name: 'X', status: 'active' }).success).toBe(false)
|
|
673
968
|
})
|
|
@@ -710,6 +1005,23 @@ describe('UpdateListRequestSchema', () => {
|
|
|
710
1005
|
expect(UpdateListRequestSchema.safeParse({ batchIds: ['batch-1'] }).success).toBe(true)
|
|
711
1006
|
})
|
|
712
1007
|
|
|
1008
|
+
it('accepts buildTemplateId when the change is explicitly confirmed', () => {
|
|
1009
|
+
expect(
|
|
1010
|
+
UpdateListRequestSchema.safeParse({
|
|
1011
|
+
buildTemplateId: 'dtc-subscription-apollo-clickup',
|
|
1012
|
+
confirmBuildTemplateChange: true
|
|
1013
|
+
}).success
|
|
1014
|
+
).toBe(true)
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
it('rejects buildTemplateId without explicit confirmation', () => {
|
|
1018
|
+
expect(
|
|
1019
|
+
UpdateListRequestSchema.safeParse({
|
|
1020
|
+
buildTemplateId: 'dtc-subscription-apollo-clickup'
|
|
1021
|
+
}).success
|
|
1022
|
+
).toBe(false)
|
|
1023
|
+
})
|
|
1024
|
+
|
|
713
1025
|
it('rejects unknown fields (strict mode)', () => {
|
|
714
1026
|
expect(UpdateListRequestSchema.safeParse({ name: 'X', extra: 'bad' }).success).toBe(false)
|
|
715
1027
|
})
|
|
@@ -1115,7 +1427,6 @@ describe('AcqListResponseSchema (forward-compat)', () => {
|
|
|
1115
1427
|
organizationId: VALID_UUID,
|
|
1116
1428
|
name: 'Test List',
|
|
1117
1429
|
description: null,
|
|
1118
|
-
type: 'standard',
|
|
1119
1430
|
batchIds: [],
|
|
1120
1431
|
instantlyCampaignId: null,
|
|
1121
1432
|
status: 'draft' as const,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
|
|
3
3
|
import { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
|
|
4
|
+
import { isProspectingBuildTemplateId } from './build-templates'
|
|
5
|
+
export { CrmPriorityBucketKeySchema, CrmPriorityBucketOverrideSchema, CrmPriorityOverrideSchema } from './crm-priority'
|
|
6
|
+
export type { CrmPriorityBucketOverride, CrmPriorityOverride, ResolvedCrmPriorityRuleConfig } from './crm-priority'
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Deal Management API Schemas
|
|
@@ -145,6 +148,16 @@ export const DealContactSummarySchema = z.object({
|
|
|
145
148
|
.nullable()
|
|
146
149
|
})
|
|
147
150
|
|
|
151
|
+
export const DealPrioritySchema = z.object({
|
|
152
|
+
bucketKey: z.enum(['needs_response', 'follow_up_due', 'waiting', 'stale', 'closed_low']),
|
|
153
|
+
rank: z.number().int(),
|
|
154
|
+
label: z.string(),
|
|
155
|
+
color: z.string(),
|
|
156
|
+
reason: z.string(),
|
|
157
|
+
latestActivityAt: z.string().nullable(),
|
|
158
|
+
nextActionAt: z.string().nullable()
|
|
159
|
+
})
|
|
160
|
+
|
|
148
161
|
/**
|
|
149
162
|
* Deal list item with joined contact (and company via contact).
|
|
150
163
|
* Matches DealListItem from @repo/core types.
|
|
@@ -174,6 +187,9 @@ export const DealListItemSchema = z.object({
|
|
|
174
187
|
closed_lost_reason: z.string().nullable(),
|
|
175
188
|
created_at: z.string(),
|
|
176
189
|
updated_at: z.string(),
|
|
190
|
+
priority: DealPrioritySchema,
|
|
191
|
+
ownership: z.enum(['us', 'them']).nullable(),
|
|
192
|
+
nextAction: z.string().nullable(),
|
|
177
193
|
// joined relation
|
|
178
194
|
contact: DealContactSummarySchema.nullable()
|
|
179
195
|
})
|
|
@@ -225,11 +241,27 @@ export const DealLookupItemSchema = z.object({
|
|
|
225
241
|
|
|
226
242
|
export const DealLookupResponseSchema = z.array(DealLookupItemSchema)
|
|
227
243
|
|
|
244
|
+
export const ConversationMessageSchema = z.object({
|
|
245
|
+
id: z.string(),
|
|
246
|
+
direction: z.enum(['inbound', 'outbound']),
|
|
247
|
+
fromEmail: z.string(),
|
|
248
|
+
toEmail: z.string(),
|
|
249
|
+
subject: z.string().nullable(),
|
|
250
|
+
body: z.string(),
|
|
251
|
+
sentAt: z.string().nullable()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
export const DealConversationSchema = z.object({
|
|
255
|
+
messages: z.array(ConversationMessageSchema)
|
|
256
|
+
})
|
|
257
|
+
|
|
228
258
|
/**
|
|
229
259
|
* Deal detail shape — currently the same as a list item (full joined record).
|
|
230
|
-
*
|
|
260
|
+
* Additive fields keep existing DealListItem callers compatible.
|
|
231
261
|
*/
|
|
232
|
-
export const DealDetailResponseSchema = DealListItemSchema
|
|
262
|
+
export const DealDetailResponseSchema = DealListItemSchema.extend({
|
|
263
|
+
conversation: DealConversationSchema
|
|
264
|
+
})
|
|
233
265
|
|
|
234
266
|
/**
|
|
235
267
|
* Single acq_deal_notes row (camelCase API representation).
|
|
@@ -291,9 +323,11 @@ export const DealSchemas = {
|
|
|
291
323
|
ExecuteActionRequest: ExecuteActionRequestSchema,
|
|
292
324
|
|
|
293
325
|
// Responses
|
|
326
|
+
DealPriority: DealPrioritySchema,
|
|
294
327
|
DealListResponse: DealListResponseSchema,
|
|
295
328
|
DealSummaryResponse: DealSummaryResponseSchema,
|
|
296
329
|
DealLookupResponse: DealLookupResponseSchema,
|
|
330
|
+
ConversationMessage: ConversationMessageSchema,
|
|
297
331
|
DealDetailResponse: DealDetailResponseSchema,
|
|
298
332
|
DealNoteResponse: DealNoteResponseSchema,
|
|
299
333
|
DealNoteListResponse: DealNoteListResponseSchema,
|
|
@@ -318,10 +352,12 @@ export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
|
|
|
318
352
|
export type TransitionDealStateRequest = z.infer<typeof TransitionDealStateRequestSchema>
|
|
319
353
|
export type ExecuteActionParams = z.infer<typeof ExecuteActionParamsSchema>
|
|
320
354
|
export type ExecuteActionRequest = z.infer<typeof ExecuteActionRequestSchema>
|
|
355
|
+
export type DealPriorityResponse = z.infer<typeof DealPrioritySchema>
|
|
321
356
|
export type DealListResponse = z.infer<typeof DealListResponseSchema>
|
|
322
357
|
export type DealSummaryResponse = z.infer<typeof DealSummaryResponseSchema>
|
|
323
358
|
export type DealLookupItem = z.infer<typeof DealLookupItemSchema>
|
|
324
359
|
export type DealLookupResponse = z.infer<typeof DealLookupResponseSchema>
|
|
360
|
+
export type ConversationMessage = z.infer<typeof ConversationMessageSchema>
|
|
325
361
|
export type DealDetailResponse = z.infer<typeof DealDetailResponseSchema>
|
|
326
362
|
export type DealNoteResponse = z.infer<typeof DealNoteResponseSchema>
|
|
327
363
|
export type DealNoteListResponse = z.infer<typeof DealNoteListResponseSchema>
|
|
@@ -399,6 +435,41 @@ export const PipelineConfigSchema = z.object({
|
|
|
399
435
|
stages: z.array(PipelineStageSchema).optional()
|
|
400
436
|
})
|
|
401
437
|
|
|
438
|
+
export const BuildPlanSnapshotStepSchema = z
|
|
439
|
+
.object({
|
|
440
|
+
id: z.string().trim().min(1).max(100),
|
|
441
|
+
label: z.string().trim().min(1).max(120),
|
|
442
|
+
description: z.string().trim().min(1).max(2000).optional(),
|
|
443
|
+
primaryEntity: z.enum(['company', 'contact']),
|
|
444
|
+
outputs: z.array(z.enum(['company', 'contact', 'export'])).min(1),
|
|
445
|
+
stageKey: z.string().trim().min(1).max(100),
|
|
446
|
+
dependsOn: z.array(z.string().trim().min(1).max(100)).optional(),
|
|
447
|
+
dependencyMode: z.literal('per-record-eligibility'),
|
|
448
|
+
capabilityKey: z.string().trim().min(1).max(100),
|
|
449
|
+
defaultBatchSize: z.number().int().positive(),
|
|
450
|
+
maxBatchSize: z.number().int().positive()
|
|
451
|
+
})
|
|
452
|
+
.refine((step) => step.defaultBatchSize <= step.maxBatchSize, {
|
|
453
|
+
message: 'defaultBatchSize must be less than or equal to maxBatchSize',
|
|
454
|
+
path: ['defaultBatchSize']
|
|
455
|
+
})
|
|
456
|
+
|
|
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
|
+
})
|
|
462
|
+
|
|
463
|
+
export const AcqListMetadataSchema = z
|
|
464
|
+
.object({
|
|
465
|
+
buildPlanSnapshot: BuildPlanSnapshotSchema.optional()
|
|
466
|
+
})
|
|
467
|
+
.catchall(z.unknown())
|
|
468
|
+
|
|
469
|
+
export const ProspectingBuildTemplateIdSchema = z.string().trim().min(1).max(100).refine(isProspectingBuildTemplateId, {
|
|
470
|
+
message: 'buildTemplateId must match a known prospecting build template'
|
|
471
|
+
})
|
|
472
|
+
|
|
402
473
|
// ---------------------------------------------------------------------------
|
|
403
474
|
// List telemetry / progress schemas
|
|
404
475
|
// ---------------------------------------------------------------------------
|
|
@@ -450,6 +521,7 @@ export const CreateListRequestSchema = z
|
|
|
450
521
|
name: z.string().trim().min(1).max(255),
|
|
451
522
|
description: z.string().trim().nullable().optional(),
|
|
452
523
|
status: ListStatusSchema.optional(),
|
|
524
|
+
buildTemplateId: ProspectingBuildTemplateIdSchema.optional(),
|
|
453
525
|
scrapingConfig: ScrapingConfigSchema.optional(),
|
|
454
526
|
icp: IcpRubricSchema.optional(),
|
|
455
527
|
pipelineConfig: PipelineConfigSchema.optional()
|
|
@@ -460,11 +532,24 @@ export const UpdateListRequestSchema = z
|
|
|
460
532
|
.object({
|
|
461
533
|
name: z.string().trim().min(1).max(255).optional(),
|
|
462
534
|
description: z.string().trim().nullable().optional(),
|
|
463
|
-
batchIds: z.array(z.string()).optional()
|
|
535
|
+
batchIds: z.array(z.string()).optional(),
|
|
536
|
+
buildTemplateId: ProspectingBuildTemplateIdSchema.optional(),
|
|
537
|
+
confirmBuildTemplateChange: z.literal(true).optional()
|
|
464
538
|
})
|
|
465
539
|
.strict()
|
|
466
|
-
.refine(
|
|
467
|
-
|
|
540
|
+
.refine(
|
|
541
|
+
(data) =>
|
|
542
|
+
data.name !== undefined ||
|
|
543
|
+
data.description !== undefined ||
|
|
544
|
+
data.batchIds !== undefined ||
|
|
545
|
+
data.buildTemplateId !== undefined,
|
|
546
|
+
{
|
|
547
|
+
message: 'At least one field (name, description, batchIds, or buildTemplateId) must be provided'
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
.refine((data) => data.buildTemplateId === undefined || data.confirmBuildTemplateChange === true, {
|
|
551
|
+
message: 'confirmBuildTemplateChange must be true when changing buildTemplateId',
|
|
552
|
+
path: ['confirmBuildTemplateChange']
|
|
468
553
|
})
|
|
469
554
|
|
|
470
555
|
/**
|
|
@@ -531,12 +616,11 @@ export const AcqListResponseSchema = z.object({
|
|
|
531
616
|
organizationId: z.string(),
|
|
532
617
|
name: z.string(),
|
|
533
618
|
description: z.string().nullable(),
|
|
534
|
-
type: z.string(),
|
|
535
619
|
batchIds: z.array(z.string()),
|
|
536
620
|
instantlyCampaignId: z.string().nullable(),
|
|
537
621
|
/** Lifecycle status (draft | enriching | launched | closing | archived). */
|
|
538
622
|
status: ListStatusSchema,
|
|
539
|
-
metadata:
|
|
623
|
+
metadata: AcqListMetadataSchema,
|
|
540
624
|
launchedAt: z.string().nullable(),
|
|
541
625
|
completedAt: z.string().nullable(),
|
|
542
626
|
createdAt: z.string(),
|
|
@@ -600,7 +684,8 @@ export const ListExecutionSummarySchema = z.object({
|
|
|
600
684
|
status: z.string(),
|
|
601
685
|
createdAt: z.string(),
|
|
602
686
|
completedAt: z.string().nullable(),
|
|
603
|
-
durationMs: z.number().int().nullable()
|
|
687
|
+
durationMs: z.number().int().nullable(),
|
|
688
|
+
input: z.unknown().nullable().optional()
|
|
604
689
|
})
|
|
605
690
|
|
|
606
691
|
export const ListExecutionsResponseSchema = z.array(ListExecutionSummarySchema)
|
|
@@ -619,6 +704,29 @@ export const AcqCompanyStatusSchema = z.enum(['active', 'invalid'])
|
|
|
619
704
|
export const AcqContactStatusSchema = z.enum(['active', 'invalid'])
|
|
620
705
|
export const AcqEmailValidSchema = z.enum(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])
|
|
621
706
|
|
|
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
|
+
|
|
622
730
|
export const CompanyIdParamsSchema = z.object({
|
|
623
731
|
companyId: UuidSchema
|
|
624
732
|
})
|
|
@@ -1005,6 +1113,9 @@ export const AcqListSchemas = {
|
|
|
1005
1113
|
IcpRubric: IcpRubricSchema,
|
|
1006
1114
|
PipelineConfig: PipelineConfigSchema,
|
|
1007
1115
|
PipelineStage: PipelineStageSchema,
|
|
1116
|
+
BuildPlanSnapshot: BuildPlanSnapshotSchema,
|
|
1117
|
+
BuildPlanSnapshotStep: BuildPlanSnapshotStepSchema,
|
|
1118
|
+
AcqListMetadata: AcqListMetadataSchema,
|
|
1008
1119
|
ProcessingStageStatus: ProcessingStageStatusSchema,
|
|
1009
1120
|
ListStageCounts: ListStageCountsSchema,
|
|
1010
1121
|
ListTelemetry: ListTelemetrySchema,
|
|
@@ -1081,6 +1192,9 @@ export type ScrapingConfig = z.infer<typeof ScrapingConfigSchema>
|
|
|
1081
1192
|
export type IcpRubric = z.infer<typeof IcpRubricSchema>
|
|
1082
1193
|
export type PipelineStage = z.infer<typeof PipelineStageSchema>
|
|
1083
1194
|
export type PipelineConfig = z.infer<typeof PipelineConfigSchema>
|
|
1195
|
+
export type BuildPlanSnapshotStep = z.infer<typeof BuildPlanSnapshotStepSchema>
|
|
1196
|
+
export type BuildPlanSnapshot = z.infer<typeof BuildPlanSnapshotSchema>
|
|
1197
|
+
export type AcqListMetadata = z.infer<typeof AcqListMetadataSchema>
|
|
1084
1198
|
export type ProcessingStageStatus = z.infer<typeof ProcessingStageStatusSchema>
|
|
1085
1199
|
export type ListStageCountsInput = z.infer<typeof ListStageCountsSchema>['stageCounts']
|
|
1086
1200
|
export type ListTelemetryInput = z.infer<typeof ListTelemetrySchema>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DEFAULT_ORGANIZATION_MODEL_PROSPECTING } from '../../organization-model/domains/prospecting'
|
|
2
|
+
import type { BuildPlanSnapshot, BuildPlanSnapshotStep } from './types'
|
|
3
|
+
|
|
4
|
+
export const PROSPECTING_BUILD_TEMPLATE_OPTIONS = DEFAULT_ORGANIZATION_MODEL_PROSPECTING.buildTemplates.map(
|
|
5
|
+
({ id, label, description }) => ({
|
|
6
|
+
id,
|
|
7
|
+
label,
|
|
8
|
+
description
|
|
9
|
+
})
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_PROSPECTING_BUILD_TEMPLATE_ID = DEFAULT_ORGANIZATION_MODEL_PROSPECTING.defaultBuildTemplateId
|
|
13
|
+
|
|
14
|
+
export function isProspectingBuildTemplateId(value: string): boolean {
|
|
15
|
+
return PROSPECTING_BUILD_TEMPLATE_OPTIONS.some((template) => template.id === value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createBuildPlanSnapshotFromTemplateId(templateId: string): BuildPlanSnapshot | null {
|
|
19
|
+
const template = DEFAULT_ORGANIZATION_MODEL_PROSPECTING.buildTemplates.find((item) => item.id === templateId)
|
|
20
|
+
if (!template) return null
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
templateId: template.id,
|
|
24
|
+
templateLabel: template.label,
|
|
25
|
+
steps: template.steps.map((step): BuildPlanSnapshotStep => {
|
|
26
|
+
const snapshotStep: BuildPlanSnapshotStep = {
|
|
27
|
+
id: step.id,
|
|
28
|
+
label: step.label,
|
|
29
|
+
primaryEntity: step.primaryEntity,
|
|
30
|
+
outputs: [...step.outputs],
|
|
31
|
+
stageKey: step.stageKey,
|
|
32
|
+
dependencyMode: step.dependencyMode,
|
|
33
|
+
capabilityKey: step.capabilityKey,
|
|
34
|
+
defaultBatchSize: step.defaultBatchSize,
|
|
35
|
+
maxBatchSize: step.maxBatchSize
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (step.description) snapshotStep.description = step.description
|
|
39
|
+
if (step.dependsOn?.length) snapshotStep.dependsOn = [...step.dependsOn]
|
|
40
|
+
|
|
41
|
+
return snapshotStep
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|