@things-factory/worklist 10.1.202 → 10.1.205
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/activity/activity-list-page.d.ts +3 -0
- package/dist-client/pages/activity/activity-list-page.js +25 -13
- package/dist-client/pages/activity/activity-list-page.js.map +1 -1
- package/dist-client/pages/activity/activity-partial-view.js +1 -1
- package/dist-client/pages/activity/activity-partial-view.js.map +1 -1
- package/dist-client/pages/activity/assignee-roles-cell.d.ts +32 -0
- package/dist-client/pages/activity/assignee-roles-cell.js +26 -0
- package/dist-client/pages/activity/assignee-roles-cell.js.map +1 -0
- package/dist-client/pages/activity-catalog/activity-catalog-page.js +1 -1
- package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
- package/dist-client/pages/todo/done-list-calendar-page.d.ts +2 -0
- package/dist-client/pages/todo/done-list-calendar-page.js +63 -64
- package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
- package/dist-client/pages/work-door/door-sentence.d.ts +1 -1
- package/dist-client/pages/work-door/door-sentence.js +2 -2
- package/dist-client/pages/work-door/door-sentence.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types/activity-instance-type.d.ts +2 -2
- package/dist-client/types/activity-instance-type.js.map +1 -1
- package/dist-client/types/activity-instance.d.ts +1 -1
- package/dist-client/types/activity-instance.js.map +1 -1
- package/dist-client/types/activity.d.ts +1 -1
- package/dist-client/types/activity.js.map +1 -1
- package/dist-client/work-calendar-sources.d.ts +53 -0
- package/dist-client/work-calendar-sources.js +47 -0
- package/dist-client/work-calendar-sources.js.map +1 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.d.ts +2 -2
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +2 -2
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -1
- package/dist-server/controllers/activity-instance/abort.js +1 -1
- package/dist-server/controllers/activity-instance/abort.js.map +1 -1
- package/dist-server/controllers/activity-instance/assign.d.ts +2 -1
- package/dist-server/controllers/activity-instance/assign.js +104 -2
- package/dist-server/controllers/activity-instance/assign.js.map +1 -1
- package/dist-server/controllers/activity-instance/assignee-input.d.ts +25 -0
- package/dist-server/controllers/activity-instance/assignee-input.js +28 -0
- package/dist-server/controllers/activity-instance/assignee-input.js.map +1 -0
- package/dist-server/controllers/activity-instance/draft.js +7 -2
- package/dist-server/controllers/activity-instance/draft.js.map +1 -1
- package/dist-server/controllers/activity-instance/end.js +1 -1
- package/dist-server/controllers/activity-instance/end.js.map +1 -1
- package/dist-server/controllers/activity-instance/entitlement.d.ts +1 -1
- package/dist-server/controllers/activity-instance/entitlement.js +7 -5
- package/dist-server/controllers/activity-instance/entitlement.js.map +1 -1
- package/dist-server/controllers/activity-instance/issue.js +28 -7
- package/dist-server/controllers/activity-instance/issue.js.map +1 -1
- package/dist-server/controllers/activity-instance/pick.js +56 -4
- package/dist-server/controllers/activity-instance/pick.js.map +1 -1
- package/dist-server/controllers/activity-read/assert-may-read.js +2 -1
- package/dist-server/controllers/activity-read/assert-may-read.js.map +1 -1
- package/dist-server/controllers/activity-read/participates.d.ts +2 -2
- package/dist-server/controllers/activity-read/participates.js +2 -2
- package/dist-server/controllers/activity-read/participates.js.map +1 -1
- package/dist-server/controllers/activity-thread/check-authority.js +38 -5
- package/dist-server/controllers/activity-thread/check-authority.js.map +1 -1
- package/dist-server/controllers/common.d.ts +13 -0
- package/dist-server/controllers/common.js +43 -15
- package/dist-server/controllers/common.js.map +1 -1
- package/dist-server/service/activity/activity-history.d.ts +1 -2
- package/dist-server/service/activity/activity-history.js +4 -8
- package/dist-server/service/activity/activity-history.js.map +1 -1
- package/dist-server/service/activity/activity-mutation.js +39 -8
- package/dist-server/service/activity/activity-mutation.js.map +1 -1
- package/dist-server/service/activity/activity-query.d.ts +1 -1
- package/dist-server/service/activity/activity-query.js +9 -9
- package/dist-server/service/activity/activity-query.js.map +1 -1
- package/dist-server/service/activity/activity-type.d.ts +2 -2
- package/dist-server/service/activity/activity-type.js +12 -6
- package/dist-server/service/activity/activity-type.js.map +1 -1
- package/dist-server/service/activity/activity.d.ts +2 -2
- package/dist-server/service/activity/activity.js +15 -7
- package/dist-server/service/activity/activity.js.map +1 -1
- package/dist-server/service/activity/event-subscriber.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-history.d.ts +1 -2
- package/dist-server/service/activity-instance/activity-instance-history.js +4 -8
- package/dist-server/service/activity-instance/activity-instance-history.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-mutation.d.ts +1 -1
- package/dist-server/service/activity-instance/activity-instance-mutation.js +10 -5
- package/dist-server/service/activity-instance/activity-instance-mutation.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-query.d.ts +1 -2
- package/dist-server/service/activity-instance/activity-instance-query.js +16 -25
- package/dist-server/service/activity-instance/activity-instance-query.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-type.d.ts +2 -2
- package/dist-server/service/activity-instance/activity-instance-type.js +12 -6
- package/dist-server/service/activity-instance/activity-instance-type.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance.d.ts +2 -2
- package/dist-server/service/activity-instance/activity-instance.js +15 -7
- package/dist-server/service/activity-instance/activity-instance.js.map +1 -1
- package/dist-server/service/activity-instance/assignee-pool.d.ts +59 -0
- package/dist-server/service/activity-instance/assignee-pool.js +99 -0
- package/dist-server/service/activity-instance/assignee-pool.js.map +1 -0
- package/dist-server/service/activity-instance/event-subscriber.d.ts +9 -1
- package/dist-server/service/activity-instance/event-subscriber.js +20 -4
- package/dist-server/service/activity-instance/event-subscriber.js.map +1 -1
- package/dist-server/service/activity-summary/activity-summary-query.js +3 -3
- package/dist-server/service/activity-summary/activity-summary-query.js.map +1 -1
- package/dist-server/service/activity-thread/my-open-threads.d.ts +1 -1
- package/dist-server/service/activity-thread/my-open-threads.js +19 -4
- package/dist-server/service/activity-thread/my-open-threads.js.map +1 -1
- package/dist-server/service/subscription/subscription-filters.d.ts +3 -2
- package/dist-server/service/subscription/subscription-filters.js +6 -3
- package/dist-server/service/subscription/subscription-filters.js.map +1 -1
- package/dist-server/service/work-door/work-door-service.js +9 -6
- package/dist-server/service/work-door/work-door-service.js.map +1 -1
- package/dist-server/service/work-door/work-door-type.d.ts +1 -1
- package/dist-server/service/work-door/work-door-type.js +4 -4
- package/dist-server/service/work-door/work-door-type.js.map +1 -1
- package/dist-server/service/work-door/work-door.d.ts +3 -3
- package/dist-server/service/work-door/work-door.js +2 -2
- package/dist-server/service/work-door/work-door.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
- package/tests/assign-role-pool.test.ts +508 -0
- package/tests/assignee-pool-history.test.ts +81 -0
- package/tests/may-read-case.test.ts +3 -2
- package/tests/org-member-domain.test.ts +10 -4
- package/tests/pick-race.pg.test.ts +81 -3
- package/tests/pool-pick-waiting-thread.test.ts +254 -0
- package/tests/published-shape.test.ts +2 -2
- package/tests/role-pool-race.pg.test.ts +182 -0
- package/tests/work-door.test.ts +3 -3
- package/translations/en.json +6 -0
- package/translations/es.json +6 -0
- package/translations/ja.json +6 -0
- package/translations/ko.json +6 -0
- package/translations/ms.json +6 -0
- package/translations/zh.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/worklist",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.205",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
"@operato/moment-timezone-es": "^10.0.0",
|
|
55
55
|
"@operato/p13n": "^10.29.43",
|
|
56
56
|
"@operato/popup": "^10.29.43",
|
|
57
|
-
"@things-factory/attachment-base": "^10.1.
|
|
58
|
-
"@things-factory/auth-base": "^10.1.
|
|
59
|
-
"@things-factory/board-service": "^10.1.
|
|
60
|
-
"@things-factory/context-ui": "^10.1.
|
|
61
|
-
"@things-factory/i18n-base": "^10.1.
|
|
62
|
-
"@things-factory/organization": "^10.1.
|
|
63
|
-
"@things-factory/personalization": "^10.1.
|
|
64
|
-
"@things-factory/scheduler-client": "^10.1.
|
|
65
|
-
"@things-factory/shell": "^10.1.
|
|
57
|
+
"@things-factory/attachment-base": "^10.1.205",
|
|
58
|
+
"@things-factory/auth-base": "^10.1.205",
|
|
59
|
+
"@things-factory/board-service": "^10.1.205",
|
|
60
|
+
"@things-factory/context-ui": "^10.1.205",
|
|
61
|
+
"@things-factory/i18n-base": "^10.1.205",
|
|
62
|
+
"@things-factory/organization": "^10.1.205",
|
|
63
|
+
"@things-factory/personalization": "^10.1.205",
|
|
64
|
+
"@things-factory/scheduler-client": "^10.1.205",
|
|
65
|
+
"@things-factory/shell": "^10.1.205"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "7311492aa5eaa00993c0325053f5f7ae8b1fb024"
|
|
68
68
|
}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A case put in a role's pool by assign, and a role among assignees refused (ruling 2026-10-20).
|
|
5
|
+
*
|
|
6
|
+
* The pool is what issue() already makes for an activity that names assignee roles: `assigneeRoles`,
|
|
7
|
+
* `threadsMin` 1, one unassigned thread. The to-do list reads exactly that, so the check that matters
|
|
8
|
+
* is the list - a member sees the case, someone else does not. Real entities on sqlite; the race that
|
|
9
|
+
* the UPDATE closes is staged by handing assign a read taken before someone picked the case.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* Nobody here manages activity instances - the role is the only way in. */
|
|
13
|
+
jest.mock('@things-factory/auth-base', () => {
|
|
14
|
+
const actual = jest.requireActual('@things-factory/auth-base')
|
|
15
|
+
return { ...actual, checkPermission: async () => false }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const { createSqliteTestDataSource } = require(join(__dirname, '..', '..', 'ops-master', 'test', 'support', 'database.cjs'))
|
|
19
|
+
const DIST = join(__dirname, '..', 'dist-server')
|
|
20
|
+
const { Activity } = require(join(DIST, 'service', 'activity', 'activity.js'))
|
|
21
|
+
const { ActivityInstance } = require(join(DIST, 'service', 'activity-instance', 'activity-instance.js'))
|
|
22
|
+
const { ActivityThread } = require(join(DIST, 'service', 'activity-thread', 'activity-thread.js'))
|
|
23
|
+
const { ActivityApproval } = require(join(DIST, 'service', 'activity-approval', 'activity-approval.js'))
|
|
24
|
+
const { myOpenThreadsWhere } = require(join(DIST, 'service', 'activity-thread', 'my-open-threads.js'))
|
|
25
|
+
const { assign } = require(join(DIST, 'controllers', 'activity-instance', 'assign.js'))
|
|
26
|
+
const { issue } = require(join(DIST, 'controllers', 'activity-instance', 'issue.js'))
|
|
27
|
+
const { pick } = require(join(DIST, 'controllers', 'activity-instance', 'pick.js'))
|
|
28
|
+
const { start } = require(join(DIST, 'controllers', 'activity-thread', 'start.js'))
|
|
29
|
+
const { checkAuthority } = require(join(DIST, 'controllers', 'activity-thread', 'check-authority.js'))
|
|
30
|
+
const { ActivityMutation } = require(join(DIST, 'service', 'activity', 'activity-mutation.js'))
|
|
31
|
+
const { Domain } = require('@things-factory/shell')
|
|
32
|
+
const { Role, User } = require('@things-factory/auth-base')
|
|
33
|
+
|
|
34
|
+
let db: any
|
|
35
|
+
let here: any
|
|
36
|
+
let elsewhere: any
|
|
37
|
+
let manager: any
|
|
38
|
+
let member: any
|
|
39
|
+
let other: any
|
|
40
|
+
let pool: any
|
|
41
|
+
let anotherPool: any
|
|
42
|
+
let emptyPool: any
|
|
43
|
+
let alsoEmpty: any
|
|
44
|
+
let both: any
|
|
45
|
+
let theirPool: any
|
|
46
|
+
let activity: any
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
db = createSqliteTestDataSource([Activity, ActivityInstance, ActivityThread, ActivityApproval])
|
|
50
|
+
await db.initialize()
|
|
51
|
+
|
|
52
|
+
here = await db.getRepository(Domain).save({ name: 'here', subdomain: 'here' })
|
|
53
|
+
elsewhere = await db.getRepository(Domain).save({ name: 'elsewhere', subdomain: 'elsewhere' })
|
|
54
|
+
|
|
55
|
+
const person = (name: string, domain: any) =>
|
|
56
|
+
db.getRepository(User).save({ name, email: `${name}@example.com`, password: 'x', status: 'activated', userType: 'user', domains: [domain] })
|
|
57
|
+
|
|
58
|
+
manager = await person('manager', here)
|
|
59
|
+
member = await person('member', here)
|
|
60
|
+
other = await person('other', here)
|
|
61
|
+
both = await person('both', here)
|
|
62
|
+
const theirs = await person('theirs', elsewhere)
|
|
63
|
+
|
|
64
|
+
pool = await db.getRepository(Role).save({ domain: here, name: 'finance', users: [member, both] })
|
|
65
|
+
anotherPool = await db.getRepository(Role).save({ domain: here, name: 'support', users: [other, both] })
|
|
66
|
+
emptyPool = await db.getRepository(Role).save({ domain: here, name: 'nobody', users: [] })
|
|
67
|
+
alsoEmpty = await db.getRepository(Role).save({ domain: here, name: 'nobody-either', users: [] })
|
|
68
|
+
theirPool = await db.getRepository(Role).save({ domain: elsewhere, name: 'theirs', users: [theirs] })
|
|
69
|
+
|
|
70
|
+
activity = await db.getRepository(Activity).save({ domain: here, name: 'tax-invoice', activityType: 'standard', startingType: 'issue' })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterAll(async () => db?.destroy())
|
|
74
|
+
|
|
75
|
+
const as = (user: any, tx: any) => ({ state: { domain: here, user, tx }, t: (key: string) => key }) as any
|
|
76
|
+
|
|
77
|
+
const aCase = (fields: any = {}) =>
|
|
78
|
+
db.getRepository(ActivityInstance).save({ domain: here, name: 'case', state: 'issued', activity, ...fields })
|
|
79
|
+
|
|
80
|
+
const assignAs = (user: any, args: any) => db.transaction((tx: any) => assign(args, as(user, tx)))
|
|
81
|
+
|
|
82
|
+
const reread = (instance: any) =>
|
|
83
|
+
db.getRepository(ActivityInstance).findOne({ where: { id: instance.id }, relations: ['assigneeRoles'] })
|
|
84
|
+
|
|
85
|
+
/** The pool's role ids, in no particular order - sorted to compare. */
|
|
86
|
+
const poolOf = (row: any) => (row?.assigneeRoles || []).map((role: any) => role.id).sort()
|
|
87
|
+
|
|
88
|
+
const threadsOf = (instance: any) => db.getRepository(ActivityThread).find({ where: { activityInstance: { id: instance.id } } })
|
|
89
|
+
|
|
90
|
+
/** The to-do list's own condition (my-open-threads.ts), for one case. */
|
|
91
|
+
const onTheListOf = (user: any, roles: any[], instance: any) =>
|
|
92
|
+
db
|
|
93
|
+
.getRepository(ActivityThread)
|
|
94
|
+
.createQueryBuilder('at')
|
|
95
|
+
.innerJoin('at.activityInstance', 'ai')
|
|
96
|
+
.where(myOpenThreadsWhere({ userId: user.id, roles: roles.map(role => role.id) }))
|
|
97
|
+
.andWhere('ai.id = :id', { id: instance.id })
|
|
98
|
+
.getCount()
|
|
99
|
+
|
|
100
|
+
/** A transaction whose first read of the case is one taken earlier - someone else wrote in between. */
|
|
101
|
+
const seesTheOldRead = (tx: any, stale: any) => {
|
|
102
|
+
let given = false
|
|
103
|
+
|
|
104
|
+
return new Proxy(tx, {
|
|
105
|
+
get(target, key) {
|
|
106
|
+
if (key !== 'getRepository') return Reflect.get(target, key)
|
|
107
|
+
|
|
108
|
+
return (entity: any) => {
|
|
109
|
+
const repository = target.getRepository(entity)
|
|
110
|
+
if (entity !== ActivityInstance) return repository
|
|
111
|
+
|
|
112
|
+
const once = Object.create(repository)
|
|
113
|
+
once.findOne = async (...args: any[]) => (given ? repository.findOne(...args) : ((given = true), { ...stale }))
|
|
114
|
+
return once
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('assign to a role', () => {
|
|
121
|
+
it('puts a case nobody holds in the pool: the role, one waiting thread, still issued', async () => {
|
|
122
|
+
const instance = await aCase()
|
|
123
|
+
|
|
124
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })
|
|
125
|
+
|
|
126
|
+
const after = await reread(instance)
|
|
127
|
+
expect(after.state).toBe('issued')
|
|
128
|
+
expect(poolOf(after)).toEqual([pool.id])
|
|
129
|
+
expect(after.threadsMin).toBe(1)
|
|
130
|
+
|
|
131
|
+
const threads = await threadsOf(instance)
|
|
132
|
+
expect(threads.map((thread: any) => thread.state)).toEqual(['unassigned'])
|
|
133
|
+
expect(threads[0].assigneeId).toBeFalsy()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('a member of the role finds it on their list; someone of another role does not', async () => {
|
|
137
|
+
const instance = await aCase()
|
|
138
|
+
|
|
139
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })
|
|
140
|
+
|
|
141
|
+
expect(await onTheListOf(member, [pool], instance)).toBe(1)
|
|
142
|
+
expect(await onTheListOf(other, [anotherPool], instance)).toBe(0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('a case already in a pool moves to the other pool and keeps its one waiting thread', async () => {
|
|
146
|
+
const instance = await aCase()
|
|
147
|
+
|
|
148
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })
|
|
149
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [anotherPool.id] })
|
|
150
|
+
|
|
151
|
+
expect(poolOf(await reread(instance))).toEqual([anotherPool.id])
|
|
152
|
+
expect((await threadsOf(instance)).length).toBe(1)
|
|
153
|
+
expect(await onTheListOf(other, [anotherPool], instance)).toBe(1)
|
|
154
|
+
expect(await onTheListOf(member, [pool], instance)).toBe(0)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it.each(['assigned', 'started', 'unassigned', 'ended', 'draft'])('a case %s is refused by name and left as it was', async state => {
|
|
158
|
+
const instance = await aCase({ state })
|
|
159
|
+
|
|
160
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })).rejects.toThrow(
|
|
161
|
+
'error.activity-instance should not be assigned'
|
|
162
|
+
)
|
|
163
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
164
|
+
expect((await threadsOf(instance)).length).toBe(0)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("another tenant's role is refused by name", async () => {
|
|
168
|
+
const instance = await aCase()
|
|
169
|
+
|
|
170
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [theirPool.id] })).rejects.toThrow(
|
|
171
|
+
'error.activity-instance role not in this domain'
|
|
172
|
+
)
|
|
173
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('a role nobody holds is refused by name - nobody would see the case', async () => {
|
|
177
|
+
const instance = await aCase()
|
|
178
|
+
|
|
179
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [emptyPool.id] })).rejects.toThrow(
|
|
180
|
+
'error.activity-instance role has no members'
|
|
181
|
+
)
|
|
182
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
183
|
+
expect((await threadsOf(instance)).length).toBe(0)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('people and a role in one call are refused by name', async () => {
|
|
187
|
+
const instance = await aCase()
|
|
188
|
+
|
|
189
|
+
await expect(assignAs(manager, { id: instance.id, assignees: [member.email], assigneeRoles: [pool.id] })).rejects.toThrow(
|
|
190
|
+
'error.activity-instance assign people or a role, not both'
|
|
191
|
+
)
|
|
192
|
+
expect((await threadsOf(instance)).length).toBe(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it.each(['assigned', 'started', 'submitted', 'ended'])(
|
|
196
|
+
'a case still marked issued whose thread someone holds (%s) is refused by name and left as it was',
|
|
197
|
+
async held => {
|
|
198
|
+
/* The state is worked out again only when a thread changes - a member taking the waiting thread is a step ahead of it. */
|
|
199
|
+
const instance = await aCase({ assigneeRoles: [pool], threadsMin: 1 })
|
|
200
|
+
await db.getRepository(ActivityThread).save({ domain: here, activityInstance: instance, assignee: member, state: held })
|
|
201
|
+
|
|
202
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [anotherPool.id] })).rejects.toThrow(
|
|
203
|
+
'error.activity-approval taken by someone else'
|
|
204
|
+
)
|
|
205
|
+
expect(poolOf(await reread(instance))).toEqual([pool.id])
|
|
206
|
+
expect((await threadsOf(instance)).map((thread: any) => thread.state)).toEqual([held])
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
it('someone picked it after assign read it: refused by name, no pool, no second thread', async () => {
|
|
211
|
+
const instance = await aCase()
|
|
212
|
+
const stale = await db.getRepository(ActivityInstance).findOne({ where: { id: instance.id }, relations: ['activity'] })
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* Picked in between: pick took the case first, and its thread is not yet there for this transaction to
|
|
216
|
+
* read - only the case's UPDATE can tell.
|
|
217
|
+
*/
|
|
218
|
+
await db.getRepository(ActivityInstance).update({ id: instance.id }, { state: 'assigned' })
|
|
219
|
+
|
|
220
|
+
await expect(
|
|
221
|
+
db.transaction((tx: any) => assign({ id: instance.id, assigneeRoles: [pool.id] }, as(manager, seesTheOldRead(tx, stale))))
|
|
222
|
+
).rejects.toThrow('error.activity-approval taken by someone else')
|
|
223
|
+
|
|
224
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
225
|
+
expect((await threadsOf(instance)).length).toBe(0)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('makes the very pool issue() makes for an activity naming an assignee role', async () => {
|
|
229
|
+
const shape = async (instance: any) => {
|
|
230
|
+
const row = await reread(instance)
|
|
231
|
+
return {
|
|
232
|
+
state: row.state,
|
|
233
|
+
assigneeRoles: poolOf(row),
|
|
234
|
+
threadsMin: row.threadsMin,
|
|
235
|
+
threads: (await threadsOf(instance)).map((thread: any) => [thread.state, thread.assigneeId || null]),
|
|
236
|
+
member: await onTheListOf(member, [pool], instance),
|
|
237
|
+
other: await onTheListOf(other, [anotherPool], instance)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const issued = await db.transaction((tx: any) =>
|
|
242
|
+
issue({ activityId: activity.id, assignees: [], assigneeRoles: [pool], threadsMin: 1 }, as(manager, tx))
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const assigned = await aCase()
|
|
246
|
+
await assignAs(manager, { id: assigned.id, assigneeRoles: [pool.id] })
|
|
247
|
+
|
|
248
|
+
expect(await shape(assigned)).toEqual(await shape(issued))
|
|
249
|
+
expect(await shape(assigned)).toMatchObject({ state: 'issued', threads: [['unassigned', null]], member: 1, other: 0 })
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('assigning people still works as before', async () => {
|
|
253
|
+
const instance = await aCase()
|
|
254
|
+
|
|
255
|
+
await assignAs(manager, { id: instance.id, assignees: [member.email] })
|
|
256
|
+
|
|
257
|
+
const threads = await threadsOf(instance)
|
|
258
|
+
expect(threads.map((thread: any) => [thread.state, thread.assigneeId])).toEqual([['assigned', member.id]])
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('the other ways into a waiting case', () => {
|
|
263
|
+
const pooled = async () => {
|
|
264
|
+
const instance = await aCase()
|
|
265
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })
|
|
266
|
+
return instance
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
it('a member claiming the waiting thread holds the case, and the case says so', async () => {
|
|
270
|
+
const instance = await pooled()
|
|
271
|
+
const [waiting] = await threadsOf(instance)
|
|
272
|
+
|
|
273
|
+
await db.transaction((tx: any) => start(waiting.id, {}, '', as(member, tx)))
|
|
274
|
+
|
|
275
|
+
expect((await threadsOf(instance)).map((thread: any) => [thread.state, thread.assigneeId])).toEqual([['started', member.id]])
|
|
276
|
+
expect((await reread(instance)).state).not.toBe('issued')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('a claim on a waiting thread someone claimed meanwhile is refused by name, and the thread stays theirs', async () => {
|
|
280
|
+
const instance = await pooled()
|
|
281
|
+
const [waiting] = await threadsOf(instance)
|
|
282
|
+
const stale = { ...waiting }
|
|
283
|
+
|
|
284
|
+
await db.getRepository(ActivityThread).update({ id: waiting.id }, { state: 'assigned', assignee: { id: other.id } })
|
|
285
|
+
|
|
286
|
+
await expect(db.transaction((tx: any) => checkAuthority(stale, as(member, tx)))).rejects.toThrow(
|
|
287
|
+
'error.activity-approval taken by someone else'
|
|
288
|
+
)
|
|
289
|
+
expect((await threadsOf(instance)).map((thread: any) => thread.assigneeId)).toEqual([other.id])
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('pick that read the case before it went to a pool: someone outside the role is refused by name', async () => {
|
|
293
|
+
const instance = await aCase()
|
|
294
|
+
const stale = await db.getRepository(ActivityInstance).findOne({ where: { id: instance.id }, relations: ['activity'] })
|
|
295
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })
|
|
296
|
+
|
|
297
|
+
await expect(db.transaction((tx: any) => pick(instance.id, as(other, seesTheOldRead(tx, stale))))).rejects.toThrow(
|
|
298
|
+
'error.activity-instance is addressed to another role'
|
|
299
|
+
)
|
|
300
|
+
expect(poolOf(await reread(instance))).toEqual([pool.id])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('pick that read the case before it went to a pool: a member takes it, and the pool is not wiped', async () => {
|
|
304
|
+
const instance = await aCase()
|
|
305
|
+
const stale = await db.getRepository(ActivityInstance).findOne({ where: { id: instance.id }, relations: ['activity'] })
|
|
306
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id] })
|
|
307
|
+
|
|
308
|
+
await db.transaction((tx: any) => pick(instance.id, as(member, seesTheOldRead(tx, stale))))
|
|
309
|
+
|
|
310
|
+
const after = await reread(instance)
|
|
311
|
+
expect(poolOf(after)).toEqual([pool.id])
|
|
312
|
+
expect(after.threadsMin).toBe(1)
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
describe('a pool of several roles - anyone holding any one of them (ADR-0091 decision 9)', () => {
|
|
317
|
+
const twoPools = async () => {
|
|
318
|
+
const instance = await aCase()
|
|
319
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [pool.id, anotherPool.id] })
|
|
320
|
+
return instance
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
it('is one case with one waiting thread, in both pools', async () => {
|
|
324
|
+
const instance = await twoPools()
|
|
325
|
+
|
|
326
|
+
expect(poolOf(await reread(instance))).toEqual([pool.id, anotherPool.id].sort())
|
|
327
|
+
expect((await threadsOf(instance)).map((thread: any) => thread.state)).toEqual(['unassigned'])
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('reaches the to-do list of a holder of either role, once for someone holding both, and nobody else', async () => {
|
|
331
|
+
const instance = await twoPools()
|
|
332
|
+
|
|
333
|
+
expect(await onTheListOf(member, [pool], instance)).toBe(1)
|
|
334
|
+
/* Only the second role. */
|
|
335
|
+
expect(await onTheListOf(other, [anotherPool], instance)).toBe(1)
|
|
336
|
+
expect(await onTheListOf(both, [pool, anotherPool], instance)).toBe(1)
|
|
337
|
+
expect(await onTheListOf(manager, [emptyPool], instance)).toBe(0)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('someone holding only the second role picks it', async () => {
|
|
341
|
+
const instance = await twoPools()
|
|
342
|
+
|
|
343
|
+
await db.transaction((tx: any) => pick(instance.id, as(other, tx)))
|
|
344
|
+
|
|
345
|
+
expect((await threadsOf(instance)).some((thread: any) => thread.assigneeId === other.id)).toBe(true)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('someone holding only the second role claims the waiting thread', async () => {
|
|
349
|
+
const instance = await twoPools()
|
|
350
|
+
const [waiting] = await threadsOf(instance)
|
|
351
|
+
|
|
352
|
+
await db.transaction((tx: any) => start(waiting.id, {}, '', as(other, tx)))
|
|
353
|
+
|
|
354
|
+
expect((await threadsOf(instance)).map((thread: any) => [thread.state, thread.assigneeId])).toEqual([['started', other.id]])
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('someone holding none of them is refused the claim', async () => {
|
|
358
|
+
const instance = await twoPools()
|
|
359
|
+
const [waiting] = await threadsOf(instance)
|
|
360
|
+
|
|
361
|
+
await expect(db.transaction((tx: any) => start(waiting.id, {}, '', as(manager, tx)))).rejects.toThrow(
|
|
362
|
+
'error.not authorized to handle this task'
|
|
363
|
+
)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('roles whose people together are nobody are refused by name; one of them with people is enough', async () => {
|
|
367
|
+
const instance = await aCase()
|
|
368
|
+
|
|
369
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [emptyPool.id, alsoEmpty.id] })).rejects.toThrow(
|
|
370
|
+
'error.activity-instance role has no members'
|
|
371
|
+
)
|
|
372
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
373
|
+
|
|
374
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [emptyPool.id, anotherPool.id] })
|
|
375
|
+
expect(poolOf(await reread(instance))).toEqual([emptyPool.id, anotherPool.id].sort())
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('the same role twice is refused by name, and the case is left as it was', async () => {
|
|
379
|
+
const instance = await aCase()
|
|
380
|
+
|
|
381
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [pool.id, pool.id] })).rejects.toMatchObject({
|
|
382
|
+
code: 'assignee-role-named-twice'
|
|
383
|
+
})
|
|
384
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
385
|
+
expect((await threadsOf(instance)).length).toBe(0)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it("another tenant's role among them is refused by name", async () => {
|
|
389
|
+
const instance = await aCase()
|
|
390
|
+
|
|
391
|
+
await expect(assignAs(manager, { id: instance.id, assigneeRoles: [pool.id, theirPool.id] })).rejects.toMatchObject({
|
|
392
|
+
code: 'assignee-role-not-in-this-domain'
|
|
393
|
+
})
|
|
394
|
+
expect(poolOf(await reread(instance))).toEqual([])
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('moving a pool of two to a pool of one keeps the one waiting thread', async () => {
|
|
398
|
+
const instance = await twoPools()
|
|
399
|
+
|
|
400
|
+
await assignAs(manager, { id: instance.id, assigneeRoles: [anotherPool.id] })
|
|
401
|
+
|
|
402
|
+
expect(poolOf(await reread(instance))).toEqual([anotherPool.id])
|
|
403
|
+
expect((await threadsOf(instance)).length).toBe(1)
|
|
404
|
+
expect(await onTheListOf(member, [pool], instance)).toBe(0)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('issued to everyone of the pool (threadsMin 0), each person gets one thread however many of its roles they hold', async () => {
|
|
408
|
+
const issued = await db.transaction((tx: any) =>
|
|
409
|
+
issue({ activityId: activity.id, assignees: [], assigneeRoles: [pool, anotherPool], threadsMin: 0 }, as(manager, tx))
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
const holders = (await threadsOf(issued)).map((thread: any) => thread.assigneeId).sort()
|
|
413
|
+
expect(holders).toEqual([member.id, other.id, both.id].sort())
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it("an activity's pool of two is the pool of every case issued from it", async () => {
|
|
417
|
+
const saved = await db.transaction((tx: any) =>
|
|
418
|
+
new ActivityMutation().createActivity({ name: 'two-pools', assigneeRoles: [{ id: pool.id }, { id: anotherPool.id }] }, as(manager, tx))
|
|
419
|
+
)
|
|
420
|
+
const issued = await db.transaction((tx: any) => issue({ activityId: saved.id, assignees: [], threadsMin: 1 }, as(manager, tx)))
|
|
421
|
+
|
|
422
|
+
expect(poolOf(await reread(issued))).toEqual([pool.id, anotherPool.id].sort())
|
|
423
|
+
expect(await onTheListOf(other, [anotherPool], issued)).toBe(1)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it.each([
|
|
427
|
+
['the same role twice', () => [{ id: pool.id }, { id: pool.id }], 'assignee-role-named-twice'],
|
|
428
|
+
["another tenant's role", () => [{ id: pool.id }, { id: theirPool.id }], 'assignee-role-not-in-this-domain']
|
|
429
|
+
])('an activity saved with %s is refused by name and nothing is written', async (_, roles, code) => {
|
|
430
|
+
const before = await db.getRepository(Activity).count()
|
|
431
|
+
|
|
432
|
+
await expect(
|
|
433
|
+
db.transaction((tx: any) => new ActivityMutation().createActivity({ name: `bad-${code}`, assigneeRoles: roles() }, as(manager, tx)))
|
|
434
|
+
).rejects.toMatchObject({ code })
|
|
435
|
+
|
|
436
|
+
expect(await db.getRepository(Activity).count()).toBe(before)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('a case issued with the same role twice is refused by name and nothing is written', async () => {
|
|
440
|
+
const before = await db.getRepository(ActivityInstance).count()
|
|
441
|
+
|
|
442
|
+
await expect(
|
|
443
|
+
db.transaction((tx: any) => issue({ activityId: activity.id, assignees: [], assigneeRoles: [pool, pool], threadsMin: 1 }, as(manager, tx)))
|
|
444
|
+
).rejects.toMatchObject({ code: 'assignee-role-named-twice' })
|
|
445
|
+
|
|
446
|
+
expect(await db.getRepository(ActivityInstance).count()).toBe(before)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
describe('a role among assignees', () => {
|
|
451
|
+
const issueAs = (user: any, input: any) => db.transaction((tx: any) => issue(input, as(user, tx)))
|
|
452
|
+
|
|
453
|
+
it('handed in when issuing, is refused by name and nothing is written', async () => {
|
|
454
|
+
const before = await db.getRepository(ActivityInstance).count()
|
|
455
|
+
|
|
456
|
+
await expect(
|
|
457
|
+
issueAs(manager, { activityId: activity.id, assignees: [{ type: 'Role', value: pool.id }] })
|
|
458
|
+
).rejects.toMatchObject({ code: 'role-is-not-an-assignee', message: 'error.activity-instance a role is not an assignee' })
|
|
459
|
+
|
|
460
|
+
expect(await db.getRepository(ActivityInstance).count()).toBe(before)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('saved on the activity before this, is refused when a case of it is issued', async () => {
|
|
464
|
+
const saved = await db
|
|
465
|
+
.getRepository(Activity)
|
|
466
|
+
.save({ domain: here, name: 'payroll', activityType: 'standard', startingType: 'issue', assignees: [{ type: 'Role', value: pool.id }] })
|
|
467
|
+
|
|
468
|
+
await expect(issueAs(manager, { activityId: saved.id })).rejects.toMatchObject({ code: 'role-is-not-an-assignee' })
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const mutation = new ActivityMutation()
|
|
472
|
+
const role = () => [{ type: 'Role', value: pool.id }]
|
|
473
|
+
|
|
474
|
+
it.each([
|
|
475
|
+
['created', (context: any) => mutation.createActivity({ name: 'with-role', assignees: role() }, context)],
|
|
476
|
+
['updated', async (context: any) => mutation.updateActivity(activity.id, { assignees: role() }, context)],
|
|
477
|
+
['saved in a grid', (context: any) => mutation.updateMultipleActivity([{ name: 'grid-role', cuFlag: '+', assignees: role() }], context)],
|
|
478
|
+
['imported', (context: any) => mutation.importActivities([{ name: 'imported-role', assignees: role() }], context)]
|
|
479
|
+
])('on an activity being %s, is refused by name and nothing is written', async (_, save) => {
|
|
480
|
+
const before = await db.getRepository(Activity).count()
|
|
481
|
+
|
|
482
|
+
await expect(db.transaction((tx: any) => save(as(manager, tx)))).rejects.toMatchObject({ code: 'role-is-not-an-assignee' })
|
|
483
|
+
|
|
484
|
+
expect(await db.getRepository(Activity).count()).toBe(before)
|
|
485
|
+
expect((await db.getRepository(Activity).findOneBy({ id: activity.id })).assignees).toBeFalsy()
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it('an activity saved with people among its assignees stands', async () => {
|
|
489
|
+
const saved = await db.transaction((tx: any) =>
|
|
490
|
+
mutation.createActivity({ name: 'with-person', assignees: [{ type: 'Myself' }] }, as(manager, tx))
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
expect(saved.assignees).toEqual([{ type: 'Myself' }])
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('a person among assignees is issued as before', async () => {
|
|
497
|
+
const issued = await issueAs(manager, { activityId: activity.id, assignees: [{ type: 'Myself' }] })
|
|
498
|
+
|
|
499
|
+
expect((await threadsOf(issued)).map((thread: any) => [thread.state, thread.assigneeId])).toEqual([['assigned', manager.id]])
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('the pool named by assigneeRoles is issued as before', async () => {
|
|
503
|
+
const issued = await issueAs(manager, { activityId: activity.id, assignees: [], assigneeRoles: [pool], threadsMin: 1 })
|
|
504
|
+
|
|
505
|
+
expect((await threadsOf(issued)).map((thread: any) => thread.state)).toEqual(['unassigned'])
|
|
506
|
+
expect(await onTheListOf(member, [pool], issued)).toBe(1)
|
|
507
|
+
})
|
|
508
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The history row of an activity or a case keeps the pool **as it was saved** (ADR-0091 decision 9).
|
|
5
|
+
*
|
|
6
|
+
* The pool is a join table, so history keeps its ids, copied by name off the saved object's RelationId.
|
|
7
|
+
* typeorm fills that in with the new pool once the join rows are written (measured 2026-10-20), so the copy
|
|
8
|
+
* is the pool after the save. Real entities and subscribers on sqlite.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { createSqliteTestDataSource } = require(join(__dirname, '..', '..', 'ops-master', 'test', 'support', 'database.cjs'))
|
|
12
|
+
const DIST = join(__dirname, '..', 'dist-server')
|
|
13
|
+
const { Activity } = require(join(DIST, 'service', 'activity', 'activity.js'))
|
|
14
|
+
const { ActivityHistory } = require(join(DIST, 'service', 'activity', 'activity-history.js'))
|
|
15
|
+
const { ActivityHistoryEntitySubscriber } = require(join(DIST, 'service', 'activity', 'event-subscriber.js'))
|
|
16
|
+
const { ActivityInstance } = require(join(DIST, 'service', 'activity-instance', 'activity-instance.js'))
|
|
17
|
+
const { ActivityInstanceHistory } = require(join(DIST, 'service', 'activity-instance', 'activity-instance-history.js'))
|
|
18
|
+
const { ActivityInstanceHistoryEntitySubscriber } = require(join(DIST, 'service', 'activity-instance', 'event-subscriber.js'))
|
|
19
|
+
const { ActivityMutation } = require(join(DIST, 'service', 'activity', 'activity-mutation.js'))
|
|
20
|
+
const { withPoolIds } = require(join(DIST, 'service', 'activity-instance', 'event-subscriber.js'))
|
|
21
|
+
const { Domain } = require('@things-factory/shell')
|
|
22
|
+
const { Role, User } = require('@things-factory/auth-base')
|
|
23
|
+
|
|
24
|
+
let db: any
|
|
25
|
+
let here: any
|
|
26
|
+
let manager: any
|
|
27
|
+
let finance: any
|
|
28
|
+
let support: any
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
db = createSqliteTestDataSource([Activity, ActivityHistory, ActivityInstance, ActivityInstanceHistory], {
|
|
32
|
+
subscribers: [ActivityHistoryEntitySubscriber, ActivityInstanceHistoryEntitySubscriber]
|
|
33
|
+
})
|
|
34
|
+
await db.initialize()
|
|
35
|
+
|
|
36
|
+
here = await db.getRepository(Domain).save({ name: 'here', subdomain: 'here' })
|
|
37
|
+
manager = await db.getRepository(User).save({ name: 'm', email: 'm@example.com', password: 'x', domains: [here] })
|
|
38
|
+
finance = await db.getRepository(Role).save({ domain: here, name: 'finance' })
|
|
39
|
+
support = await db.getRepository(Role).save({ domain: here, name: 'support' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterAll(async () => db?.destroy())
|
|
43
|
+
|
|
44
|
+
const as = (tx: any) => ({ state: { domain: here, user: manager, tx }, t: (key: string) => key }) as any
|
|
45
|
+
const latest = async (entity: any, originalId: string) =>
|
|
46
|
+
(await db.getRepository(entity).find({ where: { originalId }, order: { version: 'DESC' } }))[0]
|
|
47
|
+
|
|
48
|
+
it("an activity's history keeps the pool it was changed to, not the one before", async () => {
|
|
49
|
+
const mutation = new ActivityMutation()
|
|
50
|
+
const created = await db.transaction((tx: any) => mutation.createActivity({ name: 'pay', assigneeRoles: [{ id: finance.id }] }, as(tx)))
|
|
51
|
+
|
|
52
|
+
await db.transaction((tx: any) =>
|
|
53
|
+
mutation.updateActivity(created.id, { assigneeRoles: [{ id: finance.id }, { id: support.id }] }, as(tx))
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect([...(await latest(ActivityHistory, created.id)).assigneeRoleIds].sort()).toEqual([finance.id, support.id].sort())
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("a case's history keeps the pool it was saved with", async () => {
|
|
60
|
+
const saved = await db.getRepository(ActivityInstance).save({ domain: here, name: 'case', state: 'issued', assigneeRoles: [support] })
|
|
61
|
+
|
|
62
|
+
expect((await latest(ActivityInstanceHistory, saved.id)).assigneeRoleIds).toEqual([support.id])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* What is published goes through withPoolIds - the subscription filter decides who hears of a case by the
|
|
67
|
+
* pool's ids on it (subscription-filters.ts). A row saved without its pool in hand still carries them.
|
|
68
|
+
*/
|
|
69
|
+
it('a case published without its pool in hand carries the pool read from the join table', async () => {
|
|
70
|
+
const saved = await db.getRepository(ActivityInstance).save({ domain: here, name: 'published', state: 'issued', assigneeRoles: [finance, support] })
|
|
71
|
+
const read = await db.getRepository(ActivityInstance).findOne({ where: { id: saved.id } })
|
|
72
|
+
delete read.assigneeRoleIds
|
|
73
|
+
|
|
74
|
+
expect([...(await withPoolIds(read, db.manager)).assigneeRoleIds].sort()).toEqual([finance.id, support.id].sort())
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('a case published with its pool in hand carries those ids - the new pool, not a read before', async () => {
|
|
78
|
+
const published = await withPoolIds({ id: 'x', assigneeRoleIds: ['old'], assigneeRoles: [{ id: 'new' }] } as any, db.manager)
|
|
79
|
+
|
|
80
|
+
expect(published.assigneeRoleIds).toEqual(['new'])
|
|
81
|
+
})
|
|
@@ -128,7 +128,8 @@ describe('one instance by id', () => {
|
|
|
128
128
|
id: 'i-1',
|
|
129
129
|
domainId: 'd-here',
|
|
130
130
|
creatorId: 'u-operator',
|
|
131
|
-
|
|
131
|
+
/* The reader holds only the second role of the pool. */
|
|
132
|
+
assigneeRoleIds: ['r-other', 'r-waiting']
|
|
132
133
|
})
|
|
133
134
|
mockRows.set('ActivityThread:*', [{ id: 't-1', assigneeId: 'u-worker' }])
|
|
134
135
|
})
|
|
@@ -151,7 +152,7 @@ describe('one instance by id', () => {
|
|
|
151
152
|
})
|
|
152
153
|
|
|
153
154
|
it('the waiting or supervisory role counts only when held in this domain', async () => {
|
|
154
|
-
mockRows.set('ActivityInstance:i-1', { id: 'i-1', domainId: 'd-here',
|
|
155
|
+
mockRows.set('ActivityInstance:i-1', { id: 'i-1', domainId: 'd-here', assigneeRoleIds: ['r-other', 'r-waiting'], supervisoryRoleId: 'r-super' })
|
|
155
156
|
|
|
156
157
|
await expect(read(contextFor('u-elsewhere', ['r-waiting', 'r-super'], 'd-other'))).rejects.toThrow(
|
|
157
158
|
'error.activity-case not a participant'
|
|
@@ -52,20 +52,26 @@ const resolve = (item: any) =>
|
|
|
52
52
|
|
|
53
53
|
it.each([
|
|
54
54
|
['Employee', 'employee'],
|
|
55
|
-
['Department', 'department']
|
|
56
|
-
['Role', 'role']
|
|
55
|
+
['Department', 'department']
|
|
57
56
|
])('an entry of type %s names the person of this domain', async (type, key) => {
|
|
58
57
|
expect(await resolve({ type, value: mine[key].id })).toMatchObject({ id: mine.user.id })
|
|
59
58
|
})
|
|
60
59
|
|
|
61
60
|
it.each([
|
|
62
61
|
['Employee', 'employee'],
|
|
63
|
-
['Department', 'department']
|
|
64
|
-
['Role', 'role']
|
|
62
|
+
['Department', 'department']
|
|
65
63
|
])("an entry of type %s naming another tenant's is nobody here", async (type, key) => {
|
|
66
64
|
expect(await resolve({ type, value: theirs[key].id })).toBeFalsy()
|
|
67
65
|
})
|
|
68
66
|
|
|
67
|
+
/* A role is not one person - it answered the role's first user (ruling 2026-10-20, assignee-input.ts). */
|
|
68
|
+
it.each([
|
|
69
|
+
['of this domain', () => mine],
|
|
70
|
+
["of another tenant's", () => theirs]
|
|
71
|
+
])('an entry of type Role %s is refused, never one person', async (_, whose) => {
|
|
72
|
+
await expect(resolve({ type: 'Role', value: whose().role.id })).rejects.toMatchObject({ code: 'role-is-not-an-assignee' })
|
|
73
|
+
})
|
|
74
|
+
|
|
69
75
|
describe('assign', () => {
|
|
70
76
|
const assignTo = (instance: any, emails: string[]) =>
|
|
71
77
|
db.transaction((tx: any) =>
|