@elevasis/core 0.2.0 → 0.3.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 +60 -103
- package/dist/index.js +162 -109
- package/dist/organization-model/index.d.ts +60 -103
- package/dist/organization-model/index.js +162 -109
- package/package.json +1 -1
- package/src/README.md +24 -17
- package/src/__tests__/template-foundations-compatibility.test.ts +28 -36
- package/src/auth/multi-tenancy/types.ts +4 -11
- package/src/auth/multi-tenancy/users/api-schemas.ts +1 -1
- package/src/business/base-entities.test.ts +481 -0
- package/src/business/base-entities.ts +241 -0
- package/src/business/delivery/types.ts +1 -1
- package/src/business/index.ts +3 -0
- package/src/execution/index.ts +3 -6
- package/src/index.ts +1 -1
- package/src/organization-model/README.md +25 -26
- package/src/organization-model/__tests__/graph.test.ts +103 -71
- package/src/organization-model/__tests__/resolve.test.ts +20 -29
- package/src/organization-model/contracts.ts +3 -0
- package/src/organization-model/defaults.ts +40 -6
- package/src/organization-model/domains/features.ts +19 -54
- package/src/organization-model/domains/navigation.ts +25 -16
- package/src/organization-model/domains/shared.ts +1 -10
- package/src/organization-model/foundation.ts +96 -0
- package/src/organization-model/graph/build.ts +34 -67
- package/src/organization-model/graph/schema.ts +2 -4
- package/src/organization-model/graph/types.ts +3 -15
- package/src/organization-model/index.ts +2 -0
- package/src/organization-model/organization-model.mdx +34 -36
- package/src/organization-model/published.ts +12 -3
- package/src/organization-model/schema.ts +38 -34
- package/src/organization-model/types.ts +5 -10
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/sse/events.ts +1 -34
- package/src/projects/api-schemas.ts +2 -1
- package/src/reference/_generated/contracts.md +10 -31
- package/src/reference/glossary.md +14 -18
- package/src/supabase/database.types.ts +0 -107
- package/src/test-utils/rls/RLSTestContext.ts +1 -31
- package/src/execution/calibration/__tests__/schemas.test.ts +0 -320
- package/src/execution/calibration/index.ts +0 -3
- package/src/execution/calibration/schemas.ts +0 -121
- package/src/execution/calibration/sse-events.ts +0 -125
- package/src/execution/calibration/types.ts +0 -190
|
@@ -9,18 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Per-user-per-org config (stored in org_memberships.config)
|
|
12
|
-
* Controls which features a specific member can access within their org
|
|
13
|
-
*
|
|
12
|
+
* Controls which features a specific member can access within their org.
|
|
13
|
+
* Keys are feature IDs from the organization model (e.g. crm, lead-gen, projects, seo).
|
|
14
14
|
*/
|
|
15
15
|
export interface MembershipFeatureConfig {
|
|
16
|
-
features?:
|
|
17
|
-
operations?: boolean
|
|
18
|
-
monitoring?: boolean
|
|
19
|
-
acquisition?: boolean
|
|
20
|
-
delivery?: boolean
|
|
21
|
-
calibration?: boolean
|
|
22
|
-
seo?: boolean
|
|
23
|
-
}
|
|
16
|
+
features?: Record<string, boolean>
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
/**
|
|
@@ -37,7 +30,7 @@ export interface UserConfig {
|
|
|
37
30
|
| 'aurora'
|
|
38
31
|
| 'rose-gold'
|
|
39
32
|
| 'midnight'
|
|
40
|
-
| '
|
|
33
|
+
| 'titanium'
|
|
41
34
|
| 'obsidian'
|
|
42
35
|
| 'honey'
|
|
43
36
|
| 'abyss'
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Entity Contracts - Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that:
|
|
5
|
+
* - Base interfaces accept generic TMeta narrowing
|
|
6
|
+
* - Zod schemas parse valid data correctly
|
|
7
|
+
* - Schema extension via .extend() works for client metadata narrowing
|
|
8
|
+
* - Default metadata falls back to Record<string, unknown>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { z } from 'zod'
|
|
13
|
+
import {
|
|
14
|
+
BaseProjectSchema,
|
|
15
|
+
BaseMilestoneSchema,
|
|
16
|
+
BaseTaskSchema,
|
|
17
|
+
BaseDealSchema,
|
|
18
|
+
BaseCompanySchema,
|
|
19
|
+
BaseContactSchema,
|
|
20
|
+
type BaseProject,
|
|
21
|
+
type BaseMilestone,
|
|
22
|
+
type BaseTask,
|
|
23
|
+
type BaseDeal,
|
|
24
|
+
type BaseCompany,
|
|
25
|
+
type BaseContact
|
|
26
|
+
} from './base-entities'
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Fixtures
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const ORG_ID = '00000000-0000-0000-0000-000000000001'
|
|
33
|
+
const TIMESTAMPS = {
|
|
34
|
+
createdAt: '2026-04-17T00:00:00.000Z',
|
|
35
|
+
updatedAt: '2026-04-17T00:00:00.000Z'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// BaseProjectSchema
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
describe('BaseProjectSchema', () => {
|
|
43
|
+
const validProject = {
|
|
44
|
+
id: '00000000-0000-0000-0000-000000000010',
|
|
45
|
+
organizationId: ORG_ID,
|
|
46
|
+
name: 'Website Rebuild',
|
|
47
|
+
kind: 'client_engagement',
|
|
48
|
+
status: 'active',
|
|
49
|
+
description: 'Rebuild the client website.',
|
|
50
|
+
metadata: { budget: 50000 },
|
|
51
|
+
...TIMESTAMPS
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
it('parses a valid project', () => {
|
|
55
|
+
const result = BaseProjectSchema.safeParse(validProject)
|
|
56
|
+
expect(result.success).toBe(true)
|
|
57
|
+
if (result.success) {
|
|
58
|
+
expect(result.data.name).toBe('Website Rebuild')
|
|
59
|
+
expect(result.data.kind).toBe('client_engagement')
|
|
60
|
+
expect(result.data.status).toBe('active')
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('accepts null description', () => {
|
|
65
|
+
const result = BaseProjectSchema.safeParse({ ...validProject, description: null })
|
|
66
|
+
expect(result.success).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('defaults metadata to empty object when omitted', () => {
|
|
70
|
+
const { metadata: _meta, ...withoutMeta } = validProject
|
|
71
|
+
const result = BaseProjectSchema.safeParse(withoutMeta)
|
|
72
|
+
expect(result.success).toBe(true)
|
|
73
|
+
if (result.success) {
|
|
74
|
+
expect(result.data.metadata).toEqual({})
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('rejects missing required fields', () => {
|
|
79
|
+
const result = BaseProjectSchema.safeParse({ id: 'x', organizationId: ORG_ID })
|
|
80
|
+
expect(result.success).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('supports schema extension for narrowed metadata', () => {
|
|
84
|
+
const ProjectMetaSchema = z.object({ budget: z.number(), riskScore: z.enum(['low', 'medium', 'high']) })
|
|
85
|
+
const NarrowedProjectSchema = BaseProjectSchema.extend({ metadata: ProjectMetaSchema })
|
|
86
|
+
|
|
87
|
+
const result = NarrowedProjectSchema.safeParse({
|
|
88
|
+
...validProject,
|
|
89
|
+
metadata: { budget: 75000, riskScore: 'low' }
|
|
90
|
+
})
|
|
91
|
+
expect(result.success).toBe(true)
|
|
92
|
+
if (result.success) {
|
|
93
|
+
expect(result.data.metadata.budget).toBe(75000)
|
|
94
|
+
expect(result.data.metadata.riskScore).toBe('low')
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('rejects invalid narrowed metadata via extended schema', () => {
|
|
99
|
+
const ProjectMetaSchema = z.object({ budget: z.number() })
|
|
100
|
+
const NarrowedProjectSchema = BaseProjectSchema.extend({ metadata: ProjectMetaSchema })
|
|
101
|
+
|
|
102
|
+
const result = NarrowedProjectSchema.safeParse({
|
|
103
|
+
...validProject,
|
|
104
|
+
metadata: { budget: 'not-a-number' }
|
|
105
|
+
})
|
|
106
|
+
expect(result.success).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// BaseMilestoneSchema
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
describe('BaseMilestoneSchema', () => {
|
|
115
|
+
const validMilestone = {
|
|
116
|
+
id: '00000000-0000-0000-0000-000000000020',
|
|
117
|
+
organizationId: ORG_ID,
|
|
118
|
+
projectId: '00000000-0000-0000-0000-000000000010',
|
|
119
|
+
name: 'Phase 1 Complete',
|
|
120
|
+
status: 'upcoming',
|
|
121
|
+
description: null,
|
|
122
|
+
metadata: {},
|
|
123
|
+
...TIMESTAMPS
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
it('parses a valid milestone', () => {
|
|
127
|
+
const result = BaseMilestoneSchema.safeParse(validMilestone)
|
|
128
|
+
expect(result.success).toBe(true)
|
|
129
|
+
if (result.success) {
|
|
130
|
+
expect(result.data.projectId).toBe('00000000-0000-0000-0000-000000000010')
|
|
131
|
+
expect(result.data.status).toBe('upcoming')
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('defaults metadata to empty object when omitted', () => {
|
|
136
|
+
const { metadata: _meta, ...withoutMeta } = validMilestone
|
|
137
|
+
const result = BaseMilestoneSchema.safeParse(withoutMeta)
|
|
138
|
+
expect(result.success).toBe(true)
|
|
139
|
+
if (result.success) {
|
|
140
|
+
expect(result.data.metadata).toEqual({})
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('supports schema extension for narrowed metadata', () => {
|
|
145
|
+
const MilestoneMetaSchema = z.object({ deliverables: z.array(z.string()) })
|
|
146
|
+
const NarrowedSchema = BaseMilestoneSchema.extend({ metadata: MilestoneMetaSchema })
|
|
147
|
+
|
|
148
|
+
const result = NarrowedSchema.safeParse({
|
|
149
|
+
...validMilestone,
|
|
150
|
+
metadata: { deliverables: ['doc-1', 'doc-2'] }
|
|
151
|
+
})
|
|
152
|
+
expect(result.success).toBe(true)
|
|
153
|
+
if (result.success) {
|
|
154
|
+
expect(result.data.metadata.deliverables).toHaveLength(2)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// BaseTaskSchema
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
describe('BaseTaskSchema', () => {
|
|
164
|
+
const validTask = {
|
|
165
|
+
id: '00000000-0000-0000-0000-000000000030',
|
|
166
|
+
organizationId: ORG_ID,
|
|
167
|
+
projectId: '00000000-0000-0000-0000-000000000010',
|
|
168
|
+
name: 'Write API docs',
|
|
169
|
+
status: 'planned',
|
|
170
|
+
type: 'documentation',
|
|
171
|
+
description: 'Document all endpoints.',
|
|
172
|
+
metadata: { priority: 'high' },
|
|
173
|
+
...TIMESTAMPS
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
it('parses a valid task', () => {
|
|
177
|
+
const result = BaseTaskSchema.safeParse(validTask)
|
|
178
|
+
expect(result.success).toBe(true)
|
|
179
|
+
if (result.success) {
|
|
180
|
+
expect(result.data.type).toBe('documentation')
|
|
181
|
+
expect(result.data.status).toBe('planned')
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('accepts null description', () => {
|
|
186
|
+
const result = BaseTaskSchema.safeParse({ ...validTask, description: null })
|
|
187
|
+
expect(result.success).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('supports schema extension for narrowed metadata', () => {
|
|
191
|
+
const TaskMetaSchema = z.object({ storyPoints: z.number().int(), sprint: z.string().optional() })
|
|
192
|
+
const NarrowedSchema = BaseTaskSchema.extend({ metadata: TaskMetaSchema })
|
|
193
|
+
|
|
194
|
+
const result = NarrowedSchema.safeParse({
|
|
195
|
+
...validTask,
|
|
196
|
+
metadata: { storyPoints: 3, sprint: 'S4' }
|
|
197
|
+
})
|
|
198
|
+
expect(result.success).toBe(true)
|
|
199
|
+
if (result.success) {
|
|
200
|
+
expect(result.data.metadata.storyPoints).toBe(3)
|
|
201
|
+
expect(result.data.metadata.sprint).toBe('S4')
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// BaseDealSchema
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
describe('BaseDealSchema', () => {
|
|
211
|
+
const validDeal = {
|
|
212
|
+
id: '00000000-0000-0000-0000-000000000040',
|
|
213
|
+
organizationId: ORG_ID,
|
|
214
|
+
contactEmail: 'prospect@example.com',
|
|
215
|
+
stage: 'proposal',
|
|
216
|
+
metadata: {},
|
|
217
|
+
...TIMESTAMPS
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
it('parses a valid deal', () => {
|
|
221
|
+
const result = BaseDealSchema.safeParse(validDeal)
|
|
222
|
+
expect(result.success).toBe(true)
|
|
223
|
+
if (result.success) {
|
|
224
|
+
expect(result.data.contactEmail).toBe('prospect@example.com')
|
|
225
|
+
expect(result.data.stage).toBe('proposal')
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('accepts null stage', () => {
|
|
230
|
+
const result = BaseDealSchema.safeParse({ ...validDeal, stage: null })
|
|
231
|
+
expect(result.success).toBe(true)
|
|
232
|
+
if (result.success) {
|
|
233
|
+
expect(result.data.stage).toBeNull()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('defaults metadata to empty object when omitted', () => {
|
|
238
|
+
const { metadata: _meta, ...withoutMeta } = validDeal
|
|
239
|
+
const result = BaseDealSchema.safeParse(withoutMeta)
|
|
240
|
+
expect(result.success).toBe(true)
|
|
241
|
+
if (result.success) {
|
|
242
|
+
expect(result.data.metadata).toEqual({})
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('supports schema extension for narrowed metadata', () => {
|
|
247
|
+
const DealMetaSchema = z.object({ initialFee: z.number(), monthlyFee: z.number() })
|
|
248
|
+
const NarrowedSchema = BaseDealSchema.extend({ metadata: DealMetaSchema })
|
|
249
|
+
|
|
250
|
+
const result = NarrowedSchema.safeParse({
|
|
251
|
+
...validDeal,
|
|
252
|
+
metadata: { initialFee: 2000, monthlyFee: 500 }
|
|
253
|
+
})
|
|
254
|
+
expect(result.success).toBe(true)
|
|
255
|
+
if (result.success) {
|
|
256
|
+
expect(result.data.metadata.initialFee).toBe(2000)
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// BaseCompanySchema
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
describe('BaseCompanySchema', () => {
|
|
266
|
+
const validCompany = {
|
|
267
|
+
id: '00000000-0000-0000-0000-000000000050',
|
|
268
|
+
organizationId: ORG_ID,
|
|
269
|
+
name: 'Acme Corp',
|
|
270
|
+
domain: 'acme.com',
|
|
271
|
+
status: 'active',
|
|
272
|
+
metadata: {},
|
|
273
|
+
...TIMESTAMPS
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
it('parses a valid company', () => {
|
|
277
|
+
const result = BaseCompanySchema.safeParse(validCompany)
|
|
278
|
+
expect(result.success).toBe(true)
|
|
279
|
+
if (result.success) {
|
|
280
|
+
expect(result.data.name).toBe('Acme Corp')
|
|
281
|
+
expect(result.data.domain).toBe('acme.com')
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('accepts null domain', () => {
|
|
286
|
+
const result = BaseCompanySchema.safeParse({ ...validCompany, domain: null })
|
|
287
|
+
expect(result.success).toBe(true)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('supports schema extension for narrowed metadata', () => {
|
|
291
|
+
const CompanyMetaSchema = z.object({ reviewCount: z.number().int(), rating: z.number() })
|
|
292
|
+
const NarrowedSchema = BaseCompanySchema.extend({ metadata: CompanyMetaSchema })
|
|
293
|
+
|
|
294
|
+
const result = NarrowedSchema.safeParse({
|
|
295
|
+
...validCompany,
|
|
296
|
+
metadata: { reviewCount: 120, rating: 4.7 }
|
|
297
|
+
})
|
|
298
|
+
expect(result.success).toBe(true)
|
|
299
|
+
if (result.success) {
|
|
300
|
+
expect(result.data.metadata.reviewCount).toBe(120)
|
|
301
|
+
expect(result.data.metadata.rating).toBe(4.7)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// BaseContactSchema
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
describe('BaseContactSchema', () => {
|
|
311
|
+
const validContact = {
|
|
312
|
+
id: '00000000-0000-0000-0000-000000000060',
|
|
313
|
+
organizationId: ORG_ID,
|
|
314
|
+
email: 'jane@example.com',
|
|
315
|
+
firstName: 'Jane',
|
|
316
|
+
lastName: 'Doe',
|
|
317
|
+
status: 'active',
|
|
318
|
+
metadata: {},
|
|
319
|
+
...TIMESTAMPS
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
it('parses a valid contact', () => {
|
|
323
|
+
const result = BaseContactSchema.safeParse(validContact)
|
|
324
|
+
expect(result.success).toBe(true)
|
|
325
|
+
if (result.success) {
|
|
326
|
+
expect(result.data.email).toBe('jane@example.com')
|
|
327
|
+
expect(result.data.firstName).toBe('Jane')
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('accepts null firstName and lastName', () => {
|
|
332
|
+
const result = BaseContactSchema.safeParse({ ...validContact, firstName: null, lastName: null })
|
|
333
|
+
expect(result.success).toBe(true)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('supports schema extension for narrowed metadata', () => {
|
|
337
|
+
const ContactMetaSchema = z.object({ linkedinScore: z.number().optional(), tags: z.array(z.string()) })
|
|
338
|
+
const NarrowedSchema = BaseContactSchema.extend({ metadata: ContactMetaSchema })
|
|
339
|
+
|
|
340
|
+
const result = NarrowedSchema.safeParse({
|
|
341
|
+
...validContact,
|
|
342
|
+
metadata: { tags: ['enterprise', 'warm-lead'], linkedinScore: 8 }
|
|
343
|
+
})
|
|
344
|
+
expect(result.success).toBe(true)
|
|
345
|
+
if (result.success) {
|
|
346
|
+
expect(result.data.metadata.tags).toContain('enterprise')
|
|
347
|
+
expect(result.data.metadata.linkedinScore).toBe(8)
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// TypeScript generic interface narrowing (compile-time — exercised at runtime)
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
describe('BaseProject<TMeta> generic interface', () => {
|
|
357
|
+
it('accepts unnarrowed metadata as Record<string, unknown>', () => {
|
|
358
|
+
const project: BaseProject = {
|
|
359
|
+
id: '00000000-0000-0000-0000-000000000010',
|
|
360
|
+
organizationId: ORG_ID,
|
|
361
|
+
name: 'Generic Project',
|
|
362
|
+
kind: 'internal',
|
|
363
|
+
status: 'active',
|
|
364
|
+
description: null,
|
|
365
|
+
metadata: { anything: true },
|
|
366
|
+
...TIMESTAMPS
|
|
367
|
+
}
|
|
368
|
+
expect(project.metadata['anything']).toBe(true)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('accepts narrowed metadata type', () => {
|
|
372
|
+
type ProjectMeta = { budget: number; riskScore: 'low' | 'medium' | 'high' }
|
|
373
|
+
const project: BaseProject<ProjectMeta> = {
|
|
374
|
+
id: '00000000-0000-0000-0000-000000000011',
|
|
375
|
+
organizationId: ORG_ID,
|
|
376
|
+
name: 'Budget Project',
|
|
377
|
+
kind: 'client_engagement',
|
|
378
|
+
status: 'on_track',
|
|
379
|
+
description: 'A budgeted project',
|
|
380
|
+
metadata: { budget: 100000, riskScore: 'low' },
|
|
381
|
+
...TIMESTAMPS
|
|
382
|
+
}
|
|
383
|
+
expect(project.metadata.budget).toBe(100000)
|
|
384
|
+
expect(project.metadata.riskScore).toBe('low')
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
describe('BaseDeal<TMeta> generic interface', () => {
|
|
389
|
+
it('accepts unnarrowed metadata', () => {
|
|
390
|
+
const deal: BaseDeal = {
|
|
391
|
+
id: '00000000-0000-0000-0000-000000000040',
|
|
392
|
+
organizationId: ORG_ID,
|
|
393
|
+
contactEmail: 'test@example.com',
|
|
394
|
+
stage: 'interested',
|
|
395
|
+
metadata: {},
|
|
396
|
+
...TIMESTAMPS
|
|
397
|
+
}
|
|
398
|
+
expect(deal.stage).toBe('interested')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('accepts narrowed metadata with deal financials', () => {
|
|
402
|
+
type DealMeta = { contractValue: number; currency: string }
|
|
403
|
+
const deal: BaseDeal<DealMeta> = {
|
|
404
|
+
id: '00000000-0000-0000-0000-000000000041',
|
|
405
|
+
organizationId: ORG_ID,
|
|
406
|
+
contactEmail: 'client@bigco.com',
|
|
407
|
+
stage: 'closing',
|
|
408
|
+
metadata: { contractValue: 24000, currency: 'USD' },
|
|
409
|
+
...TIMESTAMPS
|
|
410
|
+
}
|
|
411
|
+
expect(deal.metadata.contractValue).toBe(24000)
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('BaseMilestone<TMeta> generic interface', () => {
|
|
416
|
+
it('accepts narrowed metadata', () => {
|
|
417
|
+
type MilestoneMeta = { deliverables: string[] }
|
|
418
|
+
const milestone: BaseMilestone<MilestoneMeta> = {
|
|
419
|
+
id: '00000000-0000-0000-0000-000000000020',
|
|
420
|
+
organizationId: ORG_ID,
|
|
421
|
+
projectId: '00000000-0000-0000-0000-000000000010',
|
|
422
|
+
name: 'Launch',
|
|
423
|
+
status: 'in_progress',
|
|
424
|
+
description: null,
|
|
425
|
+
metadata: { deliverables: ['landing-page', 'email-campaign'] },
|
|
426
|
+
...TIMESTAMPS
|
|
427
|
+
}
|
|
428
|
+
expect(milestone.metadata.deliverables).toHaveLength(2)
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
describe('BaseTask<TMeta> generic interface', () => {
|
|
433
|
+
it('accepts narrowed metadata', () => {
|
|
434
|
+
type TaskMeta = { storyPoints: number }
|
|
435
|
+
const task: BaseTask<TaskMeta> = {
|
|
436
|
+
id: '00000000-0000-0000-0000-000000000030',
|
|
437
|
+
organizationId: ORG_ID,
|
|
438
|
+
projectId: '00000000-0000-0000-0000-000000000010',
|
|
439
|
+
name: 'Deploy to prod',
|
|
440
|
+
status: 'in_progress',
|
|
441
|
+
type: 'code',
|
|
442
|
+
description: null,
|
|
443
|
+
metadata: { storyPoints: 5 },
|
|
444
|
+
...TIMESTAMPS
|
|
445
|
+
}
|
|
446
|
+
expect(task.metadata.storyPoints).toBe(5)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
describe('BaseCompany<TMeta> generic interface', () => {
|
|
451
|
+
it('accepts narrowed metadata', () => {
|
|
452
|
+
type CompanyMeta = { verticalScore: number }
|
|
453
|
+
const company: BaseCompany<CompanyMeta> = {
|
|
454
|
+
id: '00000000-0000-0000-0000-000000000050',
|
|
455
|
+
organizationId: ORG_ID,
|
|
456
|
+
name: 'Tech Startup',
|
|
457
|
+
domain: 'techstartup.io',
|
|
458
|
+
status: 'active',
|
|
459
|
+
metadata: { verticalScore: 92 },
|
|
460
|
+
...TIMESTAMPS
|
|
461
|
+
}
|
|
462
|
+
expect(company.metadata.verticalScore).toBe(92)
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
describe('BaseContact<TMeta> generic interface', () => {
|
|
467
|
+
it('accepts narrowed metadata', () => {
|
|
468
|
+
type ContactMeta = { personalizationScore: number; openingLine: string }
|
|
469
|
+
const contact: BaseContact<ContactMeta> = {
|
|
470
|
+
id: '00000000-0000-0000-0000-000000000060',
|
|
471
|
+
organizationId: ORG_ID,
|
|
472
|
+
email: 'ceo@example.com',
|
|
473
|
+
firstName: 'Alex',
|
|
474
|
+
lastName: 'Smith',
|
|
475
|
+
status: 'active',
|
|
476
|
+
metadata: { personalizationScore: 87, openingLine: 'Loved your post on AI automation.' },
|
|
477
|
+
...TIMESTAMPS
|
|
478
|
+
}
|
|
479
|
+
expect(contact.metadata.personalizationScore).toBe(87)
|
|
480
|
+
})
|
|
481
|
+
})
|