@stamhoofd/backend 2.124.0 → 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 +127 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +18 -4
- package/src/seeds/{1782421124-group-permissions.ts → 1782421125-group-permissions.ts} +0 -4
- package/src/sql-filters/organizations.ts +10 -0
- package/src/sql-sorters/organizations.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.124.
|
|
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.124.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.124.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.124.
|
|
63
|
-
"@stamhoofd/crons": "2.124.
|
|
64
|
-
"@stamhoofd/email": "2.124.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.124.
|
|
66
|
-
"@stamhoofd/logging": "2.124.
|
|
67
|
-
"@stamhoofd/models": "2.124.
|
|
68
|
-
"@stamhoofd/object-differ": "2.124.
|
|
69
|
-
"@stamhoofd/queues": "2.124.
|
|
70
|
-
"@stamhoofd/sql": "2.124.
|
|
71
|
-
"@stamhoofd/structures": "2.124.
|
|
72
|
-
"@stamhoofd/types": "2.124.
|
|
73
|
-
"@stamhoofd/utility": "2.124.
|
|
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.124.
|
|
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
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import type { Organization, Token } from '@stamhoofd/models';
|
|
3
|
-
import { OrganizationFactory, UserFactory } from '@stamhoofd/models';
|
|
4
|
-
import { LimitedFilteredRequest, PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
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';
|
|
5
6
|
import { TestUtils } from '@stamhoofd/test-utils';
|
|
6
7
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
7
8
|
import { initPlatformAdmin } from '../../../../tests/init/index.js';
|
|
@@ -37,6 +38,22 @@ describe('Endpoint.GetOrganizationsEndpoint', () => {
|
|
|
37
38
|
return response.body.results.map(o => o.id);
|
|
38
39
|
};
|
|
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
|
+
|
|
40
57
|
// Creates an admin (user with permissions) for the given organization
|
|
41
58
|
const createAdmin = async (organization: Organization, data: { firstName?: string; lastName?: string; email?: string }) => {
|
|
42
59
|
return await new UserFactory({
|
|
@@ -104,4 +121,112 @@ describe('Endpoint.GetOrganizationsEndpoint', () => {
|
|
|
104
121
|
expect(results).toContain(organization.id);
|
|
105
122
|
expect(results).not.toContain(otherOrganization.id);
|
|
106
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
|
+
});
|
|
107
232
|
});
|
|
@@ -125,7 +125,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
125
125
|
query.limit(q.limit);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
console.log('GetOrganizationsEndpoint query', query.getSQL());
|
|
128
|
+
// console.log('GetOrganizationsEndpoint query', query.getSQL());
|
|
129
129
|
|
|
130
130
|
return query;
|
|
131
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[] = [];
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -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
|
};
|