@elevasis/core 0.23.0 → 0.24.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 +1326 -552
- package/dist/index.js +869 -154
- package/dist/knowledge/index.d.ts +487 -209
- package/dist/knowledge/index.js +104 -1
- package/dist/organization-model/index.d.ts +1326 -552
- package/dist/organization-model/index.js +869 -154
- package/dist/test-utils/index.d.ts +357 -72
- package/dist/test-utils/index.js +795 -142
- package/package.json +5 -5
- package/src/README.md +14 -14
- package/src/__tests__/publish.test.ts +24 -24
- package/src/__tests__/template-core-compatibility.test.ts +9 -12
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2102 -2096
- package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -30
- package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -217
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -69
- package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -37
- package/src/auth/multi-tenancy/index.ts +26 -26
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -104
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +143 -143
- package/src/auth/multi-tenancy/memberships/index.ts +26 -26
- package/src/auth/multi-tenancy/memberships/membership.ts +130 -130
- package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -194
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -136
- package/src/auth/multi-tenancy/permissions.test.ts +42 -42
- package/src/auth/multi-tenancy/permissions.ts +123 -123
- package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -78
- package/src/auth/multi-tenancy/role-management/index.ts +16 -16
- package/src/auth/multi-tenancy/theme-presets.ts +45 -45
- package/src/auth/multi-tenancy/types.ts +57 -57
- package/src/auth/multi-tenancy/users/api-schemas.ts +165 -165
- package/src/business/README.md +2 -2
- package/src/business/acquisition/activity-events.test.ts +250 -250
- package/src/business/acquisition/activity-events.ts +93 -93
- package/src/business/acquisition/api-schemas.test.ts +1883 -1843
- package/src/business/acquisition/api-schemas.ts +1492 -1497
- package/src/business/acquisition/build-templates.test.ts +240 -240
- package/src/business/acquisition/build-templates.ts +98 -98
- package/src/business/acquisition/crm-next-action.test.ts +262 -262
- package/src/business/acquisition/crm-next-action.ts +220 -220
- package/src/business/acquisition/crm-priority.test.ts +216 -216
- package/src/business/acquisition/crm-priority.ts +349 -349
- package/src/business/acquisition/crm-state-actions.test.ts +153 -153
- package/src/business/acquisition/deal-ownership.test.ts +351 -351
- package/src/business/acquisition/deal-ownership.ts +120 -120
- package/src/business/acquisition/derive-actions.test.ts +129 -104
- package/src/business/acquisition/derive-actions.ts +74 -84
- package/src/business/acquisition/index.ts +171 -170
- package/src/business/acquisition/ontology-validation.ts +309 -0
- package/src/business/acquisition/stateful.ts +30 -30
- package/src/business/acquisition/types.ts +396 -396
- package/src/business/clients/api-schemas.test.ts +115 -115
- package/src/business/clients/api-schemas.ts +158 -158
- package/src/business/clients/index.ts +1 -1
- package/src/business/crm/api-schemas.ts +40 -40
- package/src/business/crm/index.ts +1 -1
- package/src/business/deals/api-schemas.ts +87 -87
- package/src/business/deals/index.ts +1 -1
- package/src/business/index.ts +5 -5
- package/src/business/projects/types.ts +144 -144
- package/src/commands/queue/types/task.ts +15 -15
- package/src/execution/core/runner-types.ts +61 -61
- package/src/execution/core/sse-executions.ts +7 -7
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -10
- package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -16
- package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -4
- package/src/execution/engine/agent/core/types.ts +25 -25
- package/src/execution/engine/agent/index.ts +6 -6
- package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -24
- package/src/execution/engine/index.ts +443 -443
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -298
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -55
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -107
- package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -48
- package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -99
- package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -1
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -363
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -162
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -316
- package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -18
- package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -194
- package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -7
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -204
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -105
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -428
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -2
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1474
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -103
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -88
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -141
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -76
- package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -182
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -310
- package/src/execution/engine/tools/integration/service.test.ts +239 -239
- package/src/execution/engine/tools/integration/service.ts +172 -172
- package/src/execution/engine/tools/integration/tool.ts +255 -255
- package/src/execution/engine/tools/lead-service-types.ts +1005 -1005
- package/src/execution/engine/tools/messages.ts +43 -43
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -7
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -6
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -6
- package/src/execution/engine/tools/platform/acquisition/types.ts +280 -280
- package/src/execution/engine/tools/platform/email/types.ts +97 -97
- package/src/execution/engine/tools/registry.ts +704 -704
- package/src/execution/engine/tools/tool-maps.ts +831 -831
- package/src/execution/engine/tools/types.ts +234 -234
- package/src/execution/engine/workflow/types.ts +202 -202
- package/src/execution/external/__tests__/api-schemas.test.ts +127 -127
- package/src/execution/external/api-schemas.ts +40 -40
- package/src/execution/external/index.ts +1 -1
- package/src/index.ts +18 -18
- package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -420
- package/src/integrations/credentials/api-schemas.ts +146 -146
- package/src/integrations/credentials/schemas.ts +200 -200
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -7
- package/src/integrations/oauth/provider-registry.ts +74 -74
- package/src/integrations/oauth/server/credentials.ts +43 -43
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -327
- package/src/integrations/webhook-endpoints/api-schemas.ts +103 -103
- package/src/integrations/webhook-endpoints/types.ts +58 -58
- package/src/knowledge/README.md +32 -32
- package/src/knowledge/__tests__/queries.test.ts +626 -535
- package/src/knowledge/format.ts +99 -99
- package/src/knowledge/index.ts +5 -5
- package/src/knowledge/published.ts +5 -5
- package/src/knowledge/queries.ts +269 -218
- package/src/operations/activities/api-schemas.ts +80 -80
- package/src/operations/activities/types.ts +64 -64
- package/src/organization-model/README.md +149 -149
- package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -210
- package/src/organization-model/__tests__/defaults.test.ts +168 -168
- package/src/organization-model/__tests__/domains/actions.test.ts +78 -56
- package/src/organization-model/__tests__/domains/customers.test.ts +299 -299
- package/src/organization-model/__tests__/domains/entities.test.ts +56 -56
- package/src/organization-model/__tests__/domains/goals.test.ts +493 -493
- package/src/organization-model/__tests__/domains/identity.test.ts +280 -280
- package/src/organization-model/__tests__/domains/navigation.test.ts +268 -268
- package/src/organization-model/__tests__/domains/offerings.test.ts +414 -414
- package/src/organization-model/__tests__/domains/policies.test.ts +323 -323
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +293 -293
- package/src/organization-model/__tests__/domains/resources.test.ts +382 -283
- package/src/organization-model/__tests__/domains/roles.test.ts +463 -463
- package/src/organization-model/__tests__/domains/statuses.test.ts +246 -246
- package/src/organization-model/__tests__/domains/systems.test.ts +209 -209
- package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -361
- package/src/organization-model/__tests__/foundation.test.ts +77 -77
- package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
- package/src/organization-model/__tests__/graph.test.ts +1246 -887
- package/src/organization-model/__tests__/icons.test.ts +10 -1
- package/src/organization-model/__tests__/knowledge.test.ts +251 -15
- package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
- package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
- package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
- package/src/organization-model/__tests__/resolve.test.ts +274 -164
- package/src/organization-model/__tests__/schema.test.ts +834 -301
- package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
- package/src/organization-model/catalogs/lead-gen.ts +144 -144
- package/src/organization-model/content-kinds/config.ts +36 -36
- package/src/organization-model/content-kinds/index.ts +76 -72
- package/src/organization-model/content-kinds/pipeline.ts +68 -68
- package/src/organization-model/content-kinds/registry.ts +44 -44
- package/src/organization-model/content-kinds/status.ts +71 -71
- package/src/organization-model/content-kinds/template.ts +83 -83
- package/src/organization-model/content-kinds/types.ts +117 -117
- package/src/organization-model/contracts.ts +27 -27
- package/src/organization-model/defaults.ts +40 -50
- package/src/organization-model/domains/actions.ts +333 -239
- package/src/organization-model/domains/customers.ts +78 -78
- package/src/organization-model/domains/entities.ts +144 -144
- package/src/organization-model/domains/goals.ts +83 -83
- package/src/organization-model/domains/knowledge.ts +117 -101
- package/src/organization-model/domains/navigation.ts +139 -139
- package/src/organization-model/domains/offerings.ts +71 -71
- package/src/organization-model/domains/policies.ts +102 -102
- package/src/organization-model/domains/projects.ts +14 -14
- package/src/organization-model/domains/prospecting.ts +395 -395
- package/src/organization-model/domains/resources.ts +167 -132
- package/src/organization-model/domains/roles.ts +96 -96
- package/src/organization-model/domains/sales.test.ts +218 -218
- package/src/organization-model/domains/sales.ts +380 -380
- package/src/organization-model/domains/shared.ts +63 -63
- package/src/organization-model/domains/statuses.ts +339 -339
- package/src/organization-model/domains/systems.ts +217 -172
- package/src/organization-model/foundation.ts +75 -75
- package/src/organization-model/graph/build.ts +1016 -888
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/link.ts +10 -10
- package/src/organization-model/graph/schema.ts +76 -70
- package/src/organization-model/graph/types.ts +73 -67
- package/src/organization-model/helpers.ts +289 -241
- package/src/organization-model/icons.ts +78 -66
- package/src/organization-model/index.ts +130 -128
- package/src/organization-model/migration-helpers.ts +247 -244
- package/src/organization-model/ontology.ts +661 -0
- package/src/organization-model/organization-graph.mdx +110 -90
- package/src/organization-model/organization-model.mdx +226 -219
- package/src/organization-model/published.ts +289 -235
- package/src/organization-model/resolve.ts +146 -91
- package/src/organization-model/schema.ts +790 -671
- package/src/organization-model/surface-projection.ts +212 -212
- package/src/organization-model/types.ts +177 -167
- package/src/platform/api/types.ts +38 -38
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/index.ts +23 -23
- package/src/platform/registry/__tests__/command-view.test.ts +10 -10
- package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
- package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
- package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
- package/src/platform/registry/__tests__/validation.test.ts +1347 -1347
- package/src/platform/registry/command-view.ts +10 -10
- package/src/platform/registry/index.ts +103 -103
- package/src/platform/registry/resource-link.ts +32 -32
- package/src/platform/registry/resource-registry.ts +890 -890
- package/src/platform/registry/serialization.ts +295 -295
- package/src/platform/registry/serialized-types.ts +166 -166
- package/src/platform/registry/stats-types.ts +68 -68
- package/src/platform/registry/types.ts +425 -425
- package/src/platform/registry/validation.ts +745 -745
- package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
- package/src/platform/utils/validation.ts +425 -425
- package/src/projects/api-schemas.test.ts +39 -39
- package/src/projects/api-schemas.ts +291 -291
- package/src/reference/_generated/contracts.md +2101 -2096
- package/src/reference/glossary.md +76 -76
- package/src/scaffold-registry/__tests__/index.test.ts +206 -206
- package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
- package/src/scaffold-registry/index.ts +392 -392
- package/src/scaffold-registry/schema.ts +243 -243
- package/src/server.ts +289 -289
- package/src/supabase/database.types.ts +3153 -3153
- package/src/test-utils/README.md +37 -37
- package/src/test-utils/entities.ts +108 -108
- package/src/test-utils/fixtures/memberships.ts +82 -82
- package/src/test-utils/index.ts +12 -12
- package/src/test-utils/organization-model.ts +65 -65
- package/src/test-utils/published.ts +6 -6
- package/src/test-utils/rls/RLSTestContext.ts +588 -588
- package/src/test-utils/test-utils.test.ts +44 -44
|
@@ -1,414 +1,414 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
OfferingsDomainSchema,
|
|
4
|
-
ProductSchema,
|
|
5
|
-
PricingModelSchema,
|
|
6
|
-
DEFAULT_ORGANIZATION_MODEL_OFFERINGS
|
|
7
|
-
} from '../../domains/offerings'
|
|
8
|
-
import { resolveOrganizationModel } from '../../resolve'
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Group 1: ProductSchema — positive parse
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
describe('ProductSchema — positive parse', () => {
|
|
15
|
-
it('accepts a fully-populated product', () => {
|
|
16
|
-
const result = ProductSchema.safeParse({
|
|
17
|
-
id: 'product-starter-plan',
|
|
18
|
-
order: 10,
|
|
19
|
-
name: 'Starter Plan',
|
|
20
|
-
description: 'Full-service automation for small teams.',
|
|
21
|
-
pricingModel: 'subscription',
|
|
22
|
-
price: 299,
|
|
23
|
-
currency: 'USD',
|
|
24
|
-
targetSegmentIds: ['seg-smb'],
|
|
25
|
-
deliveryFeatureId: 'sales.crm'
|
|
26
|
-
})
|
|
27
|
-
expect(result.success).toBe(true)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('accepts a minimal product — only id and order required', () => {
|
|
31
|
-
const result = ProductSchema.safeParse({ id: 'product-minimal', order: 10 })
|
|
32
|
-
expect(result.success).toBe(true)
|
|
33
|
-
if (result.success) {
|
|
34
|
-
expect(result.data.id).toBe('product-minimal')
|
|
35
|
-
expect(result.data.name).toBe('')
|
|
36
|
-
expect(result.data.description).toBe('')
|
|
37
|
-
expect(result.data.pricingModel).toBe('custom')
|
|
38
|
-
expect(result.data.price).toBe(0)
|
|
39
|
-
expect(result.data.currency).toBe('USD')
|
|
40
|
-
expect(result.data.targetSegmentIds).toEqual([])
|
|
41
|
-
expect(result.data.deliveryFeatureId).toBeUndefined()
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('trims whitespace from id and name', () => {
|
|
46
|
-
const result = ProductSchema.safeParse({
|
|
47
|
-
id: ' product-trim ',
|
|
48
|
-
order: 10,
|
|
49
|
-
name: ' Trimmed Name '
|
|
50
|
-
})
|
|
51
|
-
expect(result.success).toBe(true)
|
|
52
|
-
if (result.success) {
|
|
53
|
-
expect(result.data.id).toBe('product-trim')
|
|
54
|
-
expect(result.data.name).toBe('Trimmed Name')
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('accepts price of 0 (free tier)', () => {
|
|
59
|
-
const result = ProductSchema.safeParse({
|
|
60
|
-
id: 'product-free',
|
|
61
|
-
order: 10,
|
|
62
|
-
pricingModel: 'subscription',
|
|
63
|
-
price: 0
|
|
64
|
-
})
|
|
65
|
-
expect(result.success).toBe(true)
|
|
66
|
-
if (result.success) {
|
|
67
|
-
expect(result.data.price).toBe(0)
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('accepts all four pricing model values', () => {
|
|
72
|
-
const models = ['one-time', 'subscription', 'usage-based', 'custom'] as const
|
|
73
|
-
for (const pricingModel of models) {
|
|
74
|
-
const result = ProductSchema.safeParse({ id: `product-${pricingModel}`, order: 10, pricingModel })
|
|
75
|
-
expect(result.success).toBe(true)
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Group 2: ProductSchema — default values
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
describe('ProductSchema — default values', () => {
|
|
85
|
-
it('pricingModel defaults to "custom"', () => {
|
|
86
|
-
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
87
|
-
expect(result.success).toBe(true)
|
|
88
|
-
if (result.success) {
|
|
89
|
-
expect(result.data.pricingModel).toBe('custom')
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('price defaults to 0', () => {
|
|
94
|
-
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
95
|
-
expect(result.success).toBe(true)
|
|
96
|
-
if (result.success) {
|
|
97
|
-
expect(result.data.price).toBe(0)
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('currency defaults to "USD"', () => {
|
|
102
|
-
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
103
|
-
expect(result.success).toBe(true)
|
|
104
|
-
if (result.success) {
|
|
105
|
-
expect(result.data.currency).toBe('USD')
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('targetSegmentIds defaults to empty array', () => {
|
|
110
|
-
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
111
|
-
expect(result.success).toBe(true)
|
|
112
|
-
if (result.success) {
|
|
113
|
-
expect(result.data.targetSegmentIds).toEqual([])
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('deliveryFeatureId defaults to undefined (field is optional)', () => {
|
|
118
|
-
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
119
|
-
expect(result.success).toBe(true)
|
|
120
|
-
if (result.success) {
|
|
121
|
-
expect(result.data.deliveryFeatureId).toBeUndefined()
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
// Group 3: ProductSchema — negative parse (wrong types / constraints)
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
describe('ProductSchema — negative parse', () => {
|
|
131
|
-
it('rejects missing id', () => {
|
|
132
|
-
const result = ProductSchema.safeParse({ order: 10, name: 'No ID Product' })
|
|
133
|
-
expect(result.success).toBe(false)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('rejects empty string id', () => {
|
|
137
|
-
const result = ProductSchema.safeParse({ id: '', order: 10 })
|
|
138
|
-
expect(result.success).toBe(false)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('rejects whitespace-only id (trims to empty string)', () => {
|
|
142
|
-
const result = ProductSchema.safeParse({ id: ' ', order: 10 })
|
|
143
|
-
expect(result.success).toBe(false)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('rejects an unknown pricingModel value', () => {
|
|
147
|
-
const result = ProductSchema.safeParse({ id: 'product-bad-model', order: 10, pricingModel: 'freemium' })
|
|
148
|
-
expect(result.success).toBe(false)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('rejects price below 0', () => {
|
|
152
|
-
const result = ProductSchema.safeParse({ id: 'product-negative-price', order: 10, price: -1 })
|
|
153
|
-
expect(result.success).toBe(false)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('rejects price as a string', () => {
|
|
157
|
-
const result = ProductSchema.safeParse({ id: 'product-string-price', order: 10, price: '299' })
|
|
158
|
-
expect(result.success).toBe(false)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('rejects targetSegmentIds as a non-array value', () => {
|
|
162
|
-
const result = ProductSchema.safeParse({ id: 'product-bad-segments', order: 10, targetSegmentIds: 'seg-a' })
|
|
163
|
-
expect(result.success).toBe(false)
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Group 4: PricingModelSchema — enum coverage
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
describe('PricingModelSchema — enum', () => {
|
|
172
|
-
it('has exactly four values', () => {
|
|
173
|
-
const result = PricingModelSchema.safeParse('one-time')
|
|
174
|
-
expect(result.success).toBe(true)
|
|
175
|
-
// Verify the full enum options list matches expected set
|
|
176
|
-
expect(PricingModelSchema.options).toEqual(['one-time', 'subscription', 'usage-based', 'custom'])
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('rejects an empty string', () => {
|
|
180
|
-
const result = PricingModelSchema.safeParse('')
|
|
181
|
-
expect(result.success).toBe(false)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('rejects a numeric value', () => {
|
|
185
|
-
const result = PricingModelSchema.safeParse(42)
|
|
186
|
-
expect(result.success).toBe(false)
|
|
187
|
-
})
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
// Group 5: OfferingsDomainSchema — structural
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
|
-
describe('OfferingsDomainSchema — structural', () => {
|
|
195
|
-
it('accepts an empty products record', () => {
|
|
196
|
-
const result = OfferingsDomainSchema.safeParse({})
|
|
197
|
-
expect(result.success).toBe(true)
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('defaults products to empty record when omitted', () => {
|
|
201
|
-
const result = OfferingsDomainSchema.safeParse({})
|
|
202
|
-
expect(result.success).toBe(true)
|
|
203
|
-
if (result.success) {
|
|
204
|
-
expect(result.data).toEqual({})
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('accepts multiple valid products', () => {
|
|
209
|
-
const result = OfferingsDomainSchema.safeParse({
|
|
210
|
-
'product-a': { id: 'product-a', order: 10, name: 'Product A', pricingModel: 'subscription' },
|
|
211
|
-
'product-b': { id: 'product-b', order: 20, name: 'Product B', pricingModel: 'one-time', price: 999 }
|
|
212
|
-
})
|
|
213
|
-
expect(result.success).toBe(true)
|
|
214
|
-
if (result.success) {
|
|
215
|
-
expect(Object.keys(result.data)).toHaveLength(2)
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('rejects a record where entry id does not match map key', () => {
|
|
220
|
-
const result = OfferingsDomainSchema.safeParse({
|
|
221
|
-
'product-a': { id: 'product-b', order: 10, name: 'Mismatched' }
|
|
222
|
-
})
|
|
223
|
-
expect(result.success).toBe(false)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it('DEFAULT_ORGANIZATION_MODEL_OFFERINGS constant matches schema parse of empty object', () => {
|
|
227
|
-
const result = OfferingsDomainSchema.safeParse({})
|
|
228
|
-
expect(result.success).toBe(true)
|
|
229
|
-
if (result.success) {
|
|
230
|
-
expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_OFFERINGS)
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
// Group 6: Cross-ref validation via resolveOrganizationModel
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
|
|
239
|
-
describe('resolveOrganizationModel — offerings cross-ref (targetSegmentIds)', () => {
|
|
240
|
-
it('passes when targetSegmentIds references a declared customer segment', () => {
|
|
241
|
-
expect(() =>
|
|
242
|
-
resolveOrganizationModel({
|
|
243
|
-
customers: {
|
|
244
|
-
'seg-agencies': { id: 'seg-agencies', order: 10, name: 'SMB Agencies' }
|
|
245
|
-
},
|
|
246
|
-
offerings: {
|
|
247
|
-
'product-starter': {
|
|
248
|
-
id: 'product-starter',
|
|
249
|
-
order: 10,
|
|
250
|
-
name: 'Starter Plan',
|
|
251
|
-
pricingModel: 'subscription',
|
|
252
|
-
targetSegmentIds: ['seg-agencies']
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
})
|
|
256
|
-
).not.toThrow()
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('throws when targetSegmentIds references a non-existent segment', () => {
|
|
260
|
-
expect(() =>
|
|
261
|
-
resolveOrganizationModel({
|
|
262
|
-
customers: {},
|
|
263
|
-
offerings: {
|
|
264
|
-
'product-bad-ref': {
|
|
265
|
-
id: 'product-bad-ref',
|
|
266
|
-
order: 10,
|
|
267
|
-
targetSegmentIds: ['nonexistent-segment']
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
).toThrow()
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('throws with a message referencing the unknown segment id', () => {
|
|
275
|
-
let errorMessage = ''
|
|
276
|
-
try {
|
|
277
|
-
resolveOrganizationModel({
|
|
278
|
-
customers: {},
|
|
279
|
-
offerings: {
|
|
280
|
-
'product-path-check': {
|
|
281
|
-
id: 'product-path-check',
|
|
282
|
-
order: 10,
|
|
283
|
-
targetSegmentIds: ['ghost-segment']
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
} catch (e) {
|
|
288
|
-
errorMessage = String(e)
|
|
289
|
-
}
|
|
290
|
-
expect(errorMessage).toContain('ghost-segment')
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('passes when targetSegmentIds is empty (no refs required)', () => {
|
|
294
|
-
expect(() =>
|
|
295
|
-
resolveOrganizationModel({
|
|
296
|
-
offerings: {
|
|
297
|
-
'product-no-targets': { id: 'product-no-targets', order: 10, targetSegmentIds: [] }
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
).not.toThrow()
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('passes when multiple products each reference a valid segment', () => {
|
|
304
|
-
expect(() =>
|
|
305
|
-
resolveOrganizationModel({
|
|
306
|
-
customers: {
|
|
307
|
-
'seg-a': { id: 'seg-a', order: 10, name: 'Segment A' },
|
|
308
|
-
'seg-b': { id: 'seg-b', order: 20, name: 'Segment B' }
|
|
309
|
-
},
|
|
310
|
-
offerings: {
|
|
311
|
-
'product-1': { id: 'product-1', order: 10, targetSegmentIds: ['seg-a'] },
|
|
312
|
-
'product-2': { id: 'product-2', order: 20, targetSegmentIds: ['seg-a', 'seg-b'] }
|
|
313
|
-
}
|
|
314
|
-
})
|
|
315
|
-
).not.toThrow()
|
|
316
|
-
})
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
// Group 7: Cross-ref validation — deliveryFeatureId
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
|
|
323
|
-
describe('resolveOrganizationModel — offerings cross-ref (deliveryFeatureId)', () => {
|
|
324
|
-
it('passes when deliveryFeatureId references a declared system', () => {
|
|
325
|
-
// 'sales.crm' is in DEFAULT_ORGANIZATION_MODEL.systems
|
|
326
|
-
expect(() =>
|
|
327
|
-
resolveOrganizationModel({
|
|
328
|
-
offerings: {
|
|
329
|
-
'product-with-delivery': {
|
|
330
|
-
id: 'product-with-delivery',
|
|
331
|
-
order: 10,
|
|
332
|
-
deliveryFeatureId: 'sales.crm'
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
).not.toThrow()
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('throws when deliveryFeatureId references a non-existent system', () => {
|
|
340
|
-
expect(() =>
|
|
341
|
-
resolveOrganizationModel({
|
|
342
|
-
offerings: {
|
|
343
|
-
'product-bad-delivery': {
|
|
344
|
-
id: 'product-bad-delivery',
|
|
345
|
-
order: 10,
|
|
346
|
-
deliveryFeatureId: 'nonexistent-feature'
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
).toThrow()
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('throws with a message referencing the unknown system id', () => {
|
|
354
|
-
let errorMessage = ''
|
|
355
|
-
try {
|
|
356
|
-
resolveOrganizationModel({
|
|
357
|
-
offerings: {
|
|
358
|
-
'product-delivery-path': {
|
|
359
|
-
id: 'product-delivery-path',
|
|
360
|
-
order: 10,
|
|
361
|
-
deliveryFeatureId: 'ghost-feature'
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
})
|
|
365
|
-
} catch (e) {
|
|
366
|
-
errorMessage = String(e)
|
|
367
|
-
}
|
|
368
|
-
expect(errorMessage).toContain('ghost-feature')
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
it('passes when deliveryFeatureId is absent (field is optional)', () => {
|
|
372
|
-
expect(() =>
|
|
373
|
-
resolveOrganizationModel({
|
|
374
|
-
offerings: {
|
|
375
|
-
'product-no-delivery': { id: 'product-no-delivery', order: 10 }
|
|
376
|
-
}
|
|
377
|
-
})
|
|
378
|
-
).not.toThrow()
|
|
379
|
-
})
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
// Group 8: Integration — resolveOrganizationModel general
|
|
384
|
-
// ---------------------------------------------------------------------------
|
|
385
|
-
|
|
386
|
-
describe('resolveOrganizationModel — offerings domain integration', () => {
|
|
387
|
-
it('merges partial offerings override and preserves empty products default', () => {
|
|
388
|
-
const model = resolveOrganizationModel({ offerings: {} })
|
|
389
|
-
expect(Object.keys(model.offerings)).toHaveLength(0)
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
it('omitting offerings key entirely resolves to default empty products', () => {
|
|
393
|
-
const model = resolveOrganizationModel({})
|
|
394
|
-
expect(model.offerings).toBeDefined()
|
|
395
|
-
expect(Object.keys(model.offerings)).toHaveLength(0)
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
it('does not bleed offerings changes into other top-level domains', () => {
|
|
399
|
-
const model = resolveOrganizationModel({
|
|
400
|
-
customers: {
|
|
401
|
-
'seg-isolation': { id: 'seg-isolation', order: 10 }
|
|
402
|
-
},
|
|
403
|
-
offerings: {
|
|
404
|
-
'product-isolation': { id: 'product-isolation', order: 10, targetSegmentIds: ['seg-isolation'] }
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
expect(model.identity).toBeDefined()
|
|
408
|
-
expect(model.customers).toBeDefined()
|
|
409
|
-
expect(model.systems).toBeDefined()
|
|
410
|
-
// Phase 4 (D8): model.navigation removed from top-level OM.
|
|
411
|
-
expect(model.navigation).toBeDefined()
|
|
412
|
-
expect(model.knowledge).toBeDefined()
|
|
413
|
-
})
|
|
414
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
OfferingsDomainSchema,
|
|
4
|
+
ProductSchema,
|
|
5
|
+
PricingModelSchema,
|
|
6
|
+
DEFAULT_ORGANIZATION_MODEL_OFFERINGS
|
|
7
|
+
} from '../../domains/offerings'
|
|
8
|
+
import { resolveOrganizationModel } from '../../resolve'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Group 1: ProductSchema — positive parse
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
describe('ProductSchema — positive parse', () => {
|
|
15
|
+
it('accepts a fully-populated product', () => {
|
|
16
|
+
const result = ProductSchema.safeParse({
|
|
17
|
+
id: 'product-starter-plan',
|
|
18
|
+
order: 10,
|
|
19
|
+
name: 'Starter Plan',
|
|
20
|
+
description: 'Full-service automation for small teams.',
|
|
21
|
+
pricingModel: 'subscription',
|
|
22
|
+
price: 299,
|
|
23
|
+
currency: 'USD',
|
|
24
|
+
targetSegmentIds: ['seg-smb'],
|
|
25
|
+
deliveryFeatureId: 'sales.crm'
|
|
26
|
+
})
|
|
27
|
+
expect(result.success).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('accepts a minimal product — only id and order required', () => {
|
|
31
|
+
const result = ProductSchema.safeParse({ id: 'product-minimal', order: 10 })
|
|
32
|
+
expect(result.success).toBe(true)
|
|
33
|
+
if (result.success) {
|
|
34
|
+
expect(result.data.id).toBe('product-minimal')
|
|
35
|
+
expect(result.data.name).toBe('')
|
|
36
|
+
expect(result.data.description).toBe('')
|
|
37
|
+
expect(result.data.pricingModel).toBe('custom')
|
|
38
|
+
expect(result.data.price).toBe(0)
|
|
39
|
+
expect(result.data.currency).toBe('USD')
|
|
40
|
+
expect(result.data.targetSegmentIds).toEqual([])
|
|
41
|
+
expect(result.data.deliveryFeatureId).toBeUndefined()
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('trims whitespace from id and name', () => {
|
|
46
|
+
const result = ProductSchema.safeParse({
|
|
47
|
+
id: ' product-trim ',
|
|
48
|
+
order: 10,
|
|
49
|
+
name: ' Trimmed Name '
|
|
50
|
+
})
|
|
51
|
+
expect(result.success).toBe(true)
|
|
52
|
+
if (result.success) {
|
|
53
|
+
expect(result.data.id).toBe('product-trim')
|
|
54
|
+
expect(result.data.name).toBe('Trimmed Name')
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('accepts price of 0 (free tier)', () => {
|
|
59
|
+
const result = ProductSchema.safeParse({
|
|
60
|
+
id: 'product-free',
|
|
61
|
+
order: 10,
|
|
62
|
+
pricingModel: 'subscription',
|
|
63
|
+
price: 0
|
|
64
|
+
})
|
|
65
|
+
expect(result.success).toBe(true)
|
|
66
|
+
if (result.success) {
|
|
67
|
+
expect(result.data.price).toBe(0)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('accepts all four pricing model values', () => {
|
|
72
|
+
const models = ['one-time', 'subscription', 'usage-based', 'custom'] as const
|
|
73
|
+
for (const pricingModel of models) {
|
|
74
|
+
const result = ProductSchema.safeParse({ id: `product-${pricingModel}`, order: 10, pricingModel })
|
|
75
|
+
expect(result.success).toBe(true)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Group 2: ProductSchema — default values
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
describe('ProductSchema — default values', () => {
|
|
85
|
+
it('pricingModel defaults to "custom"', () => {
|
|
86
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
87
|
+
expect(result.success).toBe(true)
|
|
88
|
+
if (result.success) {
|
|
89
|
+
expect(result.data.pricingModel).toBe('custom')
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('price defaults to 0', () => {
|
|
94
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
95
|
+
expect(result.success).toBe(true)
|
|
96
|
+
if (result.success) {
|
|
97
|
+
expect(result.data.price).toBe(0)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('currency defaults to "USD"', () => {
|
|
102
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
103
|
+
expect(result.success).toBe(true)
|
|
104
|
+
if (result.success) {
|
|
105
|
+
expect(result.data.currency).toBe('USD')
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('targetSegmentIds defaults to empty array', () => {
|
|
110
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
111
|
+
expect(result.success).toBe(true)
|
|
112
|
+
if (result.success) {
|
|
113
|
+
expect(result.data.targetSegmentIds).toEqual([])
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('deliveryFeatureId defaults to undefined (field is optional)', () => {
|
|
118
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults', order: 10 })
|
|
119
|
+
expect(result.success).toBe(true)
|
|
120
|
+
if (result.success) {
|
|
121
|
+
expect(result.data.deliveryFeatureId).toBeUndefined()
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Group 3: ProductSchema — negative parse (wrong types / constraints)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe('ProductSchema — negative parse', () => {
|
|
131
|
+
it('rejects missing id', () => {
|
|
132
|
+
const result = ProductSchema.safeParse({ order: 10, name: 'No ID Product' })
|
|
133
|
+
expect(result.success).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('rejects empty string id', () => {
|
|
137
|
+
const result = ProductSchema.safeParse({ id: '', order: 10 })
|
|
138
|
+
expect(result.success).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('rejects whitespace-only id (trims to empty string)', () => {
|
|
142
|
+
const result = ProductSchema.safeParse({ id: ' ', order: 10 })
|
|
143
|
+
expect(result.success).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('rejects an unknown pricingModel value', () => {
|
|
147
|
+
const result = ProductSchema.safeParse({ id: 'product-bad-model', order: 10, pricingModel: 'freemium' })
|
|
148
|
+
expect(result.success).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('rejects price below 0', () => {
|
|
152
|
+
const result = ProductSchema.safeParse({ id: 'product-negative-price', order: 10, price: -1 })
|
|
153
|
+
expect(result.success).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('rejects price as a string', () => {
|
|
157
|
+
const result = ProductSchema.safeParse({ id: 'product-string-price', order: 10, price: '299' })
|
|
158
|
+
expect(result.success).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('rejects targetSegmentIds as a non-array value', () => {
|
|
162
|
+
const result = ProductSchema.safeParse({ id: 'product-bad-segments', order: 10, targetSegmentIds: 'seg-a' })
|
|
163
|
+
expect(result.success).toBe(false)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Group 4: PricingModelSchema — enum coverage
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
describe('PricingModelSchema — enum', () => {
|
|
172
|
+
it('has exactly four values', () => {
|
|
173
|
+
const result = PricingModelSchema.safeParse('one-time')
|
|
174
|
+
expect(result.success).toBe(true)
|
|
175
|
+
// Verify the full enum options list matches expected set
|
|
176
|
+
expect(PricingModelSchema.options).toEqual(['one-time', 'subscription', 'usage-based', 'custom'])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('rejects an empty string', () => {
|
|
180
|
+
const result = PricingModelSchema.safeParse('')
|
|
181
|
+
expect(result.success).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('rejects a numeric value', () => {
|
|
185
|
+
const result = PricingModelSchema.safeParse(42)
|
|
186
|
+
expect(result.success).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Group 5: OfferingsDomainSchema — structural
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
describe('OfferingsDomainSchema — structural', () => {
|
|
195
|
+
it('accepts an empty products record', () => {
|
|
196
|
+
const result = OfferingsDomainSchema.safeParse({})
|
|
197
|
+
expect(result.success).toBe(true)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('defaults products to empty record when omitted', () => {
|
|
201
|
+
const result = OfferingsDomainSchema.safeParse({})
|
|
202
|
+
expect(result.success).toBe(true)
|
|
203
|
+
if (result.success) {
|
|
204
|
+
expect(result.data).toEqual({})
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('accepts multiple valid products', () => {
|
|
209
|
+
const result = OfferingsDomainSchema.safeParse({
|
|
210
|
+
'product-a': { id: 'product-a', order: 10, name: 'Product A', pricingModel: 'subscription' },
|
|
211
|
+
'product-b': { id: 'product-b', order: 20, name: 'Product B', pricingModel: 'one-time', price: 999 }
|
|
212
|
+
})
|
|
213
|
+
expect(result.success).toBe(true)
|
|
214
|
+
if (result.success) {
|
|
215
|
+
expect(Object.keys(result.data)).toHaveLength(2)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('rejects a record where entry id does not match map key', () => {
|
|
220
|
+
const result = OfferingsDomainSchema.safeParse({
|
|
221
|
+
'product-a': { id: 'product-b', order: 10, name: 'Mismatched' }
|
|
222
|
+
})
|
|
223
|
+
expect(result.success).toBe(false)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('DEFAULT_ORGANIZATION_MODEL_OFFERINGS constant matches schema parse of empty object', () => {
|
|
227
|
+
const result = OfferingsDomainSchema.safeParse({})
|
|
228
|
+
expect(result.success).toBe(true)
|
|
229
|
+
if (result.success) {
|
|
230
|
+
expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_OFFERINGS)
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Group 6: Cross-ref validation via resolveOrganizationModel
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
describe('resolveOrganizationModel — offerings cross-ref (targetSegmentIds)', () => {
|
|
240
|
+
it('passes when targetSegmentIds references a declared customer segment', () => {
|
|
241
|
+
expect(() =>
|
|
242
|
+
resolveOrganizationModel({
|
|
243
|
+
customers: {
|
|
244
|
+
'seg-agencies': { id: 'seg-agencies', order: 10, name: 'SMB Agencies' }
|
|
245
|
+
},
|
|
246
|
+
offerings: {
|
|
247
|
+
'product-starter': {
|
|
248
|
+
id: 'product-starter',
|
|
249
|
+
order: 10,
|
|
250
|
+
name: 'Starter Plan',
|
|
251
|
+
pricingModel: 'subscription',
|
|
252
|
+
targetSegmentIds: ['seg-agencies']
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
).not.toThrow()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('throws when targetSegmentIds references a non-existent segment', () => {
|
|
260
|
+
expect(() =>
|
|
261
|
+
resolveOrganizationModel({
|
|
262
|
+
customers: {},
|
|
263
|
+
offerings: {
|
|
264
|
+
'product-bad-ref': {
|
|
265
|
+
id: 'product-bad-ref',
|
|
266
|
+
order: 10,
|
|
267
|
+
targetSegmentIds: ['nonexistent-segment']
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
).toThrow()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('throws with a message referencing the unknown segment id', () => {
|
|
275
|
+
let errorMessage = ''
|
|
276
|
+
try {
|
|
277
|
+
resolveOrganizationModel({
|
|
278
|
+
customers: {},
|
|
279
|
+
offerings: {
|
|
280
|
+
'product-path-check': {
|
|
281
|
+
id: 'product-path-check',
|
|
282
|
+
order: 10,
|
|
283
|
+
targetSegmentIds: ['ghost-segment']
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
} catch (e) {
|
|
288
|
+
errorMessage = String(e)
|
|
289
|
+
}
|
|
290
|
+
expect(errorMessage).toContain('ghost-segment')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('passes when targetSegmentIds is empty (no refs required)', () => {
|
|
294
|
+
expect(() =>
|
|
295
|
+
resolveOrganizationModel({
|
|
296
|
+
offerings: {
|
|
297
|
+
'product-no-targets': { id: 'product-no-targets', order: 10, targetSegmentIds: [] }
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
).not.toThrow()
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('passes when multiple products each reference a valid segment', () => {
|
|
304
|
+
expect(() =>
|
|
305
|
+
resolveOrganizationModel({
|
|
306
|
+
customers: {
|
|
307
|
+
'seg-a': { id: 'seg-a', order: 10, name: 'Segment A' },
|
|
308
|
+
'seg-b': { id: 'seg-b', order: 20, name: 'Segment B' }
|
|
309
|
+
},
|
|
310
|
+
offerings: {
|
|
311
|
+
'product-1': { id: 'product-1', order: 10, targetSegmentIds: ['seg-a'] },
|
|
312
|
+
'product-2': { id: 'product-2', order: 20, targetSegmentIds: ['seg-a', 'seg-b'] }
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
).not.toThrow()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Group 7: Cross-ref validation — deliveryFeatureId
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
describe('resolveOrganizationModel — offerings cross-ref (deliveryFeatureId)', () => {
|
|
324
|
+
it('passes when deliveryFeatureId references a declared system', () => {
|
|
325
|
+
// 'sales.crm' is in DEFAULT_ORGANIZATION_MODEL.systems
|
|
326
|
+
expect(() =>
|
|
327
|
+
resolveOrganizationModel({
|
|
328
|
+
offerings: {
|
|
329
|
+
'product-with-delivery': {
|
|
330
|
+
id: 'product-with-delivery',
|
|
331
|
+
order: 10,
|
|
332
|
+
deliveryFeatureId: 'sales.crm'
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
).not.toThrow()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('throws when deliveryFeatureId references a non-existent system', () => {
|
|
340
|
+
expect(() =>
|
|
341
|
+
resolveOrganizationModel({
|
|
342
|
+
offerings: {
|
|
343
|
+
'product-bad-delivery': {
|
|
344
|
+
id: 'product-bad-delivery',
|
|
345
|
+
order: 10,
|
|
346
|
+
deliveryFeatureId: 'nonexistent-feature'
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
).toThrow()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('throws with a message referencing the unknown system id', () => {
|
|
354
|
+
let errorMessage = ''
|
|
355
|
+
try {
|
|
356
|
+
resolveOrganizationModel({
|
|
357
|
+
offerings: {
|
|
358
|
+
'product-delivery-path': {
|
|
359
|
+
id: 'product-delivery-path',
|
|
360
|
+
order: 10,
|
|
361
|
+
deliveryFeatureId: 'ghost-feature'
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
} catch (e) {
|
|
366
|
+
errorMessage = String(e)
|
|
367
|
+
}
|
|
368
|
+
expect(errorMessage).toContain('ghost-feature')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('passes when deliveryFeatureId is absent (field is optional)', () => {
|
|
372
|
+
expect(() =>
|
|
373
|
+
resolveOrganizationModel({
|
|
374
|
+
offerings: {
|
|
375
|
+
'product-no-delivery': { id: 'product-no-delivery', order: 10 }
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
).not.toThrow()
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Group 8: Integration — resolveOrganizationModel general
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
describe('resolveOrganizationModel — offerings domain integration', () => {
|
|
387
|
+
it('merges partial offerings override and preserves empty products default', () => {
|
|
388
|
+
const model = resolveOrganizationModel({ offerings: {} })
|
|
389
|
+
expect(Object.keys(model.offerings)).toHaveLength(0)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('omitting offerings key entirely resolves to default empty products', () => {
|
|
393
|
+
const model = resolveOrganizationModel({})
|
|
394
|
+
expect(model.offerings).toBeDefined()
|
|
395
|
+
expect(Object.keys(model.offerings)).toHaveLength(0)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('does not bleed offerings changes into other top-level domains', () => {
|
|
399
|
+
const model = resolveOrganizationModel({
|
|
400
|
+
customers: {
|
|
401
|
+
'seg-isolation': { id: 'seg-isolation', order: 10 }
|
|
402
|
+
},
|
|
403
|
+
offerings: {
|
|
404
|
+
'product-isolation': { id: 'product-isolation', order: 10, targetSegmentIds: ['seg-isolation'] }
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
expect(model.identity).toBeDefined()
|
|
408
|
+
expect(model.customers).toBeDefined()
|
|
409
|
+
expect(model.systems).toBeDefined()
|
|
410
|
+
// Phase 4 (D8): model.navigation removed from top-level OM.
|
|
411
|
+
expect(model.navigation).toBeDefined()
|
|
412
|
+
expect(model.knowledge).toBeDefined()
|
|
413
|
+
})
|
|
414
|
+
})
|