@things-factory/worklist 9.1.19 → 10.0.0-beta.2

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 (103) hide show
  1. package/dist-client/components/activity-thread-timeline.d.ts +1 -9
  2. package/dist-client/components/activity-thread-timeline.js +1 -3
  3. package/dist-client/components/activity-thread-timeline.js.map +1 -1
  4. package/dist-client/pages/activity/activity-list-page.d.ts +1 -7
  5. package/dist-client/pages/activity/activity-list-page.js +125 -234
  6. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  7. package/dist-client/pages/activity/activity-page.d.ts +1 -7
  8. package/dist-client/pages/activity/activity-page.js +51 -93
  9. package/dist-client/pages/activity/activity-page.js.map +1 -1
  10. package/dist-client/pages/activity/starter-list-page.d.ts +1 -7
  11. package/dist-client/pages/activity/starter-list-page.js +33 -62
  12. package/dist-client/pages/activity/starter-list-page.js.map +1 -1
  13. package/dist-client/pages/activity-approval/activity-approval-list-page.d.ts +1 -7
  14. package/dist-client/pages/activity-approval/activity-approval-list-page.js +50 -95
  15. package/dist-client/pages/activity-approval/activity-approval-list-page.js.map +1 -1
  16. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +1 -7
  17. package/dist-client/pages/activity-approval/activity-approval-page.js +73 -119
  18. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  19. package/dist-client/pages/activity-instance/activity-instance-list-page.d.ts +0 -6
  20. package/dist-client/pages/activity-instance/activity-instance-list-page.js +63 -120
  21. package/dist-client/pages/activity-instance/activity-instance-list-page.js.map +1 -1
  22. package/dist-client/pages/activity-instance/activity-instance-search-page.d.ts +1 -7
  23. package/dist-client/pages/activity-instance/activity-instance-search-page.js +55 -101
  24. package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
  25. package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -7
  26. package/dist-client/pages/activity-instance/activity-instance-start-page.js +65 -109
  27. package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
  28. package/dist-client/pages/activity-stats/activity-stats-list-page.d.ts +1 -7
  29. package/dist-client/pages/activity-stats/activity-stats-list-page.js +50 -95
  30. package/dist-client/pages/activity-stats/activity-stats-list-page.js.map +1 -1
  31. package/dist-client/pages/activity-store/activity-store-page.d.ts +1 -7
  32. package/dist-client/pages/activity-store/activity-store-page.js +2 -3
  33. package/dist-client/pages/activity-store/activity-store-page.js.map +1 -1
  34. package/dist-client/pages/activity-supervisor/reporter-list-page.d.ts +1 -7
  35. package/dist-client/pages/activity-supervisor/reporter-list-page.js +36 -66
  36. package/dist-client/pages/activity-supervisor/reporter-list-page.js.map +1 -1
  37. package/dist-client/pages/activity-template/activity-template-list-page.d.ts +1 -7
  38. package/dist-client/pages/activity-template/activity-template-list-page.js +70 -134
  39. package/dist-client/pages/activity-template/activity-template-list-page.js.map +1 -1
  40. package/dist-client/pages/activity-thread/activity-thread-list-page.d.ts +1 -7
  41. package/dist-client/pages/activity-thread/activity-thread-list-page.js +49 -93
  42. package/dist-client/pages/activity-thread/activity-thread-list-page.js.map +1 -1
  43. package/dist-client/pages/activity-thread/activity-thread-page.d.ts +1 -7
  44. package/dist-client/pages/activity-thread/activity-thread-page.js +80 -135
  45. package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
  46. package/dist-client/pages/activity-thread/activity-thread-view-page.d.ts +1 -7
  47. package/dist-client/pages/activity-thread/activity-thread-view-page.js +54 -80
  48. package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
  49. package/dist-client/pages/activity-thread/activity-thread-view.js +4 -0
  50. package/dist-client/pages/activity-thread/activity-thread-view.js.map +1 -1
  51. package/dist-client/pages/dashboard/dashboard-home.js +3 -5
  52. package/dist-client/pages/dashboard/dashboard-home.js.map +1 -1
  53. package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +0 -6
  54. package/dist-client/pages/installable-activity/installable-activity-list-page.js +68 -130
  55. package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
  56. package/dist-client/pages/todo/approval-done-list-page.d.ts +1 -7
  57. package/dist-client/pages/todo/approval-done-list-page.js +53 -100
  58. package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
  59. package/dist-client/pages/todo/approval-pending-list-page.d.ts +0 -6
  60. package/dist-client/pages/todo/approval-pending-list-page.js +63 -119
  61. package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
  62. package/dist-client/pages/todo/done-list-calendar-page.d.ts +1 -7
  63. package/dist-client/pages/todo/done-list-calendar-page.js +2 -3
  64. package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
  65. package/dist-client/pages/todo/done-list-page.d.ts +1 -7
  66. package/dist-client/pages/todo/done-list-page.js +56 -106
  67. package/dist-client/pages/todo/done-list-page.js.map +1 -1
  68. package/dist-client/pages/todo/draft-list-page.d.ts +1 -7
  69. package/dist-client/pages/todo/draft-list-page.js +49 -88
  70. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  71. package/dist-client/pages/todo/pickable-list-page.d.ts +1 -7
  72. package/dist-client/pages/todo/pickable-list-page.js +48 -91
  73. package/dist-client/pages/todo/pickable-list-page.js.map +1 -1
  74. package/dist-client/pages/todo/todo-list-page.d.ts +0 -6
  75. package/dist-client/pages/todo/todo-list-page.js +56 -106
  76. package/dist-client/pages/todo/todo-list-page.js.map +1 -1
  77. package/dist-client/pages/worklist-home.js +2 -3
  78. package/dist-client/pages/worklist-home.js.map +1 -1
  79. package/dist-client/route.d.ts +1 -1
  80. package/dist-client/templates/activity-approval-context-template.js +8 -12
  81. package/dist-client/templates/activity-approval-context-template.js.map +1 -1
  82. package/dist-client/templates/activity-instance-context-template.js +8 -12
  83. package/dist-client/templates/activity-instance-context-template.js.map +1 -1
  84. package/dist-client/templates/activity-thread-context-template.js +8 -12
  85. package/dist-client/templates/activity-thread-context-template.js.map +1 -1
  86. package/dist-client/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/controllers/activity-approval/approve.js +2 -2
  88. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  89. package/dist-server/controllers/activity-thread/submit.js +2 -2
  90. package/dist-server/controllers/activity-thread/submit.js.map +1 -1
  91. package/dist-server/service/index.d.ts +2 -2
  92. package/dist-server/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +16 -16
  94. package/spec/integration/approval-mixed-types.spec.ts +491 -0
  95. package/spec/integration/approval-role-based.spec.ts +389 -0
  96. package/spec/integration/instance-lifecycle.spec.ts +406 -0
  97. package/spec/integration/role-approval-edge-cases.spec.ts +581 -0
  98. package/spec/unit/controllers/activity-instance-issue.spec.ts +360 -0
  99. package/spec/unit/controllers/activity-thread-submit.spec.ts +384 -0
  100. package/spec/unit/role-approval-escalate-logic.spec.ts +499 -0
  101. package/spec/unit/role-approval-submit-logic.spec.ts +481 -0
  102. package/spec/unit/thread-state-helpers.spec.ts +253 -0
  103. package/translations/en.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/worklist",
