@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.
Files changed (88) hide show
  1. package/dist/index.d.ts +1718 -19
  2. package/dist/index.js +369 -25
  3. package/dist/organization-model/index.d.ts +1718 -19
  4. package/dist/organization-model/index.js +369 -25
  5. package/dist/test-utils/index.d.ts +1108 -371
  6. package/dist/test-utils/index.js +357 -17
  7. package/package.json +5 -1
  8. package/src/__tests__/publish.test.ts +14 -13
  9. package/src/__tests__/template-core-compatibility.test.ts +4 -4
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1109 -882
  11. package/src/auth/multi-tenancy/index.ts +3 -0
  12. package/src/auth/multi-tenancy/theme-presets.ts +45 -0
  13. package/src/auth/multi-tenancy/types.ts +57 -83
  14. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -194
  15. package/src/business/acquisition/activity-events.ts +13 -4
  16. package/src/business/acquisition/api-schemas.test.ts +315 -4
  17. package/src/business/acquisition/api-schemas.ts +122 -8
  18. package/src/business/acquisition/build-templates.ts +44 -0
  19. package/src/business/acquisition/crm-next-action.test.ts +262 -0
  20. package/src/business/acquisition/crm-next-action.ts +220 -0
  21. package/src/business/acquisition/crm-priority.test.ts +216 -0
  22. package/src/business/acquisition/crm-priority.ts +349 -0
  23. package/src/business/acquisition/crm-state-actions.test.ts +151 -160
  24. package/src/business/acquisition/deal-ownership.test.ts +351 -0
  25. package/src/business/acquisition/deal-ownership.ts +120 -0
  26. package/src/business/acquisition/derive-actions.test.ts +101 -37
  27. package/src/business/acquisition/derive-actions.ts +102 -87
  28. package/src/business/acquisition/index.ts +10 -0
  29. package/src/business/acquisition/types.ts +400 -366
  30. package/src/business/crm/api-schemas.ts +40 -0
  31. package/src/business/crm/index.ts +1 -0
  32. package/src/business/deals/api-schemas.ts +79 -0
  33. package/src/business/deals/index.ts +1 -0
  34. package/src/business/projects/types.ts +124 -88
  35. package/src/execution/core/runner-types.ts +61 -80
  36. package/src/execution/engine/index.ts +4 -3
  37. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -104
  38. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1473
  39. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -102
  40. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -179
  41. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -309
  42. package/src/execution/engine/tools/integration/tool.ts +255 -253
  43. package/src/execution/engine/tools/lead-service-types.ts +939 -924
  44. package/src/execution/engine/tools/messages.ts +43 -0
  45. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -5
  46. package/src/execution/engine/tools/platform/acquisition/types.ts +5 -2
  47. package/src/execution/engine/tools/platform/email/types.ts +97 -96
  48. package/src/execution/engine/tools/registry.ts +4 -3
  49. package/src/execution/engine/tools/tool-maps.ts +3 -1
  50. package/src/execution/engine/tools/types.ts +234 -233
  51. package/src/execution/engine/workflow/types.ts +195 -193
  52. package/src/execution/external/api-schemas.ts +40 -0
  53. package/src/execution/external/index.ts +1 -0
  54. package/src/knowledge/README.md +32 -0
  55. package/src/knowledge/__tests__/queries.test.ts +504 -0
  56. package/src/knowledge/format.ts +99 -0
  57. package/src/knowledge/index.ts +5 -0
  58. package/src/knowledge/queries.ts +256 -0
  59. package/src/organization-model/__tests__/defaults.test.ts +172 -172
  60. package/src/organization-model/__tests__/foundation.test.ts +7 -7
  61. package/src/organization-model/__tests__/icons.test.ts +27 -0
  62. package/src/organization-model/__tests__/knowledge.test.ts +214 -0
  63. package/src/organization-model/contracts.ts +17 -15
  64. package/src/organization-model/defaults.ts +74 -19
  65. package/src/organization-model/domains/knowledge.ts +53 -0
  66. package/src/organization-model/domains/navigation.ts +416 -399
  67. package/src/organization-model/domains/prospecting.ts +204 -1
  68. package/src/organization-model/domains/sales.test.ts +29 -0
  69. package/src/organization-model/domains/sales.ts +102 -0
  70. package/src/organization-model/domains/shared.ts +6 -5
  71. package/src/organization-model/foundation.ts +10 -6
  72. package/src/organization-model/graph/build.ts +209 -182
  73. package/src/organization-model/graph/schema.ts +37 -34
  74. package/src/organization-model/graph/types.ts +47 -31
  75. package/src/organization-model/icons.ts +81 -0
  76. package/src/organization-model/index.ts +8 -3
  77. package/src/organization-model/organization-model.mdx +1 -1
  78. package/src/organization-model/published.ts +103 -86
  79. package/src/organization-model/schema.ts +90 -85
  80. package/src/organization-model/types.ts +42 -35
  81. package/src/platform/constants/versions.ts +1 -1
  82. package/src/platform/index.ts +23 -27
  83. package/src/platform/registry/index.ts +0 -4
  84. package/src/platform/registry/resource-registry.ts +0 -77
  85. package/src/platform/registry/serialized-types.ts +148 -219
  86. package/src/platform/registry/stats-types.ts +60 -60
  87. package/src/reference/_generated/contracts.md +829 -595
  88. package/src/supabase/database.types.ts +2978 -2958
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { LEAD_GEN_PIPELINE_DEFINITIONS, LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
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
- contact: null
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
- * useDealDetail returns DealDetail which is typed as DealListItem.
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((data) => data.name !== undefined || data.description !== undefined || data.batchIds !== undefined, {
467
- message: 'At least one field (name, description, or batchIds) must be provided'
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: z.record(z.string(), z.unknown()),
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
+ }