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.
Files changed (235) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/dist/dollar.d.ts.map +1 -1
  5. package/dist/dollar.js +2 -2
  6. package/dist/dollar.js.map +1 -1
  7. package/dist/entities/organization.d.ts +4 -0
  8. package/dist/entities/organization.d.ts.map +1 -1
  9. package/dist/entities/organization.js +27 -18
  10. package/dist/entities/organization.js.map +1 -1
  11. package/dist/entities/planning.d.ts +87 -0
  12. package/dist/finance/account.d.ts +44 -0
  13. package/dist/finance/account.d.ts.map +1 -0
  14. package/dist/finance/account.js +6 -0
  15. package/dist/finance/account.js.map +1 -0
  16. package/dist/finance/authority.d.ts +78 -0
  17. package/dist/finance/authority.d.ts.map +1 -0
  18. package/dist/finance/authority.js +27 -0
  19. package/dist/finance/authority.js.map +1 -0
  20. package/dist/finance/card.d.ts +36 -0
  21. package/dist/finance/card.d.ts.map +1 -0
  22. package/dist/finance/card.js +6 -0
  23. package/dist/finance/card.js.map +1 -0
  24. package/dist/finance/identity.d.ts +30 -0
  25. package/dist/finance/identity.d.ts.map +1 -0
  26. package/dist/finance/identity.js +8 -0
  27. package/dist/finance/identity.js.map +1 -0
  28. package/dist/finance/index.d.ts +36 -0
  29. package/dist/finance/index.d.ts.map +1 -0
  30. package/dist/finance/index.js +22 -0
  31. package/dist/finance/index.js.map +1 -0
  32. package/dist/finance/ledger.d.ts +24 -0
  33. package/dist/finance/ledger.d.ts.map +1 -0
  34. package/dist/finance/ledger.js +8 -0
  35. package/dist/finance/ledger.js.map +1 -0
  36. package/dist/finance/merchant.d.ts +129 -0
  37. package/dist/finance/merchant.d.ts.map +1 -0
  38. package/dist/finance/merchant.js +21 -0
  39. package/dist/finance/merchant.js.map +1 -0
  40. package/dist/finance/outcome-contract.d.ts +139 -0
  41. package/dist/finance/outcome-contract.d.ts.map +1 -0
  42. package/dist/finance/outcome-contract.js +27 -0
  43. package/dist/finance/outcome-contract.js.map +1 -0
  44. package/dist/finance/port.d.ts +121 -0
  45. package/dist/finance/port.d.ts.map +1 -0
  46. package/dist/finance/port.js +10 -0
  47. package/dist/finance/port.js.map +1 -0
  48. package/dist/finance/pricing.d.ts +154 -0
  49. package/dist/finance/pricing.d.ts.map +1 -0
  50. package/dist/finance/pricing.js +79 -0
  51. package/dist/finance/pricing.js.map +1 -0
  52. package/dist/finance/proof-predicate.d.ts +92 -0
  53. package/dist/finance/proof-predicate.d.ts.map +1 -0
  54. package/dist/finance/proof-predicate.js +80 -0
  55. package/dist/finance/proof-predicate.js.map +1 -0
  56. package/dist/finance/refund.d.ts +44 -0
  57. package/dist/finance/refund.d.ts.map +1 -0
  58. package/dist/finance/refund.js +41 -0
  59. package/dist/finance/refund.js.map +1 -0
  60. package/dist/finance/sla.d.ts +25 -0
  61. package/dist/finance/sla.d.ts.map +1 -0
  62. package/dist/finance/sla.js +7 -0
  63. package/dist/finance/sla.js.map +1 -0
  64. package/dist/finance/types.d.ts +79 -0
  65. package/dist/finance/types.d.ts.map +1 -0
  66. package/dist/finance/types.js +8 -0
  67. package/dist/{canvas → finance}/types.js.map +1 -1
  68. package/dist/goals.d.ts +19 -0
  69. package/dist/goals.d.ts.map +1 -1
  70. package/dist/goals.js +81 -12
  71. package/dist/goals.js.map +1 -1
  72. package/dist/index.d.ts +12 -8
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +19 -7
  75. package/dist/index.js.map +1 -1
  76. package/dist/kpis.d.ts +19 -0
  77. package/dist/kpis.d.ts.map +1 -1
  78. package/dist/kpis.js +71 -6
  79. package/dist/kpis.js.map +1 -1
  80. package/dist/metrics.d.ts.map +1 -1
  81. package/dist/metrics.js +29 -24
  82. package/dist/metrics.js.map +1 -1
  83. package/dist/okrs.d.ts +34 -0
  84. package/dist/okrs.d.ts.map +1 -1
  85. package/dist/okrs.js +135 -13
  86. package/dist/okrs.js.map +1 -1
  87. package/dist/organization.d.ts.map +1 -1
  88. package/dist/organization.js +11 -11
  89. package/dist/organization.js.map +1 -1
  90. package/dist/process.d.ts.map +1 -1
  91. package/dist/process.js +13 -12
  92. package/dist/process.js.map +1 -1
  93. package/dist/product.d.ts.map +1 -1
  94. package/dist/product.js +9 -9
  95. package/dist/product.js.map +1 -1
  96. package/dist/queries.d.ts.map +1 -1
  97. package/dist/queries.js +194 -32
  98. package/dist/queries.js.map +1 -1
  99. package/dist/roles.d.ts +25 -31
  100. package/dist/roles.d.ts.map +1 -1
  101. package/dist/roles.js +37 -10
  102. package/dist/roles.js.map +1 -1
  103. package/dist/workflow.d.ts.map +1 -1
  104. package/dist/workflow.js +13 -12
  105. package/dist/workflow.js.map +1 -1
  106. package/package.json +20 -13
  107. package/src/dollar.ts +5 -2
  108. package/src/entities/organization.ts +31 -18
  109. package/src/finance/account.ts +48 -0
  110. package/src/finance/authority.ts +42 -0
  111. package/src/finance/card.ts +38 -0
  112. package/src/finance/identity.ts +31 -0
  113. package/src/finance/index.ts +117 -0
  114. package/src/finance/ledger.ts +26 -0
  115. package/src/finance/merchant.ts +127 -0
  116. package/src/finance/outcome-contract.ts +157 -0
  117. package/src/finance/port.ts +144 -0
  118. package/src/finance/pricing.ts +197 -0
  119. package/src/finance/proof-predicate.ts +106 -0
  120. package/src/finance/refund.ts +52 -0
  121. package/src/finance/sla.ts +33 -0
  122. package/src/finance/types.ts +75 -0
  123. package/src/goals.ts +78 -12
  124. package/src/index.ts +48 -18
  125. package/src/kpis.ts +62 -8
  126. package/src/metrics.ts +92 -79
  127. package/src/okrs.ts +120 -20
  128. package/src/organization.ts +12 -15
  129. package/src/process.ts +11 -12
  130. package/src/product.ts +8 -9
  131. package/src/queries.ts +238 -75
  132. package/src/roles.ts +62 -61
  133. package/src/workflow.ts +22 -15
  134. package/test/business.test.ts +282 -0
  135. package/test/dollar.test.ts +270 -0
  136. package/test/entities.test.ts +628 -0
  137. package/test/financials.test.ts +539 -0
  138. package/test/goals.test.ts +451 -0
  139. package/{src → test}/index.test.ts +1 -1
  140. package/test/kpis.test.ts +440 -0
  141. package/test/metrics.test.ts +744 -0
  142. package/test/okrs.test.ts +741 -0
  143. package/test/organization.test.ts +548 -0
  144. package/test/process.test.ts +503 -0
  145. package/test/product.test.ts +430 -0
  146. package/test/queries.test.ts +556 -0
  147. package/test/roles.test.ts +546 -0
  148. package/test/service.test.ts +450 -0
  149. package/test/types.test.ts +1141 -0
  150. package/test/vision.test.ts +214 -0
  151. package/test/workflow.test.ts +501 -0
  152. package/vitest.config.ts +47 -0
  153. package/LICENSE +0 -21
  154. package/dist/canvas/activities.d.ts +0 -19
  155. package/dist/canvas/activities.d.ts.map +0 -1
  156. package/dist/canvas/activities.js +0 -20
  157. package/dist/canvas/activities.js.map +0 -1
  158. package/dist/canvas/channels.d.ts +0 -20
  159. package/dist/canvas/channels.d.ts.map +0 -1
  160. package/dist/canvas/channels.js +0 -21
  161. package/dist/canvas/channels.js.map +0 -1
  162. package/dist/canvas/relationships.d.ts +0 -20
  163. package/dist/canvas/relationships.d.ts.map +0 -1
  164. package/dist/canvas/relationships.js +0 -21
  165. package/dist/canvas/relationships.js.map +0 -1
  166. package/dist/canvas/resources.d.ts +0 -20
  167. package/dist/canvas/resources.d.ts.map +0 -1
  168. package/dist/canvas/resources.js +0 -30
  169. package/dist/canvas/resources.js.map +0 -1
  170. package/dist/canvas/revenue.d.ts +0 -22
  171. package/dist/canvas/revenue.d.ts.map +0 -1
  172. package/dist/canvas/revenue.js +0 -30
  173. package/dist/canvas/revenue.js.map +0 -1
  174. package/dist/canvas/segments.d.ts +0 -20
  175. package/dist/canvas/segments.d.ts.map +0 -1
  176. package/dist/canvas/segments.js +0 -28
  177. package/dist/canvas/segments.js.map +0 -1
  178. package/dist/canvas/types.d.ts +0 -232
  179. package/dist/canvas/types.d.ts.map +0 -1
  180. package/dist/canvas/types.js +0 -8
  181. package/dist/canvas/value.d.ts +0 -20
  182. package/dist/canvas/value.d.ts.map +0 -1
  183. package/dist/canvas/value.js +0 -21
  184. package/dist/canvas/value.js.map +0 -1
  185. package/src/business.js +0 -108
  186. package/src/canvas/activities.ts +0 -32
  187. package/src/canvas/canvas.ts +0 -482
  188. package/src/canvas/channels.ts +0 -34
  189. package/src/canvas/costs.ts +0 -43
  190. package/src/canvas/economics.ts +0 -99
  191. package/src/canvas/index.ts +0 -206
  192. package/src/canvas/partnerships.ts +0 -34
  193. package/src/canvas/projections.ts +0 -141
  194. package/src/canvas/relationships.ts +0 -34
  195. package/src/canvas/resources.ts +0 -43
  196. package/src/canvas/revenue.ts +0 -56
  197. package/src/canvas/segments.ts +0 -42
  198. package/src/canvas/types.ts +0 -363
  199. package/src/canvas/value.ts +0 -34
  200. package/src/dollar.js +0 -106
  201. package/src/entities/assets.js +0 -322
  202. package/src/entities/business.js +0 -369
  203. package/src/entities/communication.js +0 -254
  204. package/src/entities/customers.js +0 -988
  205. package/src/entities/financials.js +0 -931
  206. package/src/entities/goals.js +0 -799
  207. package/src/entities/index.js +0 -197
  208. package/src/entities/legal.js +0 -300
  209. package/src/entities/market.js +0 -300
  210. package/src/entities/marketing.js +0 -1156
  211. package/src/entities/offerings.js +0 -726
  212. package/src/entities/operations.js +0 -786
  213. package/src/entities/organization.js +0 -806
  214. package/src/entities/partnerships.js +0 -299
  215. package/src/entities/planning.js +0 -270
  216. package/src/entities/projects.js +0 -348
  217. package/src/entities/risk.js +0 -292
  218. package/src/entities/sales.js +0 -1247
  219. package/src/financials.js +0 -296
  220. package/src/goals.js +0 -214
  221. package/src/index.js +0 -131
  222. package/src/index.test.js +0 -274
  223. package/src/kpis.js +0 -231
  224. package/src/metrics.js +0 -324
  225. package/src/okrs.js +0 -268
  226. package/src/organization.js +0 -172
  227. package/src/process.js +0 -240
  228. package/src/product.js +0 -144
  229. package/src/queries.js +0 -414
  230. package/src/roles.js +0 -254
  231. package/src/service.js +0 -139
  232. package/src/types.js +0 -4
  233. package/src/vision.js +0 -67
  234. package/src/workflow.js +0 -246
  235. 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(workflow: WorkflowDefinition, action: WorkflowAction): WorkflowDefinition {
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
- return {
116
- ...workflow,
117
- actions: workflow.actions?.filter(a => a.order !== order),
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
- return {
134
- ...workflow,
135
- actions,
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(/(\d+)\s*(ms|millisecond|milliseconds|s|second|seconds|m|minute|minutes|h|hour|hours|d|day|days)/)
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): { valid: boolean; errors: string[] } {
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
+ })