@elevasis/core 0.22.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 +3214 -2501
- package/dist/index.js +3112 -1222
- package/dist/knowledge/index.d.ts +1108 -1264
- package/dist/knowledge/index.js +112 -9
- package/dist/organization-model/index.d.ts +3214 -2501
- package/dist/organization-model/index.js +3112 -1222
- package/dist/test-utils/index.d.ts +985 -1103
- package/dist/test-utils/index.js +2464 -1165
- 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 -80
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2389 -2121
- 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 +1493 -1500
- package/src/business/acquisition/build-templates.test.ts +240 -240
- package/src/business/acquisition/build-templates.ts +83 -41
- 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 -151
- 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 -392
- 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 +33 -32
- package/src/knowledge/__tests__/queries.test.ts +633 -541
- package/src/knowledge/format.ts +100 -99
- package/src/knowledge/index.ts +5 -5
- package/src/knowledge/published.ts +5 -5
- package/src/knowledge/queries.ts +274 -222
- package/src/operations/activities/api-schemas.ts +80 -80
- package/src/operations/activities/types.ts +64 -64
- package/src/organization-model/README.md +149 -109
- package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
- package/src/organization-model/__tests__/defaults.test.ts +168 -194
- package/src/organization-model/__tests__/domains/actions.test.ts +78 -0
- package/src/organization-model/__tests__/domains/customers.test.ts +48 -44
- package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
- package/src/organization-model/__tests__/domains/goals.test.ts +110 -96
- package/src/organization-model/__tests__/domains/identity.test.ts +4 -3
- package/src/organization-model/__tests__/domains/navigation.test.ts +222 -166
- package/src/organization-model/__tests__/domains/offerings.test.ts +83 -88
- package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +30 -30
- package/src/organization-model/__tests__/domains/resources.test.ts +396 -175
- package/src/organization-model/__tests__/domains/roles.test.ts +463 -402
- package/src/organization-model/__tests__/domains/statuses.test.ts +13 -10
- package/src/organization-model/__tests__/domains/systems.test.ts +209 -193
- package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -0
- package/src/organization-model/__tests__/foundation.test.ts +47 -75
- package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
- package/src/organization-model/__tests__/graph.test.ts +1336 -149
- package/src/organization-model/__tests__/icons.test.ts +10 -1
- package/src/organization-model/__tests__/knowledge.test.ts +418 -61
- package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
- package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -94
- package/src/organization-model/__tests__/recursive-system-schema.test.ts +549 -0
- package/src/organization-model/__tests__/resolve.test.ts +303 -42
- package/src/organization-model/__tests__/schema.test.ts +863 -153
- package/src/organization-model/__tests__/surface-projection.test.ts +284 -174
- package/src/organization-model/catalogs/lead-gen.ts +144 -0
- package/src/organization-model/content-kinds/config.ts +36 -0
- package/src/organization-model/content-kinds/index.ts +78 -0
- package/src/organization-model/content-kinds/pipeline.ts +68 -0
- package/src/organization-model/content-kinds/registry.ts +44 -0
- package/src/organization-model/content-kinds/status.ts +71 -0
- package/src/organization-model/content-kinds/template.ts +83 -0
- package/src/organization-model/content-kinds/types.ts +117 -0
- package/src/organization-model/contracts.ts +27 -17
- package/src/organization-model/defaults.ts +489 -107
- package/src/organization-model/domains/actions.ts +333 -0
- package/src/organization-model/domains/customers.ts +10 -7
- package/src/organization-model/domains/entities.ts +144 -0
- package/src/organization-model/domains/goals.ts +9 -6
- package/src/organization-model/domains/knowledge.ts +128 -54
- package/src/organization-model/domains/navigation.ts +139 -416
- package/src/organization-model/domains/offerings.ts +15 -10
- package/src/organization-model/domains/policies.ts +102 -0
- package/src/organization-model/domains/projects.ts +6 -40
- package/src/organization-model/domains/prospecting.ts +395 -514
- package/src/organization-model/domains/resources.ts +173 -81
- package/src/organization-model/domains/roles.ts +96 -93
- package/src/organization-model/domains/sales.test.ts +218 -218
- package/src/organization-model/domains/sales.ts +380 -589
- package/src/organization-model/domains/shared.ts +8 -8
- package/src/organization-model/domains/statuses.ts +298 -89
- package/src/organization-model/domains/systems.ts +240 -38
- package/src/organization-model/foundation.ts +35 -48
- package/src/organization-model/graph/build.ts +1035 -279
- 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 +77 -56
- package/src/organization-model/graph/types.ts +75 -56
- package/src/organization-model/helpers.ts +312 -59
- package/src/organization-model/icons.ts +78 -66
- package/src/organization-model/index.ts +129 -16
- package/src/organization-model/migration-helpers.ts +252 -0
- package/src/organization-model/ontology.ts +661 -0
- package/src/organization-model/organization-graph.mdx +110 -89
- package/src/organization-model/organization-model.mdx +226 -171
- package/src/organization-model/published.ts +295 -139
- package/src/organization-model/resolve.ts +139 -21
- package/src/organization-model/schema.ts +841 -301
- package/src/organization-model/surface-projection.ts +212 -218
- package/src/organization-model/types.ts +181 -90
- 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 +5 -7
- package/src/platform/registry/__tests__/resource-link.test.ts +35 -30
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +17 -32
- package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
- package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2051
- package/src/platform/registry/__tests__/validation.test.ts +1347 -1343
- 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 -878
- 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 -743
- 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 +2389 -2121
- 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 -3093
- 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 -49
- package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
- package/src/organization-model/domains/features.ts +0 -31
- package/src/organization-model/domains/operations.ts +0 -85
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { getDealOwnership } from './deal-ownership'
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Helpers
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
function makeEvent(type: string, timestamp: string): Record<string, unknown> {
|
|
9
|
-
return { type, timestamp }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Closed deals
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
describe('getDealOwnership — closed deals', () => {
|
|
17
|
-
it('returns null for closed_won regardless of activity_log', () => {
|
|
18
|
-
const deal = {
|
|
19
|
-
state_key: 'closed_won',
|
|
20
|
-
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
21
|
-
}
|
|
22
|
-
expect(getDealOwnership(deal)).toBeNull()
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('returns null for closed_lost regardless of activity_log', () => {
|
|
26
|
-
const deal = {
|
|
27
|
-
state_key: 'closed_lost',
|
|
28
|
-
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
29
|
-
}
|
|
30
|
-
expect(getDealOwnership(deal)).toBeNull()
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Empty / missing activity log
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
describe('getDealOwnership — empty activity log', () => {
|
|
39
|
-
it('returns null when activity_log is an empty array', () => {
|
|
40
|
-
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: [] })).toBeNull()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('returns null when activity_log is null', () => {
|
|
44
|
-
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: null })).toBeNull()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('returns null when activity_log is undefined', () => {
|
|
48
|
-
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: undefined })).toBeNull()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('returns null when activity_log is a non-array value', () => {
|
|
52
|
-
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: 'bad' })).toBeNull()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns null when activity_log contains no inbound or outbound events', () => {
|
|
56
|
-
const deal = {
|
|
57
|
-
state_key: 'discovery_replied',
|
|
58
|
-
activity_log: [
|
|
59
|
-
makeEvent('deal_created', '2026-01-01T00:00:00Z'),
|
|
60
|
-
makeEvent('stage_change', '2026-01-02T00:00:00Z')
|
|
61
|
-
]
|
|
62
|
-
}
|
|
63
|
-
expect(getDealOwnership(deal)).toBeNull()
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// us ownership (inbound is most recent)
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
describe('getDealOwnership — us ownership', () => {
|
|
72
|
-
it('returns "us" when inbound event is newer than outbound event', () => {
|
|
73
|
-
const deal = {
|
|
74
|
-
state_key: 'discovery_replied',
|
|
75
|
-
activity_log: [
|
|
76
|
-
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z'),
|
|
77
|
-
makeEvent('reply_received', '2026-01-11T10:00:00Z')
|
|
78
|
-
]
|
|
79
|
-
}
|
|
80
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('returns "us" when only a reply_received event exists', () => {
|
|
84
|
-
const deal = {
|
|
85
|
-
state_key: 'discovery_replied',
|
|
86
|
-
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
87
|
-
}
|
|
88
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('returns "us" with mixed irrelevant events present', () => {
|
|
92
|
-
const deal = {
|
|
93
|
-
state_key: 'discovery_replied',
|
|
94
|
-
activity_log: [
|
|
95
|
-
makeEvent('deal_created', '2026-01-01T00:00:00Z'),
|
|
96
|
-
makeEvent('reply_sent_to_lead', '2026-01-10T08:00:00Z'),
|
|
97
|
-
makeEvent('state_change', '2026-01-10T09:00:00Z'),
|
|
98
|
-
makeEvent('reply_received', '2026-01-10T12:00:00Z')
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('returns "us" picking the latest of multiple inbound events', () => {
|
|
105
|
-
const deal = {
|
|
106
|
-
state_key: 'discovery_replied',
|
|
107
|
-
activity_log: [
|
|
108
|
-
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z'),
|
|
109
|
-
makeEvent('reply_received', '2026-01-09T10:00:00Z'),
|
|
110
|
-
makeEvent('reply_received', '2026-01-11T10:00:00Z')
|
|
111
|
-
]
|
|
112
|
-
}
|
|
113
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// them ownership (outbound is most recent or tied)
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
describe('getDealOwnership — them ownership', () => {
|
|
122
|
-
it('returns "them" when outbound event is newer than inbound event', () => {
|
|
123
|
-
const deal = {
|
|
124
|
-
state_key: 'discovery_replied',
|
|
125
|
-
activity_log: [
|
|
126
|
-
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
127
|
-
makeEvent('reply_sent_to_lead', '2026-01-11T10:00:00Z')
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('returns "them" on exact timestamp tie (outbound wins tie-break)', () => {
|
|
134
|
-
const deal = {
|
|
135
|
-
state_key: 'discovery_replied',
|
|
136
|
-
activity_log: [
|
|
137
|
-
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
138
|
-
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')
|
|
139
|
-
]
|
|
140
|
-
}
|
|
141
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('returns "them" when only outbound events exist', () => {
|
|
145
|
-
const deal = {
|
|
146
|
-
state_key: 'discovery_replied',
|
|
147
|
-
activity_log: [makeEvent('booking_nudge_sent', '2026-01-10T10:00:00Z')]
|
|
148
|
-
}
|
|
149
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('returns "them" for each outbound type independently', () => {
|
|
153
|
-
const outboundTypes = [
|
|
154
|
-
'reply_sent_to_lead',
|
|
155
|
-
'booking_nudge_sent',
|
|
156
|
-
'reminder_sent',
|
|
157
|
-
'rebook_sent',
|
|
158
|
-
'followup_email_sent',
|
|
159
|
-
'reply_followup_sent',
|
|
160
|
-
'lead_deferred_next_step'
|
|
161
|
-
] as const
|
|
162
|
-
|
|
163
|
-
for (const type of outboundTypes) {
|
|
164
|
-
const deal = {
|
|
165
|
-
state_key: 'discovery_replied',
|
|
166
|
-
activity_log: [makeEvent(type, '2026-01-10T10:00:00Z')]
|
|
167
|
-
}
|
|
168
|
-
expect(getDealOwnership(deal), `expected "them" for outbound type: ${type}`).toBe('them')
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('returns "them" picking the latest of multiple outbound events', () => {
|
|
173
|
-
const deal = {
|
|
174
|
-
state_key: 'discovery_replied',
|
|
175
|
-
activity_log: [
|
|
176
|
-
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
177
|
-
makeEvent('reply_sent_to_lead', '2026-01-09T10:00:00Z'),
|
|
178
|
-
makeEvent('booking_nudge_sent', '2026-01-11T10:00:00Z')
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('treats canonical and legacy follow-up events as outbound ownership inputs', () => {
|
|
185
|
-
const canonical = {
|
|
186
|
-
state_key: 'followup_1_sent',
|
|
187
|
-
activity_log: [
|
|
188
|
-
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
189
|
-
makeEvent('followup_email_sent', '2026-01-11T10:00:00Z')
|
|
190
|
-
]
|
|
191
|
-
}
|
|
192
|
-
const legacy = {
|
|
193
|
-
state_key: 'followup_1_sent',
|
|
194
|
-
activity_log: [
|
|
195
|
-
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
196
|
-
makeEvent('reply_followup_sent', '2026-01-11T10:00:00Z')
|
|
197
|
-
]
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
expect(getDealOwnership(canonical)).toBe('them')
|
|
201
|
-
expect(getDealOwnership(legacy)).toBe('them')
|
|
202
|
-
})
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// Timestamp COALESCE — alternate timestamp field names
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
|
|
209
|
-
describe('getDealOwnership — timestamp field coalesce', () => {
|
|
210
|
-
it('resolves timestamp from occurredAt field', () => {
|
|
211
|
-
const deal = {
|
|
212
|
-
state_key: 'discovery_replied',
|
|
213
|
-
activity_log: [
|
|
214
|
-
{ type: 'reply_received', occurredAt: '2026-01-11T10:00:00Z' },
|
|
215
|
-
{ type: 'reply_sent_to_lead', occurredAt: '2026-01-10T10:00:00Z' }
|
|
216
|
-
]
|
|
217
|
-
}
|
|
218
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('resolves timestamp from createdAt field', () => {
|
|
222
|
-
const deal = {
|
|
223
|
-
state_key: 'discovery_replied',
|
|
224
|
-
activity_log: [{ type: 'reply_received', createdAt: '2026-01-10T10:00:00Z' }]
|
|
225
|
-
}
|
|
226
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('resolves timestamp from updatedAt field', () => {
|
|
230
|
-
const deal = {
|
|
231
|
-
state_key: 'discovery_replied',
|
|
232
|
-
activity_log: [{ type: 'reply_sent_to_lead', updatedAt: '2026-01-10T10:00:00Z' }]
|
|
233
|
-
}
|
|
234
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('resolves timestamp from sentAt field', () => {
|
|
238
|
-
const deal = {
|
|
239
|
-
state_key: 'discovery_replied',
|
|
240
|
-
activity_log: [{ type: 'reply_sent_to_lead', sentAt: '2026-01-10T10:00:00Z' }]
|
|
241
|
-
}
|
|
242
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('prefers timestamp over occurredAt when both present', () => {
|
|
246
|
-
// outbound has timestamp=newer, inbound has occurredAt=older → "them"
|
|
247
|
-
const deal = {
|
|
248
|
-
state_key: 'discovery_replied',
|
|
249
|
-
activity_log: [
|
|
250
|
-
{ type: 'reply_received', timestamp: '2026-01-09T00:00:00Z', occurredAt: '2026-01-12T00:00:00Z' },
|
|
251
|
-
{ type: 'reply_sent_to_lead', timestamp: '2026-01-10T00:00:00Z' }
|
|
252
|
-
]
|
|
253
|
-
}
|
|
254
|
-
// inbound timestamp=Jan 9, outbound timestamp=Jan 10 → outbound newer → "them"
|
|
255
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
256
|
-
})
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// Malformed entries — must be silently skipped
|
|
261
|
-
// ---------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
describe('getDealOwnership — malformed events skipped', () => {
|
|
264
|
-
it('skips entries with no timestamp field', () => {
|
|
265
|
-
const deal = {
|
|
266
|
-
state_key: 'discovery_replied',
|
|
267
|
-
activity_log: [
|
|
268
|
-
{ type: 'reply_received' }, // no timestamp
|
|
269
|
-
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')
|
|
270
|
-
]
|
|
271
|
-
}
|
|
272
|
-
// Only the outbound event has a valid timestamp — result is "them"
|
|
273
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('skips entries with an invalid timestamp string', () => {
|
|
277
|
-
const deal = {
|
|
278
|
-
state_key: 'discovery_replied',
|
|
279
|
-
activity_log: [
|
|
280
|
-
{ type: 'reply_received', timestamp: 'not-a-date' },
|
|
281
|
-
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')
|
|
282
|
-
]
|
|
283
|
-
}
|
|
284
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
it('skips null entries in the array', () => {
|
|
288
|
-
const deal = {
|
|
289
|
-
state_key: 'discovery_replied',
|
|
290
|
-
activity_log: [null, makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
291
|
-
}
|
|
292
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('skips non-object entries in the array', () => {
|
|
296
|
-
const deal = {
|
|
297
|
-
state_key: 'discovery_replied',
|
|
298
|
-
activity_log: ['string', 42, makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')]
|
|
299
|
-
}
|
|
300
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('skips array-in-array entries', () => {
|
|
304
|
-
const deal = {
|
|
305
|
-
state_key: 'discovery_replied',
|
|
306
|
-
activity_log: [[], makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
307
|
-
}
|
|
308
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it('skips entries with missing type field', () => {
|
|
312
|
-
const deal = {
|
|
313
|
-
state_key: 'discovery_replied',
|
|
314
|
-
activity_log: [
|
|
315
|
-
{ timestamp: '2026-01-10T10:00:00Z' }, // no type
|
|
316
|
-
makeEvent('reply_sent_to_lead', '2026-01-11T10:00:00Z')
|
|
317
|
-
]
|
|
318
|
-
}
|
|
319
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it('returns null when all entries are malformed', () => {
|
|
323
|
-
const deal = {
|
|
324
|
-
state_key: 'discovery_replied',
|
|
325
|
-
activity_log: [null, 'bad', { type: 'reply_received' }]
|
|
326
|
-
}
|
|
327
|
-
expect(getDealOwnership(deal)).toBeNull()
|
|
328
|
-
})
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
// state_key variants (non-closed)
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
|
|
335
|
-
describe('getDealOwnership — non-closed state_key variants', () => {
|
|
336
|
-
it('evaluates ownership for null state_key', () => {
|
|
337
|
-
const deal = {
|
|
338
|
-
state_key: null,
|
|
339
|
-
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
340
|
-
}
|
|
341
|
-
expect(getDealOwnership(deal)).toBe('us')
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('evaluates ownership for discovery_link_sent state', () => {
|
|
345
|
-
const deal = {
|
|
346
|
-
state_key: 'discovery_link_sent',
|
|
347
|
-
activity_log: [makeEvent('booking_nudge_sent', '2026-01-10T10:00:00Z')]
|
|
348
|
-
}
|
|
349
|
-
expect(getDealOwnership(deal)).toBe('them')
|
|
350
|
-
})
|
|
351
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getDealOwnership } from './deal-ownership'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
function makeEvent(type: string, timestamp: string): Record<string, unknown> {
|
|
9
|
+
return { type, timestamp }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Closed deals
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe('getDealOwnership — closed deals', () => {
|
|
17
|
+
it('returns null for closed_won regardless of activity_log', () => {
|
|
18
|
+
const deal = {
|
|
19
|
+
state_key: 'closed_won',
|
|
20
|
+
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
21
|
+
}
|
|
22
|
+
expect(getDealOwnership(deal)).toBeNull()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns null for closed_lost regardless of activity_log', () => {
|
|
26
|
+
const deal = {
|
|
27
|
+
state_key: 'closed_lost',
|
|
28
|
+
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
29
|
+
}
|
|
30
|
+
expect(getDealOwnership(deal)).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Empty / missing activity log
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe('getDealOwnership — empty activity log', () => {
|
|
39
|
+
it('returns null when activity_log is an empty array', () => {
|
|
40
|
+
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: [] })).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns null when activity_log is null', () => {
|
|
44
|
+
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: null })).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns null when activity_log is undefined', () => {
|
|
48
|
+
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: undefined })).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns null when activity_log is a non-array value', () => {
|
|
52
|
+
expect(getDealOwnership({ state_key: 'discovery_replied', activity_log: 'bad' })).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns null when activity_log contains no inbound or outbound events', () => {
|
|
56
|
+
const deal = {
|
|
57
|
+
state_key: 'discovery_replied',
|
|
58
|
+
activity_log: [
|
|
59
|
+
makeEvent('deal_created', '2026-01-01T00:00:00Z'),
|
|
60
|
+
makeEvent('stage_change', '2026-01-02T00:00:00Z')
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
expect(getDealOwnership(deal)).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// us ownership (inbound is most recent)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe('getDealOwnership — us ownership', () => {
|
|
72
|
+
it('returns "us" when inbound event is newer than outbound event', () => {
|
|
73
|
+
const deal = {
|
|
74
|
+
state_key: 'discovery_replied',
|
|
75
|
+
activity_log: [
|
|
76
|
+
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z'),
|
|
77
|
+
makeEvent('reply_received', '2026-01-11T10:00:00Z')
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns "us" when only a reply_received event exists', () => {
|
|
84
|
+
const deal = {
|
|
85
|
+
state_key: 'discovery_replied',
|
|
86
|
+
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
87
|
+
}
|
|
88
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns "us" with mixed irrelevant events present', () => {
|
|
92
|
+
const deal = {
|
|
93
|
+
state_key: 'discovery_replied',
|
|
94
|
+
activity_log: [
|
|
95
|
+
makeEvent('deal_created', '2026-01-01T00:00:00Z'),
|
|
96
|
+
makeEvent('reply_sent_to_lead', '2026-01-10T08:00:00Z'),
|
|
97
|
+
makeEvent('state_change', '2026-01-10T09:00:00Z'),
|
|
98
|
+
makeEvent('reply_received', '2026-01-10T12:00:00Z')
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns "us" picking the latest of multiple inbound events', () => {
|
|
105
|
+
const deal = {
|
|
106
|
+
state_key: 'discovery_replied',
|
|
107
|
+
activity_log: [
|
|
108
|
+
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z'),
|
|
109
|
+
makeEvent('reply_received', '2026-01-09T10:00:00Z'),
|
|
110
|
+
makeEvent('reply_received', '2026-01-11T10:00:00Z')
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// them ownership (outbound is most recent or tied)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
describe('getDealOwnership — them ownership', () => {
|
|
122
|
+
it('returns "them" when outbound event is newer than inbound event', () => {
|
|
123
|
+
const deal = {
|
|
124
|
+
state_key: 'discovery_replied',
|
|
125
|
+
activity_log: [
|
|
126
|
+
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
127
|
+
makeEvent('reply_sent_to_lead', '2026-01-11T10:00:00Z')
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('returns "them" on exact timestamp tie (outbound wins tie-break)', () => {
|
|
134
|
+
const deal = {
|
|
135
|
+
state_key: 'discovery_replied',
|
|
136
|
+
activity_log: [
|
|
137
|
+
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
138
|
+
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('returns "them" when only outbound events exist', () => {
|
|
145
|
+
const deal = {
|
|
146
|
+
state_key: 'discovery_replied',
|
|
147
|
+
activity_log: [makeEvent('booking_nudge_sent', '2026-01-10T10:00:00Z')]
|
|
148
|
+
}
|
|
149
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('returns "them" for each outbound type independently', () => {
|
|
153
|
+
const outboundTypes = [
|
|
154
|
+
'reply_sent_to_lead',
|
|
155
|
+
'booking_nudge_sent',
|
|
156
|
+
'reminder_sent',
|
|
157
|
+
'rebook_sent',
|
|
158
|
+
'followup_email_sent',
|
|
159
|
+
'reply_followup_sent',
|
|
160
|
+
'lead_deferred_next_step'
|
|
161
|
+
] as const
|
|
162
|
+
|
|
163
|
+
for (const type of outboundTypes) {
|
|
164
|
+
const deal = {
|
|
165
|
+
state_key: 'discovery_replied',
|
|
166
|
+
activity_log: [makeEvent(type, '2026-01-10T10:00:00Z')]
|
|
167
|
+
}
|
|
168
|
+
expect(getDealOwnership(deal), `expected "them" for outbound type: ${type}`).toBe('them')
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('returns "them" picking the latest of multiple outbound events', () => {
|
|
173
|
+
const deal = {
|
|
174
|
+
state_key: 'discovery_replied',
|
|
175
|
+
activity_log: [
|
|
176
|
+
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
177
|
+
makeEvent('reply_sent_to_lead', '2026-01-09T10:00:00Z'),
|
|
178
|
+
makeEvent('booking_nudge_sent', '2026-01-11T10:00:00Z')
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('treats canonical and legacy follow-up events as outbound ownership inputs', () => {
|
|
185
|
+
const canonical = {
|
|
186
|
+
state_key: 'followup_1_sent',
|
|
187
|
+
activity_log: [
|
|
188
|
+
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
189
|
+
makeEvent('followup_email_sent', '2026-01-11T10:00:00Z')
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
const legacy = {
|
|
193
|
+
state_key: 'followup_1_sent',
|
|
194
|
+
activity_log: [
|
|
195
|
+
makeEvent('reply_received', '2026-01-10T10:00:00Z'),
|
|
196
|
+
makeEvent('reply_followup_sent', '2026-01-11T10:00:00Z')
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expect(getDealOwnership(canonical)).toBe('them')
|
|
201
|
+
expect(getDealOwnership(legacy)).toBe('them')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Timestamp COALESCE — alternate timestamp field names
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
describe('getDealOwnership — timestamp field coalesce', () => {
|
|
210
|
+
it('resolves timestamp from occurredAt field', () => {
|
|
211
|
+
const deal = {
|
|
212
|
+
state_key: 'discovery_replied',
|
|
213
|
+
activity_log: [
|
|
214
|
+
{ type: 'reply_received', occurredAt: '2026-01-11T10:00:00Z' },
|
|
215
|
+
{ type: 'reply_sent_to_lead', occurredAt: '2026-01-10T10:00:00Z' }
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('resolves timestamp from createdAt field', () => {
|
|
222
|
+
const deal = {
|
|
223
|
+
state_key: 'discovery_replied',
|
|
224
|
+
activity_log: [{ type: 'reply_received', createdAt: '2026-01-10T10:00:00Z' }]
|
|
225
|
+
}
|
|
226
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('resolves timestamp from updatedAt field', () => {
|
|
230
|
+
const deal = {
|
|
231
|
+
state_key: 'discovery_replied',
|
|
232
|
+
activity_log: [{ type: 'reply_sent_to_lead', updatedAt: '2026-01-10T10:00:00Z' }]
|
|
233
|
+
}
|
|
234
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('resolves timestamp from sentAt field', () => {
|
|
238
|
+
const deal = {
|
|
239
|
+
state_key: 'discovery_replied',
|
|
240
|
+
activity_log: [{ type: 'reply_sent_to_lead', sentAt: '2026-01-10T10:00:00Z' }]
|
|
241
|
+
}
|
|
242
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('prefers timestamp over occurredAt when both present', () => {
|
|
246
|
+
// outbound has timestamp=newer, inbound has occurredAt=older → "them"
|
|
247
|
+
const deal = {
|
|
248
|
+
state_key: 'discovery_replied',
|
|
249
|
+
activity_log: [
|
|
250
|
+
{ type: 'reply_received', timestamp: '2026-01-09T00:00:00Z', occurredAt: '2026-01-12T00:00:00Z' },
|
|
251
|
+
{ type: 'reply_sent_to_lead', timestamp: '2026-01-10T00:00:00Z' }
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
// inbound timestamp=Jan 9, outbound timestamp=Jan 10 → outbound newer → "them"
|
|
255
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Malformed entries — must be silently skipped
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
describe('getDealOwnership — malformed events skipped', () => {
|
|
264
|
+
it('skips entries with no timestamp field', () => {
|
|
265
|
+
const deal = {
|
|
266
|
+
state_key: 'discovery_replied',
|
|
267
|
+
activity_log: [
|
|
268
|
+
{ type: 'reply_received' }, // no timestamp
|
|
269
|
+
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
// Only the outbound event has a valid timestamp — result is "them"
|
|
273
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('skips entries with an invalid timestamp string', () => {
|
|
277
|
+
const deal = {
|
|
278
|
+
state_key: 'discovery_replied',
|
|
279
|
+
activity_log: [
|
|
280
|
+
{ type: 'reply_received', timestamp: 'not-a-date' },
|
|
281
|
+
makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('skips null entries in the array', () => {
|
|
288
|
+
const deal = {
|
|
289
|
+
state_key: 'discovery_replied',
|
|
290
|
+
activity_log: [null, makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
291
|
+
}
|
|
292
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('skips non-object entries in the array', () => {
|
|
296
|
+
const deal = {
|
|
297
|
+
state_key: 'discovery_replied',
|
|
298
|
+
activity_log: ['string', 42, makeEvent('reply_sent_to_lead', '2026-01-10T10:00:00Z')]
|
|
299
|
+
}
|
|
300
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('skips array-in-array entries', () => {
|
|
304
|
+
const deal = {
|
|
305
|
+
state_key: 'discovery_replied',
|
|
306
|
+
activity_log: [[], makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
307
|
+
}
|
|
308
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('skips entries with missing type field', () => {
|
|
312
|
+
const deal = {
|
|
313
|
+
state_key: 'discovery_replied',
|
|
314
|
+
activity_log: [
|
|
315
|
+
{ timestamp: '2026-01-10T10:00:00Z' }, // no type
|
|
316
|
+
makeEvent('reply_sent_to_lead', '2026-01-11T10:00:00Z')
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('returns null when all entries are malformed', () => {
|
|
323
|
+
const deal = {
|
|
324
|
+
state_key: 'discovery_replied',
|
|
325
|
+
activity_log: [null, 'bad', { type: 'reply_received' }]
|
|
326
|
+
}
|
|
327
|
+
expect(getDealOwnership(deal)).toBeNull()
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// state_key variants (non-closed)
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
describe('getDealOwnership — non-closed state_key variants', () => {
|
|
336
|
+
it('evaluates ownership for null state_key', () => {
|
|
337
|
+
const deal = {
|
|
338
|
+
state_key: null,
|
|
339
|
+
activity_log: [makeEvent('reply_received', '2026-01-10T10:00:00Z')]
|
|
340
|
+
}
|
|
341
|
+
expect(getDealOwnership(deal)).toBe('us')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('evaluates ownership for discovery_link_sent state', () => {
|
|
345
|
+
const deal = {
|
|
346
|
+
state_key: 'discovery_link_sent',
|
|
347
|
+
activity_log: [makeEvent('booking_nudge_sent', '2026-01-10T10:00:00Z')]
|
|
348
|
+
}
|
|
349
|
+
expect(getDealOwnership(deal)).toBe('them')
|
|
350
|
+
})
|
|
351
|
+
})
|