3
- "version": "9.1.19",
3
+ "version": "10.0.0-beta.2",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -28,20 +28,20 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@material/web": "^2.0.0",
31
- "@operato/event-view": "^9.0.0",
32
- "@operato/graphql": "^9.0.0",
33
- "@operato/grist-editor": "^9.0.0",
34
- "@operato/moment-timezone-es": "^9.0.0",
35
- "@operato/p13n": "^9.0.0",
36
- "@things-factory/attachment-base": "^9.1.19",
37
- "@things-factory/auth-base": "^9.1.19",
38
- "@things-factory/board-service": "^9.1.19",
39
- "@things-factory/board-ui": "^9.1.19",
40
- "@things-factory/context-ui": "^9.1.19",
41
- "@things-factory/organization": "^9.1.19",
42
- "@things-factory/personalization": "^9.1.19",
43
- "@things-factory/scheduler-client": "^9.1.19",
44
- "@things-factory/shell": "^9.1.19"
31
+ "@operato/event-view": "^10.0.0-beta.1",
32
+ "@operato/graphql": "^10.0.0-beta.1",
33
+ "@operato/grist-editor": "^10.0.0-beta.1",
34
+ "@operato/moment-timezone-es": "^10.0.0-beta.1",
35
+ "@operato/p13n": "^10.0.0-beta.1",
36
+ "@things-factory/attachment-base": "^10.0.0-beta.2",
37
+ "@things-factory/auth-base": "^10.0.0-beta.2",
38
+ "@things-factory/board-service": "^10.0.0-beta.2",
39
+ "@things-factory/board-ui": "^10.0.0-beta.2",
40
+ "@things-factory/context-ui": "^10.0.0-beta.2",
41
+ "@things-factory/organization": "^10.0.0-beta.2",
42
+ "@things-factory/personalization": "^10.0.0-beta.2",
43
+ "@things-factory/scheduler-client": "^10.0.0-beta.2",
44
+ "@things-factory/shell": "^10.0.0-beta.2"
45
45
  },
