business-as-code 2.1.3 → 2.4.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/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/dist/dollar.d.ts.map +1 -1
- package/dist/dollar.js +2 -2
- package/dist/dollar.js.map +1 -1
- package/dist/entities/organization.d.ts +4 -0
- package/dist/entities/organization.d.ts.map +1 -1
- package/dist/entities/organization.js +27 -18
- package/dist/entities/organization.js.map +1 -1
- package/dist/entities/planning.d.ts +87 -0
- package/dist/finance/account.d.ts +44 -0
- package/dist/finance/account.d.ts.map +1 -0
- package/dist/finance/account.js +6 -0
- package/dist/finance/account.js.map +1 -0
- package/dist/finance/authority.d.ts +78 -0
- package/dist/finance/authority.d.ts.map +1 -0
- package/dist/finance/authority.js +27 -0
- package/dist/finance/authority.js.map +1 -0
- package/dist/finance/card.d.ts +36 -0
- package/dist/finance/card.d.ts.map +1 -0
- package/dist/finance/card.js +6 -0
- package/dist/finance/card.js.map +1 -0
- package/dist/finance/identity.d.ts +30 -0
- package/dist/finance/identity.d.ts.map +1 -0
- package/dist/finance/identity.js +8 -0
- package/dist/finance/identity.js.map +1 -0
- package/dist/finance/index.d.ts +36 -0
- package/dist/finance/index.d.ts.map +1 -0
- package/dist/finance/index.js +22 -0
- package/dist/finance/index.js.map +1 -0
- package/dist/finance/ledger.d.ts +24 -0
- package/dist/finance/ledger.d.ts.map +1 -0
- package/dist/finance/ledger.js +8 -0
- package/dist/finance/ledger.js.map +1 -0
- package/dist/finance/merchant.d.ts +129 -0
- package/dist/finance/merchant.d.ts.map +1 -0
- package/dist/finance/merchant.js +21 -0
- package/dist/finance/merchant.js.map +1 -0
- package/dist/finance/outcome-contract.d.ts +139 -0
- package/dist/finance/outcome-contract.d.ts.map +1 -0
- package/dist/finance/outcome-contract.js +27 -0
- package/dist/finance/outcome-contract.js.map +1 -0
- package/dist/finance/port.d.ts +121 -0
- package/dist/finance/port.d.ts.map +1 -0
- package/dist/finance/port.js +10 -0
- package/dist/finance/port.js.map +1 -0
- package/dist/finance/pricing.d.ts +154 -0
- package/dist/finance/pricing.d.ts.map +1 -0
- package/dist/finance/pricing.js +79 -0
- package/dist/finance/pricing.js.map +1 -0
- package/dist/finance/proof-predicate.d.ts +92 -0
- package/dist/finance/proof-predicate.d.ts.map +1 -0
- package/dist/finance/proof-predicate.js +80 -0
- package/dist/finance/proof-predicate.js.map +1 -0
- package/dist/finance/refund.d.ts +44 -0
- package/dist/finance/refund.d.ts.map +1 -0
- package/dist/finance/refund.js +41 -0
- package/dist/finance/refund.js.map +1 -0
- package/dist/finance/sla.d.ts +25 -0
- package/dist/finance/sla.d.ts.map +1 -0
- package/dist/finance/sla.js +7 -0
- package/dist/finance/sla.js.map +1 -0
- package/dist/finance/types.d.ts +79 -0
- package/dist/finance/types.d.ts.map +1 -0
- package/dist/finance/types.js +8 -0
- package/dist/{canvas → finance}/types.js.map +1 -1
- package/dist/goals.d.ts +19 -0
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +81 -12
- package/dist/goals.js.map +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -7
- package/dist/index.js.map +1 -1
- package/dist/kpis.d.ts +19 -0
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +71 -6
- package/dist/kpis.js.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +29 -24
- package/dist/metrics.js.map +1 -1
- package/dist/okrs.d.ts +34 -0
- package/dist/okrs.d.ts.map +1 -1
- package/dist/okrs.js +135 -13
- package/dist/okrs.js.map +1 -1
- package/dist/organization.d.ts.map +1 -1
- package/dist/organization.js +11 -11
- package/dist/organization.js.map +1 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +13 -12
- package/dist/process.js.map +1 -1
- package/dist/product.d.ts.map +1 -1
- package/dist/product.js +9 -9
- package/dist/product.js.map +1 -1
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +194 -32
- package/dist/queries.js.map +1 -1
- package/dist/roles.d.ts +25 -31
- package/dist/roles.d.ts.map +1 -1
- package/dist/roles.js +37 -10
- package/dist/roles.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +13 -12
- package/dist/workflow.js.map +1 -1
- package/package.json +20 -13
- package/src/dollar.ts +5 -2
- package/src/entities/organization.ts +31 -18
- package/src/finance/account.ts +48 -0
- package/src/finance/authority.ts +42 -0
- package/src/finance/card.ts +38 -0
- package/src/finance/identity.ts +31 -0
- package/src/finance/index.ts +117 -0
- package/src/finance/ledger.ts +26 -0
- package/src/finance/merchant.ts +127 -0
- package/src/finance/outcome-contract.ts +157 -0
- package/src/finance/port.ts +144 -0
- package/src/finance/pricing.ts +197 -0
- package/src/finance/proof-predicate.ts +106 -0
- package/src/finance/refund.ts +52 -0
- package/src/finance/sla.ts +33 -0
- package/src/finance/types.ts +75 -0
- package/src/goals.ts +78 -12
- package/src/index.ts +48 -18
- package/src/kpis.ts +62 -8
- package/src/metrics.ts +92 -79
- package/src/okrs.ts +120 -20
- package/src/organization.ts +12 -15
- package/src/process.ts +11 -12
- package/src/product.ts +8 -9
- package/src/queries.ts +238 -75
- package/src/roles.ts +62 -61
- package/src/workflow.ts +22 -15
- package/test/business.test.ts +282 -0
- package/test/dollar.test.ts +270 -0
- package/test/entities.test.ts +628 -0
- package/test/financials.test.ts +539 -0
- package/test/goals.test.ts +451 -0
- package/{src → test}/index.test.ts +1 -1
- package/test/kpis.test.ts +440 -0
- package/test/metrics.test.ts +744 -0
- package/test/okrs.test.ts +741 -0
- package/test/organization.test.ts +548 -0
- package/test/process.test.ts +503 -0
- package/test/product.test.ts +430 -0
- package/test/queries.test.ts +556 -0
- package/test/roles.test.ts +546 -0
- package/test/service.test.ts +450 -0
- package/test/types.test.ts +1141 -0
- package/test/vision.test.ts +214 -0
- package/test/workflow.test.ts +501 -0
- package/vitest.config.ts +47 -0
- package/LICENSE +0 -21
- package/dist/canvas/activities.d.ts +0 -19
- package/dist/canvas/activities.d.ts.map +0 -1
- package/dist/canvas/activities.js +0 -20
- package/dist/canvas/activities.js.map +0 -1
- package/dist/canvas/channels.d.ts +0 -20
- package/dist/canvas/channels.d.ts.map +0 -1
- package/dist/canvas/channels.js +0 -21
- package/dist/canvas/channels.js.map +0 -1
- package/dist/canvas/relationships.d.ts +0 -20
- package/dist/canvas/relationships.d.ts.map +0 -1
- package/dist/canvas/relationships.js +0 -21
- package/dist/canvas/relationships.js.map +0 -1
- package/dist/canvas/resources.d.ts +0 -20
- package/dist/canvas/resources.d.ts.map +0 -1
- package/dist/canvas/resources.js +0 -30
- package/dist/canvas/resources.js.map +0 -1
- package/dist/canvas/revenue.d.ts +0 -22
- package/dist/canvas/revenue.d.ts.map +0 -1
- package/dist/canvas/revenue.js +0 -30
- package/dist/canvas/revenue.js.map +0 -1
- package/dist/canvas/segments.d.ts +0 -20
- package/dist/canvas/segments.d.ts.map +0 -1
- package/dist/canvas/segments.js +0 -28
- package/dist/canvas/segments.js.map +0 -1
- package/dist/canvas/types.d.ts +0 -232
- package/dist/canvas/types.d.ts.map +0 -1
- package/dist/canvas/types.js +0 -8
- package/dist/canvas/value.d.ts +0 -20
- package/dist/canvas/value.d.ts.map +0 -1
- package/dist/canvas/value.js +0 -21
- package/dist/canvas/value.js.map +0 -1
- package/src/business.js +0 -108
- package/src/canvas/activities.ts +0 -32
- package/src/canvas/canvas.ts +0 -482
- package/src/canvas/channels.ts +0 -34
- package/src/canvas/costs.ts +0 -43
- package/src/canvas/economics.ts +0 -99
- package/src/canvas/index.ts +0 -206
- package/src/canvas/partnerships.ts +0 -34
- package/src/canvas/projections.ts +0 -141
- package/src/canvas/relationships.ts +0 -34
- package/src/canvas/resources.ts +0 -43
- package/src/canvas/revenue.ts +0 -56
- package/src/canvas/segments.ts +0 -42
- package/src/canvas/types.ts +0 -363
- package/src/canvas/value.ts +0 -34
- package/src/dollar.js +0 -106
- package/src/entities/assets.js +0 -322
- package/src/entities/business.js +0 -369
- package/src/entities/communication.js +0 -254
- package/src/entities/customers.js +0 -988
- package/src/entities/financials.js +0 -931
- package/src/entities/goals.js +0 -799
- package/src/entities/index.js +0 -197
- package/src/entities/legal.js +0 -300
- package/src/entities/market.js +0 -300
- package/src/entities/marketing.js +0 -1156
- package/src/entities/offerings.js +0 -726
- package/src/entities/operations.js +0 -786
- package/src/entities/organization.js +0 -806
- package/src/entities/partnerships.js +0 -299
- package/src/entities/planning.js +0 -270
- package/src/entities/projects.js +0 -348
- package/src/entities/risk.js +0 -292
- package/src/entities/sales.js +0 -1247
- package/src/financials.js +0 -296
- package/src/goals.js +0 -214
- package/src/index.js +0 -131
- package/src/index.test.js +0 -274
- package/src/kpis.js +0 -231
- package/src/metrics.js +0 -324
- package/src/okrs.js +0 -268
- package/src/organization.js +0 -172
- package/src/process.js +0 -240
- package/src/product.js +0 -144
- package/src/queries.js +0 -414
- package/src/roles.js +0 -254
- package/src/service.js +0 -139
- package/src/types.js +0 -4
- package/src/vision.js +0 -67
- package/src/workflow.js +0 -246
- package/tests/canvas.test.ts +0 -842
package/src/workflow.ts
CHANGED
|
@@ -88,20 +88,23 @@ export function getActionsByType(
|
|
|
88
88
|
workflow: WorkflowDefinition,
|
|
89
89
|
type: WorkflowAction['type']
|
|
90
90
|
): WorkflowAction[] {
|
|
91
|
-
return workflow.actions?.filter(action => action.type === type) || []
|
|
91
|
+
return workflow.actions?.filter((action) => action.type === type) || []
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Get conditional actions
|
|
96
96
|
*/
|
|
97
97
|
export function getConditionalActions(workflow: WorkflowDefinition): WorkflowAction[] {
|
|
98
|
-
return workflow.actions?.filter(action => action.condition) || []
|
|
98
|
+
return workflow.actions?.filter((action) => action.condition) || []
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* Add action to workflow
|
|
103
103
|
*/
|
|
104
|
-
export function addAction(
|
|
104
|
+
export function addAction(
|
|
105
|
+
workflow: WorkflowDefinition,
|
|
106
|
+
action: WorkflowAction
|
|
107
|
+
): WorkflowDefinition {
|
|
105
108
|
return {
|
|
106
109
|
...workflow,
|
|
107
110
|
actions: [...(workflow.actions || []), action],
|
|
@@ -112,10 +115,10 @@ export function addAction(workflow: WorkflowDefinition, action: WorkflowAction):
|
|
|
112
115
|
* Remove action from workflow
|
|
113
116
|
*/
|
|
114
117
|
export function removeAction(workflow: WorkflowDefinition, order: number): WorkflowDefinition {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
const actions = workflow.actions?.filter((a) => a.order !== order)
|
|
119
|
+
const result: WorkflowDefinition = { ...workflow }
|
|
120
|
+
if (actions !== undefined) result.actions = actions
|
|
121
|
+
return result
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/**
|
|
@@ -126,14 +129,13 @@ export function updateAction(
|
|
|
126
129
|
order: number,
|
|
127
130
|
updates: Partial<WorkflowAction>
|
|
128
131
|
): WorkflowDefinition {
|
|
129
|
-
const actions = workflow.actions?.map(action =>
|
|
132
|
+
const actions = workflow.actions?.map((action) =>
|
|
130
133
|
action.order === order ? { ...action, ...updates } : action
|
|
131
134
|
)
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
136
|
+
const result: WorkflowDefinition = { ...workflow }
|
|
137
|
+
if (actions !== undefined) result.actions = actions
|
|
138
|
+
return result
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
@@ -161,7 +163,9 @@ export function isWebhookTrigger(trigger: WorkflowTrigger): boolean {
|
|
|
161
163
|
* Parse wait duration to milliseconds
|
|
162
164
|
*/
|
|
163
165
|
export function parseWaitDuration(duration: string): number {
|
|
164
|
-
const match = duration.match(
|
|
166
|
+
const match = duration.match(
|
|
167
|
+
/(\d+)\s*(ms|millisecond|milliseconds|s|second|seconds|m|minute|minutes|h|hour|hours|d|day|days)/
|
|
168
|
+
)
|
|
165
169
|
if (!match) return 0
|
|
166
170
|
|
|
167
171
|
const value = parseInt(match[1] || '0', 10)
|
|
@@ -234,7 +238,10 @@ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
|
234
238
|
/**
|
|
235
239
|
* Validate workflow definition
|
|
236
240
|
*/
|
|
237
|
-
export function validateWorkflow(workflow: WorkflowDefinition): {
|
|
241
|
+
export function validateWorkflow(workflow: WorkflowDefinition): {
|
|
242
|
+
valid: boolean
|
|
243
|
+
errors: string[]
|
|
244
|
+
} {
|
|
238
245
|
const errors: string[] = []
|
|
239
246
|
|
|
240
247
|
if (!workflow.name) {
|
|
@@ -267,7 +274,7 @@ export function validateWorkflow(workflow: WorkflowDefinition): { valid: boolean
|
|
|
267
274
|
orders.add(action.order)
|
|
268
275
|
|
|
269
276
|
// Validate action-specific requirements
|
|
270
|
-
if (action.type === 'wait' && !action.params?.duration) {
|
|
277
|
+
if (action.type === 'wait' && !action.params?.['duration']) {
|
|
271
278
|
errors.push(`Wait action at order ${action.order} must specify duration`)
|
|
272
279
|
}
|
|
273
280
|
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for business.ts - Business entity definition and management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
Business,
|
|
8
|
+
getTotalBudget,
|
|
9
|
+
getTotalTeamSize,
|
|
10
|
+
getDepartment,
|
|
11
|
+
getTeam,
|
|
12
|
+
validateBusiness,
|
|
13
|
+
} from '../src/business.js'
|
|
14
|
+
import type { BusinessDefinition } from '../src/types.js'
|
|
15
|
+
|
|
16
|
+
describe('Business', () => {
|
|
17
|
+
describe('Business()', () => {
|
|
18
|
+
it('should create a business entity with required fields', () => {
|
|
19
|
+
const business = Business({
|
|
20
|
+
name: 'Acme Corp',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(business.name).toBe('Acme Corp')
|
|
24
|
+
expect(business.values).toEqual([])
|
|
25
|
+
expect(business.teamSize).toBe(0)
|
|
26
|
+
expect(business.metadata).toEqual({})
|
|
27
|
+
expect(business.foundedAt).toBeInstanceOf(Date)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should create a business entity with all fields', () => {
|
|
31
|
+
const foundedDate = new Date('2020-01-01')
|
|
32
|
+
const business = Business({
|
|
33
|
+
name: 'Test Corp',
|
|
34
|
+
description: 'A test company',
|
|
35
|
+
industry: 'Technology',
|
|
36
|
+
mission: 'To innovate',
|
|
37
|
+
values: ['Innovation', 'Integrity'],
|
|
38
|
+
targetMarket: 'Enterprise',
|
|
39
|
+
foundedAt: foundedDate,
|
|
40
|
+
teamSize: 50,
|
|
41
|
+
structure: {
|
|
42
|
+
departments: [
|
|
43
|
+
{
|
|
44
|
+
name: 'Engineering',
|
|
45
|
+
head: 'Jane Smith',
|
|
46
|
+
members: ['Alice', 'Bob'],
|
|
47
|
+
budget: 1000000,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
metadata: { customField: 'value' },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(business.name).toBe('Test Corp')
|
|
55
|
+
expect(business.description).toBe('A test company')
|
|
56
|
+
expect(business.industry).toBe('Technology')
|
|
57
|
+
expect(business.mission).toBe('To innovate')
|
|
58
|
+
expect(business.values).toEqual(['Innovation', 'Integrity'])
|
|
59
|
+
expect(business.targetMarket).toBe('Enterprise')
|
|
60
|
+
expect(business.foundedAt).toBe(foundedDate)
|
|
61
|
+
expect(business.teamSize).toBe(50)
|
|
62
|
+
expect(business.structure?.departments).toHaveLength(1)
|
|
63
|
+
expect(business.metadata).toEqual({ customField: 'value' })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should throw error if name is empty', () => {
|
|
67
|
+
expect(() => Business({ name: '' })).toThrow('Business name is required')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should preserve provided values and not override with defaults', () => {
|
|
71
|
+
const business = Business({
|
|
72
|
+
name: 'Test',
|
|
73
|
+
values: ['Custom'],
|
|
74
|
+
teamSize: 100,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(business.values).toEqual(['Custom'])
|
|
78
|
+
expect(business.teamSize).toBe(100)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('getTotalBudget()', () => {
|
|
83
|
+
it('should return 0 if no departments', () => {
|
|
84
|
+
const business = Business({ name: 'Test' })
|
|
85
|
+
expect(getTotalBudget(business)).toBe(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should return 0 if structure has no departments', () => {
|
|
89
|
+
const business = Business({
|
|
90
|
+
name: 'Test',
|
|
91
|
+
structure: {},
|
|
92
|
+
})
|
|
93
|
+
expect(getTotalBudget(business)).toBe(0)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should calculate total budget across departments', () => {
|
|
97
|
+
const business = Business({
|
|
98
|
+
name: 'Test',
|
|
99
|
+
structure: {
|
|
100
|
+
departments: [
|
|
101
|
+
{ name: 'Engineering', budget: 1000000 },
|
|
102
|
+
{ name: 'Sales', budget: 500000 },
|
|
103
|
+
{ name: 'HR' }, // No budget specified
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(getTotalBudget(business)).toBe(1500000)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should handle departments with zero budget', () => {
|
|
112
|
+
const business = Business({
|
|
113
|
+
name: 'Test',
|
|
114
|
+
structure: {
|
|
115
|
+
departments: [
|
|
116
|
+
{ name: 'Engineering', budget: 1000000 },
|
|
117
|
+
{ name: 'Sales', budget: 0 },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(getTotalBudget(business)).toBe(1000000)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('getTotalTeamSize()', () => {
|
|
127
|
+
it('should return teamSize if no departments', () => {
|
|
128
|
+
const business = Business({ name: 'Test', teamSize: 50 })
|
|
129
|
+
expect(getTotalTeamSize(business)).toBe(50)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should return 0 if no teamSize and no departments', () => {
|
|
133
|
+
const business = Business({ name: 'Test' })
|
|
134
|
+
expect(getTotalTeamSize(business)).toBe(0)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should calculate total team size from department members', () => {
|
|
138
|
+
const business = Business({
|
|
139
|
+
name: 'Test',
|
|
140
|
+
structure: {
|
|
141
|
+
departments: [
|
|
142
|
+
{ name: 'Engineering', members: ['Alice', 'Bob', 'Charlie'] },
|
|
143
|
+
{ name: 'Sales', members: ['David', 'Eve'] },
|
|
144
|
+
{ name: 'HR' }, // No members
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
expect(getTotalTeamSize(business)).toBe(5)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('getDepartment()', () => {
|
|
154
|
+
const business = Business({
|
|
155
|
+
name: 'Test',
|
|
156
|
+
structure: {
|
|
157
|
+
departments: [
|
|
158
|
+
{ name: 'Engineering', head: 'Jane' },
|
|
159
|
+
{ name: 'Sales', head: 'John' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should find department by name', () => {
|
|
165
|
+
const dept = getDepartment(business, 'Engineering')
|
|
166
|
+
expect(dept?.name).toBe('Engineering')
|
|
167
|
+
expect(dept?.head).toBe('Jane')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should return undefined for non-existent department', () => {
|
|
171
|
+
const dept = getDepartment(business, 'Marketing')
|
|
172
|
+
expect(dept).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should return undefined if no structure', () => {
|
|
176
|
+
const simpleBusiness = Business({ name: 'Simple' })
|
|
177
|
+
const dept = getDepartment(simpleBusiness, 'Engineering')
|
|
178
|
+
expect(dept).toBeUndefined()
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('getTeam()', () => {
|
|
183
|
+
const business = Business({
|
|
184
|
+
name: 'Test',
|
|
185
|
+
structure: {
|
|
186
|
+
teams: [
|
|
187
|
+
{ name: 'Alpha', lead: 'Alice' },
|
|
188
|
+
{ name: 'Beta', lead: 'Bob' },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should find team by name', () => {
|
|
194
|
+
const team = getTeam(business, 'Alpha')
|
|
195
|
+
expect(team?.name).toBe('Alpha')
|
|
196
|
+
expect(team?.lead).toBe('Alice')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should return undefined for non-existent team', () => {
|
|
200
|
+
const team = getTeam(business, 'Gamma')
|
|
201
|
+
expect(team).toBeUndefined()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should return undefined if no structure', () => {
|
|
205
|
+
const simpleBusiness = Business({ name: 'Simple' })
|
|
206
|
+
const team = getTeam(simpleBusiness, 'Alpha')
|
|
207
|
+
expect(team).toBeUndefined()
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe('validateBusiness()', () => {
|
|
212
|
+
it('should validate valid business', () => {
|
|
213
|
+
const business = Business({
|
|
214
|
+
name: 'Valid Corp',
|
|
215
|
+
teamSize: 50,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const result = validateBusiness(business)
|
|
219
|
+
expect(result.valid).toBe(true)
|
|
220
|
+
expect(result.errors).toHaveLength(0)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should fail validation if name is missing', () => {
|
|
224
|
+
const business: BusinessDefinition = { name: '' }
|
|
225
|
+
const result = validateBusiness(business)
|
|
226
|
+
|
|
227
|
+
expect(result.valid).toBe(false)
|
|
228
|
+
expect(result.errors).toContain('Business name is required')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should fail validation if teamSize is negative', () => {
|
|
232
|
+
const business: BusinessDefinition = { name: 'Test', teamSize: -5 }
|
|
233
|
+
const result = validateBusiness(business)
|
|
234
|
+
|
|
235
|
+
expect(result.valid).toBe(false)
|
|
236
|
+
expect(result.errors).toContain('Team size cannot be negative')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should fail validation if department has no name', () => {
|
|
240
|
+
const business: BusinessDefinition = {
|
|
241
|
+
name: 'Test',
|
|
242
|
+
structure: {
|
|
243
|
+
departments: [{ name: '', budget: 1000 }],
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
const result = validateBusiness(business)
|
|
247
|
+
|
|
248
|
+
expect(result.valid).toBe(false)
|
|
249
|
+
expect(result.errors).toContain('Department name is required')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should fail validation if department budget is negative', () => {
|
|
253
|
+
const business: BusinessDefinition = {
|
|
254
|
+
name: 'Test',
|
|
255
|
+
structure: {
|
|
256
|
+
departments: [{ name: 'Engineering', budget: -1000 }],
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
const result = validateBusiness(business)
|
|
260
|
+
|
|
261
|
+
expect(result.valid).toBe(false)
|
|
262
|
+
expect(result.errors).toContain('Department Engineering budget cannot be negative')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should return multiple errors for multiple issues', () => {
|
|
266
|
+
const business: BusinessDefinition = {
|
|
267
|
+
name: '',
|
|
268
|
+
teamSize: -5,
|
|
269
|
+
structure: {
|
|
270
|
+
departments: [
|
|
271
|
+
{ name: '', budget: 1000 },
|
|
272
|
+
{ name: 'Sales', budget: -500 },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
const result = validateBusiness(business)
|
|
277
|
+
|
|
278
|
+
expect(result.valid).toBe(false)
|
|
279
|
+
expect(result.errors.length).toBeGreaterThan(1)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for dollar.ts - $ helper for business operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
$,
|
|
8
|
+
createBusinessOperations,
|
|
9
|
+
updateContext,
|
|
10
|
+
getContext,
|
|
11
|
+
resetContext,
|
|
12
|
+
} from '../src/dollar.js'
|
|
13
|
+
|
|
14
|
+
describe('Dollar ($) Helper', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
resetContext()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('createBusinessOperations()', () => {
|
|
20
|
+
it('should create operations with default context', () => {
|
|
21
|
+
const ops = createBusinessOperations()
|
|
22
|
+
|
|
23
|
+
expect(ops.context).toEqual({})
|
|
24
|
+
expect(typeof ops.format).toBe('function')
|
|
25
|
+
expect(typeof ops.percent).toBe('function')
|
|
26
|
+
expect(typeof ops.growth).toBe('function')
|
|
27
|
+
expect(typeof ops.margin).toBe('function')
|
|
28
|
+
expect(typeof ops.roi).toBe('function')
|
|
29
|
+
expect(typeof ops.ltv).toBe('function')
|
|
30
|
+
expect(typeof ops.cac).toBe('function')
|
|
31
|
+
expect(typeof ops.burnRate).toBe('function')
|
|
32
|
+
expect(typeof ops.runway).toBe('function')
|
|
33
|
+
expect(typeof ops.log).toBe('function')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should create operations with initial context', () => {
|
|
37
|
+
const ops = createBusinessOperations({
|
|
38
|
+
business: { name: 'Test Corp' },
|
|
39
|
+
financials: { revenue: 100000 },
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(ops.context.business?.name).toBe('Test Corp')
|
|
43
|
+
expect(ops.context.financials?.revenue).toBe(100000)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('$.format()', () => {
|
|
48
|
+
it('should format currency with default USD', () => {
|
|
49
|
+
const formatted = $.format(1234.56)
|
|
50
|
+
expect(formatted).toContain('1,234.56')
|
|
51
|
+
expect(formatted).toContain('$')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should format currency with specified currency', () => {
|
|
55
|
+
const formatted = $.format(1000, 'EUR')
|
|
56
|
+
expect(formatted).toContain('1,000')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should format negative amounts', () => {
|
|
60
|
+
const formatted = $.format(-500)
|
|
61
|
+
expect(formatted).toContain('-')
|
|
62
|
+
expect(formatted).toContain('500')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should handle zero amount', () => {
|
|
66
|
+
const formatted = $.format(0)
|
|
67
|
+
expect(formatted).toContain('0')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should handle large numbers', () => {
|
|
71
|
+
const formatted = $.format(1000000000)
|
|
72
|
+
expect(formatted).toContain('1,000,000,000')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('$.percent()', () => {
|
|
77
|
+
it('should calculate percentage', () => {
|
|
78
|
+
expect($.percent(25, 100)).toBe(25)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should handle zero total', () => {
|
|
82
|
+
expect($.percent(25, 0)).toBe(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should handle over 100%', () => {
|
|
86
|
+
expect($.percent(150, 100)).toBe(150)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should handle decimal values', () => {
|
|
90
|
+
expect($.percent(33, 100)).toBe(33)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should handle zero value', () => {
|
|
94
|
+
expect($.percent(0, 100)).toBe(0)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('$.growth()', () => {
|
|
99
|
+
it('should calculate growth rate', () => {
|
|
100
|
+
expect($.growth(120, 100)).toBe(20)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should calculate negative growth', () => {
|
|
104
|
+
expect($.growth(80, 100)).toBe(-20)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle zero previous value', () => {
|
|
108
|
+
expect($.growth(100, 0)).toBe(0)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should handle same values (no growth)', () => {
|
|
112
|
+
expect($.growth(100, 100)).toBe(0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should handle doubling', () => {
|
|
116
|
+
expect($.growth(200, 100)).toBe(100)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('$.margin()', () => {
|
|
121
|
+
it('should calculate margin', () => {
|
|
122
|
+
expect($.margin(100, 60)).toBe(40)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should handle zero revenue', () => {
|
|
126
|
+
expect($.margin(0, 0)).toBe(0)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should handle 100% margin', () => {
|
|
130
|
+
expect($.margin(100, 0)).toBe(100)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should handle negative margin', () => {
|
|
134
|
+
expect($.margin(100, 150)).toBe(-50)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('$.roi()', () => {
|
|
139
|
+
it('should calculate ROI', () => {
|
|
140
|
+
expect($.roi(150, 100)).toBe(50)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should calculate negative ROI', () => {
|
|
144
|
+
expect($.roi(80, 100)).toBe(-20)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should handle zero cost', () => {
|
|
148
|
+
expect($.roi(150, 0)).toBe(0)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should handle 100% ROI', () => {
|
|
152
|
+
expect($.roi(200, 100)).toBe(100)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('$.ltv()', () => {
|
|
157
|
+
it('should calculate lifetime value', () => {
|
|
158
|
+
expect($.ltv(100, 12, 2)).toBe(2400)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should handle monthly subscription', () => {
|
|
162
|
+
// $50/month, 12 months/year, 3 year lifetime
|
|
163
|
+
expect($.ltv(50, 12, 3)).toBe(1800)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should handle zero values', () => {
|
|
167
|
+
expect($.ltv(0, 12, 2)).toBe(0)
|
|
168
|
+
expect($.ltv(100, 0, 2)).toBe(0)
|
|
169
|
+
expect($.ltv(100, 12, 0)).toBe(0)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('$.cac()', () => {
|
|
174
|
+
it('should calculate customer acquisition cost', () => {
|
|
175
|
+
expect($.cac(10000, 100)).toBe(100)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should handle zero customers', () => {
|
|
179
|
+
expect($.cac(10000, 0)).toBe(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should handle zero spend', () => {
|
|
183
|
+
expect($.cac(0, 100)).toBe(0)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('$.burnRate()', () => {
|
|
188
|
+
it('should calculate monthly burn rate', () => {
|
|
189
|
+
expect($.burnRate(100000, 70000, 3)).toBe(10000)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should handle zero months', () => {
|
|
193
|
+
expect($.burnRate(100000, 70000, 0)).toBe(0)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should handle positive cash flow', () => {
|
|
197
|
+
// If ending > starting, burn is negative (accumulating cash)
|
|
198
|
+
expect($.burnRate(100000, 130000, 3)).toBe(-10000)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('$.runway()', () => {
|
|
203
|
+
it('should calculate runway in months', () => {
|
|
204
|
+
expect($.runway(100000, 10000)).toBe(10)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should handle zero burn rate', () => {
|
|
208
|
+
expect($.runway(100000, 0)).toBe(Infinity)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should handle negative burn rate (profitable)', () => {
|
|
212
|
+
expect($.runway(100000, -10000)).toBe(Infinity)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should handle large runway', () => {
|
|
216
|
+
expect($.runway(1000000, 1000)).toBe(1000)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('Context Management', () => {
|
|
221
|
+
it('should update context', () => {
|
|
222
|
+
updateContext({ business: { name: 'Updated Corp' } })
|
|
223
|
+
const ctx = getContext()
|
|
224
|
+
expect(ctx.business?.name).toBe('Updated Corp')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should merge context updates', () => {
|
|
228
|
+
updateContext({ business: { name: 'Test Corp' } })
|
|
229
|
+
updateContext({ financials: { revenue: 100000 } })
|
|
230
|
+
|
|
231
|
+
const ctx = getContext()
|
|
232
|
+
expect(ctx.business?.name).toBe('Test Corp')
|
|
233
|
+
expect(ctx.financials?.revenue).toBe(100000)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should reset context', () => {
|
|
237
|
+
updateContext({ business: { name: 'Test Corp' } })
|
|
238
|
+
resetContext()
|
|
239
|
+
|
|
240
|
+
const ctx = getContext()
|
|
241
|
+
expect(ctx).toEqual({})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should get current context', () => {
|
|
245
|
+
updateContext({ customField: 'custom value' })
|
|
246
|
+
const ctx = getContext()
|
|
247
|
+
expect(ctx.customField).toBe('custom value')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('Default $ instance', () => {
|
|
252
|
+
it('should have all operations available', () => {
|
|
253
|
+
expect(typeof $.format).toBe('function')
|
|
254
|
+
expect(typeof $.percent).toBe('function')
|
|
255
|
+
expect(typeof $.growth).toBe('function')
|
|
256
|
+
expect(typeof $.margin).toBe('function')
|
|
257
|
+
expect(typeof $.roi).toBe('function')
|
|
258
|
+
expect(typeof $.ltv).toBe('function')
|
|
259
|
+
expect(typeof $.cac).toBe('function')
|
|
260
|
+
expect(typeof $.burnRate).toBe('function')
|
|
261
|
+
expect(typeof $.runway).toBe('function')
|
|
262
|
+
expect(typeof $.log).toBe('function')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should have context object', () => {
|
|
266
|
+
expect($.context).toBeDefined()
|
|
267
|
+
expect(typeof $.context).toBe('object')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
})
|