@elevasis/core 0.13.0 → 0.14.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 (41) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +9 -2
  3. package/dist/organization-model/index.d.ts +1 -1
  4. package/dist/organization-model/index.js +9 -2
  5. package/dist/test-utils/index.d.ts +463 -377
  6. package/dist/test-utils/index.js +9 -2
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2324 -0
  9. package/src/business/acquisition/activity-events.test.ts +250 -0
  10. package/src/business/acquisition/activity-events.ts +7 -65
  11. package/src/business/acquisition/api-schemas.test.ts +1180 -0
  12. package/src/business/acquisition/api-schemas.ts +1075 -859
  13. package/src/business/acquisition/crm-state-actions.test.ts +160 -0
  14. package/src/business/acquisition/derive-actions.test.ts +518 -0
  15. package/src/business/acquisition/derive-actions.ts +103 -90
  16. package/src/business/acquisition/index.ts +149 -111
  17. package/src/business/acquisition/stateful.ts +30 -0
  18. package/src/business/acquisition/types.ts +44 -77
  19. package/src/execution/engine/index.ts +437 -434
  20. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -360
  21. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
  22. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
  23. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
  24. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
  25. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
  26. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
  27. package/src/execution/engine/tools/integration/service.test.ts +34 -9
  28. package/src/execution/engine/tools/integration/service.ts +6 -3
  29. package/src/execution/engine/tools/lead-service-types.ts +945 -888
  30. package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
  31. package/src/execution/engine/tools/registry.ts +701 -699
  32. package/src/execution/engine/tools/tool-maps.ts +816 -791
  33. package/src/execution/engine/workflow/types.ts +11 -0
  34. package/src/organization-model/contracts.ts +4 -4
  35. package/src/organization-model/domains/navigation.ts +62 -62
  36. package/src/organization-model/domains/sales.ts +272 -0
  37. package/src/organization-model/published.ts +21 -21
  38. package/src/organization-model/resolve.ts +21 -8
  39. package/src/platform/constants/versions.ts +1 -1
  40. package/src/reference/_generated/contracts.md +2324 -0
  41. package/src/supabase/database.types.ts +2958 -2886