46
- "gitHead": "078438034dbe19915108e89ff24024f7044a85a9"
46
+ "gitHead": "ea8a08ff802ae28a37fdda605b6356482e7a6faf"
47
47
  }
@@ -0,0 +1,491 @@
1
+ /**
2
+ * Mixed Approval Line Type Integration Tests
3
+ * ROLE과 User 타입이 혼합된 다단계 승인라인 통합 테스트
4
+ */
5
+
6
+ import { TestDatabase } from '../../../../test/test-database'
7
+ import { withTestTransaction } from '../../../../test/test-context'
8
+ import {
9
+ domainFactory,
10
+ userFactory,
11
+ roleFactory,
12
+ activityInstanceFactory,
13
+ activityThreadFactory,
14
+ activityApprovalFactory
15
+ } from '../../../../test/factories'
16
+
17
+ import {
18
+ ActivityInstanceStatus,
19
+ ActivityThreadStatus,
20
+ ActivityApprovalJudgment
21
+ } from '../../../../test/entities/schemas'
22
+
23
+ import {
24
+ createRoleApprovalLineItem,
25
+ createUserApprovalLineItem,
26
+ createMultiLevelApprovalLine
27
+ } from '../../../../test/helpers/workflow-helpers'
28
+
29
+ describe('Mixed Approval Line Type Integration Tests', () => {
30
+ let testDb: TestDatabase
31
+
32
+ beforeAll(async () => {
33
+ testDb = TestDatabase.getInstance()
34
+ })
35
+
36
+ describe('ROLE → User 혼합 승인라인', () => {
37
+ it('첫 번째 레벨 ROLE, 두 번째 레벨 User 승인라인이 처리되어야 한다', async () => {
38
+ await withTestTransaction(async (context) => {
39
+ const { tx } = context.state
40
+ const domain = await domainFactory.create({}, tx)
41
+ const firstRole = await roleFactory.create({ name: 'First Approver Role', domain }, tx)
42
+ const secondUser = await userFactory.create({ name: 'Final Approver' }, tx)
43
+ const firstApprover = await userFactory.createWithRole({}, firstRole, tx)
44
+
45
+ // Given: ROLE → User 혼합 승인라인
46
+ const approvalLine = [
47
+ createRoleApprovalLineItem(firstRole),
48
+ createUserApprovalLineItem(secondUser)
49
+ ]
50
+
51
+ // 타입 확인
52
+ expect(approvalLine[0].type).toBe('Role')
53
+ expect(approvalLine[1].type).toBe('Employee')
54
+
55
+ const instance = await activityInstanceFactory.createWithApprovalLine(
56
+ approvalLine,
57
+ { state: ActivityInstanceStatus.Started },
58
+ undefined,
59
+ domain,
60
+ tx
61
+ )
62
+
63
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
64
+ { state: ActivityThreadStatus.Submitted },
65
+ instance,
66
+ firstApprover,
67
+ tx
68
+ )
69
+
70
+ // When: 첫 번째 ROLE 승인 (에스컬레이션)
71
+ const firstApproval = await tx.save('ActivityApproval', {
72
+ domain,
73
+ activityThread: thread,
74
+ round: 1,
75
+ order: 1,
76
+ judgment: ActivityApprovalJudgment.Escalated,
77
+ approverRole: firstRole,
78
+ approver: firstApprover,
79
+ terminatedAt: new Date(),
80
+ creator: firstApprover,
81
+ updater: firstApprover
82
+ }) as any
83
+
84
+ expect(firstApproval.judgment).toBe(ActivityApprovalJudgment.Escalated)
85
+ expect(firstApproval.approverRole?.id).toBe(firstRole.id)
86
+
87
+ // When: 두 번째 레벨 User 승인 생성
88
+ const order = firstApproval.order // 1
89
+ const nextLineItem = approvalLine[order] // approvalLine[1]
90
+ expect(nextLineItem.type).toBe('Employee')
91
+
92
+ const secondApproval = await tx.save('ActivityApproval', {
93
+ domain,
94
+ activityThread: thread,
95
+ round: 1,
96
+ order: order + 1,
97
+ judgment: ActivityApprovalJudgment.Pending,
98
+ approver: secondUser,
99
+ transaction: 'escalate',
100
+ creator: firstApprover,
101
+ updater: firstApprover
102
+ }) as any
103
+
104
+ // Then: User 타입 approval 생성됨
105
+ expect(secondApproval.order).toBe(2)
106
+ expect(secondApproval.approver?.id).toBe(secondUser.id)
107
+ expect(secondApproval.approverRole).toBeUndefined()
108
+
109
+ // When: 최종 User 승인
110
+ secondApproval.judgment = ActivityApprovalJudgment.Approved
111
+ secondApproval.terminatedAt = new Date()
112
+ const approved = await tx.save('ActivityApproval', secondApproval) as any
113
+
114
+ expect(approved.judgment).toBe(ActivityApprovalJudgment.Approved)
115
+ })
116
+ })
117
+ })
118
+
119
+ describe('User → ROLE 혼합 승인라인', () => {
120
+ it('첫 번째 레벨 User, 두 번째 레벨 ROLE 승인라인이 처리되어야 한다', async () => {
121
+ await withTestTransaction(async (context) => {
122
+ const { tx } = context.state
123
+ const domain = await domainFactory.create({}, tx)
124
+ const firstUser = await userFactory.create({ name: 'First Approver' }, tx)
125
+ const secondRole = await roleFactory.create({ name: 'Final Approver Role', domain }, tx)
126
+ const secondApprover = await userFactory.createWithRole({}, secondRole, tx)
127
+
128
+ // Given: User → ROLE 혼합 승인라인
129
+ const approvalLine = [
130
+ createUserApprovalLineItem(firstUser),
131
+ createRoleApprovalLineItem(secondRole)
132
+ ]
133
+
134
+ expect(approvalLine[0].type).toBe('Employee')
135
+ expect(approvalLine[1].type).toBe('Role')
136
+
137
+ const instance = await activityInstanceFactory.createWithApprovalLine(
138
+ approvalLine,
139
+ { state: ActivityInstanceStatus.Started },
140
+ undefined,
141
+ domain,
142
+ tx
143
+ )
144
+
145
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
146
+ { state: ActivityThreadStatus.Submitted },
147
+ instance,
148
+ firstUser,
149
+ tx
150
+ )
151
+
152
+ // When: 첫 번째 User 승인 (에스컬레이션)
153
+ const firstApproval = await tx.save('ActivityApproval', {
154
+ domain,
155
+ activityThread: thread,
156
+ round: 1,
157
+ order: 1,
158
+ judgment: ActivityApprovalJudgment.Escalated,
159
+ approver: firstUser,
160
+ terminatedAt: new Date(),
161
+ creator: firstUser,
162
+ updater: firstUser
163
+ }) as any
164
+
165
+ expect(firstApproval.judgment).toBe(ActivityApprovalJudgment.Escalated)
166
+ expect(firstApproval.approver?.id).toBe(firstUser.id)
167
+
168
+ // When: 두 번째 레벨 ROLE 승인 생성
169
+ const order = firstApproval.order // 1
170
+ const nextLineItem = approvalLine[order] // approvalLine[1]
171
+ expect(nextLineItem.type).toBe('Role')
172
+
173
+ const secondApproval = await tx.save('ActivityApproval', {
174
+ domain,
175
+ activityThread: thread,
176
+ round: 1,
177
+ order: order + 1,
178
+ judgment: ActivityApprovalJudgment.Pending,
179
+ approverRole: secondRole,
180
+ transaction: 'escalate',
181
+ creator: firstUser,
182
+ updater: firstUser
183
+ }) as any
184
+
185
+ // Then: ROLE 타입 approval 생성됨
186
+ expect(secondApproval.order).toBe(2)
187
+ expect(secondApproval.approverRole?.id).toBe(secondRole.id)
188
+ expect(secondApproval.approver).toBeUndefined()
189
+
190
+ // When: ROLE 소유자가 최종 승인
191
+ secondApproval.judgment = ActivityApprovalJudgment.Approved
192
+ secondApproval.approver = secondApprover
193
+ secondApproval.terminatedAt = new Date()
194
+ const approved = await tx.save('ActivityApproval', secondApproval) as any
195
+
196
+ expect(approved.judgment).toBe(ActivityApprovalJudgment.Approved)
197
+ expect(approved.approverRole?.id).toBe(secondRole.id)
198
+ expect(approved.approver?.id).toBe(secondApprover.id)
199
+ })
200
+ })
201
+ })
202
+
203
+ describe('User → ROLE → User 3단계 혼합', () => {
204
+ it('3단계 혼합 승인라인이 순서대로 처리되어야 한다', async () => {
205
+ await withTestTransaction(async (context) => {
206
+ const { tx } = context.state
207
+ const domain = await domainFactory.create({}, tx)
208
+
209
+ const firstUser = await userFactory.create({ name: 'Team Lead' }, tx)
210
+ const middleRole = await roleFactory.create({ name: 'Manager Role', domain }, tx)
211
+ const finalUser = await userFactory.create({ name: 'Director' }, tx)
212
+ const middleApprover = await userFactory.createWithRole({ name: 'Manager' }, middleRole, tx)
213
+
214
+ // Given: User → ROLE → User 3단계 승인라인
215
+ const approvalLine = [
216
+ createUserApprovalLineItem(firstUser),
217
+ createRoleApprovalLineItem(middleRole),
218
+ createUserApprovalLineItem(finalUser)
219
+ ]
220
+
221
+ expect(approvalLine[0].type).toBe('Employee')
222
+ expect(approvalLine[1].type).toBe('Role')
223
+ expect(approvalLine[2].type).toBe('Employee')
224
+
225
+ const instance = await activityInstanceFactory.createWithApprovalLine(
226
+ approvalLine,
227
+ { state: ActivityInstanceStatus.Started },
228
+ undefined,
229
+ domain,
230
+ tx
231
+ )
232
+
233
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
234
+ { state: ActivityThreadStatus.Submitted },
235
+ instance,
236
+ firstUser,
237
+ tx
238
+ )
239
+
240
+ // Level 1: User 승인 → Escalated
241
+ const approval1 = await tx.save('ActivityApproval', {
242
+ domain,
243
+ activityThread: thread,
244
+ round: 1,
245
+ order: 1,
246
+ judgment: ActivityApprovalJudgment.Escalated,
247
+ approver: firstUser,
248
+ terminatedAt: new Date(),
249
+ creator: firstUser,
250
+ updater: firstUser
251
+ }) as any
252
+
253
+ // Level 2: ROLE 승인 → Escalated
254
+ const nextType2 = approvalLine[1].type
255
+ expect(nextType2).toBe('Role')
256
+
257
+ const approval2 = await tx.save('ActivityApproval', {
258
+ domain,
259
+ activityThread: thread,
260
+ round: 1,
261
+ order: 2,
262
+ judgment: ActivityApprovalJudgment.Escalated,
263
+ approverRole: middleRole,
264
+ approver: middleApprover,
265
+ terminatedAt: new Date(),
266
+ creator: middleApprover,
267
+ updater: middleApprover
268
+ }) as any
269
+
270
+ expect(approval2.approverRole?.id).toBe(middleRole.id)
271
+ expect(approval2.approver?.id).toBe(middleApprover.id)
272
+
273
+ // Level 3: User 최종 승인 → Approved
274
+ const nextType3 = approvalLine[2].type
275
+ expect(nextType3).toBe('Employee')
276
+
277
+ const approval3 = await tx.save('ActivityApproval', {
278
+ domain,
279
+ activityThread: thread,
280
+ round: 1,
281
+ order: 3,
282
+ judgment: ActivityApprovalJudgment.Approved,
283
+ approver: finalUser,
284
+ terminatedAt: new Date(),
285
+ creator: finalUser,
286
+ updater: finalUser
287
+ }) as any
288
+
289
+ expect(approval3.judgment).toBe(ActivityApprovalJudgment.Approved)
290
+ expect(approval3.order).toBe(3)
291
+ })
292
+ })
293
+ })
294
+
295
+ describe('ROLE → ROLE → ROLE 3단계', () => {
296
+ it('3단계 ROLE 전용 승인라인에서 각 레벨의 roleId가 올바르게 참조되어야 한다', async () => {
297
+ await withTestTransaction(async (context) => {
298
+ const { tx } = context.state
299
+ const domain = await domainFactory.create({}, tx)
300
+
301
+ const role1 = await roleFactory.create({ name: 'Team Lead Role', domain }, tx)
302
+ const role2 = await roleFactory.create({ name: 'Manager Role', domain }, tx)
303
+ const role3 = await roleFactory.create({ name: 'Director Role', domain }, tx)
304
+
305
+ const approver1 = await userFactory.createWithRole({}, role1, tx)
306
+ const approver2 = await userFactory.createWithRole({}, role2, tx)
307
+ const approver3 = await userFactory.createWithRole({}, role3, tx)
308
+
309
+ // Given: ROLE → ROLE → ROLE
310
+ const approvalLine = [
311
+ createRoleApprovalLineItem(role1),
312
+ createRoleApprovalLineItem(role2),
313
+ createRoleApprovalLineItem(role3)
314
+ ]
315
+
316
+ const instance = await activityInstanceFactory.createWithApprovalLine(
317
+ approvalLine,
318
+ { state: ActivityInstanceStatus.Started },
319
+ undefined,
320
+ domain,
321
+ tx
322
+ )
323
+
324
+ // 버그 수정 검증: 각 에스컬레이션 시 올바른 인덱스 참조
325
+ // order=1 완료 후 → approvalLine[1] (role2)
326
+ expect(approvalLine[1].value).toBe(role2.id)
327
+
328
+ // order=2 완료 후 → approvalLine[2] (role3)
329
+ expect(approvalLine[2].value).toBe(role3.id)
330
+
331
+ // 수정 전 버그: 항상 approvalLine[0].value 참조 (role1.id)
332
+ // 이 테스트는 수정된 코드가 올바르게 작동하는지 확인
333
+ expect(approvalLine[0].value).not.toBe(role2.id)
334
+ expect(approvalLine[0].value).not.toBe(role3.id)
335
+ expect(approvalLine[0].value).toBe(role1.id)
336
+ })
337
+ })
338
+ })
339
+
340
+ describe('타입 분기 로직 검증', () => {
341
+ it('OrgMemberTargetType.Role 체크가 정확해야 한다', async () => {
342
+ await withTestTransaction(async (context) => {
343
+ const { tx } = context.state
344
+ const domain = await domainFactory.create({}, tx)
345
+ const role = await roleFactory.create({ name: 'Test Role', domain }, tx)
346
+ const user = await userFactory.create({ name: 'Test User' }, tx)
347
+
348
+ const roleItem = createRoleApprovalLineItem(role)
349
+ const userItem = createUserApprovalLineItem(user)
350
+
351
+ // Role 타입 분기
352
+ if (roleItem.type === 'Role') {
353
+ expect(roleItem.value).toBe(role.id)
354
+ } else {
355
+ fail('Should be Role type')
356
+ }
357
+
358
+ // Employee 타입 분기
359
+ if (userItem.type === 'Employee') {
360
+ expect(userItem.value).toBe(user.id)
361
+ } else {
362
+ fail('Should be Employee type')
363
+ }
364
+
365
+ // 타입 불일치
366
+ expect(roleItem.type).not.toBe('Employee')
367
+ expect(userItem.type).not.toBe('Role')
368
+ })
369
+ })
370
+ })
371
+
372
+ describe('반려 시나리오', () => {
373
+ it('혼합 승인라인의 어느 레벨에서든 반려할 수 있어야 한다', async () => {
374
+ await withTestTransaction(async (context) => {
375
+ const { tx } = context.state
376
+ const domain = await domainFactory.create({}, tx)
377
+ const firstRole = await roleFactory.create({ name: 'First', domain }, tx)
378
+ const secondUser = await userFactory.create({ name: 'Second' }, tx)
379
+ const firstApprover = await userFactory.createWithRole({}, firstRole, tx)
380
+
381
+ // Given: ROLE → User 승인라인
382
+ const approvalLine = [
383
+ createRoleApprovalLineItem(firstRole),
384
+ createUserApprovalLineItem(secondUser)
385
+ ]
386
+
387
+ const instance = await activityInstanceFactory.createWithApprovalLine(
388
+ approvalLine,
389
+ { state: ActivityInstanceStatus.Started },
390
+ undefined,
391
+ domain,
392
+ tx
393
+ )
394
+
395
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
396
+ { state: ActivityThreadStatus.Submitted },
397
+ instance,
398
+ firstApprover,
399
+ tx
400
+ )
401
+
402
+ // When: 첫 번째 ROLE 승인자가 반려
403
+ const rejection = await tx.save('ActivityApproval', {
404
+ domain,
405
+ activityThread: thread,
406
+ round: 1,
407
+ order: 1,
408
+ judgment: ActivityApprovalJudgment.Rejected,
409
+ approverRole: firstRole,
410
+ approver: firstApprover,
411
+ comment: 'Needs revision',
412
+ terminatedAt: new Date(),
413
+ creator: firstApprover,
414
+ updater: firstApprover
415
+ }) as any
416
+
417
+ // Then: Rejected 상태
418
+ expect(rejection.judgment).toBe(ActivityApprovalJudgment.Rejected)
419
+ expect(rejection.comment).toBe('Needs revision')
420
+
421
+ // 스레드도 Rejected 상태
422
+ thread.state = ActivityThreadStatus.Rejected
423
+ const rejectedThread = await tx.save('ActivityThread', thread) as any
424
+ expect(rejectedThread.state).toBe(ActivityThreadStatus.Rejected)
425
+ })
426
+ })
427
+
428
+ it('반려 후 재시작 시 round가 증가해야 한다', async () => {
429
+ await withTestTransaction(async (context) => {
430
+ const { tx } = context.state
431
+ const domain = await domainFactory.create({}, tx)
432
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
433
+ const user = await userFactory.create({}, tx)
434
+
435
+ // Given: round=1인 스레드
436
+ const instance = await activityInstanceFactory.createWithActivity(
437
+ { state: ActivityInstanceStatus.Started },
438
+ undefined,
439
+ domain,
440
+ tx
441
+ )
442
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
443
+ { state: ActivityThreadStatus.Rejected, round: 1 },
444
+ instance,
445
+ user,
446
+ tx
447
+ )
448
+
449
+ expect(thread.round).toBe(1)
450
+
451
+ // When: 재시작 시 round 증가
452
+ const newRound = thread.round + 1
453
+ thread.round = newRound
454
+ thread.state = ActivityThreadStatus.Started
455
+ const restarted = await tx.save('ActivityThread', thread) as any
456
+
457
+ // Then
458
+ expect(restarted.round).toBe(2)
459
+ expect(restarted.state).toBe(ActivityThreadStatus.Started)
460
+ })
461
+ })
462
+ })
463
+
464
+ describe('createMultiLevelApprovalLine 헬퍼 테스트', () => {
465
+ it('User와 Role 혼합 배열에서 올바른 타입으로 생성되어야 한다', async () => {
466
+ await withTestTransaction(async (context) => {
467
+ const { tx } = context.state
468
+ const domain = await domainFactory.create({}, tx)
469
+
470
+ const user1 = await userFactory.create({ name: 'User 1' }, tx)
471
+ const role1 = await roleFactory.create({ name: 'Role 1', domain }, tx)
472
+ const user2 = await userFactory.create({ name: 'User 2' }, tx)
473
+
474
+ // When: 혼합 배열로 승인라인 생성
475
+ const approvalLine = createMultiLevelApprovalLine([user1, role1, user2])
476
+
477
+ // Then: 타입이 올바르게 설정됨
478
+ expect(approvalLine.length).toBe(3)
479
+
480
+ expect(approvalLine[0].type).toBe('Employee')
481
+ expect(approvalLine[0].value).toBe(user1.id)
482
+
483
+ expect(approvalLine[1].type).toBe('Role')
484
+ expect(approvalLine[1].value).toBe(role1.id)
485
+
486
+ expect(approvalLine[2].type).toBe('Employee')
487
+ expect(approvalLine[2].value).toBe(user2.id)
488
+ })
489
+ })
490
+ })
491
+ })