@elevasis/core 0.14.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.
@@ -1,1075 +1,1103 @@
1
- import { z } from 'zod'
2
- import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
3
- import { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
-
5
- /**
6
- * Deal Management API Schemas
7
- *
8
- * Request/response validation for /api/deals surface.
9
- * Used by both the API route handlers and the frontend hooks.
10
- *
11
- * Table mapping:
12
- * acq_deals -> DealSchemas (list/detail)
13
- * acq_deal_notes -> DealSchemas (note shapes)
14
- * acq_deal_tasks -> DealSchemas (task shapes)
15
- */
16
-
17
- // ---------------------------------------------------------------------------
18
- // Enum literals (must match DB CHECK constraints exactly)
19
- // ---------------------------------------------------------------------------
20
-
21
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
22
-
23
- export const AcqDealTaskKindSchema = z.enum(['call', 'email', 'meeting', 'other'])
24
-
25
- // ---------------------------------------------------------------------------
26
- // Params
27
- // ---------------------------------------------------------------------------
28
-
29
- export const DealIdParamsSchema = z.object({
30
- dealId: UuidSchema
31
- })
32
-
33
- export const DealTaskIdParamsSchema = z.object({
34
- dealId: UuidSchema,
35
- taskId: UuidSchema
36
- })
37
-
38
- // ---------------------------------------------------------------------------
39
- // Query schemas (coerce strings from query params)
40
- // ---------------------------------------------------------------------------
41
-
42
- export const ListDealsQuerySchema = z
43
- .object({
44
- stage: DealStageSchema.optional(),
45
- search: z.string().optional(),
46
- limit: z.coerce.number().int().positive().default(50),
47
- offset: z.coerce.number().int().min(0).default(0)
48
- })
49
- .strict()
50
-
51
- export const DealLookupQuerySchema = z
52
- .object({
53
- search: z.string().trim().min(1).max(200).optional(),
54
- limit: z.coerce.number().int().min(1).max(25).default(10)
55
- })
56
- .strict()
57
-
58
- export const ListDealTasksDueQuerySchema = z
59
- .object({
60
- window: z.enum(['overdue', 'today', 'today_and_overdue', 'upcoming']).optional(),
61
- assigneeUserId: UuidSchema.optional()
62
- })
63
- .strict()
64
-
65
- // ---------------------------------------------------------------------------
66
- // Request body schemas (all use .strict() — rejects unknown fields)
67
- // ---------------------------------------------------------------------------
68
-
69
- export const CreateDealNoteRequestSchema = z
70
- .object({
71
- body: z.string().trim().min(1).max(10000)
72
- })
73
- .strict()
74
-
75
- export const CreateDealTaskRequestSchema = z
76
- .object({
77
- title: z.string().trim().min(1).max(255),
78
- description: z.string().nullable().optional(),
79
- kind: AcqDealTaskKindSchema.optional(),
80
- dueAt: z.string().datetime().nullable().optional(),
81
- assigneeUserId: UuidSchema.nullable().optional()
82
- })
83
- .strict()
84
-
85
- export const TransitionItemRequestSchema = z
86
- .object({
87
- pipelineKey: z.string().min(1),
88
- stageKey: z.string().min(1),
89
- stateKey: z.string().nullable().optional(),
90
- reason: z.string().optional(),
91
- expectedUpdatedAt: z.string().datetime().optional()
92
- })
93
- .strict()
94
-
95
- export const ExecuteActionParamsSchema = z
96
- .object({
97
- dealId: UuidSchema,
98
- actionKey: NonEmptyStringSchema
99
- })
100
- .strict()
101
-
102
- export const ExecuteActionRequestSchema = z
103
- .object({
104
- payload: z.record(z.string(), z.unknown()).optional()
105
- })
106
- .strict()
107
-
108
- // ---------------------------------------------------------------------------
109
- // Response schemas (no .strict() — allows forward-compatible additions)
110
- // ---------------------------------------------------------------------------
111
-
112
- /**
113
- * Contact summary nested inside DealListItem / DealDetailResponse.
114
- * Matches the joined shape returned by useDeals / useDealDetail Supabase queries.
115
- */
116
- export const DealContactSummarySchema = z.object({
117
- id: z.string(),
118
- first_name: z.string().nullable(),
119
- last_name: z.string().nullable(),
120
- email: z.string(),
121
- title: z.string().nullable(),
122
- headline: z.string().nullable(),
123
- linkedin_url: z.string().nullable(),
124
- pipeline_status: z.record(z.string(), z.unknown()).nullable(),
125
- enrichment_data: z.record(z.string(), z.unknown()).nullable(),
126
- company: z
127
- .object({
128
- id: z.string(),
129
- name: z.string(),
130
- domain: z.string().nullable(),
131
- website: z.string().nullable(),
132
- linkedin_url: z.string().nullable(),
133
- segment: z.string().nullable(),
134
- category: z.string().nullable(),
135
- num_employees: z.number().nullable()
136
- })
137
- .nullable()
138
- })
139
-
140
- /**
141
- * Deal list item with joined contact (and company via contact).
142
- * Matches DealListItem from @repo/core types.
143
- */
144
- export const DealListItemSchema = z.object({
145
- // acq_deals columns
146
- id: z.string(),
147
- organization_id: z.string(),
148
- contact_id: z.string().nullable(),
149
- contact_email: z.string(),
150
- pipeline_key: z.string(),
151
- stage_key: z.string().nullable(),
152
- state_key: z.string().nullable(),
153
- activity_log: z.unknown(),
154
- discovery_data: z.unknown().nullable(),
155
- discovery_submitted_at: z.string().nullable(),
156
- discovery_submitted_by: z.string().nullable(),
157
- proposal_data: z.unknown().nullable(),
158
- proposal_sent_at: z.string().nullable(),
159
- proposal_pdf_url: z.string().nullable(),
160
- signature_envelope_id: z.string().nullable(),
161
- source_list_id: z.string().nullable(),
162
- source_type: z.string().nullable(),
163
- initial_fee: z.number().nullable(),
164
- monthly_fee: z.number().nullable(),
165
- closed_lost_at: z.string().nullable(),
166
- closed_lost_reason: z.string().nullable(),
167
- created_at: z.string(),
168
- updated_at: z.string(),
169
- // joined relation
170
- contact: DealContactSummarySchema.nullable()
171
- })
172
-
173
- export const DealListResponseSchema = z.object({
174
- data: z.array(DealListItemSchema),
175
- total: z.number().int(),
176
- limit: z.number().int(),
177
- offset: z.number().int()
178
- })
179
-
180
- export const DealStageSummarySchema = z.object({
181
- stage: z.string(),
182
- count: z.number().int(),
183
- totalValue: z.number(),
184
- oldestUpdatedAt: z.string().nullable(),
185
- newestUpdatedAt: z.string().nullable()
186
- })
187
-
188
- export const StaleDealSummarySchema = z.object({
189
- id: z.string(),
190
- contactEmail: z.string(),
191
- stageKey: z.string(),
192
- updatedAt: z.string(),
193
- daysStale: z.number().int()
194
- })
195
-
196
- export const DealSummaryResponseSchema = z.object({
197
- totalDeals: z.number().int(),
198
- openDeals: z.number().int(),
199
- wonDeals: z.number().int(),
200
- lostDeals: z.number().int(),
201
- winRate: z.number(),
202
- avgDealSize: z.number(),
203
- totalPipelineValue: z.number(),
204
- stageSummary: z.array(DealStageSummarySchema),
205
- staleDeals: z.array(StaleDealSummarySchema)
206
- })
207
-
208
- export const DealLookupItemSchema = z.object({
209
- id: z.string(),
210
- contactEmail: z.string(),
211
- stageKey: z.string().nullable(),
212
- updatedAt: z.string(),
213
- contactName: z.string().nullable(),
214
- companyName: z.string().nullable(),
215
- displayLabel: z.string()
216
- })
217
-
218
- export const DealLookupResponseSchema = z.array(DealLookupItemSchema)
219
-
220
- /**
221
- * Deal detail shape — currently the same as a list item (full joined record).
222
- * useDealDetail returns DealDetail which is typed as DealListItem.
223
- */
224
- export const DealDetailResponseSchema = DealListItemSchema
225
-
226
- /**
227
- * Single acq_deal_notes row (camelCase API representation).
228
- */
229
- export const DealNoteResponseSchema = z.object({
230
- id: z.string(),
231
- dealId: z.string(),
232
- organizationId: z.string(),
233
- authorUserId: z.string().nullable(),
234
- body: z.string(),
235
- createdAt: z.string(),
236
- updatedAt: z.string()
237
- })
238
-
239
- export const DealNoteListResponseSchema = z.array(DealNoteResponseSchema)
240
-
241
- /**
242
- * Single acq_deal_tasks row (camelCase API representation).
243
- * Matches AcqDealTask domain type from types.ts.
244
- */
245
- export const DealTaskResponseSchema = z.object({
246
- id: z.string(),
247
- organizationId: z.string(),
248
- dealId: z.string(),
249
- title: z.string(),
250
- description: z.string().nullable(),
251
- kind: AcqDealTaskKindSchema,
252
- dueAt: z.string().nullable(),
253
- assigneeUserId: z.string().nullable(),
254
- completedAt: z.string().nullable(),
255
- completedByUserId: z.string().nullable(),
256
- createdAt: z.string(),
257
- updatedAt: z.string(),
258
- createdByUserId: z.string().nullable()
259
- })
260
-
261
- export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
262
-
263
- // ---------------------------------------------------------------------------
264
- // Bundled export
265
- // ---------------------------------------------------------------------------
266
-
267
- export const DealSchemas = {
268
- // Params
269
- DealIdParams: DealIdParamsSchema,
270
- DealTaskIdParams: DealTaskIdParamsSchema,
271
-
272
- // Queries
273
- ListDealsQuery: ListDealsQuerySchema,
274
- DealLookupQuery: DealLookupQuerySchema,
275
- ListDealTasksDueQuery: ListDealTasksDueQuerySchema,
276
-
277
- // Request bodies
278
- CreateDealNoteRequest: CreateDealNoteRequestSchema,
279
- CreateDealTaskRequest: CreateDealTaskRequestSchema,
280
- TransitionItemRequest: TransitionItemRequestSchema,
281
- ExecuteActionParams: ExecuteActionParamsSchema,
282
- ExecuteActionRequest: ExecuteActionRequestSchema,
283
-
284
- // Responses
285
- DealListResponse: DealListResponseSchema,
286
- DealSummaryResponse: DealSummaryResponseSchema,
287
- DealLookupResponse: DealLookupResponseSchema,
288
- DealDetailResponse: DealDetailResponseSchema,
289
- DealNoteResponse: DealNoteResponseSchema,
290
- DealNoteListResponse: DealNoteListResponseSchema,
291
- DealTaskResponse: DealTaskResponseSchema,
292
- DealTaskListResponse: DealTaskListResponseSchema
293
- }
294
-
295
- // ---------------------------------------------------------------------------
296
- // Inferred types
297
- // ---------------------------------------------------------------------------
298
-
299
- export type DealStage = z.infer<typeof DealStageSchema>
300
- export type AcqDealTaskKind = z.infer<typeof AcqDealTaskKindSchema>
301
- export type DealIdParams = z.infer<typeof DealIdParamsSchema>
302
- export type DealTaskIdParams = z.infer<typeof DealTaskIdParamsSchema>
303
- export type ListDealsQuery = z.infer<typeof ListDealsQuerySchema>
304
- export type DealLookupQuery = z.infer<typeof DealLookupQuerySchema>
305
- export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
306
- export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
307
- export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
308
- export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
309
- export type ExecuteActionParams = z.infer<typeof ExecuteActionParamsSchema>
310
- export type ExecuteActionRequest = z.infer<typeof ExecuteActionRequestSchema>
311
- export type DealListResponse = z.infer<typeof DealListResponseSchema>
312
- export type DealSummaryResponse = z.infer<typeof DealSummaryResponseSchema>
313
- export type DealLookupItem = z.infer<typeof DealLookupItemSchema>
314
- export type DealLookupResponse = z.infer<typeof DealLookupResponseSchema>
315
- export type DealDetailResponse = z.infer<typeof DealDetailResponseSchema>
316
- export type DealNoteResponse = z.infer<typeof DealNoteResponseSchema>
317
- export type DealNoteListResponse = z.infer<typeof DealNoteListResponseSchema>
318
- export type DealTaskResponse = z.infer<typeof DealTaskResponseSchema>
319
- export type DealTaskListResponse = z.infer<typeof DealTaskListResponseSchema>
320
-
321
- // ---------------------------------------------------------------------------
322
- // List Management API Schemas
323
- //
324
- // Request/response validation for /api/acquisition/lists surface.
325
- // Used by both the API route handlers and the frontend hooks.
326
- //
327
- // Table mapping:
328
- // acq_lists -> AcqListSchemas (list/detail/progress)
329
- // acq_list_companies -> AcqListSchemas (add/remove company membership)
330
- // acq_list_contacts -> AcqListSchemas (add/remove contact membership)
331
- // acq_list_executions -> AcqListSchemas (execution history)
332
- // ---------------------------------------------------------------------------
333
-
334
- // ---------------------------------------------------------------------------
335
- // Primitives list status enum + jsonb config schemas
336
- // ---------------------------------------------------------------------------
337
-
338
- /**
339
- * Lifecycle status enum for `acq_lists.status` (mirrors DB CHECK constraint
340
- * from migration 20260428000003_lead_gen_acq_lists_status_and_config.sql).
341
- */
342
- export const ListStatusSchema = z.enum(['draft', 'enriching', 'launched', 'closing', 'archived'])
343
-
344
- /**
345
- * Scraping criteria stored in `acq_lists.scraping_config` jsonb.
346
- * Edited via the UI; consumed by lgn-01 prospecting workflows (Apify input shape,
347
- * geography, vertical, size). All fields are optional — empty config is valid.
348
- */
349
- export const ScrapingConfigSchema = z.object({
350
- vertical: z.string().trim().max(255).optional(),
351
- geography: z.string().trim().max(500).optional(),
352
- size: z.string().trim().max(255).optional(),
353
- apifyInput: z.record(z.string(), z.unknown()).optional()
354
- })
355
-
356
- /**
357
- * ICP / qualification rubric stored in `acq_lists.icp` jsonb.
358
- * Replaces the legacy `config.qualification` blob. Consumed by the
359
- * company-qualification workflow.
360
- */
361
- export const IcpRubricSchema = z.object({
362
- qualificationRubricKey: z.string().trim().max(255).nullish(),
363
- targetDescription: z.string().optional(),
364
- minReviewCount: z.number().int().min(0).optional(),
365
- minRating: z.number().min(0).max(5).optional(),
366
- excludeFranchises: z.boolean().optional(),
367
- customRules: z.string().optional()
368
- })
369
-
370
- /**
371
- * One stage entry in a list's `pipeline_config.stages[]`. The `key` is
372
- * validated against `LEAD_GEN_STAGE_CATALOG` so list pipeline definitions
373
- * stay aligned with the org-os semantic layer.
374
- */
375
- export const PipelineStageSchema = z.object({
376
- key: z.string().refine((value) => Object.prototype.hasOwnProperty.call(LEAD_GEN_STAGE_CATALOG, value), {
377
- message: 'pipeline stage key must match LEAD_GEN_STAGE_CATALOG'
378
- }),
379
- label: z.string().optional(),
380
- enabled: z.boolean().optional(),
381
- order: z.number().int().optional()
382
- })
383
-
384
- /**
385
- * Pipeline presentation contract stored in `acq_lists.pipeline_config` jsonb.
386
- * `stages[].key` validates against the catalog; the rest is presentation only.
387
- */
388
- export const PipelineConfigSchema = z.object({
389
- stages: z.array(PipelineStageSchema).optional()
390
- })
391
-
392
- // ---------------------------------------------------------------------------
393
- // List telemetry / progress schemas
394
- // ---------------------------------------------------------------------------
395
-
396
- export const ListStageCountsSchema = z.object({
397
- stageCounts: z.object({
398
- populated: z.number().int(),
399
- extracted: z.number().int(),
400
- qualified: z.number().int(),
401
- discovered: z.number().int(),
402
- verified: z.number().int(),
403
- personalized: z.number().int(),
404
- uploaded: z.number().int()
405
- }),
406
- deliverability: z.object({
407
- valid: z.number().int(),
408
- risky: z.number().int(),
409
- invalid: z.number().int(),
410
- unknown: z.number().int(),
411
- bounced: z.number().int()
412
- })
413
- })
414
-
415
- export const ListTelemetrySchema = z.object({
416
- listId: UuidSchema,
417
- totalCompanies: z.number().int(),
418
- totalContacts: z.number().int(),
419
- stageCounts: ListStageCountsSchema.shape.stageCounts,
420
- deliverability: ListStageCountsSchema.shape.deliverability,
421
- activeWorkflows: z.array(z.string()).optional()
422
- })
423
-
424
- // ---------------------------------------------------------------------------
425
- // Params
426
- // ---------------------------------------------------------------------------
427
-
428
- export const ListIdParamsSchema = z.object({
429
- listId: UuidSchema
430
- })
431
-
432
- // ---------------------------------------------------------------------------
433
- // Request body schemas (all use .strict() — rejects unknown fields)
434
- // ---------------------------------------------------------------------------
435
-
436
- export const CreateListRequestSchema = z
437
- .object({
438
- name: z.string().trim().min(1).max(255),
439
- description: z.string().trim().nullable().optional(),
440
- status: ListStatusSchema.optional(),
441
- scrapingConfig: ScrapingConfigSchema.optional(),
442
- icp: IcpRubricSchema.optional(),
443
- pipelineConfig: PipelineConfigSchema.optional()
444
- })
445
- .strict()
446
-
447
- export const UpdateListRequestSchema = z
448
- .object({
449
- name: z.string().trim().min(1).max(255).optional(),
450
- description: z.string().trim().nullable().optional(),
451
- batchIds: z.array(z.string()).optional()
452
- })
453
- .strict()
454
- .refine((data) => data.name !== undefined || data.description !== undefined || data.batchIds !== undefined, {
455
- message: 'At least one field (name, description, or batchIds) must be provided'
456
- })
457
-
458
- /**
459
- * Status-only PATCH body for `/acquisition/lists/:listId/status`.
460
- * Replaces the previous `transitionList` flow.
461
- */
462
- export const UpdateListStatusRequestSchema = z
463
- .object({
464
- status: ListStatusSchema
465
- })
466
- .strict()
467
-
468
- /**
469
- * Partial patch for the three jsonb config columns. UI sends only the edited
470
- * subtree; server writes the field as-is (no deep merge — each column is
471
- * replaced atomically when present in the patch).
472
- */
473
- export const UpdateListConfigRequestSchema = z
474
- .object({
475
- scrapingConfig: ScrapingConfigSchema.partial().optional(),
476
- icp: IcpRubricSchema.partial().optional(),
477
- pipelineConfig: PipelineConfigSchema.partial().optional()
478
- })
479
- .strict()
480
- .refine((data) => data.scrapingConfig !== undefined || data.icp !== undefined || data.pipelineConfig !== undefined, {
481
- message: 'At least one of scrapingConfig, icp, or pipelineConfig must be provided'
482
- })
483
-
484
- export const AddCompaniesToListRequestSchema = z
485
- .object({
486
- companyIds: z.array(UuidSchema).min(1).max(1000)
487
- })
488
- .strict()
489
-
490
- export const RemoveCompaniesFromListRequestSchema = z
491
- .object({
492
- companyIds: z.array(UuidSchema).min(1).max(1000)
493
- })
494
- .strict()
495
-
496
- export const AddContactsToListRequestSchema = z
497
- .object({
498
- contactIds: z.array(UuidSchema).min(1).max(1000)
499
- })
500
- .strict()
501
-
502
- export const RecordListExecutionRequestSchema = z
503
- .object({
504
- executionId: UuidSchema,
505
- configSnapshot: z.record(z.string(), z.unknown()).optional()
506
- })
507
- .strict()
508
-
509
- // ---------------------------------------------------------------------------
510
- // Response schemas (no .strict() — allows forward-compatible additions)
511
- // ---------------------------------------------------------------------------
512
-
513
- /**
514
- * Single list as returned by /api/acquisition/lists/:id etc.
515
- * Camel-cased domain shape matching AcqList in types.ts.
516
- */
517
- export const AcqListResponseSchema = z.object({
518
- id: z.string(),
519
- organizationId: z.string(),
520
- name: z.string(),
521
- description: z.string().nullable(),
522
- type: z.string(),
523
- batchIds: z.array(z.string()),
524
- instantlyCampaignId: z.string().nullable(),
525
- /** Lifecycle status (draft | enriching | launched | closing | archived). */
526
- status: ListStatusSchema,
527
- metadata: z.record(z.string(), z.unknown()),
528
- launchedAt: z.string().nullable(),
529
- completedAt: z.string().nullable(),
530
- createdAt: z.string(),
531
- /** Scraping criteria stored as jsonb on the row. */
532
- scrapingConfig: ScrapingConfigSchema,
533
- /** ICP / qualification rubric stored as jsonb on the row. */
534
- icp: IcpRubricSchema,
535
- /** Pipeline presentation contract stored as jsonb on the row. */
536
- pipelineConfig: PipelineConfigSchema
537
- })
538
-
539
- export const AcqListListResponseSchema = z.array(AcqListResponseSchema)
540
-
541
- export const ListTelemetryResponseSchema = ListTelemetrySchema
542
-
543
- export const ListTelemetryListResponseSchema = z.array(ListTelemetrySchema)
544
-
545
- /**
546
- * Per-stage progress aggregate for a single pipeline stage.
547
- * `done` = count of members/companies where `(processing_state->>'<key>')::boolean` is true.
548
- * `total` = total member/company count for the list.
549
- */
550
- export const ListStageProgressSchema = z.object({
551
- done: z.number().int().min(0),
552
- total: z.number().int().min(0)
553
- })
554
-
555
- /**
556
- * Progress response for GET /acquisition/lists/:listId/progress.
557
- * Aggregated on-demand via COUNT(*) FILTER over processing_state flags (Decision #4).
558
- * `byStage` keys are driven by the list's pipeline_config.stages[].key.
559
- */
560
- export const ListProgressResponseSchema = z.object({
561
- totalMembers: z.number().int().min(0),
562
- totalCompanies: z.number().int().min(0),
563
- byCompanyStage: z.record(z.string(), ListStageProgressSchema),
564
- byContactStage: z.record(z.string(), ListStageProgressSchema)
565
- })
566
-
567
- /**
568
- * Row from acq_list_executions joined with the execution summary,
569
- * shaped for the /lists/:id/executions response.
570
- */
571
- export const ListExecutionSummarySchema = z.object({
572
- executionId: z.string(),
573
- resourceId: z.string(),
574
- status: z.string(),
575
- createdAt: z.string(),
576
- completedAt: z.string().nullable(),
577
- durationMs: z.number().int().nullable()
578
- })
579
-
580
- export const ListExecutionsResponseSchema = z.array(ListExecutionSummarySchema)
581
-
582
- // ---------------------------------------------------------------------------
583
- // Company / Contact API Schemas
584
- // ---------------------------------------------------------------------------
585
-
586
- const QueryBooleanSchema = z.preprocess((value) => {
587
- if (value === 'true' || value === '1' || value === true) return true
588
- if (value === 'false' || value === '0' || value === false) return false
589
- return value
590
- }, z.boolean())
591
-
592
- export const AcqCompanyStatusSchema = z.enum(['active', 'invalid'])
593
- export const AcqContactStatusSchema = z.enum(['active', 'invalid'])
594
- export const AcqEmailValidSchema = z.enum(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])
595
-
596
- export const CompanyIdParamsSchema = z.object({
597
- companyId: UuidSchema
598
- })
599
-
600
- export const ContactIdParamsSchema = z.object({
601
- contactId: UuidSchema
602
- })
603
-
604
- export const ListCompaniesQuerySchema = z
605
- .object({
606
- search: z.string().trim().min(1).max(200).optional(),
607
- listId: UuidSchema.optional(),
608
- domain: z.string().trim().min(1).max(255).optional(),
609
- website: z.string().trim().min(1).max(2048).optional(),
610
- segment: z.string().trim().min(1).max(255).optional(),
611
- category: z.string().trim().min(1).max(255).optional(),
612
- batchId: z.string().trim().min(1).max(255).optional(),
613
- status: AcqCompanyStatusSchema.optional(),
614
- includeAll: QueryBooleanSchema.optional(),
615
- limit: z.coerce.number().int().min(1).max(5000).default(50),
616
- offset: z.coerce.number().int().min(0).default(0)
617
- })
618
- .strict()
619
-
620
- export const ListContactsQuerySchema = z
621
- .object({
622
- search: z.string().trim().min(1).max(200).optional(),
623
- listId: UuidSchema.optional(),
624
- openingLineIsNull: QueryBooleanSchema.optional(),
625
- batchId: z.string().trim().min(1).max(255).optional(),
626
- contactStatus: AcqContactStatusSchema.optional(),
627
- limit: z.coerce.number().int().min(1).max(5000).default(5000),
628
- offset: z.coerce.number().int().min(0).default(0)
629
- })
630
- .strict()
631
-
632
- export const CreateCompanyRequestSchema = z
633
- .object({
634
- name: z.string().trim().min(1).max(255),
635
- domain: z.string().trim().min(1).max(255).optional(),
636
- linkedinUrl: z.string().trim().url().optional(),
637
- website: z.string().trim().url().optional(),
638
- numEmployees: z.number().int().min(0).optional(),
639
- foundedYear: z.number().int().optional(),
640
- locationCity: z.string().trim().min(1).max(255).optional(),
641
- locationState: z.string().trim().min(1).max(255).optional(),
642
- category: z.string().trim().min(1).max(255).optional(),
643
- source: z.string().trim().min(1).max(255).optional(),
644
- batchId: z.string().trim().min(1).max(255).optional(),
645
- verticalResearch: z.string().trim().min(1).max(5000).optional()
646
- })
647
- .strict()
648
-
649
- export const UpdateCompanyRequestSchema = z
650
- .object({
651
- name: z.string().trim().min(1).max(255).optional(),
652
- domain: z.string().trim().min(1).max(255).optional(),
653
- linkedinUrl: z.string().trim().url().optional(),
654
- website: z.string().trim().url().optional(),
655
- numEmployees: z.number().int().min(0).optional(),
656
- foundedYear: z.number().int().optional(),
657
- locationCity: z.string().trim().min(1).max(255).optional(),
658
- locationState: z.string().trim().min(1).max(255).optional(),
659
- category: z.string().trim().min(1).max(255).optional(),
660
- segment: z.string().trim().min(1).max(255).optional(),
661
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
662
- enrichmentData: z.record(z.string(), z.unknown()).optional(),
663
- source: z.string().trim().min(1).max(255).optional(),
664
- batchId: z.string().trim().min(1).max(255).optional(),
665
- status: AcqCompanyStatusSchema.optional(),
666
- verticalResearch: z.string().trim().min(1).max(5000).nullable().optional()
667
- })
668
- .strict()
669
- .refine(
670
- (data) =>
671
- data.name !== undefined ||
672
- data.domain !== undefined ||
673
- data.linkedinUrl !== undefined ||
674
- data.website !== undefined ||
675
- data.numEmployees !== undefined ||
676
- data.foundedYear !== undefined ||
677
- data.locationCity !== undefined ||
678
- data.locationState !== undefined ||
679
- data.category !== undefined ||
680
- data.segment !== undefined ||
681
- data.pipelineStatus !== undefined ||
682
- data.enrichmentData !== undefined ||
683
- data.source !== undefined ||
684
- data.batchId !== undefined ||
685
- data.status !== undefined ||
686
- data.verticalResearch !== undefined,
687
- {
688
- message: 'At least one field must be provided'
689
- }
690
- )
691
-
692
- export const CreateContactRequestSchema = z
693
- .object({
694
- email: z.string().trim().email(),
695
- companyId: UuidSchema.optional(),
696
- firstName: z.string().trim().min(1).max(255).optional(),
697
- lastName: z.string().trim().min(1).max(255).optional(),
698
- linkedinUrl: z.string().trim().url().optional(),
699
- title: z.string().trim().min(1).max(255).optional(),
700
- source: z.string().trim().min(1).max(255).optional(),
701
- sourceId: z.string().trim().min(1).max(255).optional(),
702
- batchId: z.string().trim().min(1).max(255).optional()
703
- })
704
- .strict()
705
-
706
- export const UpdateContactRequestSchema = z
707
- .object({
708
- companyId: UuidSchema.optional(),
709
- emailValid: AcqEmailValidSchema.optional(),
710
- firstName: z.string().trim().min(1).max(255).optional(),
711
- lastName: z.string().trim().min(1).max(255).optional(),
712
- linkedinUrl: z.string().trim().url().optional(),
713
- title: z.string().trim().min(1).max(255).optional(),
714
- headline: z.string().trim().min(1).max(5000).optional(),
715
- filterReason: z.string().trim().min(1).max(5000).optional(),
716
- openingLine: z.string().trim().min(1).max(5000).optional(),
717
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
718
- enrichmentData: z.record(z.string(), z.unknown()).optional(),
719
- status: AcqContactStatusSchema.optional()
720
- })
721
- .strict()
722
- .refine(
723
- (data) =>
724
- data.companyId !== undefined ||
725
- data.emailValid !== undefined ||
726
- data.firstName !== undefined ||
727
- data.lastName !== undefined ||
728
- data.linkedinUrl !== undefined ||
729
- data.title !== undefined ||
730
- data.headline !== undefined ||
731
- data.filterReason !== undefined ||
732
- data.openingLine !== undefined ||
733
- data.pipelineStatus !== undefined ||
734
- data.enrichmentData !== undefined ||
735
- data.status !== undefined,
736
- {
737
- message: 'At least one field must be provided'
738
- }
739
- )
740
-
741
- export const AcqCompanyResponseSchema = z.object({
742
- id: z.string(),
743
- organizationId: z.string(),
744
- name: z.string(),
745
- domain: z.string().nullable(),
746
- linkedinUrl: z.string().nullable(),
747
- website: z.string().nullable(),
748
- numEmployees: z.number().nullable(),
749
- foundedYear: z.number().nullable(),
750
- locationCity: z.string().nullable(),
751
- locationState: z.string().nullable(),
752
- category: z.string().nullable(),
753
- categoryPain: z.string().nullable(),
754
- segment: z.string().nullable(),
755
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
756
- enrichmentData: z.record(z.string(), z.unknown()).nullable(),
757
- source: z.string().nullable(),
758
- batchId: z.string().nullable(),
759
- status: AcqCompanyStatusSchema,
760
- contactCount: z.number().int().min(0),
761
- verticalResearch: z.string().nullable(),
762
- createdAt: z.string(),
763
- updatedAt: z.string()
764
- })
765
-
766
- export const AcqCompanyListResponseSchema = z.object({
767
- data: z.array(AcqCompanyResponseSchema),
768
- total: z.number().int(),
769
- limit: z.number().int(),
770
- offset: z.number().int()
771
- })
772
-
773
- export const AcqCompanyFacetsResponseSchema = z.object({
774
- segments: z.array(z.string()),
775
- categories: z.array(z.string()),
776
- statuses: z.array(AcqCompanyStatusSchema)
777
- })
778
-
779
- export const AcqContactCompanySummarySchema = z.object({
780
- id: z.string(),
781
- name: z.string(),
782
- domain: z.string().nullable(),
783
- website: z.string().nullable(),
784
- linkedinUrl: z.string().nullable(),
785
- segment: z.string().nullable(),
786
- category: z.string().nullable(),
787
- status: AcqCompanyStatusSchema
788
- })
789
-
790
- export const AcqContactResponseSchema = z.object({
791
- id: z.string(),
792
- organizationId: z.string(),
793
- companyId: z.string().nullable(),
794
- email: z.string(),
795
- emailValid: AcqEmailValidSchema.nullable(),
796
- firstName: z.string().nullable(),
797
- lastName: z.string().nullable(),
798
- linkedinUrl: z.string().nullable(),
799
- title: z.string().nullable(),
800
- headline: z.string().nullable(),
801
- filterReason: z.string().nullable(),
802
- openingLine: z.string().nullable(),
803
- source: z.string().nullable(),
804
- sourceId: z.string().nullable(),
805
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
806
- enrichmentData: z.record(z.string(), z.unknown()).nullable(),
807
- attioPersonId: z.string().nullable(),
808
- batchId: z.string().nullable(),
809
- status: AcqContactStatusSchema,
810
- company: AcqContactCompanySummarySchema.nullable().optional(),
811
- createdAt: z.string(),
812
- updatedAt: z.string()
813
- })
814
-
815
- export const AcqContactListResponseSchema = z.object({
816
- data: z.array(AcqContactResponseSchema),
817
- total: z.number().int(),
818
- limit: z.number().int(),
819
- offset: z.number().int()
820
- })
821
-
822
- // ---------------------------------------------------------------------------
823
- // Track A: Artifacts API Schemas
824
- // ---------------------------------------------------------------------------
825
-
826
- export const AcqArtifactOwnerKindSchema = z.enum(['company', 'contact', 'deal', 'list', 'list_member'])
827
-
828
- export const ListArtifactsQuerySchema = z
829
- .object({
830
- ownerKind: AcqArtifactOwnerKindSchema,
831
- ownerId: UuidSchema
832
- })
833
- .strict()
834
-
835
- export const CreateArtifactRequestSchema = z
836
- .object({
837
- ownerKind: AcqArtifactOwnerKindSchema,
838
- ownerId: UuidSchema,
839
- kind: z.string().trim().min(1).max(255),
840
- content: z.record(z.string(), z.unknown()),
841
- sourceExecutionId: UuidSchema.optional()
842
- })
843
- .strict()
844
-
845
- export const AcqArtifactResponseSchema = z.object({
846
- id: z.string(),
847
- organizationId: z.string(),
848
- ownerKind: z.string(),
849
- ownerId: z.string(),
850
- kind: z.string(),
851
- content: z.record(z.string(), z.unknown()),
852
- sourceExecutionId: z.string().nullable(),
853
- createdBy: z.string().nullable(),
854
- createdAt: z.string(),
855
- version: z.number().int()
856
- })
857
-
858
- export const AcqArtifactListResponseSchema = z.object({
859
- artifacts: z.array(AcqArtifactResponseSchema)
860
- })
861
-
862
- // ---------------------------------------------------------------------------
863
- // Track B: List Members API Schemas
864
- // ---------------------------------------------------------------------------
865
-
866
- export const ListMembersQuerySchema = z
867
- .object({
868
- limit: z.coerce.number().int().min(1).max(500).default(50),
869
- offset: z.coerce.number().int().min(0).default(0)
870
- })
871
- .strict()
872
-
873
- export const MemberIdParamsSchema = z.object({
874
- memberId: UuidSchema
875
- })
876
-
877
- export const AcqListMemberContactSummarySchema = z.object({
878
- id: z.string(),
879
- email: z.string(),
880
- firstName: z.string().nullable(),
881
- lastName: z.string().nullable(),
882
- title: z.string().nullable(),
883
- linkedinUrl: z.string().nullable(),
884
- companyId: z.string().nullable()
885
- })
886
-
887
- export const AcqListMemberResponseSchema = z.object({
888
- id: z.string(),
889
- listId: z.string(),
890
- contactId: z.string(),
891
- pipelineKey: z.string(),
892
- stageKey: z.string(),
893
- stateKey: z.string(),
894
- activityLog: z.unknown(),
895
- addedAt: z.string(),
896
- addedBy: z.string().nullable(),
897
- sourceExecutionId: z.string().nullable(),
898
- contact: AcqListMemberContactSummarySchema.nullable()
899
- })
900
-
901
- export const AcqListMembersResponseSchema = z.object({
902
- members: z.array(AcqListMemberResponseSchema)
903
- })
904
-
905
- // ---------------------------------------------------------------------------
906
- // Track B: List Companies API Schemas
907
- // ---------------------------------------------------------------------------
908
-
909
- export const ListCompanyIdParamsSchema = z.object({
910
- listCompanyId: UuidSchema
911
- })
912
-
913
- export const AcqListCompanyResponseSchema = z.object({
914
- id: z.string(),
915
- listId: z.string(),
916
- companyId: 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
- })
925
-
926
- // ---------------------------------------------------------------------------
927
- // Track B: Transition Request (shared by list, list-member, list-company)
928
- // TransitionItemRequestSchema already exists above (for deals) — reuse it.
929
- // ---------------------------------------------------------------------------
930
-
931
- export const AcqCompanySchemas = {
932
- CompanyIdParams: CompanyIdParamsSchema,
933
- ListCompaniesQuery: ListCompaniesQuerySchema,
934
- CreateCompanyRequest: CreateCompanyRequestSchema,
935
- UpdateCompanyRequest: UpdateCompanyRequestSchema,
936
- AcqCompanyResponse: AcqCompanyResponseSchema,
937
- AcqCompanyListResponse: AcqCompanyListResponseSchema,
938
- AcqCompanyFacetsResponse: AcqCompanyFacetsResponseSchema
939
- }
940
-
941
- export const AcqContactSchemas = {
942
- ContactIdParams: ContactIdParamsSchema,
943
- ListContactsQuery: ListContactsQuerySchema,
944
- CreateContactRequest: CreateContactRequestSchema,
945
- UpdateContactRequest: UpdateContactRequestSchema,
946
- AcqContactResponse: AcqContactResponseSchema,
947
- AcqContactListResponse: AcqContactListResponseSchema
948
- }
949
-
950
- export type AcqCompanyStatus = z.infer<typeof AcqCompanyStatusSchema>
951
- export type AcqContactStatus = z.infer<typeof AcqContactStatusSchema>
952
- export type AcqEmailValid = z.infer<typeof AcqEmailValidSchema>
953
- export type CompanyIdParams = z.infer<typeof CompanyIdParamsSchema>
954
- export type ContactIdParams = z.infer<typeof ContactIdParamsSchema>
955
- export type ListCompaniesQuery = z.infer<typeof ListCompaniesQuerySchema>
956
- export type ListContactsQuery = z.infer<typeof ListContactsQuerySchema>
957
- export type CreateCompanyRequest = z.infer<typeof CreateCompanyRequestSchema>
958
- export type UpdateCompanyRequest = z.infer<typeof UpdateCompanyRequestSchema>
959
- export type CreateContactRequest = z.infer<typeof CreateContactRequestSchema>
960
- export type UpdateContactRequest = z.infer<typeof UpdateContactRequestSchema>
961
- export type AcqCompanyResponse = z.infer<typeof AcqCompanyResponseSchema>
962
- export type AcqCompanyListResponse = z.infer<typeof AcqCompanyListResponseSchema>
963
- export type AcqCompanyFacetsResponse = z.infer<typeof AcqCompanyFacetsResponseSchema>
964
- export type AcqContactCompanySummary = z.infer<typeof AcqContactCompanySummarySchema>
965
- export type AcqContactResponse = z.infer<typeof AcqContactResponseSchema>
966
- export type AcqContactListResponse = z.infer<typeof AcqContactListResponseSchema>
967
-
968
- // ---------------------------------------------------------------------------
969
- // Bundled export
970
- // ---------------------------------------------------------------------------
971
-
972
- export const AcqListSchemas = {
973
- // Params
974
- ListIdParams: ListIdParamsSchema,
975
-
976
- // Primitives (for UI / tests)
977
- ListStatus: ListStatusSchema,
978
- ScrapingConfig: ScrapingConfigSchema,
979
- IcpRubric: IcpRubricSchema,
980
- PipelineConfig: PipelineConfigSchema,
981
- PipelineStage: PipelineStageSchema,
982
- ListStageCounts: ListStageCountsSchema,
983
- ListTelemetry: ListTelemetrySchema,
984
-
985
- // Requests
986
- CreateListRequest: CreateListRequestSchema,
987
- UpdateListRequest: UpdateListRequestSchema,
988
- UpdateListStatusRequest: UpdateListStatusRequestSchema,
989
- UpdateListConfigRequest: UpdateListConfigRequestSchema,
990
- AddCompaniesToListRequest: AddCompaniesToListRequestSchema,
991
- RemoveCompaniesFromListRequest: RemoveCompaniesFromListRequestSchema,
992
- AddContactsToListRequest: AddContactsToListRequestSchema,
993
- RecordListExecutionRequest: RecordListExecutionRequestSchema,
994
-
995
- // Responses
996
- AcqListResponse: AcqListResponseSchema,
997
- AcqListListResponse: AcqListListResponseSchema,
998
- ListTelemetryResponse: ListTelemetryResponseSchema,
999
- ListTelemetryListResponse: ListTelemetryListResponseSchema,
1000
- ListExecutionsResponse: ListExecutionsResponseSchema,
1001
- ListProgressResponse: ListProgressResponseSchema
1002
- }
1003
-
1004
- // ---------------------------------------------------------------------------
1005
- // Track A/B bundled schemas
1006
- // ---------------------------------------------------------------------------
1007
-
1008
- export const AcqSubstrateSchemas = {
1009
- // Artifacts
1010
- ListArtifactsQuery: ListArtifactsQuerySchema,
1011
- CreateArtifactRequest: CreateArtifactRequestSchema,
1012
- AcqArtifactResponse: AcqArtifactResponseSchema,
1013
- AcqArtifactListResponse: AcqArtifactListResponseSchema,
1014
-
1015
- // List members
1016
- ListMembersQuery: ListMembersQuerySchema,
1017
- MemberIdParams: MemberIdParamsSchema,
1018
- AcqListMemberResponse: AcqListMemberResponseSchema,
1019
- AcqListMembersResponse: AcqListMembersResponseSchema,
1020
-
1021
- // List companies
1022
- ListCompanyIdParams: ListCompanyIdParamsSchema,
1023
- AcqListCompanyResponse: AcqListCompanyResponseSchema,
1024
-
1025
- // Transition (shared with deals — TransitionItemRequestSchema)
1026
- TransitionItemRequest: TransitionItemRequestSchema
1027
- }
1028
-
1029
- // ---------------------------------------------------------------------------
1030
- // Inferred types
1031
- // ---------------------------------------------------------------------------
1032
-
1033
- // ---------------------------------------------------------------------------
1034
- // Inferred types — Track A/B substrate
1035
- // ---------------------------------------------------------------------------
1036
-
1037
- export type AcqArtifactOwnerKind = z.infer<typeof AcqArtifactOwnerKindSchema>
1038
- export type ListArtifactsQuery = z.infer<typeof ListArtifactsQuerySchema>
1039
- export type CreateArtifactRequest = z.infer<typeof CreateArtifactRequestSchema>
1040
- export type AcqArtifactResponse = z.infer<typeof AcqArtifactResponseSchema>
1041
- export type AcqArtifactListResponse = z.infer<typeof AcqArtifactListResponseSchema>
1042
- export type ListMembersQuery = z.infer<typeof ListMembersQuerySchema>
1043
- export type MemberIdParams = z.infer<typeof MemberIdParamsSchema>
1044
- export type AcqListMemberContactSummary = z.infer<typeof AcqListMemberContactSummarySchema>
1045
- export type AcqListMemberResponse = z.infer<typeof AcqListMemberResponseSchema>
1046
- export type AcqListMembersResponse = z.infer<typeof AcqListMembersResponseSchema>
1047
- export type ListCompanyIdParams = z.infer<typeof ListCompanyIdParamsSchema>
1048
- export type AcqListCompanyResponse = z.infer<typeof AcqListCompanyResponseSchema>
1049
-
1050
- // ---------------------------------------------------------------------------
1051
-
1052
- export type ListStatus = z.infer<typeof ListStatusSchema>
1053
- export type ScrapingConfig = z.infer<typeof ScrapingConfigSchema>
1054
- export type IcpRubric = z.infer<typeof IcpRubricSchema>
1055
- export type PipelineStage = z.infer<typeof PipelineStageSchema>
1056
- export type PipelineConfig = z.infer<typeof PipelineConfigSchema>
1057
- export type ListStageCountsInput = z.infer<typeof ListStageCountsSchema>['stageCounts']
1058
- export type ListTelemetryInput = z.infer<typeof ListTelemetrySchema>
1059
- export type ListIdParams = z.infer<typeof ListIdParamsSchema>
1060
- export type CreateListRequest = z.infer<typeof CreateListRequestSchema>
1061
- export type UpdateListRequest = z.infer<typeof UpdateListRequestSchema>
1062
- export type UpdateListStatusRequest = z.infer<typeof UpdateListStatusRequestSchema>
1063
- export type UpdateListConfigRequest = z.infer<typeof UpdateListConfigRequestSchema>
1064
- export type AddCompaniesToListRequest = z.infer<typeof AddCompaniesToListRequestSchema>
1065
- export type RemoveCompaniesFromListRequest = z.infer<typeof RemoveCompaniesFromListRequestSchema>
1066
- export type AddContactsToListRequest = z.infer<typeof AddContactsToListRequestSchema>
1067
- export type RecordListExecutionRequest = z.infer<typeof RecordListExecutionRequestSchema>
1068
- export type AcqListResponse = z.infer<typeof AcqListResponseSchema>
1069
- export type AcqListListResponse = z.infer<typeof AcqListListResponseSchema>
1070
- export type ListTelemetryResponse = z.infer<typeof ListTelemetryResponseSchema>
1071
- export type ListTelemetryListResponse = z.infer<typeof ListTelemetryListResponseSchema>
1072
- export type ListExecutionSummaryInput = z.infer<typeof ListExecutionSummarySchema>
1073
- export type ListExecutionsResponse = z.infer<typeof ListExecutionsResponseSchema>
1074
- export type ListStageProgress = z.infer<typeof ListStageProgressSchema>
1075
- export type ListProgress = z.infer<typeof ListProgressResponseSchema>
1
+ import { z } from 'zod'
2
+ import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
3
+ import { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
+
5
+ /**
6
+ * Deal Management API Schemas
7
+ *
8
+ * Request/response validation for /api/deals surface.
9
+ * Used by both the API route handlers and the frontend hooks.
10
+ *
11
+ * Table mapping:
12
+ * acq_deals -> DealSchemas (list/detail)
13
+ * acq_deal_notes -> DealSchemas (note shapes)
14
+ * acq_deal_tasks -> DealSchemas (task shapes)
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Enum literals (must match DB CHECK constraints exactly)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
22
+
23
+ export const AcqDealTaskKindSchema = z.enum(['call', 'email', 'meeting', 'other'])
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Params
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export const DealIdParamsSchema = z.object({
30
+ dealId: UuidSchema
31
+ })
32
+
33
+ export const DealTaskIdParamsSchema = z.object({
34
+ dealId: UuidSchema,
35
+ taskId: UuidSchema
36
+ })
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Query schemas (coerce strings from query params)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export const ListDealsQuerySchema = z
43
+ .object({
44
+ stage: DealStageSchema.optional(),
45
+ search: z.string().optional(),
46
+ limit: z.coerce.number().int().positive().default(50),
47
+ offset: z.coerce.number().int().min(0).default(0)
48
+ })
49
+ .strict()
50
+
51
+ export const DealLookupQuerySchema = z
52
+ .object({
53
+ search: z.string().trim().min(1).max(200).optional(),
54
+ limit: z.coerce.number().int().min(1).max(25).default(10)
55
+ })
56
+ .strict()
57
+
58
+ export const ListDealTasksDueQuerySchema = z
59
+ .object({
60
+ window: z.enum(['overdue', 'today', 'today_and_overdue', 'upcoming']).optional(),
61
+ assigneeUserId: UuidSchema.optional()
62
+ })
63
+ .strict()
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Request body schemas (all use .strict() — rejects unknown fields)
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export const CreateDealNoteRequestSchema = z
70
+ .object({
71
+ body: z.string().trim().min(1).max(10000)
72
+ })
73
+ .strict()
74
+
75
+ export const CreateDealTaskRequestSchema = z
76
+ .object({
77
+ title: z.string().trim().min(1).max(255),
78
+ description: z.string().nullable().optional(),
79
+ kind: AcqDealTaskKindSchema.optional(),
80
+ dueAt: z.string().datetime().nullable().optional(),
81
+ assigneeUserId: UuidSchema.nullable().optional()
82
+ })
83
+ .strict()
84
+
85
+ export const TransitionItemRequestSchema = z
86
+ .object({
87
+ pipelineKey: z.string().min(1),
88
+ stageKey: z.string().min(1),
89
+ stateKey: z.string().nullable().optional(),
90
+ reason: z.string().optional(),
91
+ expectedUpdatedAt: z.string().datetime().optional()
92
+ })
93
+ .strict()
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
+
116
+ // ---------------------------------------------------------------------------
117
+ // Response schemas (no .strict() — allows forward-compatible additions)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Contact summary nested inside DealListItem / DealDetailResponse.
122
+ * Matches the joined shape returned by useDeals / useDealDetail Supabase queries.
123
+ */
124
+ export const DealContactSummarySchema = z.object({
125
+ id: z.string(),
126
+ first_name: z.string().nullable(),
127
+ last_name: z.string().nullable(),
128
+ email: z.string(),
129
+ title: z.string().nullable(),
130
+ headline: z.string().nullable(),
131
+ linkedin_url: z.string().nullable(),
132
+ pipeline_status: z.record(z.string(), z.unknown()).nullable(),
133
+ enrichment_data: z.record(z.string(), z.unknown()).nullable(),
134
+ company: z
135
+ .object({
136
+ id: z.string(),
137
+ name: z.string(),
138
+ domain: z.string().nullable(),
139
+ website: z.string().nullable(),
140
+ linkedin_url: z.string().nullable(),
141
+ segment: z.string().nullable(),
142
+ category: z.string().nullable(),
143
+ num_employees: z.number().nullable()
144
+ })
145
+ .nullable()
146
+ })
147
+
148
+ /**
149
+ * Deal list item with joined contact (and company via contact).
150
+ * Matches DealListItem from @repo/core types.
151
+ */
152
+ export const DealListItemSchema = z.object({
153
+ // acq_deals columns
154
+ id: z.string(),
155
+ organization_id: z.string(),
156
+ contact_id: z.string().nullable(),
157
+ contact_email: z.string(),
158
+ pipeline_key: z.string(),
159
+ stage_key: z.string().nullable(),
160
+ state_key: z.string().nullable(),
161
+ activity_log: z.unknown(),
162
+ discovery_data: z.unknown().nullable(),
163
+ discovery_submitted_at: z.string().nullable(),
164
+ discovery_submitted_by: z.string().nullable(),
165
+ proposal_data: z.unknown().nullable(),
166
+ proposal_sent_at: z.string().nullable(),
167
+ proposal_pdf_url: z.string().nullable(),
168
+ signature_envelope_id: z.string().nullable(),
169
+ source_list_id: z.string().nullable(),
170
+ source_type: z.string().nullable(),
171
+ initial_fee: z.number().nullable(),
172
+ monthly_fee: z.number().nullable(),
173
+ closed_lost_at: z.string().nullable(),
174
+ closed_lost_reason: z.string().nullable(),
175
+ created_at: z.string(),
176
+ updated_at: z.string(),
177
+ // joined relation
178
+ contact: DealContactSummarySchema.nullable()
179
+ })
180
+
181
+ export const DealListResponseSchema = z.object({
182
+ data: z.array(DealListItemSchema),
183
+ total: z.number().int(),
184
+ limit: z.number().int(),
185
+ offset: z.number().int()
186
+ })
187
+
188
+ export const DealStageSummarySchema = z.object({
189
+ stage: z.string(),
190
+ count: z.number().int(),
191
+ totalValue: z.number(),
192
+ oldestUpdatedAt: z.string().nullable(),
193
+ newestUpdatedAt: z.string().nullable()
194
+ })
195
+
196
+ export const StaleDealSummarySchema = z.object({
197
+ id: z.string(),
198
+ contactEmail: z.string(),
199
+ stageKey: z.string(),
200
+ updatedAt: z.string(),
201
+ daysStale: z.number().int()
202
+ })
203
+
204
+ export const DealSummaryResponseSchema = z.object({
205
+ totalDeals: z.number().int(),
206
+ openDeals: z.number().int(),
207
+ wonDeals: z.number().int(),
208
+ lostDeals: z.number().int(),
209
+ winRate: z.number(),
210
+ avgDealSize: z.number(),
211
+ totalPipelineValue: z.number(),
212
+ stageSummary: z.array(DealStageSummarySchema),
213
+ staleDeals: z.array(StaleDealSummarySchema)
214
+ })
215
+
216
+ export const DealLookupItemSchema = z.object({
217
+ id: z.string(),
218
+ contactEmail: z.string(),
219
+ stageKey: z.string().nullable(),
220
+ updatedAt: z.string(),
221
+ contactName: z.string().nullable(),
222
+ companyName: z.string().nullable(),
223
+ displayLabel: z.string()
224
+ })
225
+
226
+ export const DealLookupResponseSchema = z.array(DealLookupItemSchema)
227
+
228
+ /**
229
+ * Deal detail shape — currently the same as a list item (full joined record).
230
+ * useDealDetail returns DealDetail which is typed as DealListItem.
231
+ */
232
+ export const DealDetailResponseSchema = DealListItemSchema
233
+
234
+ /**
235
+ * Single acq_deal_notes row (camelCase API representation).
236
+ */
237
+ export const DealNoteResponseSchema = z.object({
238
+ id: z.string(),
239
+ dealId: z.string(),
240
+ organizationId: z.string(),
241
+ authorUserId: z.string().nullable(),
242
+ body: z.string(),
243
+ createdAt: z.string(),
244
+ updatedAt: z.string()
245
+ })
246
+
247
+ export const DealNoteListResponseSchema = z.array(DealNoteResponseSchema)
248
+
249
+ /**
250
+ * Single acq_deal_tasks row (camelCase API representation).
251
+ * Matches AcqDealTask domain type from types.ts.
252
+ */
253
+ export const DealTaskResponseSchema = z.object({
254
+ id: z.string(),
255
+ organizationId: z.string(),
256
+ dealId: z.string(),
257
+ title: z.string(),
258
+ description: z.string().nullable(),
259
+ kind: AcqDealTaskKindSchema,
260
+ dueAt: z.string().nullable(),
261
+ assigneeUserId: z.string().nullable(),
262
+ completedAt: z.string().nullable(),
263
+ completedByUserId: z.string().nullable(),
264
+ createdAt: z.string(),
265
+ updatedAt: z.string(),
266
+ createdByUserId: z.string().nullable()
267
+ })
268
+
269
+ export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Bundled export
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export const DealSchemas = {
276
+ // Params
277
+ DealIdParams: DealIdParamsSchema,
278
+ DealTaskIdParams: DealTaskIdParamsSchema,
279
+
280
+ // Queries
281
+ ListDealsQuery: ListDealsQuerySchema,
282
+ DealLookupQuery: DealLookupQuerySchema,
283
+ ListDealTasksDueQuery: ListDealTasksDueQuerySchema,
284
+
285
+ // Request bodies
286
+ CreateDealNoteRequest: CreateDealNoteRequestSchema,
287
+ CreateDealTaskRequest: CreateDealTaskRequestSchema,
288
+ TransitionItemRequest: TransitionItemRequestSchema,
289
+ TransitionDealStateRequest: TransitionDealStateRequestSchema,
290
+ ExecuteActionParams: ExecuteActionParamsSchema,
291
+ ExecuteActionRequest: ExecuteActionRequestSchema,
292
+
293
+ // Responses
294
+ DealListResponse: DealListResponseSchema,
295
+ DealSummaryResponse: DealSummaryResponseSchema,
296
+ DealLookupResponse: DealLookupResponseSchema,
297
+ DealDetailResponse: DealDetailResponseSchema,
298
+ DealNoteResponse: DealNoteResponseSchema,
299
+ DealNoteListResponse: DealNoteListResponseSchema,
300
+ DealTaskResponse: DealTaskResponseSchema,
301
+ DealTaskListResponse: DealTaskListResponseSchema
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Inferred types
306
+ // ---------------------------------------------------------------------------
307
+
308
+ export type DealStage = z.infer<typeof DealStageSchema>
309
+ export type AcqDealTaskKind = z.infer<typeof AcqDealTaskKindSchema>
310
+ export type DealIdParams = z.infer<typeof DealIdParamsSchema>
311
+ export type DealTaskIdParams = z.infer<typeof DealTaskIdParamsSchema>
312
+ export type ListDealsQuery = z.infer<typeof ListDealsQuerySchema>
313
+ export type DealLookupQuery = z.infer<typeof DealLookupQuerySchema>
314
+ export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
315
+ export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
316
+ export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
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>
321
+ export type DealListResponse = z.infer<typeof DealListResponseSchema>
322
+ export type DealSummaryResponse = z.infer<typeof DealSummaryResponseSchema>
323
+ export type DealLookupItem = z.infer<typeof DealLookupItemSchema>
324
+ export type DealLookupResponse = z.infer<typeof DealLookupResponseSchema>
325
+ export type DealDetailResponse = z.infer<typeof DealDetailResponseSchema>
326
+ export type DealNoteResponse = z.infer<typeof DealNoteResponseSchema>
327
+ export type DealNoteListResponse = z.infer<typeof DealNoteListResponseSchema>
328
+ export type DealTaskResponse = z.infer<typeof DealTaskResponseSchema>
329
+ export type DealTaskListResponse = z.infer<typeof DealTaskListResponseSchema>
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // List Management API Schemas
333
+ //
334
+ // Request/response validation for /api/acquisition/lists surface.
335
+ // Used by both the API route handlers and the frontend hooks.
336
+ //
337
+ // Table mapping:
338
+ // acq_lists -> AcqListSchemas (list/detail/progress)
339
+ // acq_list_companies -> AcqListSchemas (add/remove company membership)
340
+ // acq_list_contacts -> AcqListSchemas (add/remove contact membership)
341
+ // acq_list_executions -> AcqListSchemas (execution history)
342
+ // ---------------------------------------------------------------------------
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Primitives list status enum + jsonb config schemas
346
+ // ---------------------------------------------------------------------------
347
+
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'])
353
+
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()
364
+ })
365
+
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()
378
+ })
379
+
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()
392
+ })
393
+
394
+ /**
395
+ * Pipeline presentation contract stored in `acq_lists.pipeline_config` jsonb.
396
+ * `stages[].key` validates against the catalog; the rest is presentation only.
397
+ */
398
+ export const PipelineConfigSchema = z.object({
399
+ stages: z.array(PipelineStageSchema).optional()
400
+ })
401
+
402
+ // ---------------------------------------------------------------------------
403
+ // List telemetry / progress schemas
404
+ // ---------------------------------------------------------------------------
405
+
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.
409
+ stageCounts: z.object({
410
+ populated: z.number().int(),
411
+ extracted: z.number().int(),
412
+ qualified: z.number().int(),
413
+ discovered: z.number().int(),
414
+ verified: z.number().int(),
415
+ personalized: z.number().int(),
416
+ uploaded: z.number().int()
417
+ }),
418
+ deliverability: z.object({
419
+ valid: z.number().int(),
420
+ risky: z.number().int(),
421
+ invalid: z.number().int(),
422
+ unknown: z.number().int(),
423
+ bounced: z.number().int()
424
+ })
425
+ })
426
+
427
+ export const ListTelemetrySchema = z.object({
428
+ listId: UuidSchema,
429
+ totalCompanies: z.number().int(),
430
+ totalContacts: z.number().int(),
431
+ stageCounts: ListStageCountsSchema.shape.stageCounts,
432
+ deliverability: ListStageCountsSchema.shape.deliverability,
433
+ activeWorkflows: z.array(z.string()).optional()
434
+ })
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // Params
438
+ // ---------------------------------------------------------------------------
439
+
440
+ export const ListIdParamsSchema = z.object({
441
+ listId: UuidSchema
442
+ })
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // Request body schemas (all use .strict() — rejects unknown fields)
446
+ // ---------------------------------------------------------------------------
447
+
448
+ export const CreateListRequestSchema = z
449
+ .object({
450
+ name: z.string().trim().min(1).max(255),
451
+ description: z.string().trim().nullable().optional(),
452
+ status: ListStatusSchema.optional(),
453
+ scrapingConfig: ScrapingConfigSchema.optional(),
454
+ icp: IcpRubricSchema.optional(),
455
+ pipelineConfig: PipelineConfigSchema.optional()
456
+ })
457
+ .strict()
458
+
459
+ export const UpdateListRequestSchema = z
460
+ .object({
461
+ name: z.string().trim().min(1).max(255).optional(),
462
+ description: z.string().trim().nullable().optional(),
463
+ batchIds: z.array(z.string()).optional()
464
+ })
465
+ .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'
468
+ })
469
+
470
+ /**
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).
484
+ */
485
+ export const UpdateListConfigRequestSchema = z
486
+ .object({
487
+ scrapingConfig: ScrapingConfigSchema.partial().optional(),
488
+ icp: IcpRubricSchema.partial().optional(),
489
+ pipelineConfig: PipelineConfigSchema.partial().optional()
490
+ })
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
+ })
495
+
496
+ export const AddCompaniesToListRequestSchema = z
497
+ .object({
498
+ companyIds: z.array(UuidSchema).min(1).max(1000)
499
+ })
500
+ .strict()
501
+
502
+ export const RemoveCompaniesFromListRequestSchema = z
503
+ .object({
504
+ companyIds: z.array(UuidSchema).min(1).max(1000)
505
+ })
506
+ .strict()
507
+
508
+ export const AddContactsToListRequestSchema = z
509
+ .object({
510
+ contactIds: z.array(UuidSchema).min(1).max(1000)
511
+ })
512
+ .strict()
513
+
514
+ export const RecordListExecutionRequestSchema = z
515
+ .object({
516
+ executionId: UuidSchema,
517
+ configSnapshot: z.record(z.string(), z.unknown()).optional()
518
+ })
519
+ .strict()
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Response schemas (no .strict() — allows forward-compatible additions)
523
+ // ---------------------------------------------------------------------------
524
+
525
+ /**
526
+ * Single list as returned by /api/acquisition/lists/:id etc.
527
+ * Camel-cased domain shape matching AcqList in types.ts.
528
+ */
529
+ export const AcqListResponseSchema = z.object({
530
+ id: z.string(),
531
+ organizationId: z.string(),
532
+ name: z.string(),
533
+ description: z.string().nullable(),
534
+ type: z.string(),
535
+ batchIds: z.array(z.string()),
536
+ instantlyCampaignId: z.string().nullable(),
537
+ /** Lifecycle status (draft | enriching | launched | closing | archived). */
538
+ status: ListStatusSchema,
539
+ metadata: z.record(z.string(), z.unknown()),
540
+ launchedAt: z.string().nullable(),
541
+ completedAt: z.string().nullable(),
542
+ createdAt: z.string(),
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
549
+ })
550
+
551
+ export const AcqListListResponseSchema = z.array(AcqListResponseSchema)
552
+
553
+ export const ListTelemetryResponseSchema = ListTelemetrySchema
554
+
555
+ export const ListTelemetryListResponseSchema = z.array(ListTelemetrySchema)
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
+
593
+ /**
594
+ * Row from acq_list_executions joined with the execution summary,
595
+ * shaped for the /lists/:id/executions response.
596
+ */
597
+ export const ListExecutionSummarySchema = z.object({
598
+ executionId: z.string(),
599
+ resourceId: z.string(),
600
+ status: z.string(),
601
+ createdAt: z.string(),
602
+ completedAt: z.string().nullable(),
603
+ durationMs: z.number().int().nullable()
604
+ })
605
+
606
+ export const ListExecutionsResponseSchema = z.array(ListExecutionSummarySchema)
607
+
608
+ // ---------------------------------------------------------------------------
609
+ // Company / Contact API Schemas
610
+ // ---------------------------------------------------------------------------
611
+
612
+ const QueryBooleanSchema = z.preprocess((value) => {
613
+ if (value === 'true' || value === '1' || value === true) return true
614
+ if (value === 'false' || value === '0' || value === false) return false
615
+ return value
616
+ }, z.boolean())
617
+
618
+ export const AcqCompanyStatusSchema = z.enum(['active', 'invalid'])
619
+ export const AcqContactStatusSchema = z.enum(['active', 'invalid'])
620
+ export const AcqEmailValidSchema = z.enum(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])
621
+
622
+ export const CompanyIdParamsSchema = z.object({
623
+ companyId: UuidSchema
624
+ })
625
+
626
+ export const ContactIdParamsSchema = z.object({
627
+ contactId: UuidSchema
628
+ })
629
+
630
+ export const ListCompaniesQuerySchema = z
631
+ .object({
632
+ search: z.string().trim().min(1).max(200).optional(),
633
+ listId: UuidSchema.optional(),
634
+ domain: z.string().trim().min(1).max(255).optional(),
635
+ website: z.string().trim().min(1).max(2048).optional(),
636
+ segment: z.string().trim().min(1).max(255).optional(),
637
+ category: z.string().trim().min(1).max(255).optional(),
638
+ batchId: z.string().trim().min(1).max(255).optional(),
639
+ status: AcqCompanyStatusSchema.optional(),
640
+ includeAll: QueryBooleanSchema.optional(),
641
+ limit: z.coerce.number().int().min(1).max(5000).default(50),
642
+ offset: z.coerce.number().int().min(0).default(0)
643
+ })
644
+ .strict()
645
+
646
+ export const ListContactsQuerySchema = z
647
+ .object({
648
+ search: z.string().trim().min(1).max(200).optional(),
649
+ listId: UuidSchema.optional(),
650
+ openingLineIsNull: QueryBooleanSchema.optional(),
651
+ batchId: z.string().trim().min(1).max(255).optional(),
652
+ contactStatus: AcqContactStatusSchema.optional(),
653
+ limit: z.coerce.number().int().min(1).max(5000).default(5000),
654
+ offset: z.coerce.number().int().min(0).default(0)
655
+ })
656
+ .strict()
657
+
658
+ export const CreateCompanyRequestSchema = z
659
+ .object({
660
+ name: z.string().trim().min(1).max(255),
661
+ domain: z.string().trim().min(1).max(255).optional(),
662
+ linkedinUrl: z.string().trim().url().optional(),
663
+ website: z.string().trim().url().optional(),
664
+ numEmployees: z.number().int().min(0).optional(),
665
+ foundedYear: z.number().int().optional(),
666
+ locationCity: z.string().trim().min(1).max(255).optional(),
667
+ locationState: z.string().trim().min(1).max(255).optional(),
668
+ category: z.string().trim().min(1).max(255).optional(),
669
+ source: z.string().trim().min(1).max(255).optional(),
670
+ batchId: z.string().trim().min(1).max(255).optional(),
671
+ verticalResearch: z.string().trim().min(1).max(5000).optional()
672
+ })
673
+ .strict()
674
+
675
+ export const UpdateCompanyRequestSchema = z
676
+ .object({
677
+ name: z.string().trim().min(1).max(255).optional(),
678
+ domain: z.string().trim().min(1).max(255).optional(),
679
+ linkedinUrl: z.string().trim().url().optional(),
680
+ website: z.string().trim().url().optional(),
681
+ numEmployees: z.number().int().min(0).optional(),
682
+ foundedYear: z.number().int().optional(),
683
+ locationCity: z.string().trim().min(1).max(255).optional(),
684
+ locationState: z.string().trim().min(1).max(255).optional(),
685
+ category: z.string().trim().min(1).max(255).optional(),
686
+ segment: z.string().trim().min(1).max(255).optional(),
687
+ pipelineStatus: z.record(z.string(), z.unknown()).optional(),
688
+ enrichmentData: z.record(z.string(), z.unknown()).optional(),
689
+ source: z.string().trim().min(1).max(255).optional(),
690
+ batchId: z.string().trim().min(1).max(255).optional(),
691
+ status: AcqCompanyStatusSchema.optional(),
692
+ verticalResearch: z.string().trim().min(1).max(5000).nullable().optional()
693
+ })
694
+ .strict()
695
+ .refine(
696
+ (data) =>
697
+ data.name !== undefined ||
698
+ data.domain !== undefined ||
699
+ data.linkedinUrl !== undefined ||
700
+ data.website !== undefined ||
701
+ data.numEmployees !== undefined ||
702
+ data.foundedYear !== undefined ||
703
+ data.locationCity !== undefined ||
704
+ data.locationState !== undefined ||
705
+ data.category !== undefined ||
706
+ data.segment !== undefined ||
707
+ data.pipelineStatus !== undefined ||
708
+ data.enrichmentData !== undefined ||
709
+ data.source !== undefined ||
710
+ data.batchId !== undefined ||
711
+ data.status !== undefined ||
712
+ data.verticalResearch !== undefined,
713
+ {
714
+ message: 'At least one field must be provided'
715
+ }
716
+ )
717
+
718
+ export const CreateContactRequestSchema = z
719
+ .object({
720
+ email: z.string().trim().email(),
721
+ companyId: UuidSchema.optional(),
722
+ firstName: z.string().trim().min(1).max(255).optional(),
723
+ lastName: z.string().trim().min(1).max(255).optional(),
724
+ linkedinUrl: z.string().trim().url().optional(),
725
+ title: z.string().trim().min(1).max(255).optional(),
726
+ source: z.string().trim().min(1).max(255).optional(),
727
+ sourceId: z.string().trim().min(1).max(255).optional(),
728
+ batchId: z.string().trim().min(1).max(255).optional()
729
+ })
730
+ .strict()
731
+
732
+ export const UpdateContactRequestSchema = z
733
+ .object({
734
+ companyId: UuidSchema.optional(),
735
+ emailValid: AcqEmailValidSchema.optional(),
736
+ firstName: z.string().trim().min(1).max(255).optional(),
737
+ lastName: z.string().trim().min(1).max(255).optional(),
738
+ linkedinUrl: z.string().trim().url().optional(),
739
+ title: z.string().trim().min(1).max(255).optional(),
740
+ headline: z.string().trim().min(1).max(5000).optional(),
741
+ filterReason: z.string().trim().min(1).max(5000).optional(),
742
+ openingLine: z.string().trim().min(1).max(5000).optional(),
743
+ pipelineStatus: z.record(z.string(), z.unknown()).optional(),
744
+ enrichmentData: z.record(z.string(), z.unknown()).optional(),
745
+ status: AcqContactStatusSchema.optional()
746
+ })
747
+ .strict()
748
+ .refine(
749
+ (data) =>
750
+ data.companyId !== undefined ||
751
+ data.emailValid !== undefined ||
752
+ data.firstName !== undefined ||
753
+ data.lastName !== undefined ||
754
+ data.linkedinUrl !== undefined ||
755
+ data.title !== undefined ||
756
+ data.headline !== undefined ||
757
+ data.filterReason !== undefined ||
758
+ data.openingLine !== undefined ||
759
+ data.pipelineStatus !== undefined ||
760
+ data.enrichmentData !== undefined ||
761
+ data.status !== undefined,
762
+ {
763
+ message: 'At least one field must be provided'
764
+ }
765
+ )
766
+
767
+ export const AcqCompanyResponseSchema = z.object({
768
+ id: z.string(),
769
+ organizationId: z.string(),
770
+ name: z.string(),
771
+ domain: z.string().nullable(),
772
+ linkedinUrl: z.string().nullable(),
773
+ website: z.string().nullable(),
774
+ numEmployees: z.number().nullable(),
775
+ foundedYear: z.number().nullable(),
776
+ locationCity: z.string().nullable(),
777
+ locationState: z.string().nullable(),
778
+ category: z.string().nullable(),
779
+ categoryPain: z.string().nullable(),
780
+ segment: z.string().nullable(),
781
+ pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
782
+ enrichmentData: z.record(z.string(), z.unknown()).nullable(),
783
+ source: z.string().nullable(),
784
+ batchId: z.string().nullable(),
785
+ status: AcqCompanyStatusSchema,
786
+ contactCount: z.number().int().min(0),
787
+ verticalResearch: z.string().nullable(),
788
+ createdAt: z.string(),
789
+ updatedAt: z.string()
790
+ })
791
+
792
+ export const AcqCompanyListResponseSchema = z.object({
793
+ data: z.array(AcqCompanyResponseSchema),
794
+ total: z.number().int(),
795
+ limit: z.number().int(),
796
+ offset: z.number().int()
797
+ })
798
+
799
+ export const AcqCompanyFacetsResponseSchema = z.object({
800
+ segments: z.array(z.string()),
801
+ categories: z.array(z.string()),
802
+ statuses: z.array(AcqCompanyStatusSchema)
803
+ })
804
+
805
+ export const AcqContactCompanySummarySchema = z.object({
806
+ id: z.string(),
807
+ name: z.string(),
808
+ domain: z.string().nullable(),
809
+ website: z.string().nullable(),
810
+ linkedinUrl: z.string().nullable(),
811
+ segment: z.string().nullable(),
812
+ category: z.string().nullable(),
813
+ status: AcqCompanyStatusSchema
814
+ })
815
+
816
+ export const AcqContactResponseSchema = z.object({
817
+ id: z.string(),
818
+ organizationId: z.string(),
819
+ companyId: z.string().nullable(),
820
+ email: z.string(),
821
+ emailValid: AcqEmailValidSchema.nullable(),
822
+ firstName: z.string().nullable(),
823
+ lastName: z.string().nullable(),
824
+ linkedinUrl: z.string().nullable(),
825
+ title: z.string().nullable(),
826
+ headline: z.string().nullable(),
827
+ filterReason: z.string().nullable(),
828
+ openingLine: z.string().nullable(),
829
+ source: z.string().nullable(),
830
+ sourceId: z.string().nullable(),
831
+ pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
832
+ enrichmentData: z.record(z.string(), z.unknown()).nullable(),
833
+ attioPersonId: z.string().nullable(),
834
+ batchId: z.string().nullable(),
835
+ status: AcqContactStatusSchema,
836
+ company: AcqContactCompanySummarySchema.nullable().optional(),
837
+ createdAt: z.string(),
838
+ updatedAt: z.string()
839
+ })
840
+
841
+ export const AcqContactListResponseSchema = z.object({
842
+ data: z.array(AcqContactResponseSchema),
843
+ total: z.number().int(),
844
+ limit: z.number().int(),
845
+ offset: z.number().int()
846
+ })
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
+
957
+ export const AcqCompanySchemas = {
958
+ CompanyIdParams: CompanyIdParamsSchema,
959
+ ListCompaniesQuery: ListCompaniesQuerySchema,
960
+ CreateCompanyRequest: CreateCompanyRequestSchema,
961
+ UpdateCompanyRequest: UpdateCompanyRequestSchema,
962
+ AcqCompanyResponse: AcqCompanyResponseSchema,
963
+ AcqCompanyListResponse: AcqCompanyListResponseSchema,
964
+ AcqCompanyFacetsResponse: AcqCompanyFacetsResponseSchema
965
+ }
966
+
967
+ export const AcqContactSchemas = {
968
+ ContactIdParams: ContactIdParamsSchema,
969
+ ListContactsQuery: ListContactsQuerySchema,
970
+ CreateContactRequest: CreateContactRequestSchema,
971
+ UpdateContactRequest: UpdateContactRequestSchema,
972
+ AcqContactResponse: AcqContactResponseSchema,
973
+ AcqContactListResponse: AcqContactListResponseSchema
974
+ }
975
+
976
+ export type AcqCompanyStatus = z.infer<typeof AcqCompanyStatusSchema>
977
+ export type AcqContactStatus = z.infer<typeof AcqContactStatusSchema>
978
+ export type AcqEmailValid = z.infer<typeof AcqEmailValidSchema>
979
+ export type CompanyIdParams = z.infer<typeof CompanyIdParamsSchema>
980
+ export type ContactIdParams = z.infer<typeof ContactIdParamsSchema>
981
+ export type ListCompaniesQuery = z.infer<typeof ListCompaniesQuerySchema>
982
+ export type ListContactsQuery = z.infer<typeof ListContactsQuerySchema>
983
+ export type CreateCompanyRequest = z.infer<typeof CreateCompanyRequestSchema>
984
+ export type UpdateCompanyRequest = z.infer<typeof UpdateCompanyRequestSchema>
985
+ export type CreateContactRequest = z.infer<typeof CreateContactRequestSchema>
986
+ export type UpdateContactRequest = z.infer<typeof UpdateContactRequestSchema>
987
+ export type AcqCompanyResponse = z.infer<typeof AcqCompanyResponseSchema>
988
+ export type AcqCompanyListResponse = z.infer<typeof AcqCompanyListResponseSchema>
989
+ export type AcqCompanyFacetsResponse = z.infer<typeof AcqCompanyFacetsResponseSchema>
990
+ export type AcqContactCompanySummary = z.infer<typeof AcqContactCompanySummarySchema>
991
+ export type AcqContactResponse = z.infer<typeof AcqContactResponseSchema>
992
+ export type AcqContactListResponse = z.infer<typeof AcqContactListResponseSchema>
993
+
994
+ // ---------------------------------------------------------------------------
995
+ // Bundled export
996
+ // ---------------------------------------------------------------------------
997
+
998
+ export const AcqListSchemas = {
999
+ // Params
1000
+ ListIdParams: ListIdParamsSchema,
1001
+
1002
+ // Primitives (for UI / tests)
1003
+ ListStatus: ListStatusSchema,
1004
+ ScrapingConfig: ScrapingConfigSchema,
1005
+ IcpRubric: IcpRubricSchema,
1006
+ PipelineConfig: PipelineConfigSchema,
1007
+ PipelineStage: PipelineStageSchema,
1008
+ ProcessingStageStatus: ProcessingStageStatusSchema,
1009
+ ListStageCounts: ListStageCountsSchema,
1010
+ ListTelemetry: ListTelemetrySchema,
1011
+
1012
+ // Requests
1013
+ CreateListRequest: CreateListRequestSchema,
1014
+ UpdateListRequest: UpdateListRequestSchema,
1015
+ UpdateListStatusRequest: UpdateListStatusRequestSchema,
1016
+ UpdateListConfigRequest: UpdateListConfigRequestSchema,
1017
+ AddCompaniesToListRequest: AddCompaniesToListRequestSchema,
1018
+ RemoveCompaniesFromListRequest: RemoveCompaniesFromListRequestSchema,
1019
+ AddContactsToListRequest: AddContactsToListRequestSchema,
1020
+ RecordListExecutionRequest: RecordListExecutionRequestSchema,
1021
+
1022
+ // Responses
1023
+ AcqListResponse: AcqListResponseSchema,
1024
+ AcqListListResponse: AcqListListResponseSchema,
1025
+ ListTelemetryResponse: ListTelemetryResponseSchema,
1026
+ ListTelemetryListResponse: ListTelemetryListResponseSchema,
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
1054
+ }
1055
+
1056
+ // ---------------------------------------------------------------------------
1057
+ // Inferred types
1058
+ // ---------------------------------------------------------------------------
1059
+
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>
1085
+ export type ListStageCountsInput = z.infer<typeof ListStageCountsSchema>['stageCounts']
1086
+ export type ListTelemetryInput = z.infer<typeof ListTelemetrySchema>
1087
+ export type ListIdParams = z.infer<typeof ListIdParamsSchema>
1088
+ export type CreateListRequest = z.infer<typeof CreateListRequestSchema>
1089
+ export type UpdateListRequest = z.infer<typeof UpdateListRequestSchema>
1090
+ export type UpdateListStatusRequest = z.infer<typeof UpdateListStatusRequestSchema>
1091
+ export type UpdateListConfigRequest = z.infer<typeof UpdateListConfigRequestSchema>
1092
+ export type AddCompaniesToListRequest = z.infer<typeof AddCompaniesToListRequestSchema>
1093
+ export type RemoveCompaniesFromListRequest = z.infer<typeof RemoveCompaniesFromListRequestSchema>
1094
+ export type AddContactsToListRequest = z.infer<typeof AddContactsToListRequestSchema>
1095
+ export type RecordListExecutionRequest = z.infer<typeof RecordListExecutionRequestSchema>
1096
+ export type AcqListResponse = z.infer<typeof AcqListResponseSchema>
1097
+ export type AcqListListResponse = z.infer<typeof AcqListListResponseSchema>
1098
+ export type ListTelemetryResponse = z.infer<typeof ListTelemetryResponseSchema>
1099
+ export type ListTelemetryListResponse = z.infer<typeof ListTelemetryListResponseSchema>
1100
+ export type ListExecutionSummaryInput = z.infer<typeof ListExecutionSummarySchema>
1101
+ export type ListExecutionsResponse = z.infer<typeof ListExecutionsResponseSchema>
1102
+ export type ListStageProgress = z.infer<typeof ListStageProgressSchema>
1103
+ export type ListProgress = z.infer<typeof ListProgressResponseSchema>