business-as-code 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -0
  3. package/package.json +7 -4
  4. package/src/dollar.ts +5 -2
  5. package/src/entities/organization.ts +31 -18
  6. package/src/goals.ts +78 -12
  7. package/src/index.ts +48 -18
  8. package/src/kpis.ts +62 -8
  9. package/src/metrics.ts +92 -79
  10. package/src/okrs.ts +120 -20
  11. package/src/organization.ts +12 -15
  12. package/src/process.ts +11 -12
  13. package/src/product.ts +8 -9
  14. package/src/queries.ts +238 -75
  15. package/src/roles.ts +62 -61
  16. package/src/workflow.ts +22 -15
  17. package/test/business.test.ts +282 -0
  18. package/test/dollar.test.ts +270 -0
  19. package/test/entities.test.ts +628 -0
  20. package/test/financials.test.ts +539 -0
  21. package/test/goals.test.ts +451 -0
  22. package/{src → test}/index.test.ts +1 -1
  23. package/test/kpis.test.ts +440 -0
  24. package/test/metrics.test.ts +744 -0
  25. package/test/okrs.test.ts +741 -0
  26. package/test/organization.test.ts +548 -0
  27. package/test/process.test.ts +503 -0
  28. package/test/product.test.ts +430 -0
  29. package/test/queries.test.ts +556 -0
  30. package/test/roles.test.ts +546 -0
  31. package/test/service.test.ts +450 -0
  32. package/test/types.test.ts +1141 -0
  33. package/test/vision.test.ts +214 -0
  34. package/test/workflow.test.ts +501 -0
  35. package/vitest.config.ts +47 -0
  36. package/.turbo/turbo-build.log +0 -5
  37. package/dist/business.d.ts +0 -62
  38. package/dist/business.d.ts.map +0 -1
  39. package/dist/business.js +0 -109
  40. package/dist/business.js.map +0 -1
  41. package/dist/dollar.d.ts +0 -60
  42. package/dist/dollar.d.ts.map +0 -1
  43. package/dist/dollar.js +0 -107
  44. package/dist/dollar.js.map +0 -1
  45. package/dist/entities/assets.d.ts +0 -21
  46. package/dist/entities/assets.d.ts.map +0 -1
  47. package/dist/entities/assets.js +0 -323
  48. package/dist/entities/assets.js.map +0 -1
  49. package/dist/entities/business.d.ts +0 -36
  50. package/dist/entities/business.d.ts.map +0 -1
  51. package/dist/entities/business.js +0 -370
  52. package/dist/entities/business.js.map +0 -1
  53. package/dist/entities/communication.d.ts +0 -21
  54. package/dist/entities/communication.d.ts.map +0 -1
  55. package/dist/entities/communication.js +0 -255
  56. package/dist/entities/communication.js.map +0 -1
  57. package/dist/entities/customers.d.ts +0 -58
  58. package/dist/entities/customers.d.ts.map +0 -1
  59. package/dist/entities/customers.js +0 -989
  60. package/dist/entities/customers.js.map +0 -1
  61. package/dist/entities/financials.d.ts +0 -59
  62. package/dist/entities/financials.d.ts.map +0 -1
  63. package/dist/entities/financials.js +0 -932
  64. package/dist/entities/financials.js.map +0 -1
  65. package/dist/entities/goals.d.ts +0 -58
  66. package/dist/entities/goals.d.ts.map +0 -1
  67. package/dist/entities/goals.js +0 -800
  68. package/dist/entities/goals.js.map +0 -1
  69. package/dist/entities/index.d.ts +0 -299
  70. package/dist/entities/index.d.ts.map +0 -1
  71. package/dist/entities/index.js +0 -198
  72. package/dist/entities/index.js.map +0 -1
  73. package/dist/entities/legal.d.ts +0 -21
  74. package/dist/entities/legal.d.ts.map +0 -1
  75. package/dist/entities/legal.js +0 -301
  76. package/dist/entities/legal.js.map +0 -1
  77. package/dist/entities/market.d.ts +0 -21
  78. package/dist/entities/market.d.ts.map +0 -1
  79. package/dist/entities/market.js +0 -301
  80. package/dist/entities/market.js.map +0 -1
  81. package/dist/entities/marketing.d.ts +0 -67
  82. package/dist/entities/marketing.d.ts.map +0 -1
  83. package/dist/entities/marketing.js +0 -1157
  84. package/dist/entities/marketing.js.map +0 -1
  85. package/dist/entities/offerings.d.ts +0 -51
  86. package/dist/entities/offerings.d.ts.map +0 -1
  87. package/dist/entities/offerings.js +0 -727
  88. package/dist/entities/offerings.js.map +0 -1
  89. package/dist/entities/operations.d.ts +0 -58
  90. package/dist/entities/operations.d.ts.map +0 -1
  91. package/dist/entities/operations.js +0 -787
  92. package/dist/entities/operations.js.map +0 -1
  93. package/dist/entities/organization.d.ts +0 -57
  94. package/dist/entities/organization.d.ts.map +0 -1
  95. package/dist/entities/organization.js +0 -807
  96. package/dist/entities/organization.js.map +0 -1
  97. package/dist/entities/partnerships.d.ts +0 -21
  98. package/dist/entities/partnerships.d.ts.map +0 -1
  99. package/dist/entities/partnerships.js +0 -300
  100. package/dist/entities/partnerships.js.map +0 -1
  101. package/dist/entities/planning.d.ts +0 -87
  102. package/dist/entities/planning.d.ts.map +0 -1
  103. package/dist/entities/planning.js +0 -271
  104. package/dist/entities/planning.js.map +0 -1
  105. package/dist/entities/projects.d.ts +0 -25
  106. package/dist/entities/projects.d.ts.map +0 -1
  107. package/dist/entities/projects.js +0 -349
  108. package/dist/entities/projects.js.map +0 -1
  109. package/dist/entities/risk.d.ts +0 -21
  110. package/dist/entities/risk.d.ts.map +0 -1
  111. package/dist/entities/risk.js +0 -293
  112. package/dist/entities/risk.js.map +0 -1
  113. package/dist/entities/sales.d.ts +0 -72
  114. package/dist/entities/sales.d.ts.map +0 -1
  115. package/dist/entities/sales.js +0 -1248
  116. package/dist/entities/sales.js.map +0 -1
  117. package/dist/financials.d.ts +0 -130
  118. package/dist/financials.d.ts.map +0 -1
  119. package/dist/financials.js +0 -297
  120. package/dist/financials.js.map +0 -1
  121. package/dist/goals.d.ts +0 -87
  122. package/dist/goals.d.ts.map +0 -1
  123. package/dist/goals.js +0 -215
  124. package/dist/goals.js.map +0 -1
  125. package/dist/index.d.ts +0 -97
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js +0 -132
  128. package/dist/index.js.map +0 -1
  129. package/dist/kpis.d.ts +0 -118
  130. package/dist/kpis.d.ts.map +0 -1
  131. package/dist/kpis.js +0 -232
  132. package/dist/kpis.js.map +0 -1
  133. package/dist/metrics.d.ts +0 -448
  134. package/dist/metrics.d.ts.map +0 -1
  135. package/dist/metrics.js +0 -325
  136. package/dist/metrics.js.map +0 -1
  137. package/dist/okrs.d.ts +0 -123
  138. package/dist/okrs.d.ts.map +0 -1
  139. package/dist/okrs.js +0 -269
  140. package/dist/okrs.js.map +0 -1
  141. package/dist/organization.d.ts +0 -585
  142. package/dist/organization.d.ts.map +0 -1
  143. package/dist/organization.js +0 -173
  144. package/dist/organization.js.map +0 -1
  145. package/dist/process.d.ts +0 -112
  146. package/dist/process.d.ts.map +0 -1
  147. package/dist/process.js +0 -241
  148. package/dist/process.js.map +0 -1
  149. package/dist/product.d.ts +0 -85
  150. package/dist/product.d.ts.map +0 -1
  151. package/dist/product.js +0 -145
  152. package/dist/product.js.map +0 -1
  153. package/dist/queries.d.ts +0 -304
  154. package/dist/queries.d.ts.map +0 -1
  155. package/dist/queries.js +0 -415
  156. package/dist/queries.js.map +0 -1
  157. package/dist/roles.d.ts +0 -340
  158. package/dist/roles.d.ts.map +0 -1
  159. package/dist/roles.js +0 -255
  160. package/dist/roles.js.map +0 -1
  161. package/dist/service.d.ts +0 -61
  162. package/dist/service.d.ts.map +0 -1
  163. package/dist/service.js +0 -140
  164. package/dist/service.js.map +0 -1
  165. package/dist/types.d.ts +0 -459
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -5
  168. package/dist/types.js.map +0 -1
  169. package/dist/vision.d.ts +0 -38
  170. package/dist/vision.d.ts.map +0 -1
  171. package/dist/vision.js +0 -68
  172. package/dist/vision.js.map +0 -1
  173. package/dist/workflow.d.ts +0 -115
  174. package/dist/workflow.d.ts.map +0 -1
  175. package/dist/workflow.js +0 -247
  176. package/dist/workflow.js.map +0 -1
  177. package/src/business.js +0 -108
  178. package/src/dollar.js +0 -106
  179. package/src/entities/assets.js +0 -322
  180. package/src/entities/business.js +0 -369
  181. package/src/entities/communication.js +0 -254
  182. package/src/entities/customers.js +0 -988
  183. package/src/entities/financials.js +0 -931
  184. package/src/entities/goals.js +0 -799
  185. package/src/entities/index.js +0 -197
  186. package/src/entities/legal.js +0 -300
  187. package/src/entities/market.js +0 -300
  188. package/src/entities/marketing.js +0 -1156
  189. package/src/entities/offerings.js +0 -726
  190. package/src/entities/operations.js +0 -786
  191. package/src/entities/organization.js +0 -806
  192. package/src/entities/partnerships.js +0 -299
  193. package/src/entities/planning.js +0 -270
  194. package/src/entities/projects.js +0 -348
  195. package/src/entities/risk.js +0 -292
  196. package/src/entities/sales.js +0 -1247
  197. package/src/financials.js +0 -296
  198. package/src/goals.js +0 -214
  199. package/src/index.js +0 -131
  200. package/src/index.test.js +0 -274
  201. package/src/kpis.js +0 -231
  202. package/src/metrics.js +0 -324
  203. package/src/okrs.js +0 -268
  204. package/src/organization.js +0 -172
  205. package/src/process.js +0 -240
  206. package/src/product.js +0 -144
  207. package/src/queries.js +0 -414
  208. package/src/roles.js +0 -254
  209. package/src/service.js +0 -139
  210. package/src/types.js +0 -4
  211. package/src/vision.js +0 -67
  212. package/src/workflow.js +0 -246
