@elevasis/core 0.12.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/organization-model/index.d.ts +1 -1
- package/dist/organization-model/index.js +9 -2
- package/dist/test-utils/index.d.ts +480 -389
- package/dist/test-utils/index.js +28 -2
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2324 -0
- package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -216
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +5 -19
- package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +3 -13
- package/src/auth/multi-tenancy/permissions.ts +12 -5
- package/src/business/acquisition/activity-events.test.ts +250 -0
- package/src/business/acquisition/activity-events.ts +84 -0
- package/src/business/acquisition/api-schemas.test.ts +1180 -0
- package/src/business/acquisition/api-schemas.ts +456 -235
- package/src/business/acquisition/crm-state-actions.test.ts +160 -0
- package/src/business/acquisition/derive-actions.test.ts +518 -0
- package/src/business/acquisition/derive-actions.ts +103 -0
- package/src/business/acquisition/index.ts +51 -11
- package/src/business/acquisition/stateful.ts +30 -0
- package/src/business/acquisition/types.ts +44 -77
- package/src/execution/engine/index.ts +4 -1
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +1 -2
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -361
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
- package/src/execution/engine/tools/integration/service.test.ts +34 -9
- package/src/execution/engine/tools/integration/service.ts +6 -3
- package/src/execution/engine/tools/lead-service-types.ts +90 -30
- package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
- package/src/execution/engine/tools/registry.ts +5 -4
- package/src/execution/engine/tools/tool-maps.ts +43 -21
- package/src/execution/engine/workflow/types.ts +11 -0
- package/src/organization-model/contracts.ts +4 -4
- package/src/organization-model/domains/navigation.ts +62 -62
- package/src/organization-model/domains/sales.ts +272 -0
- package/src/organization-model/organization-graph.mdx +2 -2
- package/src/organization-model/published.ts +21 -21
- package/src/organization-model/resolve.ts +21 -8
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +2324 -0
- package/src/scaffold-registry/index.ts +10 -9
- package/src/scaffold-registry/schema.ts +68 -62
- package/src/supabase/database.types.ts +2958 -2884
- package/src/test-utils/rls/RLSTestContext.ts +585 -553
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { LEAD_GEN_PIPELINE_DEFINITIONS, LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
|
|
3
|
+
import {
|
|
4
|
+
AddCompaniesToListRequestSchema,
|
|
5
|
+
AddContactsToListRequestSchema,
|
|
6
|
+
AcqArtifactOwnerKindSchema,
|
|
7
|
+
AcqContactResponseSchema,
|
|
8
|
+
AcqContactStatusSchema,
|
|
9
|
+
AcqEmailValidSchema,
|
|
10
|
+
AcqListResponseSchema,
|
|
11
|
+
CreateArtifactRequestSchema,
|
|
12
|
+
CreateCompanyRequestSchema,
|
|
13
|
+
CreateContactRequestSchema,
|
|
14
|
+
CreateDealNoteRequestSchema,
|
|
15
|
+
CreateDealTaskRequestSchema,
|
|
16
|
+
CreateListRequestSchema,
|
|
17
|
+
DealDetailResponseSchema,
|
|
18
|
+
DealListResponseSchema,
|
|
19
|
+
DealNoteResponseSchema,
|
|
20
|
+
DealStageSchema,
|
|
21
|
+
DealTaskResponseSchema,
|
|
22
|
+
ExecuteActionRequestSchema,
|
|
23
|
+
IcpRubricSchema,
|
|
24
|
+
ListArtifactsQuerySchema,
|
|
25
|
+
ListCompaniesQuerySchema,
|
|
26
|
+
ListContactsQuerySchema,
|
|
27
|
+
ListDealsQuerySchema,
|
|
28
|
+
ListDealTasksDueQuerySchema,
|
|
29
|
+
ListMembersQuerySchema,
|
|
30
|
+
ListStatusSchema,
|
|
31
|
+
PipelineStageSchema,
|
|
32
|
+
ScrapingConfigSchema,
|
|
33
|
+
TransitionItemRequestSchema,
|
|
34
|
+
UpdateCompanyRequestSchema,
|
|
35
|
+
UpdateContactRequestSchema,
|
|
36
|
+
UpdateListConfigRequestSchema,
|
|
37
|
+
UpdateListRequestSchema,
|
|
38
|
+
UpdateListStatusRequestSchema
|
|
39
|
+
} from './api-schemas'
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const VALID_UUID = '00000000-0000-4000-8000-000000000001'
|
|
46
|
+
const ISO_TS = '2026-04-27T12:34:56.000Z'
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// DealStageSchema
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe('DealStageSchema', () => {
|
|
53
|
+
it.each(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])(
|
|
54
|
+
'accepts canonical stage "%s"',
|
|
55
|
+
(stage) => {
|
|
56
|
+
expect(DealStageSchema.safeParse(stage).success).toBe(true)
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
it('rejects an unknown stage value', () => {
|
|
61
|
+
expect(DealStageSchema.safeParse('open').success).toBe(false)
|
|
62
|
+
expect(DealStageSchema.safeParse('').success).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// TransitionItemRequestSchema
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('TransitionItemRequestSchema', () => {
|
|
71
|
+
const valid = {
|
|
72
|
+
pipelineKey: 'default',
|
|
73
|
+
stageKey: 'interested',
|
|
74
|
+
stateKey: null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
it('accepts a minimal valid payload', () => {
|
|
78
|
+
expect(TransitionItemRequestSchema.safeParse(valid).success).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('accepts stateKey as null', () => {
|
|
82
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, stateKey: null })
|
|
83
|
+
expect(result.success).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('accepts stateKey as a string', () => {
|
|
87
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'discovery_replied' })
|
|
88
|
+
expect(result.success).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('accepts stateKey as undefined (optional)', () => {
|
|
92
|
+
const { stateKey: _omit, ...withoutState } = valid
|
|
93
|
+
const result = TransitionItemRequestSchema.safeParse(withoutState)
|
|
94
|
+
expect(result.success).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('accepts a valid ISO datetime for expectedUpdatedAt', () => {
|
|
98
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, expectedUpdatedAt: ISO_TS })
|
|
99
|
+
expect(result.success).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('rejects a non-ISO datetime for expectedUpdatedAt', () => {
|
|
103
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, expectedUpdatedAt: 'not-a-date' })
|
|
104
|
+
expect(result.success).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('accepts all canonical CRM deal stages', () => {
|
|
108
|
+
const stages = ['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing']
|
|
109
|
+
for (const stageKey of stages) {
|
|
110
|
+
expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'default', stageKey, stateKey: null }).success).toBe(
|
|
111
|
+
true
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('accepts lead-gen pipeline stage/state pairs from LEAD_GEN_PIPELINE_DEFINITIONS', () => {
|
|
117
|
+
for (const pipelineDefinitions of Object.values(LEAD_GEN_PIPELINE_DEFINITIONS)) {
|
|
118
|
+
for (const pipeline of pipelineDefinitions) {
|
|
119
|
+
for (const stage of pipeline.stages) {
|
|
120
|
+
for (const state of stage.states) {
|
|
121
|
+
expect(
|
|
122
|
+
TransitionItemRequestSchema.safeParse({
|
|
123
|
+
pipelineKey: pipeline.pipelineKey,
|
|
124
|
+
stageKey: stage.stageKey,
|
|
125
|
+
stateKey: state.stateKey
|
|
126
|
+
}).success
|
|
127
|
+
).toBe(true)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('rejects an empty pipelineKey', () => {
|
|
135
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: '' })
|
|
136
|
+
expect(result.success).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('rejects an empty stageKey', () => {
|
|
140
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, stageKey: '' })
|
|
141
|
+
expect(result.success).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('rejects unknown top-level fields (strict mode)', () => {
|
|
145
|
+
const result = TransitionItemRequestSchema.safeParse({ ...valid, unknownField: 'x' })
|
|
146
|
+
expect(result.success).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('rejects missing pipelineKey', () => {
|
|
150
|
+
const { pipelineKey: _omit, ...missing } = valid
|
|
151
|
+
const result = TransitionItemRequestSchema.safeParse(missing)
|
|
152
|
+
expect(result.success).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('rejects missing stageKey', () => {
|
|
156
|
+
const { stageKey: _omit, ...missing } = valid
|
|
157
|
+
const result = TransitionItemRequestSchema.safeParse(missing)
|
|
158
|
+
expect(result.success).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// ExecuteActionRequestSchema
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe('ExecuteActionRequestSchema', () => {
|
|
167
|
+
it('accepts an empty object (payload is optional)', () => {
|
|
168
|
+
expect(ExecuteActionRequestSchema.safeParse({}).success).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('accepts a payload record with arbitrary string keys', () => {
|
|
172
|
+
const result = ExecuteActionRequestSchema.safeParse({
|
|
173
|
+
payload: { channel: 'email', count: 5, nested: { ok: true } }
|
|
174
|
+
})
|
|
175
|
+
expect(result.success).toBe(true)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('accepts an empty payload record', () => {
|
|
179
|
+
expect(ExecuteActionRequestSchema.safeParse({ payload: {} }).success).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('rejects unknown top-level fields (strict mode)', () => {
|
|
183
|
+
const result = ExecuteActionRequestSchema.safeParse({ payload: {}, extra: 'bad' })
|
|
184
|
+
expect(result.success).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// CreateDealNoteRequestSchema
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
describe('CreateDealNoteRequestSchema', () => {
|
|
193
|
+
it('accepts a valid body', () => {
|
|
194
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: 'Hello' }).success).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('accepts a single character after trim', () => {
|
|
198
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: 'a' }).success).toBe(true)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('trims whitespace and accepts content that remains non-empty', () => {
|
|
202
|
+
const result = CreateDealNoteRequestSchema.safeParse({ body: ' note ' })
|
|
203
|
+
expect(result.success).toBe(true)
|
|
204
|
+
if (result.success) expect(result.data.body).toBe('note')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('rejects a whitespace-only string (empty after trim)', () => {
|
|
208
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: ' ' }).success).toBe(false)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('rejects an empty string', () => {
|
|
212
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: '' }).success).toBe(false)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('accepts a body at the max length boundary (10000 chars)', () => {
|
|
216
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: 'a'.repeat(10000) }).success).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('rejects a body exceeding the max length (10001 chars)', () => {
|
|
220
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: 'a'.repeat(10001) }).success).toBe(false)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('rejects unknown top-level fields (strict mode)', () => {
|
|
224
|
+
expect(CreateDealNoteRequestSchema.safeParse({ body: 'note', extra: 'x' }).success).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// CreateDealTaskRequestSchema
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
describe('CreateDealTaskRequestSchema', () => {
|
|
233
|
+
const valid = { title: 'Follow up call' }
|
|
234
|
+
|
|
235
|
+
it('accepts a minimal valid payload (title only)', () => {
|
|
236
|
+
expect(CreateDealTaskRequestSchema.safeParse(valid).success).toBe(true)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('kind is optional and accepts all valid values', () => {
|
|
240
|
+
for (const kind of ['call', 'email', 'meeting', 'other'] as const) {
|
|
241
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, kind }).success).toBe(true)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('rejects an invalid kind value', () => {
|
|
246
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, kind: 'sms' }).success).toBe(false)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('accepts a valid ISO datetime for dueAt', () => {
|
|
250
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, dueAt: ISO_TS }).success).toBe(true)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('rejects an invalid datetime for dueAt', () => {
|
|
254
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, dueAt: 'tomorrow' }).success).toBe(false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('accepts null for dueAt', () => {
|
|
258
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, dueAt: null }).success).toBe(true)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('accepts a valid UUID for assigneeUserId', () => {
|
|
262
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, assigneeUserId: VALID_UUID }).success).toBe(true)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('rejects a non-UUID for assigneeUserId', () => {
|
|
266
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, assigneeUserId: 'not-a-uuid' }).success).toBe(false)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('accepts null for assigneeUserId', () => {
|
|
270
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, assigneeUserId: null }).success).toBe(true)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('rejects an empty title', () => {
|
|
274
|
+
expect(CreateDealTaskRequestSchema.safeParse({ title: '' }).success).toBe(false)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('rejects a title exceeding 255 chars', () => {
|
|
278
|
+
expect(CreateDealTaskRequestSchema.safeParse({ title: 'a'.repeat(256) }).success).toBe(false)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('rejects unknown top-level fields (strict mode)', () => {
|
|
282
|
+
expect(CreateDealTaskRequestSchema.safeParse({ ...valid, bogus: true }).success).toBe(false)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// ListDealsQuerySchema
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
describe('ListDealsQuerySchema', () => {
|
|
291
|
+
it('accepts an empty query (all defaults applied)', () => {
|
|
292
|
+
const result = ListDealsQuerySchema.safeParse({})
|
|
293
|
+
expect(result.success).toBe(true)
|
|
294
|
+
if (result.success) {
|
|
295
|
+
expect(result.data.limit).toBe(50)
|
|
296
|
+
expect(result.data.offset).toBe(0)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('coerces limit from string "50" to number 50', () => {
|
|
301
|
+
const result = ListDealsQuerySchema.safeParse({ limit: '50' })
|
|
302
|
+
expect(result.success).toBe(true)
|
|
303
|
+
if (result.success) expect(result.data.limit).toBe(50)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('coerces offset from string "20" to number 20', () => {
|
|
307
|
+
const result = ListDealsQuerySchema.safeParse({ offset: '20' })
|
|
308
|
+
expect(result.success).toBe(true)
|
|
309
|
+
if (result.success) expect(result.data.offset).toBe(20)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('rejects non-numeric string for limit', () => {
|
|
313
|
+
expect(ListDealsQuerySchema.safeParse({ limit: 'abc' }).success).toBe(false)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('rejects non-numeric string for offset', () => {
|
|
317
|
+
expect(ListDealsQuerySchema.safeParse({ offset: 'abc' }).success).toBe(false)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('rejects zero or negative limit (must be positive)', () => {
|
|
321
|
+
expect(ListDealsQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
|
|
322
|
+
expect(ListDealsQuerySchema.safeParse({ limit: '-1' }).success).toBe(false)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('rejects a negative offset', () => {
|
|
326
|
+
expect(ListDealsQuerySchema.safeParse({ offset: '-1' }).success).toBe(false)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('accepts zero offset', () => {
|
|
330
|
+
expect(ListDealsQuerySchema.safeParse({ offset: '0' }).success).toBe(true)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('accepts a valid stage filter with search and pagination', () => {
|
|
334
|
+
const result = ListDealsQuerySchema.safeParse({
|
|
335
|
+
stage: 'interested',
|
|
336
|
+
search: 'acme',
|
|
337
|
+
limit: '25',
|
|
338
|
+
offset: '10'
|
|
339
|
+
})
|
|
340
|
+
expect(result.success).toBe(true)
|
|
341
|
+
if (result.success) {
|
|
342
|
+
expect(result.data.stage).toBe('interested')
|
|
343
|
+
expect(result.data.search).toBe('acme')
|
|
344
|
+
expect(result.data.limit).toBe(25)
|
|
345
|
+
expect(result.data.offset).toBe(10)
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('rejects an invalid stage value', () => {
|
|
350
|
+
expect(ListDealsQuerySchema.safeParse({ stage: 'pipeline' }).success).toBe(false)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('rejects unknown query fields (strict mode)', () => {
|
|
354
|
+
expect(ListDealsQuerySchema.safeParse({ unknownParam: 'x' }).success).toBe(false)
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// UpdateContactRequestSchema
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
describe('UpdateContactRequestSchema', () => {
|
|
363
|
+
it('rejects an object with no fields provided', () => {
|
|
364
|
+
const result = UpdateContactRequestSchema.safeParse({})
|
|
365
|
+
expect(result.success).toBe(false)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('accepts a single field: firstName', () => {
|
|
369
|
+
expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice' }).success).toBe(true)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('accepts a single field: emailValid', () => {
|
|
373
|
+
for (const v of ['VALID', 'INVALID', 'RISKY', 'UNKNOWN'] as const) {
|
|
374
|
+
expect(UpdateContactRequestSchema.safeParse({ emailValid: v }).success).toBe(true)
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('rejects an invalid emailValid value', () => {
|
|
379
|
+
expect(UpdateContactRequestSchema.safeParse({ emailValid: 'maybe' }).success).toBe(false)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('accepts a valid UUID for companyId', () => {
|
|
383
|
+
expect(UpdateContactRequestSchema.safeParse({ companyId: VALID_UUID }).success).toBe(true)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('rejects a non-UUID for companyId', () => {
|
|
387
|
+
expect(UpdateContactRequestSchema.safeParse({ companyId: 'bad-id' }).success).toBe(false)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('accepts a valid LinkedIn URL', () => {
|
|
391
|
+
expect(UpdateContactRequestSchema.safeParse({ linkedinUrl: 'https://linkedin.com/in/alice' }).success).toBe(true)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('rejects a non-URL for linkedinUrl', () => {
|
|
395
|
+
expect(UpdateContactRequestSchema.safeParse({ linkedinUrl: 'not-a-url' }).success).toBe(false)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('accepts multiple fields at once', () => {
|
|
399
|
+
expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice', lastName: 'Smith', title: 'CEO' }).success).toBe(
|
|
400
|
+
true
|
|
401
|
+
)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('rejects unknown top-level fields (strict mode)', () => {
|
|
405
|
+
expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice', unknown: 'x' }).success).toBe(false)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('rejects an empty string for firstName (min 1 after trim)', () => {
|
|
409
|
+
expect(UpdateContactRequestSchema.safeParse({ firstName: '' }).success).toBe(false)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('rejects a firstName exceeding 255 chars', () => {
|
|
413
|
+
expect(UpdateContactRequestSchema.safeParse({ firstName: 'a'.repeat(256) }).success).toBe(false)
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Response schemas — smoke-level forward-compat checks
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
describe('DealDetailResponseSchema (forward-compat)', () => {
|
|
422
|
+
const baseDeal = {
|
|
423
|
+
id: VALID_UUID,
|
|
424
|
+
organization_id: VALID_UUID,
|
|
425
|
+
contact_id: null,
|
|
426
|
+
contact_email: 'test@example.com',
|
|
427
|
+
pipeline_key: 'default',
|
|
428
|
+
stage_key: null,
|
|
429
|
+
state_key: null,
|
|
430
|
+
activity_log: [],
|
|
431
|
+
discovery_data: null,
|
|
432
|
+
discovery_submitted_at: null,
|
|
433
|
+
discovery_submitted_by: null,
|
|
434
|
+
proposal_data: null,
|
|
435
|
+
proposal_sent_at: null,
|
|
436
|
+
proposal_pdf_url: null,
|
|
437
|
+
signature_envelope_id: null,
|
|
438
|
+
source_list_id: null,
|
|
439
|
+
source_type: null,
|
|
440
|
+
initial_fee: null,
|
|
441
|
+
monthly_fee: null,
|
|
442
|
+
closed_lost_at: null,
|
|
443
|
+
closed_lost_reason: null,
|
|
444
|
+
created_at: ISO_TS,
|
|
445
|
+
updated_at: ISO_TS,
|
|
446
|
+
contact: null
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
it('accepts a deal with null contact', () => {
|
|
450
|
+
expect(DealDetailResponseSchema.safeParse(baseDeal).success).toBe(true)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('accepts a deal with nested contact that has null company', () => {
|
|
454
|
+
const withContact = {
|
|
455
|
+
...baseDeal,
|
|
456
|
+
contact: {
|
|
457
|
+
id: VALID_UUID,
|
|
458
|
+
first_name: 'Alice',
|
|
459
|
+
last_name: 'Smith',
|
|
460
|
+
email: 'alice@example.com',
|
|
461
|
+
title: null,
|
|
462
|
+
headline: null,
|
|
463
|
+
linkedin_url: null,
|
|
464
|
+
pipeline_status: null,
|
|
465
|
+
enrichment_data: null,
|
|
466
|
+
company: null
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
expect(DealDetailResponseSchema.safeParse(withContact).success).toBe(true)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('accepts extra unknown fields at top level (not strict)', () => {
|
|
473
|
+
expect(DealDetailResponseSchema.safeParse({ ...baseDeal, futureField: 'value' }).success).toBe(true)
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
describe('DealListResponseSchema', () => {
|
|
478
|
+
it('accepts a valid paginated list response', () => {
|
|
479
|
+
const result = DealListResponseSchema.safeParse({
|
|
480
|
+
data: [],
|
|
481
|
+
total: 0,
|
|
482
|
+
limit: 50,
|
|
483
|
+
offset: 0
|
|
484
|
+
})
|
|
485
|
+
expect(result.success).toBe(true)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it('rejects non-integer total', () => {
|
|
489
|
+
expect(DealListResponseSchema.safeParse({ data: [], total: 1.5, limit: 50, offset: 0 }).success).toBe(false)
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
describe('DealNoteResponseSchema', () => {
|
|
494
|
+
it('accepts a valid note response', () => {
|
|
495
|
+
const result = DealNoteResponseSchema.safeParse({
|
|
496
|
+
id: VALID_UUID,
|
|
497
|
+
dealId: VALID_UUID,
|
|
498
|
+
organizationId: VALID_UUID,
|
|
499
|
+
authorUserId: null,
|
|
500
|
+
body: 'A note',
|
|
501
|
+
createdAt: ISO_TS,
|
|
502
|
+
updatedAt: ISO_TS
|
|
503
|
+
})
|
|
504
|
+
expect(result.success).toBe(true)
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('accepts extra fields (not strict)', () => {
|
|
508
|
+
const result = DealNoteResponseSchema.safeParse({
|
|
509
|
+
id: VALID_UUID,
|
|
510
|
+
dealId: VALID_UUID,
|
|
511
|
+
organizationId: VALID_UUID,
|
|
512
|
+
authorUserId: null,
|
|
513
|
+
body: 'A note',
|
|
514
|
+
createdAt: ISO_TS,
|
|
515
|
+
updatedAt: ISO_TS,
|
|
516
|
+
futureField: 'ignored'
|
|
517
|
+
})
|
|
518
|
+
expect(result.success).toBe(true)
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe('DealTaskResponseSchema', () => {
|
|
523
|
+
it('accepts a valid task response', () => {
|
|
524
|
+
const result = DealTaskResponseSchema.safeParse({
|
|
525
|
+
id: VALID_UUID,
|
|
526
|
+
organizationId: VALID_UUID,
|
|
527
|
+
dealId: VALID_UUID,
|
|
528
|
+
title: 'Call prospect',
|
|
529
|
+
description: null,
|
|
530
|
+
kind: 'call',
|
|
531
|
+
dueAt: null,
|
|
532
|
+
assigneeUserId: null,
|
|
533
|
+
completedAt: null,
|
|
534
|
+
completedByUserId: null,
|
|
535
|
+
createdAt: ISO_TS,
|
|
536
|
+
updatedAt: ISO_TS,
|
|
537
|
+
createdByUserId: null
|
|
538
|
+
})
|
|
539
|
+
expect(result.success).toBe(true)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('rejects an invalid kind in task response', () => {
|
|
543
|
+
const result = DealTaskResponseSchema.safeParse({
|
|
544
|
+
id: VALID_UUID,
|
|
545
|
+
organizationId: VALID_UUID,
|
|
546
|
+
dealId: VALID_UUID,
|
|
547
|
+
title: 'Call',
|
|
548
|
+
description: null,
|
|
549
|
+
kind: 'text_message',
|
|
550
|
+
dueAt: null,
|
|
551
|
+
assigneeUserId: null,
|
|
552
|
+
completedAt: null,
|
|
553
|
+
completedByUserId: null,
|
|
554
|
+
createdAt: ISO_TS,
|
|
555
|
+
updatedAt: ISO_TS,
|
|
556
|
+
createdByUserId: null
|
|
557
|
+
})
|
|
558
|
+
expect(result.success).toBe(false)
|
|
559
|
+
})
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
// ListStatusSchema
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
describe('ListStatusSchema', () => {
|
|
567
|
+
it.each(['draft', 'enriching', 'launched', 'closing', 'archived'])('accepts "%s"', (status) => {
|
|
568
|
+
expect(ListStatusSchema.safeParse(status).success).toBe(true)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('rejects unknown status', () => {
|
|
572
|
+
expect(ListStatusSchema.safeParse('active').success).toBe(false)
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
// PipelineStageSchema
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
|
|
580
|
+
describe('PipelineStageSchema', () => {
|
|
581
|
+
it('accepts a key present in LEAD_GEN_STAGE_CATALOG', () => {
|
|
582
|
+
for (const key of Object.keys(LEAD_GEN_STAGE_CATALOG)) {
|
|
583
|
+
expect(PipelineStageSchema.safeParse({ key }).success).toBe(true)
|
|
584
|
+
}
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('rejects a key not in LEAD_GEN_STAGE_CATALOG', () => {
|
|
588
|
+
const result = PipelineStageSchema.safeParse({ key: 'not_a_real_stage' })
|
|
589
|
+
expect(result.success).toBe(false)
|
|
590
|
+
if (!result.success) {
|
|
591
|
+
expect(result.error.issues[0]?.message).toMatch(/LEAD_GEN_STAGE_CATALOG/)
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('accepts optional label, enabled, and order fields', () => {
|
|
596
|
+
expect(PipelineStageSchema.safeParse({ key: 'scraped', label: 'Scraped', enabled: true, order: 1 }).success).toBe(
|
|
597
|
+
true
|
|
598
|
+
)
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// ScrapingConfigSchema
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
describe('ScrapingConfigSchema', () => {
|
|
607
|
+
it('accepts an empty object (all fields optional)', () => {
|
|
608
|
+
expect(ScrapingConfigSchema.safeParse({}).success).toBe(true)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
it('accepts a fully populated config', () => {
|
|
612
|
+
expect(
|
|
613
|
+
ScrapingConfigSchema.safeParse({
|
|
614
|
+
vertical: 'SaaS',
|
|
615
|
+
geography: 'USA',
|
|
616
|
+
size: '10-50',
|
|
617
|
+
apifyInput: { actorId: 'test' }
|
|
618
|
+
}).success
|
|
619
|
+
).toBe(true)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('rejects a vertical exceeding 255 chars', () => {
|
|
623
|
+
expect(ScrapingConfigSchema.safeParse({ vertical: 'a'.repeat(256) }).success).toBe(false)
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
// IcpRubricSchema
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
|
|
631
|
+
describe('IcpRubricSchema', () => {
|
|
632
|
+
it('accepts an empty object', () => {
|
|
633
|
+
expect(IcpRubricSchema.safeParse({}).success).toBe(true)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('accepts valid minRating boundary values 0 and 5', () => {
|
|
637
|
+
expect(IcpRubricSchema.safeParse({ minRating: 0 }).success).toBe(true)
|
|
638
|
+
expect(IcpRubricSchema.safeParse({ minRating: 5 }).success).toBe(true)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
it('rejects minRating above 5', () => {
|
|
642
|
+
expect(IcpRubricSchema.safeParse({ minRating: 5.1 }).success).toBe(false)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('rejects negative minRating', () => {
|
|
646
|
+
expect(IcpRubricSchema.safeParse({ minRating: -1 }).success).toBe(false)
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// CreateListRequestSchema
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
|
|
654
|
+
describe('CreateListRequestSchema', () => {
|
|
655
|
+
it('accepts a minimal valid payload (name only)', () => {
|
|
656
|
+
expect(CreateListRequestSchema.safeParse({ name: 'My List' }).success).toBe(true)
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
it('rejects an empty name', () => {
|
|
660
|
+
expect(CreateListRequestSchema.safeParse({ name: '' }).success).toBe(false)
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
it('rejects a name exceeding 255 chars', () => {
|
|
664
|
+
expect(CreateListRequestSchema.safeParse({ name: 'a'.repeat(256) }).success).toBe(false)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it('accepts an optional status from ListStatusSchema', () => {
|
|
668
|
+
expect(CreateListRequestSchema.safeParse({ name: 'X', status: 'draft' }).success).toBe(true)
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('rejects an invalid status', () => {
|
|
672
|
+
expect(CreateListRequestSchema.safeParse({ name: 'X', status: 'active' }).success).toBe(false)
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
676
|
+
expect(CreateListRequestSchema.safeParse({ name: 'X', bogus: true }).success).toBe(false)
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('accepts nested scrapingConfig, icp, and pipelineConfig', () => {
|
|
680
|
+
expect(
|
|
681
|
+
CreateListRequestSchema.safeParse({
|
|
682
|
+
name: 'SaaS List',
|
|
683
|
+
scrapingConfig: { vertical: 'SaaS' },
|
|
684
|
+
icp: { minReviewCount: 5 },
|
|
685
|
+
pipelineConfig: { stages: [] }
|
|
686
|
+
}).success
|
|
687
|
+
).toBe(true)
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// UpdateListRequestSchema
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
describe('UpdateListRequestSchema', () => {
|
|
696
|
+
it('rejects an object with none of the required fields', () => {
|
|
697
|
+
const result = UpdateListRequestSchema.safeParse({})
|
|
698
|
+
expect(result.success).toBe(false)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('accepts providing only name', () => {
|
|
702
|
+
expect(UpdateListRequestSchema.safeParse({ name: 'New Name' }).success).toBe(true)
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
it('accepts providing only description', () => {
|
|
706
|
+
expect(UpdateListRequestSchema.safeParse({ description: 'Desc' }).success).toBe(true)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('accepts providing only batchIds', () => {
|
|
710
|
+
expect(UpdateListRequestSchema.safeParse({ batchIds: ['batch-1'] }).success).toBe(true)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
714
|
+
expect(UpdateListRequestSchema.safeParse({ name: 'X', extra: 'bad' }).success).toBe(false)
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
// UpdateListStatusRequestSchema
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
describe('UpdateListStatusRequestSchema', () => {
|
|
723
|
+
it('accepts a valid status', () => {
|
|
724
|
+
expect(UpdateListStatusRequestSchema.safeParse({ status: 'launched' }).success).toBe(true)
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it('rejects an invalid status', () => {
|
|
728
|
+
expect(UpdateListStatusRequestSchema.safeParse({ status: 'active' }).success).toBe(false)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
it('rejects missing status field', () => {
|
|
732
|
+
expect(UpdateListStatusRequestSchema.safeParse({}).success).toBe(false)
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
736
|
+
expect(UpdateListStatusRequestSchema.safeParse({ status: 'draft', extra: 'x' }).success).toBe(false)
|
|
737
|
+
})
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
// UpdateListConfigRequestSchema
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
|
|
744
|
+
describe('UpdateListConfigRequestSchema', () => {
|
|
745
|
+
it('rejects an object with none of the config fields', () => {
|
|
746
|
+
expect(UpdateListConfigRequestSchema.safeParse({}).success).toBe(false)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('accepts providing only scrapingConfig', () => {
|
|
750
|
+
expect(UpdateListConfigRequestSchema.safeParse({ scrapingConfig: { vertical: 'SaaS' } }).success).toBe(true)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
it('accepts providing only icp', () => {
|
|
754
|
+
expect(UpdateListConfigRequestSchema.safeParse({ icp: { minReviewCount: 3 } }).success).toBe(true)
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('accepts providing only pipelineConfig', () => {
|
|
758
|
+
expect(UpdateListConfigRequestSchema.safeParse({ pipelineConfig: { stages: [] } }).success).toBe(true)
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
762
|
+
expect(UpdateListConfigRequestSchema.safeParse({ scrapingConfig: {}, bogus: true }).success).toBe(false)
|
|
763
|
+
})
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// AddCompaniesToListRequestSchema / AddContactsToListRequestSchema
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
describe('AddCompaniesToListRequestSchema', () => {
|
|
771
|
+
it('accepts a valid list with one UUID', () => {
|
|
772
|
+
expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: [VALID_UUID] }).success).toBe(true)
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
it('rejects an empty array (min 1)', () => {
|
|
776
|
+
expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: [] }).success).toBe(false)
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
it('rejects more than 1000 IDs (max 1000)', () => {
|
|
780
|
+
const ids = Array.from({ length: 1001 }, (_, i) => `00000000-0000-0000-0000-${String(i).padStart(12, '0')}`)
|
|
781
|
+
expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: ids }).success).toBe(false)
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('rejects non-UUID entries', () => {
|
|
785
|
+
expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: ['not-a-uuid'] }).success).toBe(false)
|
|
786
|
+
})
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
describe('AddContactsToListRequestSchema', () => {
|
|
790
|
+
it('accepts a valid list with one UUID', () => {
|
|
791
|
+
expect(AddContactsToListRequestSchema.safeParse({ contactIds: [VALID_UUID] }).success).toBe(true)
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
it('rejects an empty array (min 1)', () => {
|
|
795
|
+
expect(AddContactsToListRequestSchema.safeParse({ contactIds: [] }).success).toBe(false)
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
// ListCompaniesQuerySchema
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
describe('ListCompaniesQuerySchema', () => {
|
|
804
|
+
it('accepts an empty query (defaults applied)', () => {
|
|
805
|
+
const result = ListCompaniesQuerySchema.safeParse({})
|
|
806
|
+
expect(result.success).toBe(true)
|
|
807
|
+
if (result.success) {
|
|
808
|
+
expect(result.data.limit).toBe(50)
|
|
809
|
+
expect(result.data.offset).toBe(0)
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('coerces limit from string "100" to number 100', () => {
|
|
814
|
+
const result = ListCompaniesQuerySchema.safeParse({ limit: '100' })
|
|
815
|
+
expect(result.success).toBe(true)
|
|
816
|
+
if (result.success) expect(result.data.limit).toBe(100)
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
it('rejects limit exceeding 5000', () => {
|
|
820
|
+
expect(ListCompaniesQuerySchema.safeParse({ limit: '5001' }).success).toBe(false)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('rejects limit less than 1', () => {
|
|
824
|
+
expect(ListCompaniesQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
it('accepts a valid status filter', () => {
|
|
828
|
+
expect(ListCompaniesQuerySchema.safeParse({ status: 'active' }).success).toBe(true)
|
|
829
|
+
expect(ListCompaniesQuerySchema.safeParse({ status: 'invalid' }).success).toBe(true)
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
it('rejects an invalid status', () => {
|
|
833
|
+
expect(ListCompaniesQuerySchema.safeParse({ status: 'pending' }).success).toBe(false)
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('accepts includeAll as boolean-like strings', () => {
|
|
837
|
+
const r1 = ListCompaniesQuerySchema.safeParse({ includeAll: 'true' })
|
|
838
|
+
expect(r1.success).toBe(true)
|
|
839
|
+
if (r1.success) expect(r1.data.includeAll).toBe(true)
|
|
840
|
+
|
|
841
|
+
const r2 = ListCompaniesQuerySchema.safeParse({ includeAll: 'false' })
|
|
842
|
+
expect(r2.success).toBe(true)
|
|
843
|
+
if (r2.success) expect(r2.data.includeAll).toBe(false)
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
847
|
+
expect(ListCompaniesQuerySchema.safeParse({ unknownField: 'x' }).success).toBe(false)
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
// ---------------------------------------------------------------------------
|
|
852
|
+
// ListContactsQuerySchema
|
|
853
|
+
// ---------------------------------------------------------------------------
|
|
854
|
+
|
|
855
|
+
describe('ListContactsQuerySchema', () => {
|
|
856
|
+
it('accepts an empty query with defaults', () => {
|
|
857
|
+
const result = ListContactsQuerySchema.safeParse({})
|
|
858
|
+
expect(result.success).toBe(true)
|
|
859
|
+
if (result.success) {
|
|
860
|
+
expect(result.data.limit).toBe(5000)
|
|
861
|
+
expect(result.data.offset).toBe(0)
|
|
862
|
+
}
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
it('accepts openingLineIsNull as "true"', () => {
|
|
866
|
+
const result = ListContactsQuerySchema.safeParse({ openingLineIsNull: 'true' })
|
|
867
|
+
expect(result.success).toBe(true)
|
|
868
|
+
if (result.success) expect(result.data.openingLineIsNull).toBe(true)
|
|
869
|
+
})
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
// ---------------------------------------------------------------------------
|
|
873
|
+
// ListDealTasksDueQuerySchema
|
|
874
|
+
// ---------------------------------------------------------------------------
|
|
875
|
+
|
|
876
|
+
describe('ListDealTasksDueQuerySchema', () => {
|
|
877
|
+
it('accepts an empty query', () => {
|
|
878
|
+
expect(ListDealTasksDueQuerySchema.safeParse({}).success).toBe(true)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
it.each(['overdue', 'today', 'today_and_overdue', 'upcoming'])('accepts window "%s"', (window) => {
|
|
882
|
+
expect(ListDealTasksDueQuerySchema.safeParse({ window }).success).toBe(true)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
it('rejects an invalid window value', () => {
|
|
886
|
+
expect(ListDealTasksDueQuerySchema.safeParse({ window: 'this_week' }).success).toBe(false)
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
it('accepts a valid UUID for assigneeUserId', () => {
|
|
890
|
+
expect(ListDealTasksDueQuerySchema.safeParse({ assigneeUserId: VALID_UUID }).success).toBe(true)
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
894
|
+
expect(ListDealTasksDueQuerySchema.safeParse({ window: 'today', extra: 'x' }).success).toBe(false)
|
|
895
|
+
})
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
// ---------------------------------------------------------------------------
|
|
899
|
+
// CreateCompanyRequestSchema
|
|
900
|
+
// ---------------------------------------------------------------------------
|
|
901
|
+
|
|
902
|
+
describe('CreateCompanyRequestSchema', () => {
|
|
903
|
+
it('accepts a minimal payload (name only)', () => {
|
|
904
|
+
expect(CreateCompanyRequestSchema.safeParse({ name: 'Acme' }).success).toBe(true)
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
it('rejects an empty name', () => {
|
|
908
|
+
expect(CreateCompanyRequestSchema.safeParse({ name: '' }).success).toBe(false)
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it('rejects a non-URL for linkedinUrl', () => {
|
|
912
|
+
expect(CreateCompanyRequestSchema.safeParse({ name: 'Acme', linkedinUrl: 'linkedin.com/co/acme' }).success).toBe(
|
|
913
|
+
false
|
|
914
|
+
)
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
it('accepts a valid URL for linkedinUrl', () => {
|
|
918
|
+
expect(
|
|
919
|
+
CreateCompanyRequestSchema.safeParse({ name: 'Acme', linkedinUrl: 'https://linkedin.com/company/acme' }).success
|
|
920
|
+
).toBe(true)
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
924
|
+
expect(CreateCompanyRequestSchema.safeParse({ name: 'Acme', bogus: true }).success).toBe(false)
|
|
925
|
+
})
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
// ---------------------------------------------------------------------------
|
|
929
|
+
// UpdateCompanyRequestSchema
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
describe('UpdateCompanyRequestSchema', () => {
|
|
933
|
+
it('rejects an object with no fields (at-least-one refine)', () => {
|
|
934
|
+
expect(UpdateCompanyRequestSchema.safeParse({}).success).toBe(false)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
it('accepts providing only name', () => {
|
|
938
|
+
expect(UpdateCompanyRequestSchema.safeParse({ name: 'Acme Corp' }).success).toBe(true)
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
it('accepts providing only status', () => {
|
|
942
|
+
expect(UpdateCompanyRequestSchema.safeParse({ status: 'active' }).success).toBe(true)
|
|
943
|
+
expect(UpdateCompanyRequestSchema.safeParse({ status: 'invalid' }).success).toBe(true)
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
it('accepts numEmployees of 0', () => {
|
|
947
|
+
expect(UpdateCompanyRequestSchema.safeParse({ numEmployees: 0 }).success).toBe(true)
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it('rejects negative numEmployees', () => {
|
|
951
|
+
expect(UpdateCompanyRequestSchema.safeParse({ numEmployees: -1 }).success).toBe(false)
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
955
|
+
expect(UpdateCompanyRequestSchema.safeParse({ name: 'X', extra: true }).success).toBe(false)
|
|
956
|
+
})
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
// CreateContactRequestSchema
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
describe('CreateContactRequestSchema', () => {
|
|
964
|
+
it('accepts a minimal payload (email only)', () => {
|
|
965
|
+
expect(CreateContactRequestSchema.safeParse({ email: 'test@example.com' }).success).toBe(true)
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
it('rejects an invalid email', () => {
|
|
969
|
+
expect(CreateContactRequestSchema.safeParse({ email: 'not-an-email' }).success).toBe(false)
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it('rejects an empty email', () => {
|
|
973
|
+
expect(CreateContactRequestSchema.safeParse({ email: '' }).success).toBe(false)
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
it('accepts an optional companyId as UUID', () => {
|
|
977
|
+
expect(CreateContactRequestSchema.safeParse({ email: 'a@b.com', companyId: VALID_UUID }).success).toBe(true)
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
981
|
+
expect(CreateContactRequestSchema.safeParse({ email: 'a@b.com', unknown: 'x' }).success).toBe(false)
|
|
982
|
+
})
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
// ---------------------------------------------------------------------------
|
|
986
|
+
// AcqEmailValidSchema / AcqContactStatusSchema / AcqCompanyStatusSchema
|
|
987
|
+
// ---------------------------------------------------------------------------
|
|
988
|
+
|
|
989
|
+
describe('AcqEmailValidSchema', () => {
|
|
990
|
+
it.each(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])('accepts "%s"', (v) => {
|
|
991
|
+
expect(AcqEmailValidSchema.safeParse(v).success).toBe(true)
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
it('rejects lowercase or unknown value', () => {
|
|
995
|
+
expect(AcqEmailValidSchema.safeParse('valid').success).toBe(false)
|
|
996
|
+
expect(AcqEmailValidSchema.safeParse('pending').success).toBe(false)
|
|
997
|
+
})
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
describe('AcqContactStatusSchema', () => {
|
|
1001
|
+
it.each(['active', 'invalid'])('accepts "%s"', (s) => {
|
|
1002
|
+
expect(AcqContactStatusSchema.safeParse(s).success).toBe(true)
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
it('rejects unknown status', () => {
|
|
1006
|
+
expect(AcqContactStatusSchema.safeParse('deleted').success).toBe(false)
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
// ---------------------------------------------------------------------------
|
|
1011
|
+
// AcqArtifactOwnerKindSchema
|
|
1012
|
+
// ---------------------------------------------------------------------------
|
|
1013
|
+
|
|
1014
|
+
describe('AcqArtifactOwnerKindSchema', () => {
|
|
1015
|
+
it.each(['company', 'contact', 'deal', 'list', 'list_member'])('accepts "%s"', (kind) => {
|
|
1016
|
+
expect(AcqArtifactOwnerKindSchema.safeParse(kind).success).toBe(true)
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
it('rejects unknown owner kind', () => {
|
|
1020
|
+
expect(AcqArtifactOwnerKindSchema.safeParse('organization').success).toBe(false)
|
|
1021
|
+
})
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
// ---------------------------------------------------------------------------
|
|
1025
|
+
// ListArtifactsQuerySchema
|
|
1026
|
+
// ---------------------------------------------------------------------------
|
|
1027
|
+
|
|
1028
|
+
describe('ListArtifactsQuerySchema', () => {
|
|
1029
|
+
it('accepts a valid query', () => {
|
|
1030
|
+
expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'deal', ownerId: VALID_UUID }).success).toBe(true)
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
it('rejects an invalid ownerKind', () => {
|
|
1034
|
+
expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'org', ownerId: VALID_UUID }).success).toBe(false)
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
it('rejects a non-UUID ownerId', () => {
|
|
1038
|
+
expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'deal', ownerId: 'not-a-uuid' }).success).toBe(false)
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
1042
|
+
expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'deal', ownerId: VALID_UUID, extra: 'x' }).success).toBe(
|
|
1043
|
+
false
|
|
1044
|
+
)
|
|
1045
|
+
})
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
// ---------------------------------------------------------------------------
|
|
1049
|
+
// CreateArtifactRequestSchema
|
|
1050
|
+
// ---------------------------------------------------------------------------
|
|
1051
|
+
|
|
1052
|
+
describe('CreateArtifactRequestSchema', () => {
|
|
1053
|
+
const valid = {
|
|
1054
|
+
ownerKind: 'deal' as const,
|
|
1055
|
+
ownerId: VALID_UUID,
|
|
1056
|
+
kind: 'proposal',
|
|
1057
|
+
content: { url: 'https://example.com' }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
it('accepts a minimal valid payload', () => {
|
|
1061
|
+
expect(CreateArtifactRequestSchema.safeParse(valid).success).toBe(true)
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
it('accepts an optional sourceExecutionId', () => {
|
|
1065
|
+
expect(CreateArtifactRequestSchema.safeParse({ ...valid, sourceExecutionId: VALID_UUID }).success).toBe(true)
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
it('rejects a non-UUID sourceExecutionId', () => {
|
|
1069
|
+
expect(CreateArtifactRequestSchema.safeParse({ ...valid, sourceExecutionId: 'not-a-uuid' }).success).toBe(false)
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it('rejects an empty kind', () => {
|
|
1073
|
+
expect(CreateArtifactRequestSchema.safeParse({ ...valid, kind: '' }).success).toBe(false)
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
1077
|
+
expect(CreateArtifactRequestSchema.safeParse({ ...valid, bogus: true }).success).toBe(false)
|
|
1078
|
+
})
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
// ---------------------------------------------------------------------------
|
|
1082
|
+
// ListMembersQuerySchema
|
|
1083
|
+
// ---------------------------------------------------------------------------
|
|
1084
|
+
|
|
1085
|
+
describe('ListMembersQuerySchema', () => {
|
|
1086
|
+
it('accepts an empty query with defaults', () => {
|
|
1087
|
+
const result = ListMembersQuerySchema.safeParse({})
|
|
1088
|
+
expect(result.success).toBe(true)
|
|
1089
|
+
if (result.success) {
|
|
1090
|
+
expect(result.data.limit).toBe(50)
|
|
1091
|
+
expect(result.data.offset).toBe(0)
|
|
1092
|
+
}
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
it('rejects limit exceeding 500', () => {
|
|
1096
|
+
expect(ListMembersQuerySchema.safeParse({ limit: '501' }).success).toBe(false)
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
it('rejects limit less than 1', () => {
|
|
1100
|
+
expect(ListMembersQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
it('rejects unknown fields (strict mode)', () => {
|
|
1104
|
+
expect(ListMembersQuerySchema.safeParse({ extra: 'x' }).success).toBe(false)
|
|
1105
|
+
})
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
// ---------------------------------------------------------------------------
|
|
1109
|
+
// AcqListResponseSchema (forward-compat)
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
|
|
1112
|
+
describe('AcqListResponseSchema (forward-compat)', () => {
|
|
1113
|
+
const baseList = {
|
|
1114
|
+
id: VALID_UUID,
|
|
1115
|
+
organizationId: VALID_UUID,
|
|
1116
|
+
name: 'Test List',
|
|
1117
|
+
description: null,
|
|
1118
|
+
type: 'standard',
|
|
1119
|
+
batchIds: [],
|
|
1120
|
+
instantlyCampaignId: null,
|
|
1121
|
+
status: 'draft' as const,
|
|
1122
|
+
metadata: {},
|
|
1123
|
+
launchedAt: null,
|
|
1124
|
+
completedAt: null,
|
|
1125
|
+
createdAt: ISO_TS,
|
|
1126
|
+
scrapingConfig: {},
|
|
1127
|
+
icp: {},
|
|
1128
|
+
pipelineConfig: {}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
it('accepts a valid list response', () => {
|
|
1132
|
+
expect(AcqListResponseSchema.safeParse(baseList).success).toBe(true)
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
it('accepts extra fields at top level (not strict)', () => {
|
|
1136
|
+
expect(AcqListResponseSchema.safeParse({ ...baseList, futureField: 'extra' }).success).toBe(true)
|
|
1137
|
+
})
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
// AcqContactResponseSchema (forward-compat)
|
|
1142
|
+
// ---------------------------------------------------------------------------
|
|
1143
|
+
|
|
1144
|
+
describe('AcqContactResponseSchema (forward-compat)', () => {
|
|
1145
|
+
const baseContact = {
|
|
1146
|
+
id: VALID_UUID,
|
|
1147
|
+
organizationId: VALID_UUID,
|
|
1148
|
+
companyId: null,
|
|
1149
|
+
email: 'alice@example.com',
|
|
1150
|
+
emailValid: null,
|
|
1151
|
+
firstName: null,
|
|
1152
|
+
lastName: null,
|
|
1153
|
+
linkedinUrl: null,
|
|
1154
|
+
title: null,
|
|
1155
|
+
headline: null,
|
|
1156
|
+
filterReason: null,
|
|
1157
|
+
openingLine: null,
|
|
1158
|
+
source: null,
|
|
1159
|
+
sourceId: null,
|
|
1160
|
+
pipelineStatus: null,
|
|
1161
|
+
enrichmentData: null,
|
|
1162
|
+
attioPersonId: null,
|
|
1163
|
+
batchId: null,
|
|
1164
|
+
status: 'active' as const,
|
|
1165
|
+
createdAt: ISO_TS,
|
|
1166
|
+
updatedAt: ISO_TS
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
it('accepts a valid contact response', () => {
|
|
1170
|
+
expect(AcqContactResponseSchema.safeParse(baseContact).success).toBe(true)
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
it('accepts extra fields (not strict)', () => {
|
|
1174
|
+
expect(AcqContactResponseSchema.safeParse({ ...baseContact, newField: 'x' }).success).toBe(true)
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
it('rejects an invalid emailValid value', () => {
|
|
1178
|
+
expect(AcqContactResponseSchema.safeParse({ ...baseContact, emailValid: 'BAD' }).success).toBe(false)
|
|
1179
|
+
})
|
|
1180
|
+
})
|