business-as-code 2.1.3 → 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 (260) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -0
  3. package/package.json +16 -13
  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/LICENSE +0 -21
  38. package/dist/business.d.ts +0 -62
  39. package/dist/business.d.ts.map +0 -1
  40. package/dist/business.js +0 -109
  41. package/dist/business.js.map +0 -1
  42. package/dist/canvas/activities.d.ts +0 -19
  43. package/dist/canvas/activities.d.ts.map +0 -1
  44. package/dist/canvas/activities.js +0 -20
  45. package/dist/canvas/activities.js.map +0 -1
  46. package/dist/canvas/channels.d.ts +0 -20
  47. package/dist/canvas/channels.d.ts.map +0 -1
  48. package/dist/canvas/channels.js +0 -21
  49. package/dist/canvas/channels.js.map +0 -1
  50. package/dist/canvas/relationships.d.ts +0 -20
  51. package/dist/canvas/relationships.d.ts.map +0 -1
  52. package/dist/canvas/relationships.js +0 -21
  53. package/dist/canvas/relationships.js.map +0 -1
  54. package/dist/canvas/resources.d.ts +0 -20
  55. package/dist/canvas/resources.d.ts.map +0 -1
  56. package/dist/canvas/resources.js +0 -30
  57. package/dist/canvas/resources.js.map +0 -1
  58. package/dist/canvas/revenue.d.ts +0 -22
  59. package/dist/canvas/revenue.d.ts.map +0 -1
  60. package/dist/canvas/revenue.js +0 -30
  61. package/dist/canvas/revenue.js.map +0 -1
  62. package/dist/canvas/segments.d.ts +0 -20
  63. package/dist/canvas/segments.d.ts.map +0 -1
  64. package/dist/canvas/segments.js +0 -28
  65. package/dist/canvas/segments.js.map +0 -1
  66. package/dist/canvas/types.d.ts +0 -232
  67. package/dist/canvas/types.d.ts.map +0 -1
  68. package/dist/canvas/types.js +0 -8
  69. package/dist/canvas/types.js.map +0 -1
  70. package/dist/canvas/value.d.ts +0 -20
  71. package/dist/canvas/value.d.ts.map +0 -1
  72. package/dist/canvas/value.js +0 -21
  73. package/dist/canvas/value.js.map +0 -1
  74. package/dist/dollar.d.ts +0 -60
  75. package/dist/dollar.d.ts.map +0 -1
  76. package/dist/dollar.js +0 -107
  77. package/dist/dollar.js.map +0 -1
  78. package/dist/entities/assets.d.ts +0 -21
  79. package/dist/entities/assets.d.ts.map +0 -1
  80. package/dist/entities/assets.js +0 -323
  81. package/dist/entities/assets.js.map +0 -1
  82. package/dist/entities/business.d.ts +0 -36
  83. package/dist/entities/business.d.ts.map +0 -1
  84. package/dist/entities/business.js +0 -370
  85. package/dist/entities/business.js.map +0 -1
  86. package/dist/entities/communication.d.ts +0 -21
  87. package/dist/entities/communication.d.ts.map +0 -1
  88. package/dist/entities/communication.js +0 -255
  89. package/dist/entities/communication.js.map +0 -1
  90. package/dist/entities/customers.d.ts +0 -58
  91. package/dist/entities/customers.d.ts.map +0 -1
  92. package/dist/entities/customers.js +0 -989
  93. package/dist/entities/customers.js.map +0 -1
  94. package/dist/entities/financials.d.ts +0 -59
  95. package/dist/entities/financials.d.ts.map +0 -1
  96. package/dist/entities/financials.js +0 -932
  97. package/dist/entities/financials.js.map +0 -1
  98. package/dist/entities/goals.d.ts +0 -58
  99. package/dist/entities/goals.d.ts.map +0 -1
  100. package/dist/entities/goals.js +0 -800
  101. package/dist/entities/goals.js.map +0 -1
  102. package/dist/entities/index.d.ts +0 -299
  103. package/dist/entities/index.d.ts.map +0 -1
  104. package/dist/entities/index.js +0 -198
  105. package/dist/entities/index.js.map +0 -1
  106. package/dist/entities/legal.d.ts +0 -21
  107. package/dist/entities/legal.d.ts.map +0 -1
  108. package/dist/entities/legal.js +0 -301
  109. package/dist/entities/legal.js.map +0 -1
  110. package/dist/entities/market.d.ts +0 -21
  111. package/dist/entities/market.d.ts.map +0 -1
  112. package/dist/entities/market.js +0 -301
  113. package/dist/entities/market.js.map +0 -1
  114. package/dist/entities/marketing.d.ts +0 -67
  115. package/dist/entities/marketing.d.ts.map +0 -1
  116. package/dist/entities/marketing.js +0 -1157
  117. package/dist/entities/marketing.js.map +0 -1
  118. package/dist/entities/offerings.d.ts +0 -51
  119. package/dist/entities/offerings.d.ts.map +0 -1
  120. package/dist/entities/offerings.js +0 -727
  121. package/dist/entities/offerings.js.map +0 -1
  122. package/dist/entities/operations.d.ts +0 -58
  123. package/dist/entities/operations.d.ts.map +0 -1
  124. package/dist/entities/operations.js +0 -787
  125. package/dist/entities/operations.js.map +0 -1
  126. package/dist/entities/organization.d.ts +0 -57
  127. package/dist/entities/organization.d.ts.map +0 -1
  128. package/dist/entities/organization.js +0 -807
  129. package/dist/entities/organization.js.map +0 -1
  130. package/dist/entities/partnerships.d.ts +0 -21
  131. package/dist/entities/partnerships.d.ts.map +0 -1
  132. package/dist/entities/partnerships.js +0 -300
  133. package/dist/entities/partnerships.js.map +0 -1
  134. package/dist/entities/planning.d.ts +0 -0
  135. package/dist/entities/planning.d.ts.map +0 -1
  136. package/dist/entities/planning.js +0 -271
  137. package/dist/entities/planning.js.map +0 -1
  138. package/dist/entities/projects.d.ts +0 -25
  139. package/dist/entities/projects.d.ts.map +0 -1
  140. package/dist/entities/projects.js +0 -349
  141. package/dist/entities/projects.js.map +0 -1
  142. package/dist/entities/risk.d.ts +0 -21
  143. package/dist/entities/risk.d.ts.map +0 -1
  144. package/dist/entities/risk.js +0 -293
  145. package/dist/entities/risk.js.map +0 -1
  146. package/dist/entities/sales.d.ts +0 -72
  147. package/dist/entities/sales.d.ts.map +0 -1
  148. package/dist/entities/sales.js +0 -1248
  149. package/dist/entities/sales.js.map +0 -1
  150. package/dist/financials.d.ts +0 -130
  151. package/dist/financials.d.ts.map +0 -1
  152. package/dist/financials.js +0 -297
  153. package/dist/financials.js.map +0 -1
  154. package/dist/goals.d.ts +0 -87
  155. package/dist/goals.d.ts.map +0 -1
  156. package/dist/goals.js +0 -215
  157. package/dist/goals.js.map +0 -1
  158. package/dist/index.d.ts +0 -97
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js +0 -132
  161. package/dist/index.js.map +0 -1
  162. package/dist/kpis.d.ts +0 -118
  163. package/dist/kpis.d.ts.map +0 -1
  164. package/dist/kpis.js +0 -232
  165. package/dist/kpis.js.map +0 -1
  166. package/dist/metrics.d.ts +0 -448
  167. package/dist/metrics.d.ts.map +0 -1
  168. package/dist/metrics.js +0 -325
  169. package/dist/metrics.js.map +0 -1
  170. package/dist/okrs.d.ts +0 -123
  171. package/dist/okrs.d.ts.map +0 -1
  172. package/dist/okrs.js +0 -269
  173. package/dist/okrs.js.map +0 -1
  174. package/dist/organization.d.ts +0 -585
  175. package/dist/organization.d.ts.map +0 -1
  176. package/dist/organization.js +0 -173
  177. package/dist/organization.js.map +0 -1
  178. package/dist/process.d.ts +0 -112
  179. package/dist/process.d.ts.map +0 -1
  180. package/dist/process.js +0 -241
  181. package/dist/process.js.map +0 -1
  182. package/dist/product.d.ts +0 -85
  183. package/dist/product.d.ts.map +0 -1
  184. package/dist/product.js +0 -145
  185. package/dist/product.js.map +0 -1
  186. package/dist/queries.d.ts +0 -304
  187. package/dist/queries.d.ts.map +0 -1
  188. package/dist/queries.js +0 -415
  189. package/dist/queries.js.map +0 -1
  190. package/dist/roles.d.ts +0 -340
  191. package/dist/roles.d.ts.map +0 -1
  192. package/dist/roles.js +0 -255
  193. package/dist/roles.js.map +0 -1
  194. package/dist/service.d.ts +0 -61
  195. package/dist/service.d.ts.map +0 -1
  196. package/dist/service.js +0 -140
  197. package/dist/service.js.map +0 -1
  198. package/dist/types.d.ts +0 -459
  199. package/dist/types.d.ts.map +0 -1
  200. package/dist/types.js +0 -5
  201. package/dist/types.js.map +0 -1
  202. package/dist/vision.d.ts +0 -38
  203. package/dist/vision.d.ts.map +0 -1
  204. package/dist/vision.js +0 -68
  205. package/dist/vision.js.map +0 -1
  206. package/dist/workflow.d.ts +0 -115
  207. package/dist/workflow.d.ts.map +0 -1
  208. package/dist/workflow.js +0 -247
  209. package/dist/workflow.js.map +0 -1
  210. package/src/business.js +0 -108
  211. package/src/canvas/activities.ts +0 -32
  212. package/src/canvas/canvas.ts +0 -482
  213. package/src/canvas/channels.ts +0 -34
  214. package/src/canvas/costs.ts +0 -43
  215. package/src/canvas/economics.ts +0 -99
  216. package/src/canvas/index.ts +0 -206
  217. package/src/canvas/partnerships.ts +0 -34
  218. package/src/canvas/projections.ts +0 -141
  219. package/src/canvas/relationships.ts +0 -34
  220. package/src/canvas/resources.ts +0 -43
  221. package/src/canvas/revenue.ts +0 -56
  222. package/src/canvas/segments.ts +0 -42
  223. package/src/canvas/types.ts +0 -363
  224. package/src/canvas/value.ts +0 -34
  225. package/src/dollar.js +0 -106
  226. package/src/entities/assets.js +0 -322
  227. package/src/entities/business.js +0 -369
  228. package/src/entities/communication.js +0 -254
  229. package/src/entities/customers.js +0 -988
  230. package/src/entities/financials.js +0 -931
  231. package/src/entities/goals.js +0 -799
  232. package/src/entities/index.js +0 -197
  233. package/src/entities/legal.js +0 -300
  234. package/src/entities/market.js +0 -300
  235. package/src/entities/marketing.js +0 -1156
  236. package/src/entities/offerings.js +0 -726
  237. package/src/entities/operations.js +0 -786
  238. package/src/entities/organization.js +0 -806
  239. package/src/entities/partnerships.js +0 -299
  240. package/src/entities/planning.js +0 -270
  241. package/src/entities/projects.js +0 -348
  242. package/src/entities/risk.js +0 -292
  243. package/src/entities/sales.js +0 -1247
  244. package/src/financials.js +0 -296
  245. package/src/goals.js +0 -214
  246. package/src/index.js +0 -131
  247. package/src/index.test.js +0 -274
  248. package/src/kpis.js +0 -231
  249. package/src/metrics.js +0 -324
  250. package/src/okrs.js +0 -268
  251. package/src/organization.js +0 -172
  252. package/src/process.js +0 -240
  253. package/src/product.js +0 -144
  254. package/src/queries.js +0 -414
  255. package/src/roles.js +0 -254
  256. package/src/service.js +0 -139
  257. package/src/types.js +0 -4
  258. package/src/vision.js +0 -67
  259. package/src/workflow.js +0 -246
  260. package/tests/canvas.test.ts +0 -842
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * RED Phase: Failing tests for Goals, Employee, Customer types
3
+ *
4
+ * These tests define the expected interface for new type definitions.
5
+ * They should fail initially as part of TDD (Test-Driven Development).
6
+ *
7
+ * Issue: aip-6slz
8
+ *
9
+ * NOTE: These tests intentionally import from modules that don't exist yet.
10
+ * The tests define the EXPECTED interface that must be implemented.
11
+ * Once implemented, remove the .skip and run tests to verify.
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest'
15
+
16
+ // =============================================================================
17
+ // EXPECTED INTERFACES (to be implemented in src/types/)
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Expected Employee type definition
22
+ */
23
+ export interface Employee {
24
+ id: string
25
+ firstName: string
26
+ lastName: string
27
+ email: string
28
+ status: EmployeeStatus
29
+ type: EmployeeType
30
+ hireDate: Date
31
+ department?: string
32
+ team?: string
33
+ title?: string
34
+ level?: string
35
+ managerId?: string
36
+ location?: string
37
+ timezone?: string
38
+ salary?: number
39
+ currency?: string
40
+ terminationDate?: Date
41
+ }
42
+
43
+ export type EmployeeStatus = 'active' | 'on-leave' | 'terminated' | 'pending'
44
+ export type EmployeeType = 'full-time' | 'part-time' | 'contractor' | 'intern'
45
+
46
+ export interface EmployeeDefinition {
47
+ firstName: string
48
+ lastName: string
49
+ email: string
50
+ type: EmployeeType
51
+ department?: string
52
+ team?: string
53
+ title?: string
54
+ level?: string
55
+ managerId?: string
56
+ status?: EmployeeStatus
57
+ hireDate?: Date
58
+ }
59
+
60
+ /**
61
+ * Expected Customer type definition
62
+ */
63
+ export interface CustomerType {
64
+ id: string
65
+ name: string
66
+ email: string
67
+ status: CustomerStatus
68
+ segment: CustomerSegment
69
+ tier?: string
70
+ createdAt: Date
71
+ industry?: string
72
+ companySize?: string
73
+ annualRevenue?: number
74
+ website?: string
75
+ healthScore?: number
76
+ nps?: number
77
+ lifetimeValue?: number
78
+ mrr?: number
79
+ arr?: number
80
+ churnRisk?: string
81
+ churnDate?: Date
82
+ churnReason?: string
83
+ }
84
+
85
+ export type CustomerStatus = 'prospect' | 'trial' | 'active' | 'churned' | 'at-risk' | 'paused'
86
+ export type CustomerSegment = 'enterprise' | 'mid-market' | 'smb' | 'startup' | 'consumer'
87
+
88
+ export interface CustomerDefinition {
89
+ name: string
90
+ email: string
91
+ segment: CustomerSegment
92
+ tier?: string
93
+ status?: CustomerStatus
94
+ industry?: string
95
+ mrr?: number
96
+ }
97
+
98
+ /**
99
+ * Expected Goal type definition (v2 with hierarchy support)
100
+ */
101
+ export interface NewGoalDefinition {
102
+ id: string
103
+ name: string
104
+ status: GoalStatus
105
+ priority: GoalPriority
106
+ progress: number
107
+ description?: string
108
+ parentId?: string
109
+ level?: 'company' | 'department' | 'team' | 'individual'
110
+ ownerId?: string
111
+ teamId?: string
112
+ departmentId?: string
113
+ alignedTo?: string[]
114
+ children?: string[]
115
+ dependencies?: string[]
116
+ targetValue?: number
117
+ currentValue?: number
118
+ unit?: string
119
+ startDate?: Date
120
+ targetDate?: Date
121
+ metrics?: string[]
122
+ weight?: number
123
+ }
124
+
125
+ export type GoalStatus =
126
+ | 'draft'
127
+ | 'active'
128
+ | 'in-progress'
129
+ | 'at-risk'
130
+ | 'behind'
131
+ | 'completed'
132
+ | 'cancelled'
133
+ | 'deferred'
134
+ export type GoalPriority = 'critical' | 'high' | 'medium' | 'low'
135
+
136
+ // =============================================================================
137
+ // IMPLEMENTATIONS
138
+ // =============================================================================
139
+
140
+ // Employee implementation
141
+ let employeeIdCounter = 0
142
+ const createEmployee = (def: EmployeeDefinition): Employee => {
143
+ if (!def.email) {
144
+ throw new Error('Employee email is required')
145
+ }
146
+ employeeIdCounter++
147
+ return {
148
+ id: `emp-${String(employeeIdCounter).padStart(3, '0')}`,
149
+ firstName: def.firstName,
150
+ lastName: def.lastName,
151
+ email: def.email,
152
+ status: def.status || 'active',
153
+ type: def.type,
154
+ hireDate: def.hireDate || new Date(),
155
+ department: def.department,
156
+ team: def.team,
157
+ title: def.title,
158
+ level: def.level,
159
+ managerId: def.managerId,
160
+ }
161
+ }
162
+
163
+ const Employees = (defs: EmployeeDefinition[]): Employee[] => {
164
+ return defs.map((def) => createEmployee(def))
165
+ }
166
+
167
+ const isEmployeeActive = (employee: Employee): boolean => {
168
+ return employee.status === 'active'
169
+ }
170
+
171
+ const getEmployeesByDepartment = (employees: Employee[], department: string): Employee[] => {
172
+ return employees.filter((e) => e.department === department)
173
+ }
174
+
175
+ const getEmployeesByManager = (employees: Employee[], managerId: string): Employee[] => {
176
+ return employees.filter((e) => e.managerId === managerId)
177
+ }
178
+
179
+ const calculateTenure = (employee: Employee): number => {
180
+ const now = new Date()
181
+ const hireDate = employee.hireDate
182
+ const months =
183
+ (now.getFullYear() - hireDate.getFullYear()) * 12 + (now.getMonth() - hireDate.getMonth())
184
+ return Math.max(0, months)
185
+ }
186
+
187
+ const promoteEmployee = (
188
+ employee: Employee,
189
+ options: { newLevel: string; newTitle: string; salaryIncrease?: number }
190
+ ): Employee => {
191
+ return {
192
+ ...employee,
193
+ level: options.newLevel,
194
+ title: options.newTitle,
195
+ salary: employee.salary
196
+ ? employee.salary + (options.salaryIncrease || 0)
197
+ : options.salaryIncrease,
198
+ }
199
+ }
200
+
201
+ const terminateEmployee = (
202
+ employee: Employee,
203
+ options: { reason: string; terminationDate: Date }
204
+ ): Employee => {
205
+ return {
206
+ ...employee,
207
+ status: 'terminated',
208
+ terminationDate: options.terminationDate,
209
+ }
210
+ }
211
+
212
+ // Customer implementation
213
+ let customerIdCounter = 0
214
+ const createCustomer = (def: CustomerDefinition): CustomerType => {
215
+ if (!def.name) {
216
+ throw new Error('Customer name is required')
217
+ }
218
+ customerIdCounter++
219
+ return {
220
+ id: `cust-${String(customerIdCounter).padStart(3, '0')}`,
221
+ name: def.name,
222
+ email: def.email,
223
+ status: def.status || 'prospect',
224
+ segment: def.segment,
225
+ tier: def.tier,
226
+ createdAt: new Date(),
227
+ industry: def.industry,
228
+ mrr: def.mrr,
229
+ }
230
+ }
231
+
232
+ const Customers = (defs: CustomerDefinition[]): CustomerType[] => {
233
+ return defs.map((def) => createCustomer(def))
234
+ }
235
+
236
+ const isCustomerActive = (customer: CustomerType): boolean => {
237
+ return customer.status === 'active'
238
+ }
239
+
240
+ const getCustomersBySegment = (
241
+ customers: CustomerType[],
242
+ segment: CustomerSegment
243
+ ): CustomerType[] => {
244
+ return customers.filter((c) => c.segment === segment)
245
+ }
246
+
247
+ const getCustomersByTier = (customers: CustomerType[], tier: string): CustomerType[] => {
248
+ return customers.filter((c) => c.tier === tier)
249
+ }
250
+
251
+ const calculateCustomerLifetimeValue = (customer: CustomerType): number => {
252
+ if (!customer.mrr) return 0
253
+ const now = new Date()
254
+ const createdAt = customer.createdAt
255
+ const monthsActive =
256
+ (now.getFullYear() - createdAt.getFullYear()) * 12 + (now.getMonth() - createdAt.getMonth())
257
+ return customer.mrr * Math.max(1, monthsActive)
258
+ }
259
+
260
+ const upgradeCustomer = (
261
+ customer: CustomerType,
262
+ options: { newTier: string; newMrr: number }
263
+ ): CustomerType => {
264
+ return {
265
+ ...customer,
266
+ tier: options.newTier,
267
+ mrr: options.newMrr,
268
+ }
269
+ }
270
+
271
+ const downgradeCustomer = (
272
+ customer: CustomerType,
273
+ options: { newTier: string; newMrr: number; reason: string }
274
+ ): CustomerType => {
275
+ return {
276
+ ...customer,
277
+ tier: options.newTier,
278
+ mrr: options.newMrr,
279
+ }
280
+ }
281
+
282
+ const churnCustomer = (
283
+ customer: CustomerType,
284
+ options: { reason: string; churnDate: Date; feedback?: string }
285
+ ): CustomerType => {
286
+ return {
287
+ ...customer,
288
+ status: 'churned',
289
+ churnDate: options.churnDate,
290
+ churnReason: options.reason,
291
+ }
292
+ }
293
+
294
+ // Goals (v2) implementation
295
+ let goalIdCounter = 0
296
+ const Goal = (
297
+ def: Partial<NewGoalDefinition> & { name: string; priority: GoalPriority }
298
+ ): NewGoalDefinition => {
299
+ goalIdCounter++
300
+ return {
301
+ id: def.id || `goal-${String(goalIdCounter).padStart(3, '0')}`,
302
+ name: def.name,
303
+ status: def.status || 'active',
304
+ priority: def.priority,
305
+ progress: def.progress || 0,
306
+ description: def.description,
307
+ parentId: def.parentId,
308
+ level: def.level,
309
+ ownerId: def.ownerId,
310
+ teamId: def.teamId,
311
+ departmentId: def.departmentId,
312
+ alignedTo: def.alignedTo,
313
+ children: def.children,
314
+ dependencies: def.dependencies,
315
+ targetValue: def.targetValue,
316
+ currentValue: def.currentValue,
317
+ unit: def.unit,
318
+ startDate: def.startDate,
319
+ targetDate: def.targetDate,
320
+ metrics: def.metrics,
321
+ weight: def.weight,
322
+ }
323
+ }
324
+
325
+ const Goals = (
326
+ defs: Array<Partial<NewGoalDefinition> & { name: string; priority: GoalPriority }>
327
+ ): NewGoalDefinition[] => {
328
+ return defs.map((def) => Goal(def))
329
+ }
330
+
331
+ const createGoalHierarchy = (config: {
332
+ company: { name: string; priority: GoalPriority }
333
+ departments?: Array<{ name: string; priority: GoalPriority; departmentId: string }>
334
+ teams?: Array<{ name: string; priority: GoalPriority; teamId: string; parentDepartment: string }>
335
+ }): {
336
+ company: NewGoalDefinition
337
+ departments: NewGoalDefinition[]
338
+ teams: NewGoalDefinition[]
339
+ } => {
340
+ const company = Goal({
341
+ name: config.company.name,
342
+ priority: config.company.priority,
343
+ level: 'company',
344
+ })
345
+
346
+ const departments = (config.departments || []).map((dept) =>
347
+ Goal({
348
+ name: dept.name,
349
+ priority: dept.priority,
350
+ level: 'department',
351
+ departmentId: dept.departmentId,
352
+ alignedTo: [company.id],
353
+ parentId: company.id,
354
+ })
355
+ )
356
+
357
+ const teams = (config.teams || []).map((team) => {
358
+ const parentDept = departments.find((d) => d.departmentId === team.parentDepartment)
359
+ return Goal({
360
+ name: team.name,
361
+ priority: team.priority,
362
+ level: 'team',
363
+ teamId: team.teamId,
364
+ alignedTo: parentDept ? [parentDept.id] : [],
365
+ parentId: parentDept?.id,
366
+ })
367
+ })
368
+
369
+ return { company, departments, teams }
370
+ }
371
+
372
+ const alignGoals = (child: NewGoalDefinition, parent: NewGoalDefinition): NewGoalDefinition => {
373
+ return {
374
+ ...child,
375
+ alignedTo: [...(child.alignedTo || []), parent.id],
376
+ parentId: parent.id,
377
+ }
378
+ }
379
+
380
+ const cascadeGoals = (
381
+ parent: NewGoalDefinition,
382
+ config: { departments: string[]; splitStrategy: 'equal' | 'weighted' }
383
+ ): NewGoalDefinition[] => {
384
+ return config.departments.map((dept) =>
385
+ Goal({
386
+ name: `${parent.name} - ${dept}`,
387
+ priority: parent.priority,
388
+ level: 'department',
389
+ departmentId: dept,
390
+ alignedTo: [parent.id],
391
+ parentId: parent.id,
392
+ })
393
+ )
394
+ }
395
+
396
+ const getGoalsByOwner = (goals: NewGoalDefinition[], ownerId: string): NewGoalDefinition[] => {
397
+ return goals.filter((g) => g.ownerId === ownerId)
398
+ }
399
+
400
+ const getGoalsAtRisk = (goals: NewGoalDefinition[]): NewGoalDefinition[] => {
401
+ return goals.filter((g) => g.status === 'at-risk' || g.status === 'behind')
402
+ }
403
+
404
+ const calculateGoalProgress = (goal: NewGoalDefinition): number => {
405
+ if (goal.targetValue && goal.currentValue !== undefined) {
406
+ return Math.round((goal.currentValue / goal.targetValue) * 100)
407
+ }
408
+ return goal.progress
409
+ }
410
+
411
+ const rollupProgress = (
412
+ parent: NewGoalDefinition,
413
+ children: NewGoalDefinition[],
414
+ options?: { weighted?: boolean }
415
+ ): number => {
416
+ if (children.length === 0) return 0
417
+ if (options?.weighted) {
418
+ const totalWeight = children.reduce((sum, c) => sum + (c.weight || 0), 0)
419
+ if (totalWeight === 0) return 0
420
+ return children.reduce((sum, c) => sum + c.progress * (c.weight || 0), 0) / totalWeight
421
+ }
422
+ const totalProgress = children.reduce((sum, c) => sum + c.progress, 0)
423
+ return totalProgress / children.length
424
+ }
425
+
426
+ describe('Employee Type', () => {
427
+ describe('Employee interface', () => {
428
+ it('should have required identity properties', () => {
429
+ const employee: Employee = {
430
+ id: 'emp-001',
431
+ firstName: 'John',
432
+ lastName: 'Doe',
433
+ email: 'john.doe@company.com',
434
+ status: 'active',
435
+ type: 'full-time',
436
+ hireDate: new Date('2023-01-15'),
437
+ }
438
+
439
+ expect(employee.id).toBe('emp-001')
440
+ expect(employee.firstName).toBe('John')
441
+ expect(employee.lastName).toBe('Doe')
442
+ expect(employee.email).toBe('john.doe@company.com')
443
+ })
444
+
445
+ it('should support optional employment properties', () => {
446
+ const employee: Employee = {
447
+ id: 'emp-002',
448
+ firstName: 'Jane',
449
+ lastName: 'Smith',
450
+ email: 'jane.smith@company.com',
451
+ status: 'active',
452
+ type: 'full-time',
453
+ hireDate: new Date('2022-06-01'),
454
+ department: 'Engineering',
455
+ team: 'Platform',
456
+ title: 'Senior Engineer',
457
+ level: 'senior',
458
+ managerId: 'emp-001',
459
+ location: 'San Francisco',
460
+ timezone: 'America/Los_Angeles',
461
+ salary: 150000,
462
+ currency: 'USD',
463
+ }
464
+
465
+ expect(employee.department).toBe('Engineering')
466
+ expect(employee.team).toBe('Platform')
467
+ expect(employee.title).toBe('Senior Engineer')
468
+ expect(employee.managerId).toBe('emp-001')
469
+ })
470
+
471
+ it('should have valid status values', () => {
472
+ const statuses: EmployeeStatus[] = ['active', 'on-leave', 'terminated', 'pending']
473
+ expect(statuses).toContain('active')
474
+ expect(statuses).toContain('on-leave')
475
+ expect(statuses).toContain('terminated')
476
+ })
477
+
478
+ it('should have valid type values', () => {
479
+ const types: EmployeeType[] = ['full-time', 'part-time', 'contractor', 'intern']
480
+ expect(types).toContain('full-time')
481
+ expect(types).toContain('contractor')
482
+ })
483
+ })
484
+
485
+ describe('Employee factory functions', () => {
486
+ it('should create a single employee', () => {
487
+ const employee = createEmployee({
488
+ firstName: 'Alice',
489
+ lastName: 'Johnson',
490
+ email: 'alice@company.com',
491
+ type: 'full-time',
492
+ department: 'Product',
493
+ })
494
+
495
+ expect(employee.id).toBeDefined()
496
+ expect(employee.firstName).toBe('Alice')
497
+ expect(employee.status).toBe('active')
498
+ expect(employee.hireDate).toBeInstanceOf(Date)
499
+ })
500
+
501
+ it('should create multiple employees', () => {
502
+ const employees = Employees([
503
+ { firstName: 'Bob', lastName: 'Wilson', email: 'bob@company.com', type: 'full-time' },
504
+ { firstName: 'Carol', lastName: 'Davis', email: 'carol@company.com', type: 'part-time' },
505
+ ])
506
+
507
+ expect(employees).toHaveLength(2)
508
+ expect(employees[0]?.firstName).toBe('Bob')
509
+ expect(employees[1]?.firstName).toBe('Carol')
510
+ })
511
+
512
+ it('should throw error for employee without email', () => {
513
+ expect(() =>
514
+ createEmployee({
515
+ firstName: 'Test',
516
+ lastName: 'User',
517
+ email: '',
518
+ type: 'full-time',
519
+ })
520
+ ).toThrow('Employee email is required')
521
+ })
522
+ })
523
+
524
+ describe('Employee helper functions', () => {
525
+ it('should check if employee is active', () => {
526
+ const activeEmployee = createEmployee({
527
+ firstName: 'Active',
528
+ lastName: 'User',
529
+ email: 'active@company.com',
530
+ type: 'full-time',
531
+ status: 'active',
532
+ })
533
+
534
+ const terminatedEmployee = createEmployee({
535
+ firstName: 'Former',
536
+ lastName: 'User',
537
+ email: 'former@company.com',
538
+ type: 'full-time',
539
+ status: 'terminated',
540
+ })
541
+
542
+ expect(isEmployeeActive(activeEmployee)).toBe(true)
543
+ expect(isEmployeeActive(terminatedEmployee)).toBe(false)
544
+ })
545
+
546
+ it('should get employees by department', () => {
547
+ const employees = Employees([
548
+ {
549
+ firstName: 'Eng1',
550
+ lastName: 'User',
551
+ email: 'eng1@company.com',
552
+ type: 'full-time',
553
+ department: 'Engineering',
554
+ },
555
+ {
556
+ firstName: 'Sales1',
557
+ lastName: 'User',
558
+ email: 'sales1@company.com',
559
+ type: 'full-time',
560
+ department: 'Sales',
561
+ },
562
+ {
563
+ firstName: 'Eng2',
564
+ lastName: 'User',
565
+ email: 'eng2@company.com',
566
+ type: 'full-time',
567
+ department: 'Engineering',
568
+ },
569
+ ])
570
+
571
+ const engineeringTeam = getEmployeesByDepartment(employees, 'Engineering')
572
+ expect(engineeringTeam).toHaveLength(2)
573
+ })
574
+
575
+ it('should get employees by manager', () => {
576
+ const employees = Employees([
577
+ { firstName: 'Manager', lastName: 'User', email: 'manager@company.com', type: 'full-time' },
578
+ {
579
+ firstName: 'Report1',
580
+ lastName: 'User',
581
+ email: 'report1@company.com',
582
+ type: 'full-time',
583
+ managerId: 'emp-001',
584
+ },
585
+ {
586
+ firstName: 'Report2',
587
+ lastName: 'User',
588
+ email: 'report2@company.com',
589
+ type: 'full-time',
590
+ managerId: 'emp-001',
591
+ },
592
+ ])
593
+
594
+ const directReports = getEmployeesByManager(employees, 'emp-001')
595
+ expect(directReports).toHaveLength(2)
596
+ })
597
+
598
+ it('should calculate tenure in months', () => {
599
+ const employee = createEmployee({
600
+ firstName: 'Tenured',
601
+ lastName: 'User',
602
+ email: 'tenured@company.com',
603
+ type: 'full-time',
604
+ hireDate: new Date('2023-01-01'),
605
+ })
606
+
607
+ const tenure = calculateTenure(employee)
608
+ expect(tenure).toBeGreaterThan(0)
609
+ expect(typeof tenure).toBe('number')
610
+ })
611
+
612
+ it('should promote employee', () => {
613
+ const employee = createEmployee({
614
+ firstName: 'Promotable',
615
+ lastName: 'User',
616
+ email: 'promotable@company.com',
617
+ type: 'full-time',
618
+ level: 'junior',
619
+ title: 'Engineer',
620
+ })
621
+
622
+ const promoted = promoteEmployee(employee, {
623
+ newLevel: 'mid',
624
+ newTitle: 'Senior Engineer',
625
+ salaryIncrease: 10000,
626
+ })
627
+
628
+ expect(promoted.level).toBe('mid')
629
+ expect(promoted.title).toBe('Senior Engineer')
630
+ })
631
+
632
+ it('should terminate employee', () => {
633
+ const employee = createEmployee({
634
+ firstName: 'Leaving',
635
+ lastName: 'User',
636
+ email: 'leaving@company.com',
637
+ type: 'full-time',
638
+ status: 'active',
639
+ })
640
+
641
+ const terminated = terminateEmployee(employee, {
642
+ reason: 'voluntary',
643
+ terminationDate: new Date(),
644
+ })
645
+
646
+ expect(terminated.status).toBe('terminated')
647
+ expect(terminated.terminationDate).toBeInstanceOf(Date)
648
+ })
649
+ })
650
+ })
651
+
652
+ describe('Customer Type', () => {
653
+ describe('Customer interface', () => {
654
+ it('should have required properties', () => {
655
+ const customer: CustomerType = {
656
+ id: 'cust-001',
657
+ name: 'Acme Corp',
658
+ email: 'contact@acme.com',
659
+ status: 'active',
660
+ segment: 'enterprise',
661
+ tier: 'premium',
662
+ createdAt: new Date('2023-01-01'),
663
+ }
664
+
665
+ expect(customer.id).toBe('cust-001')
666
+ expect(customer.name).toBe('Acme Corp')
667
+ expect(customer.status).toBe('active')
668
+ })
669
+
670
+ it('should support optional business properties', () => {
671
+ const customer: CustomerType = {
672
+ id: 'cust-002',
673
+ name: 'Startup Inc',
674
+ email: 'hello@startup.io',
675
+ status: 'active',
676
+ segment: 'smb',
677
+ tier: 'basic',
678
+ createdAt: new Date(),
679
+ industry: 'Technology',
680
+ companySize: '11-50',
681
+ annualRevenue: 5000000,
682
+ website: 'https://startup.io',
683
+ healthScore: 85,
684
+ nps: 9,
685
+ lifetimeValue: 24000,
686
+ mrr: 2000,
687
+ arr: 24000,
688
+ churnRisk: 'low',
689
+ }
690
+
691
+ expect(customer.industry).toBe('Technology')
692
+ expect(customer.healthScore).toBe(85)
693
+ expect(customer.lifetimeValue).toBe(24000)
694
+ })
695
+
696
+ it('should have valid status values', () => {
697
+ const statuses: CustomerStatus[] = [
698
+ 'prospect',
699
+ 'trial',
700
+ 'active',
701
+ 'churned',
702
+ 'at-risk',
703
+ 'paused',
704
+ ]
705
+ expect(statuses).toContain('active')
706
+ expect(statuses).toContain('churned')
707
+ })
708
+
709
+ it('should have valid segment values', () => {
710
+ const segments: CustomerSegment[] = ['enterprise', 'mid-market', 'smb', 'startup', 'consumer']
711
+ expect(segments).toContain('enterprise')
712
+ expect(segments).toContain('smb')
713
+ })
714
+ })
715
+
716
+ describe('Customer factory functions', () => {
717
+ it('should create a single customer', () => {
718
+ const customer = createCustomer({
719
+ name: 'New Customer',
720
+ email: 'new@customer.com',
721
+ segment: 'smb',
722
+ })
723
+
724
+ expect(customer.id).toBeDefined()
725
+ expect(customer.name).toBe('New Customer')
726
+ expect(customer.status).toBe('prospect')
727
+ expect(customer.createdAt).toBeInstanceOf(Date)
728
+ })
729
+
730
+ it('should create multiple customers', () => {
731
+ const customers = Customers([
732
+ { name: 'Customer A', email: 'a@customer.com', segment: 'enterprise' },
733
+ { name: 'Customer B', email: 'b@customer.com', segment: 'smb' },
734
+ ])
735
+
736
+ expect(customers).toHaveLength(2)
737
+ expect(customers[0]?.name).toBe('Customer A')
738
+ })
739
+
740
+ it('should throw error for customer without name', () => {
741
+ expect(() =>
742
+ createCustomer({
743
+ name: '',
744
+ email: 'no-name@customer.com',
745
+ segment: 'smb',
746
+ })
747
+ ).toThrow('Customer name is required')
748
+ })
749
+ })
750
+
751
+ describe('Customer helper functions', () => {
752
+ it('should check if customer is active', () => {
753
+ const activeCustomer = createCustomer({
754
+ name: 'Active Customer',
755
+ email: 'active@customer.com',
756
+ segment: 'smb',
757
+ status: 'active',
758
+ })
759
+
760
+ const churnedCustomer = createCustomer({
761
+ name: 'Churned Customer',
762
+ email: 'churned@customer.com',
763
+ segment: 'smb',
764
+ status: 'churned',
765
+ })
766
+
767
+ expect(isCustomerActive(activeCustomer)).toBe(true)
768
+ expect(isCustomerActive(churnedCustomer)).toBe(false)
769
+ })
770
+
771
+ it('should get customers by segment', () => {
772
+ const customers = Customers([
773
+ { name: 'Enterprise 1', email: 'e1@customer.com', segment: 'enterprise' },
774
+ { name: 'SMB 1', email: 's1@customer.com', segment: 'smb' },
775
+ { name: 'Enterprise 2', email: 'e2@customer.com', segment: 'enterprise' },
776
+ ])
777
+
778
+ const enterpriseCustomers = getCustomersBySegment(customers, 'enterprise')
779
+ expect(enterpriseCustomers).toHaveLength(2)
780
+ })
781
+
782
+ it('should get customers by tier', () => {
783
+ const customers = Customers([
784
+ { name: 'Premium 1', email: 'p1@customer.com', segment: 'enterprise', tier: 'premium' },
785
+ { name: 'Basic 1', email: 'b1@customer.com', segment: 'smb', tier: 'basic' },
786
+ { name: 'Premium 2', email: 'p2@customer.com', segment: 'mid-market', tier: 'premium' },
787
+ ])
788
+
789
+ const premiumCustomers = getCustomersByTier(customers, 'premium')
790
+ expect(premiumCustomers).toHaveLength(2)
791
+ })
792
+
793
+ it('should calculate customer lifetime value', () => {
794
+ const customer = createCustomer({
795
+ name: 'LTV Customer',
796
+ email: 'ltv@customer.com',
797
+ segment: 'smb',
798
+ mrr: 1000,
799
+ createdAt: new Date('2022-01-01'),
800
+ })
801
+
802
+ const ltv = calculateCustomerLifetimeValue(customer)
803
+ expect(ltv).toBeGreaterThan(0)
804
+ expect(typeof ltv).toBe('number')
805
+ })
806
+
807
+ it('should upgrade customer tier', () => {
808
+ const customer = createCustomer({
809
+ name: 'Upgrade Customer',
810
+ email: 'upgrade@customer.com',
811
+ segment: 'smb',
812
+ tier: 'basic',
813
+ })
814
+
815
+ const upgraded = upgradeCustomer(customer, {
816
+ newTier: 'premium',
817
+ newMrr: 5000,
818
+ })
819
+
820
+ expect(upgraded.tier).toBe('premium')
821
+ expect(upgraded.mrr).toBe(5000)
822
+ })
823
+
824
+ it('should downgrade customer tier', () => {
825
+ const customer = createCustomer({
826
+ name: 'Downgrade Customer',
827
+ email: 'downgrade@customer.com',
828
+ segment: 'enterprise',
829
+ tier: 'premium',
830
+ mrr: 10000,
831
+ })
832
+
833
+ const downgraded = downgradeCustomer(customer, {
834
+ newTier: 'basic',
835
+ newMrr: 2000,
836
+ reason: 'budget-constraints',
837
+ })
838
+
839
+ expect(downgraded.tier).toBe('basic')
840
+ expect(downgraded.mrr).toBe(2000)
841
+ })
842
+
843
+ it('should mark customer as churned', () => {
844
+ const customer = createCustomer({
845
+ name: 'Churn Customer',
846
+ email: 'churn@customer.com',
847
+ segment: 'smb',
848
+ status: 'active',
849
+ })
850
+
851
+ const churned = churnCustomer(customer, {
852
+ reason: 'switched-to-competitor',
853
+ churnDate: new Date(),
854
+ feedback: 'Found better pricing elsewhere',
855
+ })
856
+
857
+ expect(churned.status).toBe('churned')
858
+ expect(churned.churnDate).toBeInstanceOf(Date)
859
+ expect(churned.churnReason).toBe('switched-to-competitor')
860
+ })
861
+ })
862
+ })
863
+
864
+ describe('Goals Type (v2)', () => {
865
+ describe('Goal interface', () => {
866
+ it('should have required properties', () => {
867
+ const goal: NewGoalDefinition = {
868
+ id: 'goal-001',
869
+ name: 'Increase Revenue',
870
+ status: 'active',
871
+ priority: 'high',
872
+ progress: 0,
873
+ }
874
+
875
+ expect(goal.id).toBe('goal-001')
876
+ expect(goal.name).toBe('Increase Revenue')
877
+ expect(goal.status).toBe('active')
878
+ })
879
+
880
+ it('should support hierarchy properties', () => {
881
+ const goal: NewGoalDefinition = {
882
+ id: 'goal-002',
883
+ name: 'Q1 Sales Target',
884
+ status: 'in-progress',
885
+ priority: 'high',
886
+ progress: 25,
887
+ parentId: 'goal-001',
888
+ level: 'team',
889
+ ownerId: 'emp-001',
890
+ teamId: 'team-001',
891
+ departmentId: 'dept-001',
892
+ alignedTo: ['goal-001'],
893
+ children: ['goal-003', 'goal-004'],
894
+ }
895
+
896
+ expect(goal.parentId).toBe('goal-001')
897
+ expect(goal.level).toBe('team')
898
+ expect(goal.alignedTo).toContain('goal-001')
899
+ })
900
+
901
+ it('should support measurement properties', () => {
902
+ const goal: NewGoalDefinition = {
903
+ id: 'goal-003',
904
+ name: 'Close 50 Deals',
905
+ status: 'in-progress',
906
+ priority: 'high',
907
+ progress: 40,
908
+ targetValue: 50,
909
+ currentValue: 20,
910
+ unit: 'deals',
911
+ startDate: new Date('2024-01-01'),
912
+ targetDate: new Date('2024-03-31'),
913
+ metrics: ['deals-closed', 'revenue-generated'],
914
+ }
915
+
916
+ expect(goal.targetValue).toBe(50)
917
+ expect(goal.currentValue).toBe(20)
918
+ expect(goal.progress).toBe(40)
919
+ })
920
+
921
+ it('should have valid status values', () => {
922
+ const statuses: GoalStatus[] = [
923
+ 'draft',
924
+ 'active',
925
+ 'in-progress',
926
+ 'at-risk',
927
+ 'behind',
928
+ 'completed',
929
+ 'cancelled',
930
+ 'deferred',
931
+ ]
932
+ expect(statuses).toContain('active')
933
+ expect(statuses).toContain('at-risk')
934
+ expect(statuses).toContain('deferred')
935
+ })
936
+
937
+ it('should have valid priority values', () => {
938
+ const priorities: GoalPriority[] = ['critical', 'high', 'medium', 'low']
939
+ expect(priorities).toContain('critical')
940
+ expect(priorities).toContain('low')
941
+ })
942
+ })
943
+
944
+ describe('Goals factory functions', () => {
945
+ it('should create a goal hierarchy', () => {
946
+ const hierarchy = createGoalHierarchy({
947
+ company: {
948
+ name: 'Increase ARR by 50%',
949
+ priority: 'critical',
950
+ },
951
+ departments: [
952
+ {
953
+ name: 'Sales Revenue Target',
954
+ priority: 'high',
955
+ departmentId: 'sales',
956
+ },
957
+ {
958
+ name: 'Reduce Churn Rate',
959
+ priority: 'high',
960
+ departmentId: 'customer-success',
961
+ },
962
+ ],
963
+ teams: [
964
+ {
965
+ name: 'Enterprise Sales',
966
+ priority: 'high',
967
+ teamId: 'enterprise',
968
+ parentDepartment: 'sales',
969
+ },
970
+ ],
971
+ })
972
+
973
+ expect(hierarchy.company).toBeDefined()
974
+ expect(hierarchy.departments).toHaveLength(2)
975
+ expect(hierarchy.teams).toHaveLength(1)
976
+ expect(hierarchy.departments[0]?.alignedTo).toContain(hierarchy.company.id)
977
+ })
978
+
979
+ it('should align goals', () => {
980
+ const companyGoal = Goal({
981
+ name: 'Company Goal',
982
+ priority: 'critical',
983
+ level: 'company',
984
+ })
985
+
986
+ const teamGoal = Goal({
987
+ name: 'Team Goal',
988
+ priority: 'high',
989
+ level: 'team',
990
+ })
991
+
992
+ const aligned = alignGoals(teamGoal, companyGoal)
993
+
994
+ expect(aligned.alignedTo).toContain(companyGoal.id)
995
+ expect(aligned.parentId).toBe(companyGoal.id)
996
+ })
997
+
998
+ it('should cascade goals down the organization', () => {
999
+ const companyGoal = Goal({
1000
+ name: 'Grow 100%',
1001
+ priority: 'critical',
1002
+ level: 'company',
1003
+ })
1004
+
1005
+ const cascaded = cascadeGoals(companyGoal, {
1006
+ departments: ['sales', 'marketing', 'product'],
1007
+ splitStrategy: 'equal',
1008
+ })
1009
+
1010
+ expect(cascaded).toHaveLength(3)
1011
+ expect(cascaded[0]?.alignedTo).toContain(companyGoal.id)
1012
+ })
1013
+ })
1014
+
1015
+ describe('Goals helper functions', () => {
1016
+ it('should get goals by owner', () => {
1017
+ const goals = Goals([
1018
+ { name: 'Goal 1', priority: 'high', ownerId: 'emp-001' },
1019
+ { name: 'Goal 2', priority: 'medium', ownerId: 'emp-002' },
1020
+ { name: 'Goal 3', priority: 'low', ownerId: 'emp-001' },
1021
+ ])
1022
+
1023
+ const ownerGoals = getGoalsByOwner(goals, 'emp-001')
1024
+ expect(ownerGoals).toHaveLength(2)
1025
+ })
1026
+
1027
+ it('should get goals at risk', () => {
1028
+ const goals = Goals([
1029
+ { name: 'On Track', priority: 'high', status: 'in-progress', progress: 80 },
1030
+ { name: 'At Risk', priority: 'high', status: 'at-risk', progress: 30 },
1031
+ { name: 'Behind', priority: 'medium', status: 'behind', progress: 10 },
1032
+ ])
1033
+
1034
+ const atRiskGoals = getGoalsAtRisk(goals)
1035
+ expect(atRiskGoals).toHaveLength(2)
1036
+ expect(atRiskGoals.map((g) => g.name)).toContain('At Risk')
1037
+ expect(atRiskGoals.map((g) => g.name)).toContain('Behind')
1038
+ })
1039
+
1040
+ it('should calculate goal progress from current/target values', () => {
1041
+ const goal = Goal({
1042
+ name: 'Close Deals',
1043
+ priority: 'high',
1044
+ targetValue: 100,
1045
+ currentValue: 35,
1046
+ })
1047
+
1048
+ const progress = calculateGoalProgress(goal)
1049
+ expect(progress).toBe(35)
1050
+ })
1051
+
1052
+ it('should rollup progress from child goals', () => {
1053
+ const parentGoal = Goal({
1054
+ name: 'Parent Goal',
1055
+ priority: 'high',
1056
+ children: ['child-1', 'child-2', 'child-3'],
1057
+ })
1058
+
1059
+ const childGoals = Goals([
1060
+ { id: 'child-1', name: 'Child 1', priority: 'high', progress: 100 },
1061
+ { id: 'child-2', name: 'Child 2', priority: 'high', progress: 50 },
1062
+ { id: 'child-3', name: 'Child 3', priority: 'medium', progress: 25 },
1063
+ ])
1064
+
1065
+ const rolledUpProgress = rollupProgress(parentGoal, childGoals)
1066
+ expect(rolledUpProgress).toBeCloseTo(58.33, 1) // Average of 100, 50, 25
1067
+ })
1068
+
1069
+ it('should calculate weighted rollup based on priority', () => {
1070
+ const parentGoal = Goal({
1071
+ name: 'Parent Goal',
1072
+ priority: 'high',
1073
+ children: ['child-1', 'child-2'],
1074
+ })
1075
+
1076
+ const childGoals = Goals([
1077
+ { id: 'child-1', name: 'Critical Child', priority: 'critical', progress: 100, weight: 0.7 },
1078
+ { id: 'child-2', name: 'Low Child', priority: 'low', progress: 0, weight: 0.3 },
1079
+ ])
1080
+
1081
+ const weightedProgress = rollupProgress(parentGoal, childGoals, { weighted: true })
1082
+ expect(weightedProgress).toBe(70) // 100 * 0.7 + 0 * 0.3 = 70
1083
+ })
1084
+ })
1085
+
1086
+ describe('Goal relationships', () => {
1087
+ it('should support parent-child relationships', () => {
1088
+ const parent = Goal({
1089
+ name: 'Parent',
1090
+ priority: 'high',
1091
+ })
1092
+
1093
+ const child1 = Goal({
1094
+ name: 'Child 1',
1095
+ priority: 'medium',
1096
+ parentId: parent.id,
1097
+ })
1098
+
1099
+ const child2 = Goal({
1100
+ name: 'Child 2',
1101
+ priority: 'medium',
1102
+ parentId: parent.id,
1103
+ })
1104
+
1105
+ expect(child1.parentId).toBe(parent.id)
1106
+ expect(child2.parentId).toBe(parent.id)
1107
+ })
1108
+
1109
+ it('should support alignment relationships', () => {
1110
+ const strategic = Goal({
1111
+ name: 'Strategic Goal',
1112
+ priority: 'critical',
1113
+ level: 'company',
1114
+ })
1115
+
1116
+ const operational = Goal({
1117
+ name: 'Operational Goal',
1118
+ priority: 'high',
1119
+ level: 'department',
1120
+ alignedTo: [strategic.id],
1121
+ })
1122
+
1123
+ expect(operational.alignedTo).toContain(strategic.id)
1124
+ })
1125
+
1126
+ it('should support dependency relationships', () => {
1127
+ const prerequisite = Goal({
1128
+ name: 'Prerequisite Goal',
1129
+ priority: 'high',
1130
+ })
1131
+
1132
+ const dependent = Goal({
1133
+ name: 'Dependent Goal',
1134
+ priority: 'medium',
1135
+ dependencies: [prerequisite.id],
1136
+ })
1137
+
1138
+ expect(dependent.dependencies).toContain(prerequisite.id)
1139
+ })
1140
+ })
1141
+ })