package/src/roles.ts CHANGED
@@ -9,6 +9,13 @@
9
9
  * @packageDocumentation
10
10
  */
11
11
 
12
+ import type { Role as OrgRole, RoleType as OrgRoleType, RoleWorkerType } from 'org.ai'
13
+ import type { Worker, WorkerRef } from 'digital-workers'
14
+
15
+ // Re-export for convenience
16
+ export type { Worker, WorkerRef } from 'digital-workers'
17
+ export type { OrgRole, OrgRoleType, RoleWorkerType }
18
+
12
19
  // =============================================================================
13
20
  // Business Role - Bridges Worker Role and Authorization
14
21
  // =============================================================================
@@ -43,7 +50,10 @@ export type BusinessRoleType =
43
50
  | string
44
51
 
45
52
  /**
46
- * Business Role - extends WorkerRole with authorization and task capabilities
53
+ * Business Role - extends org.ai Role with authorization and task capabilities
54
+ *
55
+ * Composes with org.ai Role to provide additional business-specific properties
56
+ * like authorization permissions, task capabilities, and compensation.
47
57
  *
48
58
  * @example
49
59
  * ```ts
@@ -54,6 +64,9 @@ export type BusinessRoleType =
54
64
  * department: 'Engineering',
55
65
  * description: 'Leads engineering team and makes technical decisions',
56
66
  *
67
+ * // From org.ai Role
68
+ * skills: ['TypeScript', 'Architecture', 'Team Leadership'],
69
+ *
57
70
  * // Business responsibilities
58
71
  * responsibilities: [
59
72
  * 'Lead engineering team',
@@ -78,28 +91,10 @@ export type BusinessRoleType =
78
91
  * }
79
92
  * ```
80
93
  */
