@stamhoofd/backend 2.123.2 → 2.124.1
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 +232 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +40 -18
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +18 -4
- 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 → 1782421125-group-permissions.ts} +3 -7
- package/src/sql-filters/balance-item-payments.ts +38 -1
- package/src/sql-filters/organizations.ts +46 -0
- package/src/sql-sorters/organizations.ts +22 -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.1",
|
|
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.1",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.124.1",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.124.1",
|
|
63
|
+
"@stamhoofd/crons": "2.124.1",
|
|
64
|
+
"@stamhoofd/email": "2.124.1",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.124.1",
|
|
66
|
+
"@stamhoofd/logging": "2.124.1",
|
|
67
|
+
"@stamhoofd/models": "2.124.1",
|
|
68
|
+
"@stamhoofd/object-differ": "2.124.1",
|
|
69
|
+
"@stamhoofd/queues": "2.124.1",
|
|
70
|
+
"@stamhoofd/sql": "2.124.1",
|
|
71
|
+
"@stamhoofd/structures": "2.124.1",
|
|
72
|
+
"@stamhoofd/types": "2.124.1",
|
|
73
|
+
"@stamhoofd/utility": "2.124.1",
|
|
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.1",
|
|
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": "d520495d0c1a5f376b10a60ebbceec63b2228bd5"
|
|
111
111
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import type { Organization, Token } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationFactory, STPackageFactory, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { LimitedFilteredRequest, OrganizationMetaData, OrganizationType, PermissionLevel, Permissions, STPackageBundle, type StamhoofdFilter, UmbrellaOrganization } from '@stamhoofd/structures';
|
|
5
|
+
import { Country } from '@stamhoofd/types/Country';
|
|
6
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
7
|
+
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
8
|
+
import { initPlatformAdmin } from '../../../../tests/init/index.js';
|
|
9
|
+
import { initMembershipOrganization } from '../../../../tests/init/initMembershipOrganization.js';
|
|
10
|
+
import { GetOrganizationsEndpoint } from './GetOrganizationsEndpoint.js';
|
|
11
|
+
|
|
12
|
+
const baseUrl = `/admin/organizations`;
|
|
13
|
+
const endpoint = new GetOrganizationsEndpoint();
|
|
14
|
+
|
|
15
|
+
describe('Endpoint.GetOrganizationsEndpoint', () => {
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
TestUtils.setEnvironment('userMode', 'organization');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
await initMembershipOrganization();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const search = async (searchTerm: string, token: Token): Promise<string[]> => {
|
|
25
|
+
const request = Request.get({
|
|
26
|
+
path: baseUrl,
|
|
27
|
+
// Platform admins are not scoped to an organization, so no host is required
|
|
28
|
+
host: '',
|
|
29
|
+
query: new LimitedFilteredRequest({
|
|
30
|
+
search: searchTerm,
|
|
31
|
+
limit: 100,
|
|
32
|
+
}),
|
|
33
|
+
headers: {
|
|
34
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const response = await testServer.test(endpoint, request);
|
|
38
|
+
return response.body.results.map(o => o.id);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const filter = async (filter: StamhoofdFilter, token: Token): Promise<string[]> => {
|
|
42
|
+
const request = Request.get({
|
|
43
|
+
path: baseUrl,
|
|
44
|
+
host: '',
|
|
45
|
+
query: new LimitedFilteredRequest({
|
|
46
|
+
filter,
|
|
47
|
+
limit: 100,
|
|
48
|
+
}),
|
|
49
|
+
headers: {
|
|
50
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const response = await testServer.test(endpoint, request);
|
|
54
|
+
return response.body.results.map(o => o.id);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Creates an admin (user with permissions) for the given organization
|
|
58
|
+
const createAdmin = async (organization: Organization, data: { firstName?: string; lastName?: string; email?: string }) => {
|
|
59
|
+
return await new UserFactory({
|
|
60
|
+
organization,
|
|
61
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
62
|
+
...data,
|
|
63
|
+
}).create();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
test('Searches organizations by name', async () => {
|
|
67
|
+
const { adminToken } = await initPlatformAdmin();
|
|
68
|
+
const organization = await new OrganizationFactory({ name: 'Bombilcious Tennis Club' }).create();
|
|
69
|
+
|
|
70
|
+
expect(await search('Bombilcious Tennis Club', adminToken)).toContain(organization.id);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('Searches organizations by uri', async () => {
|
|
74
|
+
const { adminToken } = await initPlatformAdmin();
|
|
75
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
76
|
+
|
|
77
|
+
expect(await search(organization.uri, adminToken)).toContain(organization.id);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('Does not search organizations by admin name', async () => {
|
|
81
|
+
const { adminToken } = await initPlatformAdmin();
|
|
82
|
+
const organization = await new OrganizationFactory({ name: 'Halfnamed Club' }).create();
|
|
83
|
+
await createAdmin(organization, { firstName: 'Zwervinkel' });
|
|
84
|
+
|
|
85
|
+
expect(await search('Zwervinkel', adminToken)).not.toContain(organization.id);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('Does not match users without permissions (non-admins)', async () => {
|
|
89
|
+
const { adminToken } = await initPlatformAdmin();
|
|
90
|
+
const organization = await new OrganizationFactory({ name: 'Memberonly Club' }).create();
|
|
91
|
+
|
|
92
|
+
// A regular user (no permissions) linked to the organization
|
|
93
|
+
await new UserFactory({ organization, firstName: 'Solitary', lastName: 'Pangolin' }).create();
|
|
94
|
+
|
|
95
|
+
expect(await search('Solitary Pangolin', adminToken)).not.toContain(organization.id);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('Searches admins by email', async () => {
|
|
99
|
+
const { adminToken } = await initPlatformAdmin();
|
|
100
|
+
const organization = await new OrganizationFactory({ name: 'Mailable Club' }).create();
|
|
101
|
+
await createAdmin(organization, { email: 'findme@admin-search-test.com' });
|
|
102
|
+
|
|
103
|
+
expect(await search('findme@admin-search-test.com', adminToken)).toContain(organization.id);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('Does search the organization name when the search contains an @', async () => {
|
|
107
|
+
const { adminToken } = await initPlatformAdmin();
|
|
108
|
+
// The organization name itself contains an @, but there is no admin with a matching email
|
|
109
|
+
const organization = await new OrganizationFactory({ name: 'weird@orgname-test.com' }).create();
|
|
110
|
+
|
|
111
|
+
expect(await search('weird@orgname-test.com', adminToken)).toContain(organization.id);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('Does not match admins of other organizations by email', async () => {
|
|
115
|
+
const { adminToken } = await initPlatformAdmin();
|
|
116
|
+
const organization = await new OrganizationFactory({ name: 'Owner Club' }).create();
|
|
117
|
+
const otherOrganization = await new OrganizationFactory({ name: 'Bystander Club' }).create();
|
|
118
|
+
await createAdmin(organization, { email: 'unique-owner@admin-search-test.com' });
|
|
119
|
+
|
|
120
|
+
const results = await search('unique-owner@admin-search-test.com', adminToken);
|
|
121
|
+
expect(results).toContain(organization.id);
|
|
122
|
+
expect(results).not.toContain(otherOrganization.id);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('Filters organizations by active package type', async () => {
|
|
126
|
+
const { adminToken } = await initPlatformAdmin();
|
|
127
|
+
|
|
128
|
+
const membersOrg = await new OrganizationFactory({ name: 'Packaged Members Club' }).create();
|
|
129
|
+
await new STPackageFactory({ organization: membersOrg, bundle: STPackageBundle.Members }).create();
|
|
130
|
+
|
|
131
|
+
const webshopsOrg = await new OrganizationFactory({ name: 'Packaged Webshops Club' }).create();
|
|
132
|
+
await new STPackageFactory({ organization: webshopsOrg, bundle: STPackageBundle.Webshops }).create();
|
|
133
|
+
|
|
134
|
+
const results = await filter({
|
|
135
|
+
packages: {
|
|
136
|
+
$elemMatch: {
|
|
137
|
+
type: {
|
|
138
|
+
$in: ['Members'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}, adminToken);
|
|
143
|
+
|
|
144
|
+
expect(results).toContain(membersOrg.id);
|
|
145
|
+
expect(results).not.toContain(webshopsOrg.id);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('Does not match organizations whose package is no longer valid', async () => {
|
|
149
|
+
const { adminToken } = await initPlatformAdmin();
|
|
150
|
+
|
|
151
|
+
const expiredOrg = await new OrganizationFactory({ name: 'Expired Members Club' }).create();
|
|
152
|
+
// removeAt in the past => package is no longer active
|
|
153
|
+
await new STPackageFactory({
|
|
154
|
+
organization: expiredOrg,
|
|
155
|
+
bundle: STPackageBundle.Members,
|
|
156
|
+
removeAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
|
157
|
+
}).create();
|
|
158
|
+
|
|
159
|
+
const results = await filter({
|
|
160
|
+
packages: {
|
|
161
|
+
$elemMatch: {
|
|
162
|
+
type: {
|
|
163
|
+
$in: ['Members'],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
}, adminToken);
|
|
168
|
+
|
|
169
|
+
expect(results).not.toContain(expiredOrg.id);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Creates an organization with a specific type and/or umbrella organization
|
|
173
|
+
const createOrganizationWithMeta = async (name: string, metaPatch: { type?: OrganizationType; umbrellaOrganization?: UmbrellaOrganization | null }) => {
|
|
174
|
+
const meta = OrganizationMetaData.create({
|
|
175
|
+
type: metaPatch.type ?? OrganizationType.Other,
|
|
176
|
+
umbrellaOrganization: metaPatch.umbrellaOrganization ?? null,
|
|
177
|
+
defaultEndDate: new Date(),
|
|
178
|
+
defaultStartDate: new Date(),
|
|
179
|
+
defaultPrices: [],
|
|
180
|
+
});
|
|
181
|
+
return await new OrganizationFactory({ name, meta }).create();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
test('Filters organizations by type', async () => {
|
|
185
|
+
const { adminToken } = await initPlatformAdmin();
|
|
186
|
+
const youthOrg = await createOrganizationWithMeta('Youthful Club', { type: OrganizationType.Youth });
|
|
187
|
+
const sportOrg = await createOrganizationWithMeta('Sporty Club', { type: OrganizationType.Sport });
|
|
188
|
+
|
|
189
|
+
const results = await filter({ type: { $in: [OrganizationType.Youth] } }, adminToken);
|
|
190
|
+
expect(results).toContain(youthOrg.id);
|
|
191
|
+
expect(results).not.toContain(sportOrg.id);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('Filters organizations by umbrella organization', async () => {
|
|
195
|
+
const { adminToken } = await initPlatformAdmin();
|
|
196
|
+
const scoutsOrg = await createOrganizationWithMeta('Scouts Club', { umbrellaOrganization: UmbrellaOrganization.ScoutsEnGidsenVlaanderen });
|
|
197
|
+
const otherOrg = await createOrganizationWithMeta('No Umbrella Club', { umbrellaOrganization: null });
|
|
198
|
+
|
|
199
|
+
const results = await filter({ umbrellaOrganization: { $in: [UmbrellaOrganization.ScoutsEnGidsenVlaanderen] } }, adminToken);
|
|
200
|
+
expect(results).toContain(scoutsOrg.id);
|
|
201
|
+
expect(results).not.toContain(otherOrg.id);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('Filters organizations by country', async () => {
|
|
205
|
+
const { adminToken } = await initPlatformAdmin();
|
|
206
|
+
// OrganizationFactory creates organizations in Belgium by default
|
|
207
|
+
const org = await new OrganizationFactory({ name: 'Belgian Club' }).create();
|
|
208
|
+
|
|
209
|
+
expect(await filter({ country: { $in: [Country.Belgium] } }, adminToken)).toContain(org.id);
|
|
210
|
+
expect(await filter({ country: { $in: [Country.Netherlands] } }, adminToken)).not.toContain(org.id);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('Filters organizations by street', async () => {
|
|
214
|
+
const { adminToken } = await initPlatformAdmin();
|
|
215
|
+
// OrganizationFactory uses 'Demostraat' as the street
|
|
216
|
+
const org = await new OrganizationFactory({ name: 'Streeted Club' }).create();
|
|
217
|
+
|
|
218
|
+
expect(await filter({ street: { $contains: 'Demostraat' } }, adminToken)).toContain(org.id);
|
|
219
|
+
expect(await filter({ street: { $contains: 'Nonexistentstreet' } }, adminToken)).not.toContain(org.id);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('Filters organizations by creation date', async () => {
|
|
223
|
+
const { adminToken } = await initPlatformAdmin();
|
|
224
|
+
const org = await new OrganizationFactory({ name: 'Freshly Created Club' }).create();
|
|
225
|
+
|
|
226
|
+
const anHourAgo = new Date(Date.now() - 1000 * 60 * 60);
|
|
227
|
+
const inAnHour = new Date(Date.now() + 1000 * 60 * 60);
|
|
228
|
+
|
|
229
|
+
expect(await filter({ createdAt: { $gt: anHourAgo } }, adminToken)).toContain(org.id);
|
|
230
|
+
expect(await filter({ createdAt: { $gt: inAnHour } }, adminToken)).not.toContain(org.id);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -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) {
|
|
@@ -103,7 +125,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
103
125
|
query.limit(q.limit);
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
console.log('GetOrganizationsEndpoint query', query.getSQL());
|
|
128
|
+
// console.log('GetOrganizationsEndpoint query', query.getSQL());
|
|
107
129
|
|
|
108
130
|
return query;
|
|
109
131
|
}
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import { DetailedReceivableBalance, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
|
+
import { DetailedReceivableBalance, PaymentStatus, PermissionLevel, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
|
-
import { BalanceItem, BalanceItemPayment, CachedBalance, MemberUser, Payment } from '@stamhoofd/models';
|
|
5
|
+
import { BalanceItem, BalanceItemPayment, CachedBalance, Member, MemberUser, Payment, Registration } from '@stamhoofd/models';
|
|
6
6
|
import { Context } from '../../../../helpers/Context.js';
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
|
|
8
8
|
import { SQL } from '@stamhoofd/sql';
|
|
@@ -35,9 +35,23 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
35
35
|
const organization = await Context.setOrganizationScope();
|
|
36
36
|
await Context.authenticate();
|
|
37
37
|
|
|
38
|
-
// If the user has permission, we'll also search if he has access to the organization's key
|
|
39
38
|
if (!await Context.auth.canManageFinances(organization.id)) {
|
|
40
|
-
|
|
39
|
+
// Group-level financial access is sufficient for member and registration balance views
|
|
40
|
+
if (request.params.type === ReceivableBalanceType.member) {
|
|
41
|
+
const member = await Member.getByIdWithUsersAndRegistrations(request.params.id);
|
|
42
|
+
if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
|
|
43
|
+
throw Context.auth.error();
|
|
44
|
+
}
|
|
45
|
+
} else if (request.params.type === ReceivableBalanceType.registration) {
|
|
46
|
+
const registration = await Registration.select().where('id', request.params.id).first(false);
|
|
47
|
+
if (!registration) throw Context.auth.error();
|
|
48
|
+
const member = await Member.getByIdWithUsersAndRegistrations(registration.memberId);
|
|
49
|
+
if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
|
|
50
|
+
throw Context.auth.error();
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
throw Context.auth.error();
|
|
54
|
+
}
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
let paymentModels: Payment[] = [];
|
|
@@ -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.
|
|
@@ -35,10 +35,6 @@ async function start(dryRun: boolean) {
|
|
|
35
35
|
.where('periodId', organization.periodId)
|
|
36
36
|
.fetch();
|
|
37
37
|
|
|
38
|
-
if (groups.length === 0) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
38
|
// handle group permissions
|
|
43
39
|
for (const group of groups) {
|
|
44
40
|
const groupPermissions = group.privateSettings.permissions;
|
|
@@ -157,7 +153,7 @@ function handlePermissionRoleForGroup(organization: Organization, group: Group,
|
|
|
157
153
|
return;
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
specificGroupPermissions.
|
|
156
|
+
specificGroupPermissions.add(convertedRessourcePermissions);
|
|
161
157
|
return;
|
|
162
158
|
}
|
|
163
159
|
|
|
@@ -209,7 +205,7 @@ function handlePermissionRoleForGroupCategory(organization: Organization, oldCat
|
|
|
209
205
|
return;
|
|
210
206
|
}
|
|
211
207
|
|
|
212
|
-
specificGroupCategoryPermissions.
|
|
208
|
+
specificGroupCategoryPermissions.add(convertedRessourcePermissions);
|
|
213
209
|
return;
|
|
214
210
|
}
|
|
215
211
|
|
|
@@ -261,7 +257,7 @@ function handlePermissionRoleForWebshop(organization: Organization, webshop: Web
|
|
|
261
257
|
return;
|
|
262
258
|
}
|
|
263
259
|
|
|
264
|
-
specificWebshopPermissions.
|
|
260
|
+
specificWebshopPermissions.add(convertedRessourcePermissions);
|
|
265
261
|
return;
|
|
266
262
|
}
|
|
267
263
|
|
|
@@ -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
|
};
|
|
@@ -29,6 +29,16 @@ export const organizationFilterCompilers: SQLFilterDefinitions = {
|
|
|
29
29
|
type: SQLValueType.Boolean,
|
|
30
30
|
nullable: false,
|
|
31
31
|
}),
|
|
32
|
+
createdAt: createColumnFilter({
|
|
33
|
+
expression: SQL.column('organizations', 'createdAt'),
|
|
34
|
+
type: SQLValueType.Datetime,
|
|
35
|
+
nullable: false,
|
|
36
|
+
}),
|
|
37
|
+
street: createColumnFilter({
|
|
38
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'address'), '$.value.street'),
|
|
39
|
+
type: SQLValueType.JSONString,
|
|
40
|
+
nullable: false,
|
|
41
|
+
}),
|
|
32
42
|
city: createColumnFilter({
|
|
33
43
|
expression: SQL.jsonExtract(SQL.column('organizations', 'address'), '$.value.city'),
|
|
34
44
|
type: SQLValueType.JSONString,
|
|
@@ -188,6 +198,42 @@ export const organizationFilterCompilers: SQLFilterDefinitions = {
|
|
|
188
198
|
}),
|
|
189
199
|
},
|
|
190
200
|
),
|
|
201
|
+
admins: createExistsFilter(
|
|
202
|
+
SQL.select()
|
|
203
|
+
.from(SQL.table('users'))
|
|
204
|
+
.where(
|
|
205
|
+
SQL.column('users', 'organizationId'),
|
|
206
|
+
SQL.column('organizations', 'id'),
|
|
207
|
+
)
|
|
208
|
+
.where(SQL.column('users', 'permissions'), SQLWhereSign.NotEqual, new SQLNull()),
|
|
209
|
+
{
|
|
210
|
+
...baseSQLFilterCompilers,
|
|
211
|
+
name: createColumnFilter({
|
|
212
|
+
expression: new SQLConcat(
|
|
213
|
+
SQL.coalesce(SQL.column('users', 'firstName'), new SQLScalar('')),
|
|
214
|
+
new SQLScalar(' '),
|
|
215
|
+
SQL.coalesce(SQL.column('users', 'lastName'), new SQLScalar('')),
|
|
216
|
+
),
|
|
217
|
+
type: SQLValueType.String,
|
|
218
|
+
nullable: false,
|
|
219
|
+
}),
|
|
220
|
+
firstName: createColumnFilter({
|
|
221
|
+
expression: SQL.column('users', 'firstName'),
|
|
222
|
+
type: SQLValueType.String,
|
|
223
|
+
nullable: true,
|
|
224
|
+
}),
|
|
225
|
+
lastName: createColumnFilter({
|
|
226
|
+
expression: SQL.column('users', 'lastName'),
|
|
227
|
+
type: SQLValueType.String,
|
|
228
|
+
nullable: true,
|
|
229
|
+
}),
|
|
230
|
+
email: createColumnFilter({
|
|
231
|
+
expression: SQL.column('users', 'email'),
|
|
232
|
+
type: SQLValueType.String,
|
|
233
|
+
nullable: false,
|
|
234
|
+
}),
|
|
235
|
+
},
|
|
236
|
+
),
|
|
191
237
|
companies: createExistsFilter(
|
|
192
238
|
/**
|
|
193
239
|
* There is a bug in MySQL 8 that is fixed in 9.3
|
|
@@ -88,4 +88,26 @@ export const organizationSorters: SQLSortDefinitions<Organization> = {
|
|
|
88
88
|
});
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
|
+
umbrellaOrganization: {
|
|
92
|
+
getValue(a) {
|
|
93
|
+
return a.meta.umbrellaOrganization;
|
|
94
|
+
},
|
|
95
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
96
|
+
return new SQLOrderBy({
|
|
97
|
+
column: SQL.jsonValue(SQL.column('meta'), '$.value.umbrellaOrganization', 'CHAR'),
|
|
98
|
+
direction,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
createdAt: {
|
|
103
|
+
getValue(a) {
|
|
104
|
+
return a.createdAt;
|
|
105
|
+
},
|
|
106
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
107
|
+
return new SQLOrderBy({
|
|
108
|
+
column: SQL.column('createdAt'),
|
|
109
|
+
direction,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
},
|
|
91
113
|
};
|
|
@@ -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
|
-
}
|