@@ -1,859 +1,1075 @@
1
- import { z } from 'zod'
2
- import { UuidSchema } from '../../platform/utils/validation'
3
-
4
- /**
5
- * Deal Management API Schemas
6
- *
7
- * Request/response validation for /api/deals surface.
8
- * Used by both the API route handlers and the frontend hooks.
9
- *
10
- * Table mapping:
11
- * acq_deals -> DealSchemas (list/detail)
12
- * acq_deal_notes -> DealSchemas (note shapes)
13
- * acq_deal_tasks -> DealSchemas (task shapes)
14
- */
15
-
16
- // ---------------------------------------------------------------------------
17
- // Enum literals (must match DB CHECK constraints exactly)
18
- // ---------------------------------------------------------------------------
19
-
20
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
21
-
22
- export const AcqDealTaskKindSchema = z.enum(['call', 'email', 'meeting', 'other'])
23
-
24
- // ---------------------------------------------------------------------------
25
- // Params
26
- // ---------------------------------------------------------------------------
27
-
28
- export const DealIdParamsSchema = z.object({
29
- dealId: UuidSchema
30
- })
31
-
32
- export const DealTaskIdParamsSchema = z.object({
33
- dealId: UuidSchema,
34
- taskId: UuidSchema
35
- })
36
-
37
- // ---------------------------------------------------------------------------
38
- // Query schemas (coerce strings from query params)
39
- // ---------------------------------------------------------------------------
40
-
41
- export const ListDealsQuerySchema = z
42
- .object({
43
- stage: DealStageSchema.optional(),
44
- search: z.string().optional(),
45
- limit: z.coerce.number().int().positive().default(50),
46
- offset: z.coerce.number().int().min(0).default(0)
47
- })
48
- .strict()
49
-
50
- export const DealLookupQuerySchema = z
51
- .object({
52
- search: z.string().trim().min(1).max(200).optional(),
53
- limit: z.coerce.number().int().min(1).max(25).default(10)
54
- })
55
- .strict()
56
-
57
- export const ListDealTasksDueQuerySchema = z
58
- .object({
59
- window: z.enum(['overdue', 'today', 'today_and_overdue', 'upcoming']).optional(),
60
- assigneeUserId: UuidSchema.optional()
61
- })
62
- .strict()
63
-
64
- // ---------------------------------------------------------------------------
65
- // Request body schemas (all use .strict() — rejects unknown fields)
66
- // ---------------------------------------------------------------------------
67
-
68
- export const CreateDealNoteRequestSchema = z
69
- .object({
70
- body: z.string().trim().min(1).max(10000)
71
- })
72
- .strict()
73
-
74
- export const CreateDealTaskRequestSchema = z
75
- .object({
76
- title: z.string().trim().min(1).max(255),
77
- description: z.string().nullable().optional(),
78
- kind: AcqDealTaskKindSchema.optional(),
79
- dueAt: z.string().datetime().nullable().optional(),
80
- assigneeUserId: UuidSchema.nullable().optional()
81
- })
82
- .strict()
83
-
84
- export const TransitionItemRequestSchema = z
85
- .object({
86
- pipelineKey: z.string().min(1),
87
- stageKey: z.string().min(1),
88
- stateKey: z.string().nullable().optional(),
89
- reason: z.string().optional(),
90
- expectedUpdatedAt: z.string().datetime().optional()
91
- })
92
- .strict()
93
-
94
- // ---------------------------------------------------------------------------
95
- // Response schemas (no .strict() — allows forward-compatible additions)
96
- // ---------------------------------------------------------------------------
97
-
98
- /**
99
- * Contact summary nested inside DealListItem / DealDetailResponse.
100
- * Matches the joined shape returned by useDeals / useDealDetail Supabase queries.
101
- */
102
- export const DealContactSummarySchema = z.object({
103
- id: z.string(),
104
- first_name: z.string().nullable(),
105
- last_name: z.string().nullable(),
106
- email: z.string(),
107
- title: z.string().nullable(),
108
- headline: z.string().nullable(),
109
- linkedin_url: z.string().nullable(),
110
- pipeline_status: z.record(z.string(), z.unknown()).nullable(),
111
- enrichment_data: z.record(z.string(), z.unknown()).nullable(),
112
- company: z
113
- .object({
114
- id: z.string(),
115
- name: z.string(),
116
- domain: z.string().nullable(),
117
- website: z.string().nullable(),
118
- linkedin_url: z.string().nullable(),
119
- segment: z.string().nullable(),
120
- category: z.string().nullable(),
121
- num_employees: z.number().nullable()
122
- })
123
- .nullable()
124
- })
125
-
126
- /**
127
- * Deal list item with joined contact (and company via contact).
128
- * Matches DealListItem from @repo/core types.
129
- */
130
- export const DealListItemSchema = z.object({
131
- // acq_deals columns
132
- id: z.string(),
133
- organization_id: z.string(),
134
- contact_id: z.string().nullable(),
135
- contact_email: z.string(),
136
- pipeline_key: z.string(),
137
- stage_key: z.string().nullable(),
138
- state_key: z.string().nullable(),
139
- activity_log: z.unknown(),
140
- discovery_data: z.unknown().nullable(),
141
- discovery_submitted_at: z.string().nullable(),
142
- discovery_submitted_by: z.string().nullable(),
143
- proposal_data: z.unknown().nullable(),
144
- proposal_sent_at: z.string().nullable(),
145
- proposal_pdf_url: z.string().nullable(),
146
- signature_envelope_id: z.string().nullable(),
147
- source_list_id: z.string().nullable(),
148
- source_type: z.string().nullable(),
149
- initial_fee: z.number().nullable(),
150
- monthly_fee: z.number().nullable(),
151
- closed_lost_at: z.string().nullable(),
152
- closed_lost_reason: z.string().nullable(),
153
- created_at: z.string(),
154
- updated_at: z.string(),
155
- // joined relation
156
- contact: DealContactSummarySchema.nullable()
157
- })
158
-
159
- export const DealListResponseSchema = z.object({
160
- data: z.array(DealListItemSchema),
161
- total: z.number().int(),
162
- limit: z.number().int(),
163
- offset: z.number().int()
164
- })
165
-
166
- export const DealStageSummarySchema = z.object({
167
- stage: z.string(),
168
- count: z.number().int(),
169
- oldestUpdatedAt: z.string().nullable(),
170
- newestUpdatedAt: z.string().nullable()
171
- })
172
-
173
- export const StaleDealSummarySchema = z.object({
174
- id: z.string(),
175
- contactEmail: z.string(),
176
- stageKey: z.string(),
177
- updatedAt: z.string(),
178
- daysStale: z.number().int()
179
- })
180
-
181
- export const DealSummaryResponseSchema = z.object({
182
- totalDeals: z.number().int(),
183
- openDeals: z.number().int(),
184
- wonDeals: z.number().int(),
185
- lostDeals: z.number().int(),
186
- winRate: z.number(),
187
- avgDealSize: z.number(),
188
- totalPipelineValue: z.number(),
189
- stageSummary: z.array(DealStageSummarySchema),
190
- staleDeals: z.array(StaleDealSummarySchema)
191
- })
192
-
193
- export const DealLookupItemSchema = z.object({
194
- id: z.string(),
195
- contactEmail: z.string(),
196
- stageKey: z.string().nullable(),
197
- updatedAt: z.string(),
198
- contactName: z.string().nullable(),
199
- companyName: z.string().nullable(),
200
- displayLabel: z.string()
201
- })
202
-
203
- export const DealLookupResponseSchema = z.array(DealLookupItemSchema)
204
-
205
- /**
206
- * Deal detail shape — currently the same as a list item (full joined record).
207
- * useDealDetail returns DealDetail which is typed as DealListItem.
208
- */
209
- export const DealDetailResponseSchema = DealListItemSchema
210
-
211
- /**
212
- * Single acq_deal_notes row (camelCase API representation).
213
- */
214
- export const DealNoteResponseSchema = z.object({
215
- id: z.string(),
216
- dealId: z.string(),
217
- organizationId: z.string(),
218
- authorUserId: z.string().nullable(),
219
- body: z.string(),
220
- createdAt: z.string(),
221
- updatedAt: z.string()
222
- })
223
-
224
- export const DealNoteListResponseSchema = z.array(DealNoteResponseSchema)
225
-
226
- /**
227
- * Single acq_deal_tasks row (camelCase API representation).
228
- * Matches AcqDealTask domain type from types.ts.
229
- */
230
- export const DealTaskResponseSchema = z.object({
231
- id: z.string(),
232
- organizationId: z.string(),
233
- dealId: z.string(),
234
- title: z.string(),
235
- description: z.string().nullable(),
236
- kind: AcqDealTaskKindSchema,
237
- dueAt: z.string().nullable(),
238
- assigneeUserId: z.string().nullable(),
239
- completedAt: z.string().nullable(),
240
- completedByUserId: z.string().nullable(),
241
- createdAt: z.string(),
242
- updatedAt: z.string(),
243
- createdByUserId: z.string().nullable()
244
- })
245
-
246
- export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
247
-
248
- // ---------------------------------------------------------------------------
249
- // Bundled export
250
- // ---------------------------------------------------------------------------
251
-
252
- export const DealSchemas = {
253
- // Params
254
- DealIdParams: DealIdParamsSchema,
255
- DealTaskIdParams: DealTaskIdParamsSchema,
256
-
257
- // Queries
258
- ListDealsQuery: ListDealsQuerySchema,
259
- DealLookupQuery: DealLookupQuerySchema,
260
- ListDealTasksDueQuery: ListDealTasksDueQuerySchema,
261
-
262
- // Request bodies
263
- CreateDealNoteRequest: CreateDealNoteRequestSchema,
264
- CreateDealTaskRequest: CreateDealTaskRequestSchema,
265
- TransitionItemRequest: TransitionItemRequestSchema,
266
-
267
- // Responses
268
- DealListResponse: DealListResponseSchema,
269
- DealSummaryResponse: DealSummaryResponseSchema,
270
- DealLookupResponse: DealLookupResponseSchema,
271
- DealDetailResponse: DealDetailResponseSchema,
272
- DealNoteResponse: DealNoteResponseSchema,
273
- DealNoteListResponse: DealNoteListResponseSchema,
274
- DealTaskResponse: DealTaskResponseSchema,
275
- DealTaskListResponse: DealTaskListResponseSchema
276
- }
277
-
278
- // ---------------------------------------------------------------------------
279
- // Inferred types
280
- // ---------------------------------------------------------------------------
281
-
282
- export type DealStage = z.infer<typeof DealStageSchema>
283
- export type AcqDealTaskKind = z.infer<typeof AcqDealTaskKindSchema>
284
- export type DealIdParams = z.infer<typeof DealIdParamsSchema>
285
- export type DealTaskIdParams = z.infer<typeof DealTaskIdParamsSchema>
286
- export type ListDealsQuery = z.infer<typeof ListDealsQuerySchema>
287
- export type DealLookupQuery = z.infer<typeof DealLookupQuerySchema>
288
- export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
289
- export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
290
- export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
291
- export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
292
- export type DealListResponse = z.infer<typeof DealListResponseSchema>
293
- export type DealSummaryResponse = z.infer<typeof DealSummaryResponseSchema>
294
- export type DealLookupItem = z.infer<typeof DealLookupItemSchema>
295
- export type DealLookupResponse = z.infer<typeof DealLookupResponseSchema>
296
- export type DealDetailResponse = z.infer<typeof DealDetailResponseSchema>
297
- export type DealNoteResponse = z.infer<typeof DealNoteResponseSchema>
298
- export type DealNoteListResponse = z.infer<typeof DealNoteListResponseSchema>
299
- export type DealTaskResponse = z.infer<typeof DealTaskResponseSchema>
300
- export type DealTaskListResponse = z.infer<typeof DealTaskListResponseSchema>
301
-
302
- // ---------------------------------------------------------------------------
303
- // List Management API Schemas
304
- //
305
- // Request/response validation for /api/acquisition/lists surface.
306
- // Used by both the API route handlers and the frontend hooks.
307
- //
308
- // Table mapping:
309
- // acq_lists -> AcqListSchemas (list/detail/progress)
310
- // acq_list_companies -> AcqListSchemas (add/remove company membership)
311
- // acq_list_contacts -> AcqListSchemas (add/remove contact membership)
312
- // acq_list_executions -> AcqListSchemas (execution history)
313
- // ---------------------------------------------------------------------------
314
-
315
- // ---------------------------------------------------------------------------
316
- // Primitives list config subtrees
317
- // ---------------------------------------------------------------------------
318
-
319
- export const ListQualificationSchema = z.object({
320
- targetDescription: z.string(),
321
- minReviewCount: z.number().int().min(0),
322
- minRating: z.number().min(0).max(5),
323
- excludeFranchises: z.boolean(),
324
- customRules: z.string()
325
- })
326
-
327
- export const ListEnrichmentSchema = z.object({
328
- emailDiscovery: z
329
- .object({
330
- primary: z.enum(['tomba', 'anymailfinder']),
331
- credentialName: z.string().optional()
332
- })
333
- .optional(),
334
- emailVerification: z
335
- .object({
336
- provider: z.literal('millionverifier'),
337
- threshold: z.enum(['ok', 'ok+catch_all']).optional()
338
- })
339
- .optional()
340
- })
341
-
342
- export const ListPersonalizationSchema = z.object({
343
- industryContext: z.string().optional(),
344
- emailBody: z.string().optional(),
345
- creativeDirection: z.string().optional(),
346
- exclusionRules: z.array(z.string()).optional()
347
- })
348
-
349
- export const PipelineStepSchema = z.object({
350
- key: z.string(),
351
- label: z.string(),
352
- resourceId: z.string(),
353
- inputTemplate: z.record(z.string(), z.unknown()),
354
- enabled: z.boolean(),
355
- order: z.number().int()
356
- })
357
-
358
- export const ListPipelineSchema = z.object({
359
- steps: z.array(PipelineStepSchema)
360
- })
361
-
362
- /**
363
- * Full ListConfig shape. `qualification` is required; everything else optional.
364
- * Matches `acq_lists.config` jsonb and ListConfig type in types.ts.
365
- */
366
- export const ListConfigSchema = z.object({
367
- qualification: ListQualificationSchema,
368
- enrichment: ListEnrichmentSchema.optional(),
369
- personalization: ListPersonalizationSchema.optional(),
370
- pipeline: ListPipelineSchema.optional()
371
- })
372
-
373
- // ---------------------------------------------------------------------------
374
- // List telemetry / progress schemas
375
- // ---------------------------------------------------------------------------
376
-
377
- export const ListStageCountsSchema = z.object({
378
- stageCounts: z.object({
379
- populated: z.number().int(),
380
- extracted: z.number().int(),
381
- qualified: z.number().int(),
382
- discovered: z.number().int(),
383
- verified: z.number().int(),
384
- personalized: z.number().int(),
385
- uploaded: z.number().int()
386
- }),
387
- deliverability: z.object({
388
- valid: z.number().int(),
389
- risky: z.number().int(),
390
- invalid: z.number().int(),
391
- unknown: z.number().int(),
392
- bounced: z.number().int()
393
- })
394
- })
395
-
396
- export const ListTelemetrySchema = z.object({
397
- listId: UuidSchema,
398
- totalCompanies: z.number().int(),
399
- totalContacts: z.number().int(),
400
- stageCounts: ListStageCountsSchema.shape.stageCounts,
401
- deliverability: ListStageCountsSchema.shape.deliverability,
402
- activeWorkflows: z.array(z.string()).optional()
403
- })
404
-
405
- // ---------------------------------------------------------------------------
406
- // Params
407
- // ---------------------------------------------------------------------------
408
-
409
- export const ListIdParamsSchema = z.object({
410
- listId: UuidSchema
411
- })
412
-
413
- // ---------------------------------------------------------------------------
414
- // Request body schemas (all use .strict() — rejects unknown fields)
415
- // ---------------------------------------------------------------------------
416
-
417
- export const CreateListRequestSchema = z
418
- .object({
419
- name: z.string().trim().min(1).max(255),
420
- description: z.string().trim().nullable().optional(),
421
- type: z.string().default('manual'),
422
- config: ListConfigSchema.optional()
423
- })
424
- .strict()
425
-
426
- export const UpdateListRequestSchema = z
427
- .object({
428
- name: z.string().trim().min(1).max(255).optional(),
429
- description: z.string().trim().nullable().optional(),
430
- status: z.string().optional(),
431
- batchIds: z.array(z.string()).optional()
432
- })
433
- .strict()
434
- .refine(
435
- (data) =>
436
- data.name !== undefined ||
437
- data.description !== undefined ||
438
- data.status !== undefined ||
439
- data.batchIds !== undefined,
440
- {
441
- message: 'At least one field (name, description, status, or batchIds) must be provided'
442
- }
443
- )
444
-
445
- /**
446
- * Partial patch for list.config — UI sends only the edited tab's subtree.
447
- * Zod v4: use .partial() on each subtree and remake the root schema.
448
- * Since the root ListConfigSchema marks `qualification` as required, we must
449
- * produce a manually-built deep-partial that makes qualification optional too.
450
- */
451
- export const UpdateListConfigRequestSchema = z
452
- .object({
453
- qualification: ListQualificationSchema.partial().optional(),
454
- enrichment: ListEnrichmentSchema.partial().optional(),
455
- personalization: ListPersonalizationSchema.partial().optional(),
456
- pipeline: ListPipelineSchema.partial().optional()
457
- })
458
- .strict()
459
-
460
- export const AddCompaniesToListRequestSchema = z
461
- .object({
462
- companyIds: z.array(UuidSchema).min(1).max(1000)
463
- })
464
- .strict()
465
-
466
- export const RemoveCompaniesFromListRequestSchema = z
467
- .object({
468
- companyIds: z.array(UuidSchema).min(1).max(1000)
469
- })
470
- .strict()
471
-
472
- export const AddContactsToListRequestSchema = z
473
- .object({
474
- contactIds: z.array(UuidSchema).min(1).max(1000)
475
- })
476
- .strict()
477
-
478
- export const RecordListExecutionRequestSchema = z
479
- .object({
480
- executionId: UuidSchema,
481
- configSnapshot: z.record(z.string(), z.unknown()).optional()
482
- })
483
- .strict()
484
-
485
- // ---------------------------------------------------------------------------
486
- // Response schemas (no .strict() — allows forward-compatible additions)
487
- // ---------------------------------------------------------------------------
488
-
489
- /**
490
- * Single list as returned by /api/acquisition/lists/:id etc.
491
- * Camel-cased domain shape matching AcqList in types.ts.
492
- */
493
- export const AcqListResponseSchema = z.object({
494
- id: z.string(),
495
- organizationId: z.string(),
496
- name: z.string(),
497
- description: z.string().nullable(),
498
- type: z.string(),
499
- batchIds: z.array(z.string()),
500
- instantlyCampaignId: z.string().nullable(),
501
- status: z.string(),
502
- metadata: z.record(z.string(), z.unknown()),
503
- launchedAt: z.string().nullable(),
504
- completedAt: z.string().nullable(),
505
- createdAt: z.string(),
506
- config: ListConfigSchema
507
- })
508
-
509
- export const AcqListListResponseSchema = z.array(AcqListResponseSchema)
510
-
511
- export const ListTelemetryResponseSchema = ListTelemetrySchema
512
-
513
- export const ListTelemetryListResponseSchema = z.array(ListTelemetrySchema)
514
-
515
- /**
516
- * Row from acq_list_executions joined with the execution summary,
517
- * shaped for the /lists/:id/executions response.
518
- */
519
- export const ListExecutionSummarySchema = z.object({
520
- executionId: z.string(),
521
- resourceId: z.string(),
522
- status: z.string(),
523
- createdAt: z.string(),
524
- completedAt: z.string().nullable(),
525
- durationMs: z.number().int().nullable()
526
- })
527
-
528
- export const ListExecutionsResponseSchema = z.array(ListExecutionSummarySchema)
529
-
530
- // ---------------------------------------------------------------------------
531
- // Company / Contact API Schemas
532
- // ---------------------------------------------------------------------------
533
-
534
- const QueryBooleanSchema = z.preprocess((value) => {
535
- if (value === 'true' || value === '1' || value === true) return true
536
- if (value === 'false' || value === '0' || value === false) return false
537
- return value
538
- }, z.boolean())
539
-
540
- export const AcqCompanyStatusSchema = z.enum(['active', 'invalid'])
541
- export const AcqContactStatusSchema = z.enum(['active', 'invalid'])
542
- export const AcqEmailValidSchema = z.enum(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])
543
-
544
- export const CompanyIdParamsSchema = z.object({
545
- companyId: UuidSchema
546
- })
547
-
548
- export const ContactIdParamsSchema = z.object({
549
- contactId: UuidSchema
550
- })
551
-
552
- export const ListCompaniesQuerySchema = z
553
- .object({
554
- search: z.string().trim().min(1).max(200).optional(),
555
- listId: UuidSchema.optional(),
556
- domain: z.string().trim().min(1).max(255).optional(),
557
- website: z.string().trim().min(1).max(2048).optional(),
558
- segment: z.string().trim().min(1).max(255).optional(),
559
- category: z.string().trim().min(1).max(255).optional(),
560
- batchId: z.string().trim().min(1).max(255).optional(),
561
- status: AcqCompanyStatusSchema.optional(),
562
- includeAll: QueryBooleanSchema.optional(),
563
- limit: z.coerce.number().int().min(1).max(5000).default(50),
564
- offset: z.coerce.number().int().min(0).default(0)
565
- })
566
- .strict()
567
-
568
- export const ListContactsQuerySchema = z
569
- .object({
570
- search: z.string().trim().min(1).max(200).optional(),
571
- listId: UuidSchema.optional(),
572
- openingLineIsNull: QueryBooleanSchema.optional(),
573
- batchId: z.string().trim().min(1).max(255).optional(),
574
- contactStatus: AcqContactStatusSchema.optional(),
575
- limit: z.coerce.number().int().min(1).max(5000).default(5000),
576
- offset: z.coerce.number().int().min(0).default(0)
577
- })
578
- .strict()
579
-
580
- export const CreateCompanyRequestSchema = z
581
- .object({
582
- name: z.string().trim().min(1).max(255),
583
- domain: z.string().trim().min(1).max(255).optional(),
584
- linkedinUrl: z.string().trim().url().optional(),
585
- website: z.string().trim().url().optional(),
586
- numEmployees: z.number().int().min(0).optional(),
587
- foundedYear: z.number().int().optional(),
588
- locationCity: z.string().trim().min(1).max(255).optional(),
589
- locationState: z.string().trim().min(1).max(255).optional(),
590
- category: z.string().trim().min(1).max(255).optional(),
591
- source: z.string().trim().min(1).max(255).optional(),
592
- batchId: z.string().trim().min(1).max(255).optional(),
593
- verticalResearch: z.string().trim().min(1).max(5000).optional()
594
- })
595
- .strict()
596
-
597
- export const UpdateCompanyRequestSchema = z
598
- .object({
599
- name: z.string().trim().min(1).max(255).optional(),
600
- domain: z.string().trim().min(1).max(255).optional(),
601
- linkedinUrl: z.string().trim().url().optional(),
602
- website: z.string().trim().url().optional(),
603
- numEmployees: z.number().int().min(0).optional(),
604
- foundedYear: z.number().int().optional(),
605
- locationCity: z.string().trim().min(1).max(255).optional(),
606
- locationState: z.string().trim().min(1).max(255).optional(),
607
- category: z.string().trim().min(1).max(255).optional(),
608
- segment: z.string().trim().min(1).max(255).optional(),
609
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
610
- enrichmentData: z.record(z.string(), z.unknown()).optional(),
611
- source: z.string().trim().min(1).max(255).optional(),
612
- batchId: z.string().trim().min(1).max(255).optional(),
613
- status: AcqCompanyStatusSchema.optional(),
614
- verticalResearch: z.string().trim().min(1).max(5000).nullable().optional()
615
- })
616
- .strict()
617
- .refine(
618
- (data) =>
619
- data.name !== undefined ||
620
- data.domain !== undefined ||
621
- data.linkedinUrl !== undefined ||
622
- data.website !== undefined ||
623
- data.numEmployees !== undefined ||
624
- data.foundedYear !== undefined ||
625
- data.locationCity !== undefined ||
626
- data.locationState !== undefined ||
627
- data.category !== undefined ||
628
- data.segment !== undefined ||
629
- data.pipelineStatus !== undefined ||
630
- data.enrichmentData !== undefined ||
631
- data.source !== undefined ||
632
- data.batchId !== undefined ||
633
- data.status !== undefined ||
634
- data.verticalResearch !== undefined,
635
- {
636
- message: 'At least one field must be provided'
637
- }
638
- )
639
-
640
- export const CreateContactRequestSchema = z
641
- .object({
642
- email: z.string().trim().email(),
643
- companyId: UuidSchema.optional(),
644
- firstName: z.string().trim().min(1).max(255).optional(),
645
- lastName: z.string().trim().min(1).max(255).optional(),
646
- linkedinUrl: z.string().trim().url().optional(),
647
- title: z.string().trim().min(1).max(255).optional(),
648
- source: z.string().trim().min(1).max(255).optional(),
649
- sourceId: z.string().trim().min(1).max(255).optional(),
650
- batchId: z.string().trim().min(1).max(255).optional()
651
- })
652
- .strict()
653
-
654
- export const UpdateContactRequestSchema = z
655
- .object({
656
- companyId: UuidSchema.optional(),
657
- emailValid: AcqEmailValidSchema.optional(),
658
- firstName: z.string().trim().min(1).max(255).optional(),
659
- lastName: z.string().trim().min(1).max(255).optional(),
660
- linkedinUrl: z.string().trim().url().optional(),
661
- title: z.string().trim().min(1).max(255).optional(),
662
- headline: z.string().trim().min(1).max(5000).optional(),
663
- filterReason: z.string().trim().min(1).max(5000).optional(),
664
- openingLine: z.string().trim().min(1).max(5000).optional(),
665
- pipelineStatus: z.record(z.string(), z.unknown()).optional(),
666
- enrichmentData: z.record(z.string(), z.unknown()).optional(),
667
- status: AcqContactStatusSchema.optional()
668
- })
669
- .strict()
670
- .refine(
671
- (data) =>
672
- data.companyId !== undefined ||
673
- data.emailValid !== undefined ||
674
- data.firstName !== undefined ||
675
- data.lastName !== undefined ||
676
- data.linkedinUrl !== undefined ||
677
- data.title !== undefined ||
678
- data.headline !== undefined ||
679
- data.filterReason !== undefined ||
680
- data.openingLine !== undefined ||
681
- data.pipelineStatus !== undefined ||
682
- data.enrichmentData !== undefined ||
683
- data.status !== undefined,
684
- {
685
- message: 'At least one field must be provided'
686
- }
687
- )
688
-
689
- export const AcqCompanyResponseSchema = z.object({
690
- id: z.string(),
691
- organizationId: z.string(),
692
- name: z.string(),
693
- domain: z.string().nullable(),
694
- linkedinUrl: z.string().nullable(),
695
- website: z.string().nullable(),
696
- numEmployees: z.number().nullable(),
697
- foundedYear: z.number().nullable(),
698
- locationCity: z.string().nullable(),
699
- locationState: z.string().nullable(),
700
- category: z.string().nullable(),
701
- categoryPain: z.string().nullable(),
702
- segment: z.string().nullable(),
703
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
704
- enrichmentData: z.record(z.string(), z.unknown()).nullable(),
705
- source: z.string().nullable(),
706
- batchId: z.string().nullable(),
707
- status: AcqCompanyStatusSchema,
708
- contactCount: z.number().int().min(0),
709
- verticalResearch: z.string().nullable(),
710
- createdAt: z.string(),
711
- updatedAt: z.string()
712
- })
713
-
714
- export const AcqCompanyListResponseSchema = z.object({
715
- data: z.array(AcqCompanyResponseSchema),
716
- total: z.number().int(),
717
- limit: z.number().int(),
718
- offset: z.number().int()
719
- })
720
-
721
- export const AcqCompanyFacetsResponseSchema = z.object({
722
- segments: z.array(z.string()),
723
- categories: z.array(z.string()),
724
- statuses: z.array(AcqCompanyStatusSchema)
725
- })
726
-
727
- export const AcqContactCompanySummarySchema = z.object({
728
- id: z.string(),
729
- name: z.string(),
730
- domain: z.string().nullable(),
731
- website: z.string().nullable(),
732
- linkedinUrl: z.string().nullable(),
733
- segment: z.string().nullable(),
734
- category: z.string().nullable(),
735
- status: AcqCompanyStatusSchema
736
- })
737
-
738
- export const AcqContactResponseSchema = z.object({
739
- id: z.string(),
740
- organizationId: z.string(),
741
- companyId: z.string().nullable(),
742
- email: z.string(),
743
- emailValid: AcqEmailValidSchema.nullable(),
744
- firstName: z.string().nullable(),
745
- lastName: z.string().nullable(),
746
- linkedinUrl: z.string().nullable(),
747
- title: z.string().nullable(),
748
- headline: z.string().nullable(),
749
- filterReason: z.string().nullable(),
750
- openingLine: z.string().nullable(),
751
- source: z.string().nullable(),
752
- sourceId: z.string().nullable(),
753
- pipelineStatus: z.record(z.string(), z.unknown()).nullable(),
754
- enrichmentData: z.record(z.string(), z.unknown()).nullable(),
755
- attioPersonId: z.string().nullable(),
756
- batchId: z.string().nullable(),
757
- status: AcqContactStatusSchema,
758
- company: AcqContactCompanySummarySchema.nullable().optional(),
759
- createdAt: z.string(),
760
- updatedAt: z.string()
761
- })
762
-
763
- export const AcqContactListResponseSchema = z.object({
764
- data: z.array(AcqContactResponseSchema),
765
- total: z.number().int(),
766
- limit: z.number().int(),
767
- offset: z.number().int()
768
- })
769
-
770
- export const AcqCompanySchemas = {
771
- CompanyIdParams: CompanyIdParamsSchema,
772
- ListCompaniesQuery: ListCompaniesQuerySchema,
773
- CreateCompanyRequest: CreateCompanyRequestSchema,
774
- UpdateCompanyRequest: UpdateCompanyRequestSchema,
775
- AcqCompanyResponse: AcqCompanyResponseSchema,
776
- AcqCompanyListResponse: AcqCompanyListResponseSchema,
777
- AcqCompanyFacetsResponse: AcqCompanyFacetsResponseSchema
778
- }
779
-
780
- export const AcqContactSchemas = {
781
- ContactIdParams: ContactIdParamsSchema,
782
- ListContactsQuery: ListContactsQuerySchema,
783
- CreateContactRequest: CreateContactRequestSchema,
784
- UpdateContactRequest: UpdateContactRequestSchema,
785
- AcqContactResponse: AcqContactResponseSchema,
786
- AcqContactListResponse: AcqContactListResponseSchema
787
- }
788
-
789
- export type AcqCompanyStatus = z.infer<typeof AcqCompanyStatusSchema>
790
- export type AcqContactStatus = z.infer<typeof AcqContactStatusSchema>
791
- export type AcqEmailValid = z.infer<typeof AcqEmailValidSchema>
792
- export type CompanyIdParams = z.infer<typeof CompanyIdParamsSchema>
793
- export type ContactIdParams = z.infer<typeof ContactIdParamsSchema>
794
- export type ListCompaniesQuery = z.infer<typeof ListCompaniesQuerySchema>
795
- export type ListContactsQuery = z.infer<typeof ListContactsQuerySchema>
796
- export type CreateCompanyRequest = z.infer<typeof CreateCompanyRequestSchema>
797
- export type UpdateCompanyRequest = z.infer<typeof UpdateCompanyRequestSchema>
798
- export type CreateContactRequest = z.infer<typeof CreateContactRequestSchema>
799
- export type UpdateContactRequest = z.infer<typeof UpdateContactRequestSchema>
800
- export type AcqCompanyResponse = z.infer<typeof AcqCompanyResponseSchema>
801
- export type AcqCompanyListResponse = z.infer<typeof AcqCompanyListResponseSchema>
802
- export type AcqCompanyFacetsResponse = z.infer<typeof AcqCompanyFacetsResponseSchema>
803
- export type AcqContactCompanySummary = z.infer<typeof AcqContactCompanySummarySchema>
804
- export type AcqContactResponse = z.infer<typeof AcqContactResponseSchema>
805
- export type AcqContactListResponse = z.infer<typeof AcqContactListResponseSchema>
806
-
807
- // ---------------------------------------------------------------------------
808
- // Bundled export
809
- // ---------------------------------------------------------------------------
810
-
811
- export const AcqListSchemas = {
812
- // Params
813
- ListIdParams: ListIdParamsSchema,
814
-
815
- // Primitives (for UI / tests)
816
- ListConfig: ListConfigSchema,
817
- ListStageCounts: ListStageCountsSchema,
818
- ListTelemetry: ListTelemetrySchema,
819
- PipelineStep: PipelineStepSchema,
820
-
821
- // Requests
822
- CreateListRequest: CreateListRequestSchema,
823
- UpdateListRequest: UpdateListRequestSchema,
824
- UpdateListConfigRequest: UpdateListConfigRequestSchema,
825
- AddCompaniesToListRequest: AddCompaniesToListRequestSchema,
826
- RemoveCompaniesFromListRequest: RemoveCompaniesFromListRequestSchema,
827
- AddContactsToListRequest: AddContactsToListRequestSchema,
828
- RecordListExecutionRequest: RecordListExecutionRequestSchema,
829
-
830
- // Responses
831
- AcqListResponse: AcqListResponseSchema,
832
- AcqListListResponse: AcqListListResponseSchema,
833
- ListTelemetryResponse: ListTelemetryResponseSchema,
834
- ListTelemetryListResponse: ListTelemetryListResponseSchema,
835
- ListExecutionsResponse: ListExecutionsResponseSchema
836
- }
837
-
838
- // ---------------------------------------------------------------------------
839
- // Inferred types
840
- // ---------------------------------------------------------------------------
841
-
842
- export type ListConfigInput = z.infer<typeof ListConfigSchema>
843
- export type ListStageCountsInput = z.infer<typeof ListStageCountsSchema>['stageCounts']
844
- export type ListTelemetryInput = z.infer<typeof ListTelemetrySchema>
845
- export type PipelineStepInput = z.infer<typeof PipelineStepSchema>
846
- export type ListIdParams = z.infer<typeof ListIdParamsSchema>
847
- export type CreateListRequest = z.infer<typeof CreateListRequestSchema>
848
- export type UpdateListRequest = z.infer<typeof UpdateListRequestSchema>
849
- export type UpdateListConfigRequest = z.infer<typeof UpdateListConfigRequestSchema>
850
- export type AddCompaniesToListRequest = z.infer<typeof AddCompaniesToListRequestSchema>
851
- export type RemoveCompaniesFromListRequest = z.infer<typeof RemoveCompaniesFromListRequestSchema>
852
- export type AddContactsToListRequest = z.infer<typeof AddContactsToListRequestSchema>
853
- export type RecordListExecutionRequest = z.infer<typeof RecordListExecutionRequestSchema>
854
- export type AcqListResponse = z.infer<typeof AcqListResponseSchema>
855
- export type AcqListListResponse = z.infer<typeof AcqListListResponseSchema>
856
- export type ListTelemetryResponse = z.infer<typeof ListTelemetryResponseSchema>
857
- export type ListTelemetryListResponse = z.infer<typeof ListTelemetryListResponseSchema>
858
- export type ListExecutionSummaryInput = z.infer<typeof ListExecutionSummarySchema>
859
- export type ListExecutionsResponse = z.infer<typeof ListExecutionsResponseSchema>
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>