81
- export interface BusinessRole {
82
- /** Unique role identifier */
83
- id: string
84
-
85
- /** Role display name */
86
- name: string
87
-
88
- /** Role type classification */
94
+ export interface BusinessRole extends Omit<OrgRole, 'permissions'> {
95
+ /** Role type classification (overrides OrgRole.type with business-specific types) */
89
96
  type: BusinessRoleType
90
97
 
91
- /** Department or team */
92
- department?: string
93
-
94
- /** Human-readable description */
95
- description?: string
96
-
97
- /** Key responsibilities */
98
- responsibilities?: string[]
99
-
100
- /** Required skills */
101
- skills?: string[]
102
-
103
98
  /**
104
99
  * Authorization permissions by resource type
105
100
  *
@@ -107,6 +102,8 @@ export interface BusinessRole {
107
102
  * - 'read', 'edit', 'delete', 'manage' (standard)
108
103
  * - 'act:*' or 'act:verb' (domain-specific verbs)
109
104
  *
105
+ * Note: This differs from OrgRole.permissions which is string[]
106
+ *
110
107
  * @example
111
108
  * ```ts
112
109
  * permissions: {
@@ -118,32 +115,8 @@ export interface BusinessRole {
118
115
  */
119
116
  permissions?: Record<string, string[]>
120
117
 
121
- /** Task types this role can handle */
122
- canHandle?: string[]
123
-
124
- /** Task types this role can delegate to others */
125
- canDelegate?: string[]
126
-
127
- /** Request types this role can approve */
128
- canApprove?: string[]
129
-
130
- /** Escalation path - role to escalate to */
131
- escalateTo?: string
132
-
133
- /** Reports to - manager role */
134
- reportsTo?: string
135
-
136
- /** Worker type preference: 'ai' | 'human' | 'hybrid' */
137
- workerType?: 'ai' | 'human' | 'hybrid'
138
-
139
- /** Level in hierarchy (1 = entry, higher = more senior) */
140
- level?: number
141
-
142
118
  /** Compensation band */
143
119
  compensationBand?: string
144
-
145
- /** Additional metadata */
146
- metadata?: Record<string, unknown>
147
120
  }
148
121
 
149
122
  // =============================================================================
@@ -263,6 +236,8 @@ export interface TaskAssignment {
263
236
 
264
237
  /**
265
238
  * Reference to an assignee (worker, team, or role)
239
+ *
240
+ * Compatible with digital-workers WorkerRef for worker assignments.
266
241
  */
267
242
  export interface AssigneeRef {
268
243
  /** Type of assignee */
@@ -273,6 +248,34 @@ export interface AssigneeRef {
273
248
  name?: string
274
249
  }
275
250
 
251
+ /**
252
+ * Convert a digital-workers WorkerRef to an AssigneeRef
253
+ */
254
+ export function workerRefToAssignee(workerRef: WorkerRef): AssigneeRef {
255
+ const result: AssigneeRef = {
256
+ type: 'worker',
257
+ id: workerRef.id,
258
+ }
259
+ if (workerRef.name !== undefined) {
260
+ result.name = workerRef.name
261
+ }
262
+ return result
263
+ }
264
+
265
+ /**
266
+ * Convert an AssigneeRef to a digital-workers WorkerRef (if type is 'worker')
267
+ */
268
+ export function assigneeToWorkerRef(assignee: AssigneeRef): WorkerRef | null {
269
+ if (assignee.type !== 'worker') return null
270
+ const result: WorkerRef = {
271
+ id: assignee.id,
272
+ }
273
+ if (assignee.name !== undefined) {
274
+ result.name = assignee.name
275
+ }
276
+ return result
277
+ }
278
+
276
279
  // =============================================================================
277
280
  // Role-Based Task Routing
278
281
  // =============================================================================
@@ -549,8 +552,8 @@ export function createBusinessRole(
549
552
 
550
553
  return {
551
554
  id,
552
- name: standard.name || template,
553
- type: standard.type || template,
555
+ name: standard['name'] || template,
556
+ type: standard['type'] || template,
554
557
  ...standard,
555
558
  ...overrides,
556
559
  } as BusinessRole
@@ -559,11 +562,7 @@ export function createBusinessRole(
559
562
  /**
560
563
  * Check if a role has permission for an action on a resource type
561
564
  */
562
- export function hasPermission(
563
- role: BusinessRole,
564
- resourceType: string,
565
- action: string
566
- ): boolean {
565
+ export function hasPermission(role: BusinessRole, resourceType: string, action: string): boolean {
567
566
  if (!role.permissions) return false
568
567
 
569
568
  // Check wildcard permissions
@@ -595,24 +594,24 @@ export function hasPermission(
595
594
  * Check if a role can handle a task type
596
595
  */
597
596
  export function canHandleTask(role: BusinessRole, taskType: string): boolean {
598
- if (!role.canHandle) return false
599
- return role.canHandle.includes(taskType) || role.canHandle.includes('*')
597
+ if (!role['canHandle']) return false
598
+ return role['canHandle'].includes(taskType) || role['canHandle'].includes('*')
600
599
  }
601
600
 
602
601
  /**
603
602
  * Check if a role can approve a request type
604
603
  */
605
604
  export function canApproveRequest(role: BusinessRole, requestType: string): boolean {
606
- if (!role.canApprove) return false
607
- return role.canApprove.includes(requestType) || role.canApprove.includes('*')
605
+ if (!role['canApprove']) return false
606
+ return role['canApprove'].includes(requestType) || role['canApprove'].includes('*')
608
607
  }
609
608
 
610
609
  /**
611
610
  * Check if a role can delegate a task type
612
611
  */
613
612
  export function canDelegateTask(role: BusinessRole, taskType: string): boolean {
614
- if (!role.canDelegate) return false
615
- return role.canDelegate.includes(taskType) || role.canDelegate.includes('*')
613
+ if (!role['canDelegate']) return false
614
+ return role['canDelegate'].includes(taskType) || role['canDelegate'].includes('*')
616
615
  }
617
616
 
618
617
  /**
@@ -623,7 +622,7 @@ export function findRoleForTask(
623
622
  rules: TaskRoutingRule[],
624
623
  context?: { amount?: number; skills?: string[] }
625
624
  ): TaskRoutingRule | undefined {
626
- const matchingRules = rules.filter(rule => rule.taskType === taskType)
625
+ const matchingRules = rules.filter((rule) => rule.taskType === taskType)
627
626
 
628
627
  if (matchingRules.length === 0) return undefined
629
628
 
@@ -633,7 +632,7 @@ export function findRoleForTask(
633
632
  if (rule.escalateAbove && context.amount > rule.escalateAbove) {
634
633
  // Find the escalated rule
635
634
  const escalatedRule = rules.find(
636
- r => r.taskType === taskType && r.requiredRole === rule.escalateTo
635
+ (r) => r.taskType === taskType && r.requiredRole === rule.escalateTo
637
636
  )
638
637
  if (escalatedRule) return escalatedRule
639
638
  }
@@ -651,7 +650,9 @@ export function createTaskAssignment(
651
650
  taskId: string,
652
651
  taskType: string,
653
652
  assignee: AssigneeRef,
654
- options?: Partial<Omit<TaskAssignment, 'id' | 'taskId' | 'taskType' | 'assignee' | 'status' | 'assignedAt'>>
653
+ options?: Partial<
654
+ Omit<TaskAssignment, 'id' | 'taskId' | 'taskType' | 'assignee' | 'status' | 'assignedAt'>
655
+ >
655
656
  ): TaskAssignment {
656
657
  return {
657
658
  id: `assign_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
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
+ })