@stamhoofd/backend 2.123.1 → 2.124.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.
- package/package.json +17 -17
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.test.ts +107 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +39 -17
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +2 -2
- package/src/excel-loaders/members.ts +6 -0
- package/src/excel-loaders/payments.ts +13 -13
- package/src/excel-loaders/registrations.ts +5 -0
- package/src/helpers/XlsxTransformerColumnHelper.test.ts +169 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +73 -1
- package/src/seeds/{1782208150-group-permissions.ts → 1782421124-group-permissions.ts} +3 -3
- package/src/sql-filters/balance-item-payments.ts +38 -1
- package/src/sql-filters/organizations.ts +36 -0
- package/src/seeds/1782296682-create-missing-group-categories.ts +0 -237
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.124.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"@simonbackx/simple-endpoints": "1.21.1",
|
|
58
58
|
"@simonbackx/simple-errors": "1.5.0",
|
|
59
59
|
"@simonbackx/simple-logging": "1.0.1",
|
|
60
|
-
"@stamhoofd/backend-env": "2.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.
|
|
63
|
-
"@stamhoofd/crons": "2.
|
|
64
|
-
"@stamhoofd/email": "2.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.
|
|
66
|
-
"@stamhoofd/logging": "2.
|
|
67
|
-
"@stamhoofd/models": "2.
|
|
68
|
-
"@stamhoofd/object-differ": "2.
|
|
69
|
-
"@stamhoofd/queues": "2.
|
|
70
|
-
"@stamhoofd/sql": "2.
|
|
71
|
-
"@stamhoofd/structures": "2.
|
|
72
|
-
"@stamhoofd/types": "2.
|
|
73
|
-
"@stamhoofd/utility": "2.
|
|
60
|
+
"@stamhoofd/backend-env": "2.124.0",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.124.0",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.124.0",
|
|
63
|
+
"@stamhoofd/crons": "2.124.0",
|
|
64
|
+
"@stamhoofd/email": "2.124.0",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.124.0",
|
|
66
|
+
"@stamhoofd/logging": "2.124.0",
|
|
67
|
+
"@stamhoofd/models": "2.124.0",
|
|
68
|
+
"@stamhoofd/object-differ": "2.124.0",
|
|
69
|
+
"@stamhoofd/queues": "2.124.0",
|
|
70
|
+
"@stamhoofd/sql": "2.124.0",
|
|
71
|
+
"@stamhoofd/structures": "2.124.0",
|
|
72
|
+
"@stamhoofd/types": "2.124.0",
|
|
73
|
+
"@stamhoofd/utility": "2.124.0",
|
|
74
74
|
"archiver": "7.0.1",
|
|
75
75
|
"axios": "1.16.0",
|
|
76
76
|
"base-x": "3.0.11",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"stripe": "16.12.0"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stamhoofd/test-utils": "2.
|
|
94
|
+
"@stamhoofd/test-utils": "2.124.0",
|
|
95
95
|
"@types/cookie": "0.6.0",
|
|
96
96
|
"@types/luxon": "3.7.1",
|
|
97
97
|
"@types/mailparser": "3.4.6",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"publishConfig": {
|
|
108
108
|
"access": "public"
|
|
109
109
|
},
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "7783583e6555a28cce7c7ad26cb223a4623ca005"
|
|
111
111
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import type { Organization, Token } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationFactory, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { LimitedFilteredRequest, PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
5
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
6
|
+
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
7
|
+
import { initPlatformAdmin } from '../../../../tests/init/index.js';
|
|
8
|
+
import { initMembershipOrganization } from '../../../../tests/init/initMembershipOrganization.js';
|
|
9
|
+
import { GetOrganizationsEndpoint } from './GetOrganizationsEndpoint.js';
|
|
10
|
+
|
|
11
|
+
const baseUrl = `/admin/organizations`;
|
|
12
|
+
const endpoint = new GetOrganizationsEndpoint();
|
|
13
|
+
|
|
14
|
+
describe('Endpoint.GetOrganizationsEndpoint', () => {
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
TestUtils.setEnvironment('userMode', 'organization');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
await initMembershipOrganization();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const search = async (searchTerm: string, token: Token): Promise<string[]> => {
|
|
24
|
+
const request = Request.get({
|
|
25
|
+
path: baseUrl,
|
|
26
|
+
// Platform admins are not scoped to an organization, so no host is required
|
|
27
|
+
host: '',
|
|
28
|
+
query: new LimitedFilteredRequest({
|
|
29
|
+
search: searchTerm,
|
|
30
|
+
limit: 100,
|
|
31
|
+
}),
|
|
32
|
+
headers: {
|
|
33
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const response = await testServer.test(endpoint, request);
|
|
37
|
+
return response.body.results.map(o => o.id);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Creates an admin (user with permissions) for the given organization
|
|
41
|
+
const createAdmin = async (organization: Organization, data: { firstName?: string; lastName?: string; email?: string }) => {
|
|
42
|
+
return await new UserFactory({
|
|
43
|
+
organization,
|
|
44
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
45
|
+
...data,
|
|
46
|
+
}).create();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
test('Searches organizations by name', async () => {
|
|
50
|
+
const { adminToken } = await initPlatformAdmin();
|
|
51
|
+
const organization = await new OrganizationFactory({ name: 'Bombilcious Tennis Club' }).create();
|
|
52
|
+
|
|
53
|
+
expect(await search('Bombilcious Tennis Club', adminToken)).toContain(organization.id);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Searches organizations by uri', async () => {
|
|
57
|
+
const { adminToken } = await initPlatformAdmin();
|
|
58
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
59
|
+
|
|
60
|
+
expect(await search(organization.uri, adminToken)).toContain(organization.id);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('Does not search organizations by admin name', async () => {
|
|
64
|
+
const { adminToken } = await initPlatformAdmin();
|
|
65
|
+
const organization = await new OrganizationFactory({ name: 'Halfnamed Club' }).create();
|
|
66
|
+
await createAdmin(organization, { firstName: 'Zwervinkel' });
|
|
67
|
+
|
|
68
|
+
expect(await search('Zwervinkel', adminToken)).not.toContain(organization.id);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Does not match users without permissions (non-admins)', async () => {
|
|
72
|
+
const { adminToken } = await initPlatformAdmin();
|
|
73
|
+
const organization = await new OrganizationFactory({ name: 'Memberonly Club' }).create();
|
|
74
|
+
|
|
75
|
+
// A regular user (no permissions) linked to the organization
|
|
76
|
+
await new UserFactory({ organization, firstName: 'Solitary', lastName: 'Pangolin' }).create();
|
|
77
|
+
|
|
78
|
+
expect(await search('Solitary Pangolin', adminToken)).not.toContain(organization.id);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('Searches admins by email', async () => {
|
|
82
|
+
const { adminToken } = await initPlatformAdmin();
|
|
83
|
+
const organization = await new OrganizationFactory({ name: 'Mailable Club' }).create();
|
|
84
|
+
await createAdmin(organization, { email: 'findme@admin-search-test.com' });
|
|
85
|
+
|
|
86
|
+
expect(await search('findme@admin-search-test.com', adminToken)).toContain(organization.id);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Does search the organization name when the search contains an @', async () => {
|
|
90
|
+
const { adminToken } = await initPlatformAdmin();
|
|
91
|
+
// The organization name itself contains an @, but there is no admin with a matching email
|
|
92
|
+
const organization = await new OrganizationFactory({ name: 'weird@orgname-test.com' }).create();
|
|
93
|
+
|
|
94
|
+
expect(await search('weird@orgname-test.com', adminToken)).toContain(organization.id);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('Does not match admins of other organizations by email', async () => {
|
|
98
|
+
const { adminToken } = await initPlatformAdmin();
|
|
99
|
+
const organization = await new OrganizationFactory({ name: 'Owner Club' }).create();
|
|
100
|
+
const otherOrganization = await new OrganizationFactory({ name: 'Bystander Club' }).create();
|
|
101
|
+
await createAdmin(organization, { email: 'unique-owner@admin-search-test.com' });
|
|
102
|
+
|
|
103
|
+
const results = await search('unique-owner@admin-search-test.com', adminToken);
|
|
104
|
+
expect(results).toContain(organization.id);
|
|
105
|
+
expect(results).not.toContain(otherOrganization.id);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -72,25 +72,47 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
if (q.search) {
|
|
75
|
-
let searchFilter: StamhoofdFilter
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
let searchFilter: StamhoofdFilter;
|
|
76
|
+
|
|
77
|
+
if (q.search.includes('@')) {
|
|
78
|
+
// Automatically search in the email addresses of admins instead of the organization name
|
|
79
|
+
searchFilter = {
|
|
80
|
+
$or: [
|
|
81
|
+
{
|
|
82
|
+
name: {
|
|
83
|
+
$contains: q.search,
|
|
84
|
+
},
|
|
83
85
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
{
|
|
87
|
+
uri: q.search,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
admins: {
|
|
91
|
+
$elemMatch: {
|
|
92
|
+
email: {
|
|
93
|
+
$contains: q.search,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
} else {
|
|
101
|
+
searchFilter = {
|
|
102
|
+
$or: [
|
|
103
|
+
{
|
|
104
|
+
name: {
|
|
105
|
+
$contains: q.search,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
uri: q.search,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
93
113
|
}
|
|
114
|
+
|
|
115
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
if (q instanceof LimitedFilteredRequest) {
|
|
@@ -146,7 +146,7 @@ export class PatchWebshopEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
146
146
|
throw new SimpleError({
|
|
147
147
|
code: 'invalid_field',
|
|
148
148
|
message: 'domainUri is reserved for internal webshop routes',
|
|
149
|
-
human: $t(
|
|
149
|
+
human: $t('%1a4'),
|
|
150
150
|
field: 'customUrl',
|
|
151
151
|
});
|
|
152
152
|
}
|
|
@@ -194,7 +194,7 @@ export class PatchWebshopEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
194
194
|
throw new SimpleError({
|
|
195
195
|
code: 'invalid_field',
|
|
196
196
|
message: 'Uri is reserved for internal webshop routes',
|
|
197
|
-
human: $t(
|
|
197
|
+
human: $t('%1a4'),
|
|
198
198
|
field: 'uri',
|
|
199
199
|
});
|
|
200
200
|
}
|
|
@@ -302,6 +302,12 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
302
302
|
}),
|
|
303
303
|
},
|
|
304
304
|
|
|
305
|
+
// Group categories
|
|
306
|
+
XlsxTransformerColumnHelper.createGroupCategoryColumns<PlatformMember>({
|
|
307
|
+
matchId: 'groupCategory',
|
|
308
|
+
getMember: member => member,
|
|
309
|
+
}),
|
|
310
|
+
|
|
305
311
|
// Registration records
|
|
306
312
|
{
|
|
307
313
|
match(id) {
|
|
@@ -178,8 +178,8 @@ function createOrderItemPaymentRows(
|
|
|
178
178
|
rows.push(
|
|
179
179
|
createSyntheticBalanceItemPayment({
|
|
180
180
|
source: item,
|
|
181
|
-
customTitle: $t('
|
|
182
|
-
description: $t('
|
|
181
|
+
customTitle: $t('%1eU'),
|
|
182
|
+
description: $t('%1eU'),
|
|
183
183
|
amount: 1,
|
|
184
184
|
price: order.data.deliveryPrice,
|
|
185
185
|
}),
|
|
@@ -190,8 +190,8 @@ function createOrderItemPaymentRows(
|
|
|
190
190
|
rows.push(
|
|
191
191
|
createSyntheticBalanceItemPayment({
|
|
192
192
|
source: item,
|
|
193
|
-
customTitle: $t('
|
|
194
|
-
description: $t('
|
|
193
|
+
customTitle: $t('%xK'),
|
|
194
|
+
description: $t('%xK'),
|
|
195
195
|
amount: 1,
|
|
196
196
|
price: order.data.administrationFee,
|
|
197
197
|
}),
|
|
@@ -203,8 +203,8 @@ function createOrderItemPaymentRows(
|
|
|
203
203
|
rows.push(
|
|
204
204
|
createSyntheticBalanceItemPayment({
|
|
205
205
|
source: item,
|
|
206
|
-
customTitle: $t('
|
|
207
|
-
description: $t('
|
|
206
|
+
customTitle: $t('%1eE'),
|
|
207
|
+
description: $t('%1eE'),
|
|
208
208
|
amount: 1,
|
|
209
209
|
price: difference,
|
|
210
210
|
}),
|
|
@@ -226,8 +226,8 @@ function addOrderDiscountRows(
|
|
|
226
226
|
rows.push(
|
|
227
227
|
createSyntheticBalanceItemPayment({
|
|
228
228
|
source: item,
|
|
229
|
-
customTitle: $t('
|
|
230
|
-
description: $t('
|
|
229
|
+
customTitle: $t('%1ei', { percentage: Formatter.percentage(order.data.percentageDiscount) }),
|
|
230
|
+
description: $t('%1ei', { percentage: Formatter.percentage(order.data.percentageDiscount) }),
|
|
231
231
|
amount: 1,
|
|
232
232
|
price: -appliedPercentageDiscount,
|
|
233
233
|
}),
|
|
@@ -240,8 +240,8 @@ function addOrderDiscountRows(
|
|
|
240
240
|
rows.push(
|
|
241
241
|
createSyntheticBalanceItemPayment({
|
|
242
242
|
source: item,
|
|
243
|
-
customTitle: $t('
|
|
244
|
-
description: $t('
|
|
243
|
+
customTitle: $t('%176'),
|
|
244
|
+
description: $t('%1eM'),
|
|
245
245
|
amount: 1,
|
|
246
246
|
price: -fixedDiscount,
|
|
247
247
|
}),
|
|
@@ -251,10 +251,10 @@ function addOrderDiscountRows(
|
|
|
251
251
|
|
|
252
252
|
function getPartialOrderPaymentDescription(order: PaymentExportOrder): string {
|
|
253
253
|
if (order.number !== null) {
|
|
254
|
-
return $t('
|
|
254
|
+
return $t('%1eO', { number: order.number.toString() });
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
return $t('
|
|
257
|
+
return $t('%1eH');
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
function createSyntheticBalanceItemPayment({
|
|
@@ -344,7 +344,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<PaymentWithItem>[] {
|
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
346
|
id: 'balanceItem.title',
|
|
347
|
-
name: $t('
|
|
347
|
+
name: $t('%vC'),
|
|
348
348
|
width: 40,
|
|
349
349
|
getValue: (object: PaymentWithItem) => ({
|
|
350
350
|
value: object.balanceItemPayment.customTitle || object.balanceItemPayment.balanceItem.itemTitle,
|
|
@@ -261,6 +261,11 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
|
|
|
261
261
|
];
|
|
262
262
|
},
|
|
263
263
|
},
|
|
264
|
+
// Group categories
|
|
265
|
+
XlsxTransformerColumnHelper.createGroupCategoryColumns<PlatformRegistration>({
|
|
266
|
+
matchId: 'groupCategory',
|
|
267
|
+
getMember: registration => registration.member,
|
|
268
|
+
}),
|
|
264
269
|
// recordAnswers
|
|
265
270
|
{
|
|
266
271
|
match(id) {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
|
|
2
|
+
import type { PlatformMember } from '@stamhoofd/structures';
|
|
3
|
+
import {
|
|
4
|
+
Group,
|
|
5
|
+
GroupCategory,
|
|
6
|
+
GroupCategorySettings,
|
|
7
|
+
GroupSettings,
|
|
8
|
+
MemberDetails,
|
|
9
|
+
MembersBlob,
|
|
10
|
+
MemberWithRegistrationsBlob,
|
|
11
|
+
Organization,
|
|
12
|
+
OrganizationRegistrationPeriod,
|
|
13
|
+
OrganizationRegistrationPeriodSettings,
|
|
14
|
+
Platform,
|
|
15
|
+
PlatformFamily,
|
|
16
|
+
Registration,
|
|
17
|
+
RegistrationPeriod,
|
|
18
|
+
TranslatedString,
|
|
19
|
+
} from '@stamhoofd/structures';
|
|
20
|
+
import { XlsxTransformerColumnHelper } from './XlsxTransformerColumnHelper.js';
|
|
21
|
+
|
|
22
|
+
describe('XlsxTransformerColumnHelper', () => {
|
|
23
|
+
describe('createGroupCategoryColumns', () => {
|
|
24
|
+
function createGroup(organization: Organization, periodId: string, name: string) {
|
|
25
|
+
return Group.create({
|
|
26
|
+
organizationId: organization.id,
|
|
27
|
+
periodId,
|
|
28
|
+
settings: GroupSettings.create({
|
|
29
|
+
name: new TranslatedString(name),
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setup() {
|
|
35
|
+
const period = RegistrationPeriod.create({});
|
|
36
|
+
|
|
37
|
+
// Build the organization with two categories, each holding a few groups
|
|
38
|
+
const organization = Organization.create({});
|
|
39
|
+
|
|
40
|
+
const kapoenen = createGroup(organization, period.id, 'Kapoenen');
|
|
41
|
+
const wouters = createGroup(organization, period.id, 'Wouters');
|
|
42
|
+
const jonggivers = createGroup(organization, period.id, 'Jonggivers');
|
|
43
|
+
const groepsleiding = createGroup(organization, period.id, 'Groepsleiding');
|
|
44
|
+
const takleiding = createGroup(organization, period.id, 'Takleiding');
|
|
45
|
+
|
|
46
|
+
const ageGroupsCategory = GroupCategory.create({
|
|
47
|
+
settings: GroupCategorySettings.create({ name: 'Leeftijdsgroepen' }),
|
|
48
|
+
groupIds: [kapoenen.id, wouters.id, jonggivers.id],
|
|
49
|
+
});
|
|
50
|
+
const leadershipCategory = GroupCategory.create({
|
|
51
|
+
settings: GroupCategorySettings.create({ name: 'Leiding' }),
|
|
52
|
+
groupIds: [groepsleiding.id, takleiding.id],
|
|
53
|
+
});
|
|
54
|
+
const root = GroupCategory.create({
|
|
55
|
+
id: 'root',
|
|
56
|
+
categoryIds: [ageGroupsCategory.id, leadershipCategory.id],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
organization.period = OrganizationRegistrationPeriod.create({
|
|
60
|
+
period,
|
|
61
|
+
groups: [kapoenen, wouters, jonggivers, groepsleiding, takleiding],
|
|
62
|
+
settings: OrganizationRegistrationPeriodSettings.create({
|
|
63
|
+
categories: [root, ageGroupsCategory, leadershipCategory],
|
|
64
|
+
rootCategoryId: root.id,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return { organization, period, ageGroupsCategory, leadershipCategory, groups: { kapoenen, wouters, jonggivers, groepsleiding, takleiding } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createMember(organization: Organization, registeredGroups: Group[]) {
|
|
72
|
+
const blob = MembersBlob.create({
|
|
73
|
+
organizations: [organization],
|
|
74
|
+
members: [
|
|
75
|
+
MemberWithRegistrationsBlob.create({
|
|
76
|
+
details: MemberDetails.create({ firstName: 'John', lastName: 'Doe' }),
|
|
77
|
+
registrations: registeredGroups.map(group => Registration.create({
|
|
78
|
+
organizationId: organization.id,
|
|
79
|
+
group,
|
|
80
|
+
groupPrice: group.settings.prices[0],
|
|
81
|
+
registeredAt: new Date(),
|
|
82
|
+
})),
|
|
83
|
+
}),
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const family = PlatformFamily.create(blob, { platform: Platform.create({}), contextOrganization: organization });
|
|
88
|
+
return family.members[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getColumn(categoryId: string): XlsxTransformerConcreteColumn<PlatformMember> {
|
|
92
|
+
const column = XlsxTransformerColumnHelper.createGroupCategoryColumns<PlatformMember>({
|
|
93
|
+
matchId: 'groupCategory',
|
|
94
|
+
getMember: member => member,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!('match' in column)) {
|
|
98
|
+
throw new Error('Expected a match column');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const matched = column.match(`groupCategory.${categoryId}`);
|
|
102
|
+
expect(matched).toBeDefined();
|
|
103
|
+
expect(matched).toHaveLength(1);
|
|
104
|
+
return matched![0];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it('lists the groups a member is registered for within a category', () => {
|
|
108
|
+
const { organization, ageGroupsCategory, leadershipCategory, groups } = setup();
|
|
109
|
+
const member = createMember(organization, [groups.kapoenen, groups.jonggivers, groups.groepsleiding]);
|
|
110
|
+
|
|
111
|
+
expect(getColumn(ageGroupsCategory.id).getValue(member).value).toBe('Jonggivers, Kapoenen');
|
|
112
|
+
expect(getColumn(leadershipCategory.id).getValue(member).value).toBe('Groepsleiding');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns an empty value when the member has no registrations in the category', () => {
|
|
116
|
+
const { organization, leadershipCategory, groups } = setup();
|
|
117
|
+
const member = createMember(organization, [groups.kapoenen]);
|
|
118
|
+
|
|
119
|
+
expect(getColumn(leadershipCategory.id).getValue(member).value).toBe('');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not match ids without a category id', () => {
|
|
123
|
+
const column = XlsxTransformerColumnHelper.createGroupCategoryColumns<PlatformMember>({
|
|
124
|
+
matchId: 'groupCategory',
|
|
125
|
+
getMember: member => member,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!('match' in column)) {
|
|
129
|
+
throw new Error('Expected a match column');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(column.match('groupCategory')).toBeUndefined();
|
|
133
|
+
expect(column.match('groupCategory.')).toBeUndefined();
|
|
134
|
+
expect(column.match('somethingElse.123')).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('resolves groups in nested subcategories', () => {
|
|
138
|
+
const period = RegistrationPeriod.create({});
|
|
139
|
+
const organization = Organization.create({});
|
|
140
|
+
|
|
141
|
+
const kapoenen = createGroup(organization, period.id, 'Kapoenen');
|
|
142
|
+
const welpen = createGroup(organization, period.id, 'Welpen');
|
|
143
|
+
|
|
144
|
+
const subCategory = GroupCategory.create({
|
|
145
|
+
settings: GroupCategorySettings.create({ name: 'Jongste tak' }),
|
|
146
|
+
groupIds: [welpen.id],
|
|
147
|
+
});
|
|
148
|
+
const ageGroupsCategory = GroupCategory.create({
|
|
149
|
+
settings: GroupCategorySettings.create({ name: 'Leeftijdsgroepen' }),
|
|
150
|
+
groupIds: [kapoenen.id],
|
|
151
|
+
categoryIds: [subCategory.id],
|
|
152
|
+
});
|
|
153
|
+
const root = GroupCategory.create({ id: 'root', categoryIds: [ageGroupsCategory.id] });
|
|
154
|
+
|
|
155
|
+
organization.period = OrganizationRegistrationPeriod.create({
|
|
156
|
+
period,
|
|
157
|
+
groups: [kapoenen, welpen],
|
|
158
|
+
settings: OrganizationRegistrationPeriodSettings.create({
|
|
159
|
+
categories: [root, ageGroupsCategory, subCategory],
|
|
160
|
+
rootCategoryId: root.id,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const member = createMember(organization, [kapoenen, welpen]);
|
|
165
|
+
|
|
166
|
+
expect(getColumn(ageGroupsCategory.id).getValue(member).value).toBe('Kapoenen, Welpen');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { XlsxTransformerColumn, XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
|
|
2
2
|
import { isXlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
|
|
3
|
-
import type { Address, Parent, PlatformMember, RecordAnswer, RecordSettings } from '@stamhoofd/structures';
|
|
3
|
+
import type { Address, GroupCategory, Parent, PlatformMember, RecordAnswer, RecordSettings } from '@stamhoofd/structures';
|
|
4
4
|
import { CountryHelper, ParentTypeHelper, RecordCategory, RecordType } from '@stamhoofd/structures';
|
|
5
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
6
|
|
|
6
7
|
export class XlsxTransformerColumnHelper {
|
|
7
8
|
static formatBoolean(value: boolean | undefined | null): string {
|
|
@@ -242,6 +243,77 @@ export class XlsxTransformerColumnHelper {
|
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Creates a dynamic column for a group category. The matched id is `${matchId}.${categoryId}`, where categoryId
|
|
248
|
+
* refers to a category in the current period of the organization the member is registered at. The value is a
|
|
249
|
+
* comma separated list of all the groups the member is registered for in the current period that belong to that
|
|
250
|
+
* category (including its subcategories).
|
|
251
|
+
*/
|
|
252
|
+
static createGroupCategoryColumns<T>({ matchId, getMember }: { matchId: string; getMember: (object: T) => PlatformMember }): XlsxTransformerColumn<T> {
|
|
253
|
+
return {
|
|
254
|
+
match(id) {
|
|
255
|
+
if (!id.startsWith(matchId + '.')) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const categoryId = id.substring(matchId.length + 1);
|
|
260
|
+
if (!categoryId) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
id: `${matchId}.${categoryId}`,
|
|
267
|
+
name: $t(`%M2`),
|
|
268
|
+
width: 40,
|
|
269
|
+
getValue: (object: T) => {
|
|
270
|
+
const member = getMember(object);
|
|
271
|
+
|
|
272
|
+
for (const organization of member.organizations) {
|
|
273
|
+
const categories = organization.period.settings.categories;
|
|
274
|
+
if (!categories.find(c => c.id === categoryId)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const groupIds = XlsxTransformerColumnHelper.collectGroupIdsForCategory(categoryId, categories);
|
|
279
|
+
const registrations = member.filterRegistrations({ groupIds, organizationId: organization.id });
|
|
280
|
+
const names = Formatter.uniqueArray(registrations.map(r => r.group.settings.name.toString())).sort();
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
value: names.join(', '),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
value: '',
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Recursively collects all group ids that belong to a category and its subcategories.
|
|
299
|
+
*/
|
|
300
|
+
private static collectGroupIdsForCategory(categoryId: string, categories: GroupCategory[], seen = new Set<string>()): string[] {
|
|
301
|
+
if (seen.has(categoryId)) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
seen.add(categoryId);
|
|
305
|
+
|
|
306
|
+
const category = categories.find(c => c.id === categoryId);
|
|
307
|
+
if (!category) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return [
|
|
312
|
+
...category.groupIds,
|
|
313
|
+
...category.categoryIds.flatMap(id => XlsxTransformerColumnHelper.collectGroupIdsForCategory(id, categories, seen)),
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
245
317
|
/**
|
|
246
318
|
* Makes it possible to reuse an XlsxTransformerColumn, for example member columns exist, PlatformRegistration has
|
|
247
319
|
* a property member, so we can reuse the member columns for PlatformRegistration.
|
|
@@ -157,7 +157,7 @@ function handlePermissionRoleForGroup(organization: Organization, group: Group,
|
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
specificGroupPermissions.
|
|
160
|
+
specificGroupPermissions.add(convertedRessourcePermissions);
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -209,7 +209,7 @@ function handlePermissionRoleForGroupCategory(organization: Organization, oldCat
|
|
|
209
209
|
return;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
specificGroupCategoryPermissions.
|
|
212
|
+
specificGroupCategoryPermissions.add(convertedRessourcePermissions);
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -261,7 +261,7 @@ function handlePermissionRoleForWebshop(organization: Organization, webshop: Web
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
specificWebshopPermissions.
|
|
264
|
+
specificWebshopPermissions.add(convertedRessourcePermissions);
|
|
265
265
|
return;
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SQLFilterDefinitions } from '@stamhoofd/sql';
|
|
2
|
-
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLValueType } from '@stamhoofd/sql';
|
|
3
3
|
|
|
4
4
|
export const balanceItemPaymentsCompilers: SQLFilterDefinitions = {
|
|
5
5
|
...baseSQLFilterCompilers,
|
|
@@ -30,5 +30,42 @@ export const balanceItemPaymentsCompilers: SQLFilterDefinitions = {
|
|
|
30
30
|
type: SQLValueType.String,
|
|
31
31
|
nullable: true,
|
|
32
32
|
}),
|
|
33
|
+
type: createColumnFilter({
|
|
34
|
+
expression: SQL.column('balance_items', 'type'),
|
|
35
|
+
type: SQLValueType.String,
|
|
36
|
+
nullable: false,
|
|
37
|
+
}),
|
|
38
|
+
registration: createExistsFilter(
|
|
39
|
+
SQL.select()
|
|
40
|
+
.from(SQL.table('registrations'))
|
|
41
|
+
.where(
|
|
42
|
+
SQL.column('registrations', 'id'),
|
|
43
|
+
SQL.column('balance_items', 'registrationId'),
|
|
44
|
+
),
|
|
45
|
+
{
|
|
46
|
+
...baseSQLFilterCompilers,
|
|
47
|
+
groupId: createColumnFilter({
|
|
48
|
+
expression: SQL.column('registrations', 'groupId'),
|
|
49
|
+
type: SQLValueType.String,
|
|
50
|
+
nullable: false,
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
order: createExistsFilter(
|
|
55
|
+
SQL.select()
|
|
56
|
+
.from(SQL.table('webshop_orders'))
|
|
57
|
+
.where(
|
|
58
|
+
SQL.column('webshop_orders', 'id'),
|
|
59
|
+
SQL.column('balance_items', 'orderId'),
|
|
60
|
+
),
|
|
61
|
+
{
|
|
62
|
+
...baseSQLFilterCompilers,
|
|
63
|
+
webshopId: createColumnFilter({
|
|
64
|
+
expression: SQL.column('webshop_orders', 'webshopId'),
|
|
65
|
+
type: SQLValueType.String,
|
|
66
|
+
nullable: false,
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
),
|
|
33
70
|
},
|
|
34
71
|
};
|
|
@@ -188,6 +188,42 @@ export const organizationFilterCompilers: SQLFilterDefinitions = {
|
|
|
188
188
|
}),
|
|
189
189
|
},
|
|
190
190
|
),
|
|
191
|
+
admins: createExistsFilter(
|
|
192
|
+
SQL.select()
|
|
193
|
+
.from(SQL.table('users'))
|
|
194
|
+
.where(
|
|
195
|
+
SQL.column('users', 'organizationId'),
|
|
196
|
+
SQL.column('organizations', 'id'),
|
|
197
|
+
)
|
|
198
|
+
.where(SQL.column('users', 'permissions'), SQLWhereSign.NotEqual, new SQLNull()),
|
|
199
|
+
{
|
|
200
|
+
...baseSQLFilterCompilers,
|
|
201
|
+
name: createColumnFilter({
|
|
202
|
+
expression: new SQLConcat(
|
|
203
|
+
SQL.coalesce(SQL.column('users', 'firstName'), new SQLScalar('')),
|
|
204
|
+
new SQLScalar(' '),
|
|
205
|
+
SQL.coalesce(SQL.column('users', 'lastName'), new SQLScalar('')),
|
|
206
|
+
),
|
|
207
|
+
type: SQLValueType.String,
|
|
208
|
+
nullable: false,
|
|
209
|
+
}),
|
|
210
|
+
firstName: createColumnFilter({
|
|
211
|
+
expression: SQL.column('users', 'firstName'),
|
|
212
|
+
type: SQLValueType.String,
|
|
213
|
+
nullable: true,
|
|
214
|
+
}),
|
|
215
|
+
lastName: createColumnFilter({
|
|
216
|
+
expression: SQL.column('users', 'lastName'),
|
|
217
|
+
type: SQLValueType.String,
|
|
218
|
+
nullable: true,
|
|
219
|
+
}),
|
|
220
|
+
email: createColumnFilter({
|
|
221
|
+
expression: SQL.column('users', 'email'),
|
|
222
|
+
type: SQLValueType.String,
|
|
223
|
+
nullable: false,
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
),
|
|
191
227
|
companies: createExistsFilter(
|
|
192
228
|
/**
|
|
193
229
|
* There is a bug in MySQL 8 that is fixed in 9.3
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
-
import { Group, Organization, OrganizationRegistrationPeriod, V1GroupMigrationData } from '@stamhoofd/models';
|
|
3
|
-
import { GroupCategory, GroupCategorySettings, GroupType } from '@stamhoofd/structures';
|
|
4
|
-
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
-
|
|
6
|
-
export default new Migration(async () => {
|
|
7
|
-
if (STAMHOOFD.environment === 'test') {
|
|
8
|
-
console.log('skipped in tests');
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
13
|
-
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const dryRun = false;
|
|
18
|
-
await start(dryRun);
|
|
19
|
-
|
|
20
|
-
if (dryRun) {
|
|
21
|
-
throw new Error('Migration did not finish because of dryRun');
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// only check organizations that were checked in the 'fix' migration
|
|
26
|
-
const relevantOrganizations = new Set(['f2543d6b-523e-4b03-848b-838200b1ff41', '11a478dd-e8cf-4160-80dd-8b48d20a6d39', '6d1c32cf-2ef0-4738-bacb-d553488f97b9', '3e6f5cb9-e130-4dda-92c9-26682fa09509', '57815733-5750-492f-beb1-1efd6229803a', '7677e32d-763a-4033-bc74-30d13f27556d', '7ac75a0f-f365-4ddc-836c-66a3267f24f0', '9dced404-a92d-44c4-b301-ecd29b6ac4e0', '97928f8a-b8d1-44cc-918f-09d0ae896c3b', 'f0edb665-1ce0-442a-88e7-5a62ea78c5fc', '842ada22-82f3-4cef-a1da-ae5da20b208d', '33744473-9ad9-4d3e-a9fc-31cd1b6a2753', 'ed6e74f1-51a0-4e04-8195-e722b015eeb5', '10827cc5-5f29-46cf-b26e-5fd11a9342cc', '5f4d4a26-43c3-4f80-86ad-64d07d5a46a7', '74e2bcbf-0ff9-484b-988a-e11d147122c0', 'c69512bc-ea0c-427a-ab90-08c3dcf1c856', '16742d64-0b31-4ce7-9c1e-6194882045b9', '95d59803-c50e-4bc1-adc9-90ade32b7579', 'eb6ccffa-41c5-45df-b4e4-1eb5749bc0fe', '1a534005-2784-400c-b8fe-bde09c901259', '27e06924-49a6-468c-a16e-b5735ace1894', '7a86f3db-08d8-4752-a77d-eb85f2167942', 'f562c735-7bf4-4e2e-8c0f-6584e8e96a1d', 'e2afe517-cd35-4d9e-a561-125ad184a2ff', 'fd934e40-734c-442d-b338-c3ae288dd55d']);
|
|
27
|
-
|
|
28
|
-
// only check groups that were checked in the 'fix' migration
|
|
29
|
-
const relevantGroups = new Set([
|
|
30
|
-
'020a313c-0e97-4068-84ba-783d91e768a4', '02420f62-1a07-4e2a-ae92-13c35320fb27', '0b6d8422-857f-46e4-a632-a4af322c9193', '12ba896a-e064-4697-a5df-69664c9bd8cb', '134e33f9-3560-4eca-977b-aaef01af5ed4', '1a9bb59a-c85a-4338-94c6-f82061f1ee0e', '1bf5519d-4645-4c51-8ae5-7a202bb74e9e', '1c86c401-c32b-4c6f-a2b5-fb15649efa74', '1ddc6fde-f512-43ac-8e15-2496601e900e', '20cab6ae-fb30-4de8-948e-60caf0979ff7', '22194f9d-fba2-4497-b7b0-de8ba6e884ff', '2384f112-d50d-4fcf-b6f2-45db73a8944c', '26693e8a-b73d-4f28-a41c-d281ba91fb3e', '287ff7b8-3319-40fb-ad25-26b139db3272', '2e473bf7-36f9-4276-8770-032db776d154', '3282e30c-a3db-4f00-b827-646fcc532c43', '3490cfd8-4c0b-487f-8f0d-c96c62950160', '3d68762b-8133-42e0-a642-7649864b09b3', '4a7d205b-e996-4841-b21c-1336642e670d', '52b0285c-4935-4577-bc7b-30d0b8804fc9', '579263b6-ceb1-4be0-a31e-9a4a904f0cae', '5870423f-6faf-492e-accb-4bd110365328', '60e24adb-5bb3-48de-9008-5b80c905a01a', '6535d9b4-f2ad-4f7e-b7e2-9e50ddf5eff6', '6a508234-08aa-4b0b-9647-7aeb9d80fa5a', '722f3bdf-d9df-4a78-a4c1-783c9af3d21b', '754bb111-caad-49b3-8dde-5bd0f3ad9104', '76aef892-19b5-4deb-9276-0c6b19b652fd', '78fa525b-e346-4b78-9e25-f9b1b5134b77', '7989ea9d-354d-4eff-9350-8eb8a6c4732b', '7aba3792-0138-4302-8550-2ccb456a37ad', '7c480cd5-8db3-4c03-8f0b-a88556df0234', '8113c2a2-e788-4f11-815f-4b019d7ca966', '82958c24-bc12-45f2-9d0d-3c8b902df052', '88c28670-cf97-42e2-ae9e-d836d349c2e6', '88d87c1a-749f-453c-a60b-e54a7001a9f0', '8983c1d5-e8aa-4628-a251-17220336fad5', '89ced76d-8c98-4f83-bffa-8b467289972d', '8abed111-cda2-4bb8-8d98-21291a85fb4f', '8bb9b1c6-2d40-4a69-bbd5-63ce152b1a91', '8f98e452-e174-4946-96ec-888cc3498f56', '8fdffd17-84fa-41f7-8b74-744578d4b9d9', '92ae627b-8aa4-4197-885e-603d4a1be8aa', '9852662c-7e9f-4c26-a6d3-6335b49b9b3b', 'a06ddcb5-dc9c-4bec-a0f8-40fd4ab20016', 'a25bbeae-86b9-4162-8570-d121845f26e9', 'a690c59b-72eb-40bb-b181-d9dc904f9ec1', 'a9b0a795-3b7e-40bb-b8ec-65280d149dba', 'ab9ddbb9-21bc-4a8e-be7f-036554b70db4', 'bea51786-4d47-4bcb-bf79-fc636f047cbb', 'c806a74d-f0ae-4473-86bd-facb92085eb1', 'c9064135-322e-4848-9de0-61011d067697', 'cb7e738f-bcb5-4bc5-9962-2608ea9c4094', 'cbc21e01-bb7e-4f1d-94c8-d4aac3b7247d', 'cf766627-3e32-45a1-8d62-865b6cc1d4ed', 'd0cf99ce-24e9-4912-9dc8-660b4ff57dcc', 'd2a536af-f23d-4c3e-8275-dc2e5116ab60', 'd715451c-6292-4d58-83f8-ffce115fca21', 'd76b57b8-10c4-43e4-94d1-bb66d1053bb4', 'dcffb511-131b-401c-a4b5-13810596bc42', 'df010c46-fc79-401c-a4a4-1fddb478acff', 'dfaf11b8-f835-4952-99e1-2b0279931b05', 'e0b8e823-4213-4919-b1ed-70395ad15df2', 'e9470495-097c-47b5-8e63-cd4f7370fa2d', 'e9fe89f8-780b-417f-a4ee-b48e0b0bb8ad', 'eda36826-ee68-414d-a773-cb41e1a1e2d6', 'edaa90fa-869b-4f3c-8b90-4ea41fe9a364', 'f163d78a-9539-43a3-aeb7-443559599342', 'f24e2ca9-3036-4839-9ff4-91bd4ecb5c91', 'f665642c-93e7-4886-bd6e-f94252d54e77', 'f7e5436c-855d-4bf9-b2de-94e9d0fd3052', 'f822b26a-715a-4154-b2a2-b11e38373fad', 'fe2e4ec5-cee4-4648-ae87-2f8dee7f42c8', 'ff3a7e68-feb7-4aca-983d-a8f8315851e7',
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
async function start(dryRun: boolean) {
|
|
34
|
-
await SeedTools.loop({
|
|
35
|
-
query: Organization.select(),
|
|
36
|
-
batchSize: 50,
|
|
37
|
-
useTransactionPerBatch: true,
|
|
38
|
-
action: async (organization: Organization) => {
|
|
39
|
-
if (!relevantOrganizations.has(organization.id)) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await createMissingGroupCategories(organization, dryRun);
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function createMissingGroupCategories(organization: Organization, dryRun: boolean) {
|
|
49
|
-
const organizationPeriods = await OrganizationRegistrationPeriod.select()
|
|
50
|
-
.where('organizationId', organization.id)
|
|
51
|
-
// exclude current period
|
|
52
|
-
.andWhereNot('periodId', organization.periodId)
|
|
53
|
-
.fetch();
|
|
54
|
-
|
|
55
|
-
if (organizationPeriods.length > 1) {
|
|
56
|
-
console.error('found multiple periods for organization: ' + organization.id);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const organizationPeriod of organizationPeriods) {
|
|
60
|
-
const groups: Group[] = await Group.select()
|
|
61
|
-
.where('organizationId', organization.id)
|
|
62
|
-
// only for period being looped
|
|
63
|
-
.andWhere('periodId', organizationPeriod.periodId)
|
|
64
|
-
.andWhere('type', GroupType.Membership)
|
|
65
|
-
.andWhere('deletedAt', null)
|
|
66
|
-
.fetch();
|
|
67
|
-
|
|
68
|
-
if (groups.length === 0) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const allCategories = organizationPeriod.settings.categories;
|
|
73
|
-
|
|
74
|
-
for (const group of groups) {
|
|
75
|
-
const category = allCategories.find(c => c.groupIds.includes(group.id));
|
|
76
|
-
|
|
77
|
-
// only create missing category if group is not in any category yet
|
|
78
|
-
if (!category) {
|
|
79
|
-
const oldGroupId = await getOldGroupId(group);
|
|
80
|
-
if (!relevantGroups.has(oldGroupId)) {
|
|
81
|
-
throw new Error('not expected');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const oldGroup = await Group.getByID(oldGroupId);
|
|
85
|
-
if (!oldGroup) {
|
|
86
|
-
throw new Error('Old group not found: ' + oldGroupId);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const { path, organizationPeriod: currentOrganizationPeriod } = await findPath(oldGroup);
|
|
90
|
-
|
|
91
|
-
const allArchivedCategories = organizationPeriod.settings.categories;
|
|
92
|
-
|
|
93
|
-
const archivedRoot = organizationPeriod.settings.rootCategory;
|
|
94
|
-
if (!archivedRoot) {
|
|
95
|
-
throw new Error('archived root category not found');
|
|
96
|
-
}
|
|
97
|
-
let thisLayer = getChildCategories(archivedRoot, allArchivedCategories);
|
|
98
|
-
const equalArchivedPath: GroupCategory[] = [organizationPeriod.settings.rootCategory!];
|
|
99
|
-
|
|
100
|
-
for (const currentPeriodCategory of path) {
|
|
101
|
-
if (currentPeriodCategory === currentOrganizationPeriod.settings.rootCategory) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// only exact match for last category is needed
|
|
106
|
-
const exactMatch = currentPeriodCategory !== path[path.length - 1];
|
|
107
|
-
const equalArchivedCategories = thisLayer.filter(archivedCategory => isEqualCategory(currentPeriodCategory, archivedCategory, exactMatch));
|
|
108
|
-
if (equalArchivedCategories.length > 1) {
|
|
109
|
-
throw new Error(`Found multiple equal categories (${currentPeriodCategory.settings.name}): ` + equalArchivedCategories.map(c => c.settings.name).join(', '));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (equalArchivedCategories.length === 0) {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const equalArchivedCategory = equalArchivedCategories[0];
|
|
117
|
-
thisLayer = getChildCategories(equalArchivedCategory, allArchivedCategories);
|
|
118
|
-
equalArchivedPath.push(equalArchivedCategory);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// logging
|
|
122
|
-
console.log('organization: ', organization.name);
|
|
123
|
-
console.log('group: ', group.settings.name.toString());
|
|
124
|
-
logPath(path, 'current');
|
|
125
|
-
logPath(equalArchivedPath, 'archived');
|
|
126
|
-
|
|
127
|
-
if (path.length === equalArchivedPath.length) {
|
|
128
|
-
// complete match -> add group to last category
|
|
129
|
-
const lastCategory = equalArchivedPath[equalArchivedPath.length - 1];
|
|
130
|
-
|
|
131
|
-
if (lastCategory.groupIds.length) {
|
|
132
|
-
throw new Error('not expected last category has group ids');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// create new category
|
|
136
|
-
const newCategory = createCategoryForOriginalGroup(oldGroup, [group.id]);
|
|
137
|
-
lastCategory.categoryIds.push(newCategory.id);
|
|
138
|
-
organizationPeriod.settings.categories.push(newCategory);
|
|
139
|
-
|
|
140
|
-
if (!dryRun) {
|
|
141
|
-
await organizationPeriod.save();
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
// should not happen
|
|
145
|
-
console.error('no complete match');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function getChildCategories(category: GroupCategory, allCategories: GroupCategory[]) {
|
|
153
|
-
const results: GroupCategory[] = [];
|
|
154
|
-
|
|
155
|
-
for (const childId of category.categoryIds) {
|
|
156
|
-
const child = allCategories.find(c => c.id === childId);
|
|
157
|
-
if (!child) {
|
|
158
|
-
throw new Error('child category not found');
|
|
159
|
-
}
|
|
160
|
-
results.push(child);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return results;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function getOldGroupId(group: Group) {
|
|
167
|
-
const data = await V1GroupMigrationData.select().where('newGroupId', group.id).first(true);
|
|
168
|
-
return data.oldGroupId;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Find the path from the old group to the root category.
|
|
173
|
-
*/
|
|
174
|
-
async function findPath(oldGroup: Group): Promise<{ path: GroupCategory[]; organizationPeriod: OrganizationRegistrationPeriod }> {
|
|
175
|
-
/**
|
|
176
|
-
* Returns all the parent categories of the group starting with the root category (= start) or null if not found.
|
|
177
|
-
*/
|
|
178
|
-
function getPathToGroup(groupId: string, start: GroupCategory, allCategories: GroupCategory[]): GroupCategory[] | null {
|
|
179
|
-
if (start.groupIds.includes(groupId)) {
|
|
180
|
-
return [start];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const childCategoryId of start.categoryIds) {
|
|
184
|
-
const childCategory = allCategories.find(c => c.id === childCategoryId);
|
|
185
|
-
if (childCategory) {
|
|
186
|
-
const childPath = getPathToGroup(groupId, childCategory, allCategories);
|
|
187
|
-
if (childPath !== null) {
|
|
188
|
-
return [start, ...childPath];
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const organizationPeriod = await OrganizationRegistrationPeriod.select()
|
|
197
|
-
.where('organizationId', oldGroup.organizationId)
|
|
198
|
-
.where('periodId', oldGroup.periodId)
|
|
199
|
-
.first(true);
|
|
200
|
-
|
|
201
|
-
const root = organizationPeriod.settings.rootCategory;
|
|
202
|
-
if (!root) {
|
|
203
|
-
throw new Error('root category not found');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const path = getPathToGroup(oldGroup.id, root, organizationPeriod.settings.categories);
|
|
207
|
-
if (path === null) {
|
|
208
|
-
throw new Error('path not found');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
organizationPeriod,
|
|
213
|
-
path,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function logPath(path: GroupCategory[], prefix: string): void {
|
|
218
|
-
console.log(`${prefix} path: ${path.map(c => c.settings.name).join(' > ')}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function isEqualCategory(currentPeriodCategory: GroupCategory, archivedPeriodCategory: GroupCategory, exactMatch: boolean) {
|
|
222
|
-
if (exactMatch) {
|
|
223
|
-
return archivedPeriodCategory.settings.name === currentPeriodCategory.settings.name;
|
|
224
|
-
}
|
|
225
|
-
return archivedPeriodCategory.settings.name.startsWith(currentPeriodCategory.settings.name);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function createCategoryForOriginalGroup(originalGroup: Group, groupIds: string[]) {
|
|
229
|
-
return GroupCategory.create({
|
|
230
|
-
settings: GroupCategorySettings.create({
|
|
231
|
-
name: originalGroup.settings.name.toString(),
|
|
232
|
-
public: false,
|
|
233
|
-
maximumRegistrations: null,
|
|
234
|
-
}),
|
|
235
|
-
groupIds,
|
|
236
|
-
});
|
|
237
|
-
}
|