@stamhoofd/backend 2.100.1 → 2.102.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 +10 -10
- package/src/crons.ts +1 -1
- package/src/email-recipient-loaders/receivable-balances.ts +18 -11
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +1 -0
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +12 -8
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +30 -7
- package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +4 -4
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +60 -8
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +6 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +12 -9
- package/src/excel-loaders/receivable-balances.ts +2 -2
- package/src/helpers/AdminPermissionChecker.ts +66 -8
- package/src/helpers/AuthenticatedStructures.ts +11 -6
- package/src/helpers/MemberUserSyncer.ts +2 -2
- package/src/seeds/{1735577912-update-cached-outstanding-balance-from-items.ts → 1760535589-update-cached-outstanding-balance-from-items.ts} +2 -0
- package/src/services/BalanceItemService.ts +12 -0
- package/src/sql-filters/base-registration-filter-compilers.ts +59 -0
- package/src/sql-filters/orders.ts +97 -1
- package/src/sql-filters/receivable-balances.ts +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.102.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
46
46
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
47
47
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
48
|
-
"@stamhoofd/backend-i18n": "2.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.
|
|
50
|
-
"@stamhoofd/email": "2.
|
|
51
|
-
"@stamhoofd/models": "2.
|
|
52
|
-
"@stamhoofd/queues": "2.
|
|
53
|
-
"@stamhoofd/sql": "2.
|
|
54
|
-
"@stamhoofd/structures": "2.
|
|
55
|
-
"@stamhoofd/utility": "2.
|
|
48
|
+
"@stamhoofd/backend-i18n": "2.102.0",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.102.0",
|
|
50
|
+
"@stamhoofd/email": "2.102.0",
|
|
51
|
+
"@stamhoofd/models": "2.102.0",
|
|
52
|
+
"@stamhoofd/queues": "2.102.0",
|
|
53
|
+
"@stamhoofd/sql": "2.102.0",
|
|
54
|
+
"@stamhoofd/structures": "2.102.0",
|
|
55
|
+
"@stamhoofd/utility": "2.102.0",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"axios": "^1.8.2",
|
|
58
58
|
"cookie": "^0.7.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "0dcf9d704578c9f1972f7068b8727a5572c723c7"
|
|
74
74
|
}
|
package/src/crons.ts
CHANGED
|
@@ -14,7 +14,7 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
|
|
|
14
14
|
|
|
15
15
|
const recipients: EmailRecipient[] = [];
|
|
16
16
|
for (const balance of result.results) {
|
|
17
|
-
const balanceItemModels = await CachedBalance.balanceForObjects(balance.organizationId, [balance.object.id], balance.objectType
|
|
17
|
+
const balanceItemModels = balance.objectType === ReceivableBalanceType.organization ? (await CachedBalance.balanceForObjects(balance.organizationId, [balance.object.id], balance.objectType)) : [];
|
|
18
18
|
const balanceItems = balanceItemModels.map(i => i.getStructure());
|
|
19
19
|
|
|
20
20
|
const filteredContacts = balance.object.contacts.filter(c => compiledFilter(c));
|
|
@@ -22,7 +22,7 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
|
|
|
22
22
|
for (const email of contact.emails) {
|
|
23
23
|
const recipient = EmailRecipient.create({
|
|
24
24
|
objectId: balance.id, // Note: not set member, user or organization id here - should be the queryable balance id
|
|
25
|
-
userId: balance.objectType === ReceivableBalanceType.user ? balance.object.id : null,
|
|
25
|
+
userId: balance.objectType === ReceivableBalanceType.user || balance.objectType === ReceivableBalanceType.userWithoutMembers ? balance.object.id : null,
|
|
26
26
|
memberId: balance.objectType === ReceivableBalanceType.member ? balance.object.id : null,
|
|
27
27
|
firstName: contact.firstName,
|
|
28
28
|
lastName: contact.lastName,
|
|
@@ -32,15 +32,22 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
|
|
|
32
32
|
token: 'objectName',
|
|
33
33
|
value: balance.object.name,
|
|
34
34
|
}),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
...(
|
|
36
|
+
balance.objectType === ReceivableBalanceType.organization
|
|
37
|
+
? [
|
|
38
|
+
Replacement.create({
|
|
39
|
+
token: 'outstandingBalance',
|
|
40
|
+
value: Formatter.price(balance.amountOpen),
|
|
41
|
+
}),
|
|
42
|
+
Replacement.create({
|
|
43
|
+
token: 'balanceTable',
|
|
44
|
+
value: '',
|
|
45
|
+
html: BalanceItemStruct.getDetailsHTMLTable(balanceItems),
|
|
46
|
+
}),
|
|
47
|
+
]
|
|
48
|
+
: []
|
|
49
|
+
),
|
|
50
|
+
|
|
44
51
|
...(contact.meta && contact.meta.url && typeof contact.meta.url === 'string'
|
|
45
52
|
? [Replacement.create({
|
|
46
53
|
token: 'paymentUrl',
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { Group } from '@stamhoofd/models';
|
|
3
2
|
import { FilterWrapperMarker, PermissionLevel, StamhoofdFilter, unwrapFilter, WrapperFilter } from '@stamhoofd/structures';
|
|
4
3
|
import { Context } from '../../../../helpers/Context';
|
|
5
4
|
|
|
@@ -55,17 +54,22 @@ export async function validateGroupFilter({ filter, permissionLevel, key }: { fi
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const groups = await
|
|
59
|
-
Context.auth.cacheGroups(groups);
|
|
57
|
+
const groups = await Context.auth.getGroups(groupIds as string[]);
|
|
60
58
|
|
|
61
|
-
console.log('Fetching members for groups', groups.map(g => g.settings.name.toString()));
|
|
59
|
+
console.log('Fetching members for groups before', groups.map(g => g.settings.name.toString()));
|
|
62
60
|
|
|
63
61
|
for (const group of groups) {
|
|
64
62
|
if (!await Context.auth.canAccessGroup(group, permissionLevel)) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
if (permissionLevel !== PermissionLevel.Read || !group.settings.implicitlyAllowViewRegistrations) {
|
|
64
|
+
throw Context.auth.error({
|
|
65
|
+
message: 'You do not have access to this group',
|
|
66
|
+
human: $t(`45eedf49-0f0a-442c-a0bd-7881c2682698`, { groupName: group.settings.name }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Return false so we add additional scope filters (only view overlap)
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
@@ -99,14 +99,26 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
99
99
|
if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
|
|
100
100
|
// Can access full history for now
|
|
101
101
|
scopeFilter = {
|
|
102
|
-
|
|
102
|
+
member: {
|
|
103
|
+
registrations: {
|
|
104
|
+
$elemMatch: {
|
|
105
|
+
organizationId: organization.id,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
103
109
|
};
|
|
104
110
|
}
|
|
105
111
|
else {
|
|
106
112
|
// Can only access current period
|
|
107
113
|
scopeFilter = {
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
member: {
|
|
115
|
+
registrations: {
|
|
116
|
+
$elemMatch: {
|
|
117
|
+
organizationId: organization.id,
|
|
118
|
+
periodId: organization.periodId,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
110
122
|
};
|
|
111
123
|
}
|
|
112
124
|
}
|
|
@@ -130,8 +142,14 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
scopeFilter = {
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
member: {
|
|
146
|
+
registrations: {
|
|
147
|
+
$elemMatch: {
|
|
148
|
+
groupId: {
|
|
149
|
+
$in: groupIds,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
135
153
|
},
|
|
136
154
|
};
|
|
137
155
|
}
|
|
@@ -196,8 +214,13 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
for (const registration of data) {
|
|
199
|
-
if (
|
|
200
|
-
|
|
217
|
+
if (registration.group.settings.implicitlyAllowViewRegistrations) {
|
|
218
|
+
// okay, only need to check if we can access the members (next step)
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
if (!await Context.auth.canAccessRegistration(registration, permissionLevel)) {
|
|
222
|
+
throw Context.auth.error();
|
|
223
|
+
}
|
|
201
224
|
}
|
|
202
225
|
}
|
|
203
226
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { CachedBalance, Organization } from '@stamhoofd/models';
|
|
3
|
-
import { PayableBalance, PayableBalanceCollection } from '@stamhoofd/structures';
|
|
3
|
+
import { PayableBalance, PayableBalanceCollection, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -31,12 +31,12 @@ export class GetUserPayableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
31
31
|
const organization = await Context.setUserOrganizationScope();
|
|
32
32
|
const { user } = await Context.authenticate();
|
|
33
33
|
|
|
34
|
-
return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([user.id], organization));
|
|
34
|
+
return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([user.id], organization, ReceivableBalanceType.user));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
static async getBillingStatusForObjects(objectIds: string[], organization
|
|
37
|
+
static async getBillingStatusForObjects(objectIds: string[], organization: Organization | null, objectType: ReceivableBalanceType) {
|
|
38
38
|
// Load cached balances
|
|
39
|
-
const receivableBalances = await CachedBalance.getForObjects(objectIds, organization?.id);
|
|
39
|
+
const receivableBalances = await CachedBalance.getForObjects(objectIds, organization?.id, objectType);
|
|
40
40
|
|
|
41
41
|
const organizationIds = Formatter.uniqueArray(receivableBalances.map(b => b.organizationId));
|
|
42
42
|
|
|
@@ -5,7 +5,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
7
|
import { BalanceItem, BalanceItemPayment, CachedBalance, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
8
|
-
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
|
|
8
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, ReceivableBalanceType, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
10
10
|
|
|
11
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -315,7 +315,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
315
315
|
);
|
|
316
316
|
|
|
317
317
|
// Never reuse a registration that has a balance - that means they had a cancellation fee and not all balance items were canceled (we don't want to merge in that state)
|
|
318
|
-
const balances = await CachedBalance.getForObjects(possibleReuseRegistrations.map(r => r.id), null);
|
|
318
|
+
const balances = await CachedBalance.getForObjects(possibleReuseRegistrations.map(r => r.id), null, ReceivableBalanceType.registration);
|
|
319
319
|
|
|
320
320
|
reuseRegistration = possibleReuseRegistrations.find((r) => {
|
|
321
321
|
const balance = balances.filter(b => b.objectId === r.id).reduce((a, b) => a + b.amountOpen + b.amountPaid + b.amountPending, 0);
|
package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { PayableBalanceCollection } from '@stamhoofd/structures';
|
|
2
|
+
import { PayableBalanceCollection, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { Context } from '../../../../helpers/Context';
|
|
5
5
|
import { GetUserPayableBalanceEndpoint } from '../../../global/registration/GetUserPayableBalanceEndpoint';
|
|
@@ -34,6 +34,6 @@ export class GetOrganizationPayableBalanceEndpoint extends Endpoint<Params, Quer
|
|
|
34
34
|
throw Context.auth.error();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([organization.id], null));
|
|
37
|
+
return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([organization.id], null, ReceivableBalanceType.organization));
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { DetailedReceivableBalance, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
|
-
import { BalanceItem, BalanceItemPayment, CachedBalance, Payment } from '@stamhoofd/models';
|
|
4
|
+
import { BalanceItem, BalanceItemPayment, CachedBalance, MemberUser, Payment } from '@stamhoofd/models';
|
|
5
5
|
import { Context } from '../../../../helpers/Context';
|
|
6
6
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
7
7
|
import { SQL } from '@stamhoofd/sql';
|
|
8
8
|
import { BalanceItemService } from '../../../../services/BalanceItemService';
|
|
9
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
10
|
|
|
10
11
|
type Params = { id: string; type: ReceivableBalanceType };
|
|
11
12
|
type Query = undefined;
|
|
@@ -38,14 +39,13 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
38
39
|
throw Context.auth.error();
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
// Flush caches (this makes sure that we do a reload in the frontend after a registration or change, we get the newest balances)
|
|
42
|
-
await BalanceItemService.flushCaches(organization.id);
|
|
43
|
-
|
|
44
|
-
const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type, true);
|
|
45
42
|
let paymentModels: Payment[] = [];
|
|
46
43
|
|
|
47
44
|
switch (request.params.type) {
|
|
48
45
|
case ReceivableBalanceType.organization: {
|
|
46
|
+
// Force cache updates, because sometimes the cache could be out of date
|
|
47
|
+
BalanceItemService.scheduleOrganizationUpdate(organization.id, request.params.id);
|
|
48
|
+
|
|
49
49
|
paymentModels = await Payment.select()
|
|
50
50
|
.where('organizationId', organization.id)
|
|
51
51
|
.andWhere(
|
|
@@ -66,6 +66,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
case ReceivableBalanceType.member: {
|
|
69
|
+
// Force cache updates, because sometimes the cache could be out of date
|
|
70
|
+
BalanceItemService.scheduleMemberUpdate(organization.id, request.params.id);
|
|
71
|
+
|
|
69
72
|
paymentModels = await Payment.select()
|
|
70
73
|
.where('organizationId', organization.id)
|
|
71
74
|
.join(
|
|
@@ -86,7 +89,17 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
case ReceivableBalanceType.user: {
|
|
89
|
-
|
|
92
|
+
const memberUsers = await MemberUser.select().where('usersId', request.params.id).fetch();
|
|
93
|
+
const memberIds = Formatter.uniqueArray(memberUsers.map(mu => mu.membersId));
|
|
94
|
+
|
|
95
|
+
// Force cache updates, because sometimes the cache could be out of date
|
|
96
|
+
BalanceItemService.scheduleUserUpdate(organization.id, request.params.id);
|
|
97
|
+
|
|
98
|
+
for (const memberId of memberIds) {
|
|
99
|
+
BalanceItemService.scheduleMemberUpdate(organization.id, memberId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const q = Payment.select()
|
|
90
103
|
.where('organizationId', organization.id)
|
|
91
104
|
.join(
|
|
92
105
|
SQL.join(BalanceItemPayment.table)
|
|
@@ -95,8 +108,44 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
95
108
|
.join(
|
|
96
109
|
SQL.join(BalanceItem.table)
|
|
97
110
|
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (memberIds.length === 0) {
|
|
114
|
+
q.where(SQL.column(BalanceItem.table, 'userId'), request.params.id);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
q.where(
|
|
118
|
+
SQL.where(SQL.column(BalanceItem.table, 'userId'), request.params.id)
|
|
119
|
+
.or(SQL.column(BalanceItem.table, 'memberId'), memberIds),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
paymentModels = await q
|
|
124
|
+
.andWhere(
|
|
125
|
+
SQL.whereNot('status', PaymentStatus.Failed),
|
|
98
126
|
)
|
|
99
|
-
.
|
|
127
|
+
.groupBy(SQL.column(Payment.table, 'id'))
|
|
128
|
+
.fetch();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case ReceivableBalanceType.userWithoutMembers: {
|
|
133
|
+
// Force cache updates, because sometimes the cache could be out of date
|
|
134
|
+
BalanceItemService.scheduleUserUpdate(organization.id, request.params.id);
|
|
135
|
+
|
|
136
|
+
const q = Payment.select()
|
|
137
|
+
.where('organizationId', organization.id)
|
|
138
|
+
.join(
|
|
139
|
+
SQL.join(BalanceItemPayment.table)
|
|
140
|
+
.where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
|
|
141
|
+
)
|
|
142
|
+
.join(
|
|
143
|
+
SQL.join(BalanceItem.table)
|
|
144
|
+
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
145
|
+
)
|
|
146
|
+
.where(SQL.column(BalanceItem.table, 'userId'), request.params.id);
|
|
147
|
+
|
|
148
|
+
paymentModels = await q
|
|
100
149
|
.andWhere(
|
|
101
150
|
SQL.whereNot('status', PaymentStatus.Failed),
|
|
102
151
|
)
|
|
@@ -126,10 +175,13 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
126
175
|
}
|
|
127
176
|
}
|
|
128
177
|
|
|
178
|
+
// Flush caches (this makes sure that we do a reload in the frontend after a registration or change, we get the newest balances)
|
|
179
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
180
|
+
const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type);
|
|
129
181
|
const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
|
|
130
182
|
const payments = await AuthenticatedStructures.paymentsGeneral(paymentModels, false);
|
|
131
183
|
|
|
132
|
-
const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
|
|
184
|
+
const balances = await CachedBalance.getForObjects([request.params.id], organization.id, request.params.type);
|
|
133
185
|
|
|
134
186
|
const created = new CachedBalance();
|
|
135
187
|
created.amountOpen = 0;
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts
CHANGED
|
@@ -83,7 +83,12 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
users: {
|
|
86
|
-
$elemMatch: {
|
|
86
|
+
$elemMatch: {
|
|
87
|
+
$or: {
|
|
88
|
+
name: { $contains: q.search },
|
|
89
|
+
email: { $contains: q.search },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
87
92
|
},
|
|
88
93
|
},
|
|
89
94
|
],
|
|
@@ -37,7 +37,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
37
37
|
const organization = await Context.setOrganizationScope();
|
|
38
38
|
await Context.authenticate();
|
|
39
39
|
|
|
40
|
-
if (!await Context.auth.
|
|
40
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
41
41
|
throw Context.auth.error();
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -214,6 +214,14 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
214
214
|
for (const groupPut of patch.groups.getPuts()) {
|
|
215
215
|
shouldUpdateSetupSteps = true;
|
|
216
216
|
groupPut.put.settings.throwIfInvalidPrices();
|
|
217
|
+
if (groupPut.put.type === GroupType.EventRegistration) {
|
|
218
|
+
throw new SimpleError({
|
|
219
|
+
code: 'invalid_group_type',
|
|
220
|
+
message: 'Cannot create groups for events via this endpoint',
|
|
221
|
+
human: $t(`40dde58e-47fb-4adb-971a-537b16c479d5`),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
217
225
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, period, { allowedIds });
|
|
218
226
|
deleteUnreachable = true;
|
|
219
227
|
forceGroupIds.push(group.id);
|
|
@@ -528,14 +536,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
528
536
|
static async createGroup(struct: GroupStruct, organizationId: string, period: RegistrationPeriod, options?: { allowedIds?: string[] }): Promise<Group> {
|
|
529
537
|
const allowedIds = options?.allowedIds ?? [];
|
|
530
538
|
|
|
531
|
-
if (struct.type
|
|
539
|
+
if (struct.type !== GroupType.EventRegistration && !allowedIds.includes(struct.id)) {
|
|
532
540
|
if (!await Context.auth.hasFullAccess(organizationId)) {
|
|
533
|
-
|
|
534
|
-
// Ok
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
throw Context.auth.error($t(`153a7443-e2d9-4126-8e10-089b54964fb8`));
|
|
538
|
-
}
|
|
541
|
+
throw Context.auth.error($t(`153a7443-e2d9-4126-8e10-089b54964fb8`));
|
|
539
542
|
}
|
|
540
543
|
}
|
|
541
544
|
else {
|
|
@@ -587,7 +590,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
587
590
|
model.settings.registeredMembers = 0;
|
|
588
591
|
model.settings.reservedMembers = 0;
|
|
589
592
|
|
|
590
|
-
if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
593
|
+
if (struct.type !== GroupType.EventRegistration && !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
591
594
|
// Create a temporary permission role for this user
|
|
592
595
|
const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId);
|
|
593
596
|
if (!organizationPermissions) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XlsxBuiltInNumberFormat, XlsxTransformerColumn, XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
|
|
2
|
-
import { BalanceItemRelationType, BalanceItemWithPayments, DetailedReceivableBalance, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemStatusName, getBalanceItemTypeName,
|
|
2
|
+
import { BalanceItemRelationType, BalanceItemWithPayments, DetailedReceivableBalance, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemStatusName, getBalanceItemTypeName, getReceivableBalanceTypeName, PaginatedResponse, ReceivableBalance } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
4
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
|
|
5
5
|
import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
|
|
@@ -287,7 +287,7 @@ function getGeneralColumns(): XlsxTransformerConcreteColumn<ReceivableBalance>[]
|
|
|
287
287
|
name: $t(`a0dfe596-0670-48bc-a5f3-2c9308c70a17`),
|
|
288
288
|
width: 10,
|
|
289
289
|
getValue: (object: ReceivableBalance) => ({
|
|
290
|
-
value:
|
|
290
|
+
value: getReceivableBalanceTypeName(object.objectType),
|
|
291
291
|
}),
|
|
292
292
|
},
|
|
293
293
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
import { BalanceItem, CachedBalance, Document, Email, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
4
|
-
import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
|
|
4
|
+
import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, ReceivableBalanceType, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { MemberRecordStore } from '../services/MemberRecordStore';
|
|
7
7
|
import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
|
|
@@ -70,6 +70,7 @@ export class AdminPermissionChecker {
|
|
|
70
70
|
return await cache;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
console.log('Get group', groupId);
|
|
73
74
|
const promise = Group.select()
|
|
74
75
|
.where('id', groupId)
|
|
75
76
|
.first(false);
|
|
@@ -80,6 +81,46 @@ export class AdminPermissionChecker {
|
|
|
80
81
|
return group;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
async getGroups(groupIds: string[]): Promise<Group[]> {
|
|
85
|
+
const cached: Group[] = [];
|
|
86
|
+
const remainingIds: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (const groupId of groupIds) {
|
|
89
|
+
const cache = this.groupsCache.get(groupId);
|
|
90
|
+
if (cache !== undefined) {
|
|
91
|
+
const resolved = await cache;
|
|
92
|
+
if (resolved) {
|
|
93
|
+
cached.push(resolved);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Not found, no need to readd
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
remainingIds.push(groupId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (remainingIds.length > 0) {
|
|
105
|
+
console.log('Get groups', remainingIds);
|
|
106
|
+
const promise = Group.select()
|
|
107
|
+
.where('id', remainingIds)
|
|
108
|
+
.fetch();
|
|
109
|
+
|
|
110
|
+
for (const groupId of remainingIds) {
|
|
111
|
+
this.groupsCache.set(groupId, promise.then(list => list.find(l => l.id === groupId) ?? null));
|
|
112
|
+
}
|
|
113
|
+
const groups = await promise;
|
|
114
|
+
cached.push(...groups);
|
|
115
|
+
|
|
116
|
+
for (const groupId of remainingIds) {
|
|
117
|
+
this.groupsCache.set(groupId, groups.find(l => l.id === groupId) ?? null);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return cached;
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
cacheGroup(group: Group) {
|
|
84
125
|
this.groupsCache.set(group.id, group);
|
|
85
126
|
}
|
|
@@ -178,7 +219,7 @@ export class AdminPermissionChecker {
|
|
|
178
219
|
async canAccessGroup(group: Group, permissionLevel: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
179
220
|
// Check permissions aren't scoped to a specific organization, and they mismatch
|
|
180
221
|
if (!this.checkScope(group.organizationId)) {
|
|
181
|
-
return false;
|
|
222
|
+
// return false;
|
|
182
223
|
}
|
|
183
224
|
const organization = await this.getOrganization(group.organizationId);
|
|
184
225
|
|
|
@@ -246,8 +287,13 @@ export class AdminPermissionChecker {
|
|
|
246
287
|
return true;
|
|
247
288
|
}
|
|
248
289
|
if (asOrganizationId) {
|
|
249
|
-
if (group.settings.allowRegistrationsByOrganization) {
|
|
250
|
-
|
|
290
|
+
if (group.settings.allowRegistrationsByOrganization && !group.getStructure().closed) {
|
|
291
|
+
if (group.organizationId !== asOrganizationId) {
|
|
292
|
+
return await this.hasFullAccess(asOrganizationId);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
return await this.hasSomeAccess(asOrganizationId);
|
|
296
|
+
}
|
|
251
297
|
}
|
|
252
298
|
}
|
|
253
299
|
return false;
|
|
@@ -337,7 +383,7 @@ export class AdminPermissionChecker {
|
|
|
337
383
|
}
|
|
338
384
|
|
|
339
385
|
for (const registration of member.registrations) {
|
|
340
|
-
if (await this.canAccessRegistration(registration, permissionLevel)) {
|
|
386
|
+
if (await this.canAccessRegistration(registration, permissionLevel, false)) {
|
|
341
387
|
return true;
|
|
342
388
|
}
|
|
343
389
|
}
|
|
@@ -359,7 +405,7 @@ export class AdminPermissionChecker {
|
|
|
359
405
|
*/
|
|
360
406
|
async canDeleteMember(member: MemberWithRegistrations) {
|
|
361
407
|
if (member.registrations.length === 0 && this.isUserManager(member)) {
|
|
362
|
-
const cachedBalance = await CachedBalance.getForObjects([member.id]);
|
|
408
|
+
const cachedBalance = await CachedBalance.getForObjects([member.id], null, ReceivableBalanceType.member);
|
|
363
409
|
if (cachedBalance.length === 0 || (cachedBalance[0].amountOpen === 0 && cachedBalance[0].amountPending === 0)) {
|
|
364
410
|
const platformMemberships = await MemberPlatformMembership.where({ memberId: member.id });
|
|
365
411
|
if (platformMemberships.length === 0) {
|
|
@@ -378,7 +424,7 @@ export class AdminPermissionChecker {
|
|
|
378
424
|
/**
|
|
379
425
|
* Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
|
|
380
426
|
*/
|
|
381
|
-
async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read) {
|
|
427
|
+
async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read, checkMember = true) {
|
|
382
428
|
if (registration.deactivatedAt || !registration.registeredAt) {
|
|
383
429
|
// No full access: cannot access deactivated registrations
|
|
384
430
|
return false;
|
|
@@ -404,7 +450,7 @@ export class AdminPermissionChecker {
|
|
|
404
450
|
}
|
|
405
451
|
}
|
|
406
452
|
|
|
407
|
-
const group = await this.getGroup(registration.groupId);
|
|
453
|
+
const group = Registration.group.isLoaded(registration) ? ((registration as any).group as Group) : await this.getGroup(registration.groupId);
|
|
408
454
|
if (!group || group.deletedAt) {
|
|
409
455
|
return false;
|
|
410
456
|
}
|
|
@@ -413,6 +459,17 @@ export class AdminPermissionChecker {
|
|
|
413
459
|
return true;
|
|
414
460
|
}
|
|
415
461
|
|
|
462
|
+
if (permissionLevel === PermissionLevel.Read && checkMember && group.settings.implicitlyAllowViewRegistrations) {
|
|
463
|
+
// We can also view this registration if we have access to the member
|
|
464
|
+
const members = await Member.getBlobByIds(registration.memberId);
|
|
465
|
+
|
|
466
|
+
if (members.length === 1) {
|
|
467
|
+
if (await this.canAccessMember(members[0], permissionLevel)) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
416
473
|
return false;
|
|
417
474
|
}
|
|
418
475
|
|
|
@@ -1298,6 +1355,7 @@ export class AdminPermissionChecker {
|
|
|
1298
1355
|
}
|
|
1299
1356
|
|
|
1300
1357
|
if (hasTemporaryMemberAccess(this.user.id, member.id, PermissionLevel.Full)) {
|
|
1358
|
+
// You created this member, so temporary can read all records in order to set the member up correctly
|
|
1301
1359
|
return {
|
|
1302
1360
|
canAccess: true,
|
|
1303
1361
|
record: record.record,
|
|
@@ -399,10 +399,10 @@ export class AuthenticatedStructures {
|
|
|
399
399
|
if (Context.organization) {
|
|
400
400
|
await BalanceItemService.flushCaches(Context.organization.id);
|
|
401
401
|
}
|
|
402
|
-
const balances = await CachedBalance.getForObjects(registrationIds, null);
|
|
402
|
+
const balances = await CachedBalance.getForObjects(registrationIds, null, ReceivableBalanceType.registration);
|
|
403
403
|
const memberIds = members.map(m => m.id);
|
|
404
404
|
const allMemberBalances = Context.organization
|
|
405
|
-
? (await CachedBalance.getForObjects(memberIds, Context.organization.id))
|
|
405
|
+
? (await CachedBalance.getForObjects(memberIds, Context.organization.id, ReceivableBalanceType.member))
|
|
406
406
|
: [];
|
|
407
407
|
|
|
408
408
|
if (includeUser) {
|
|
@@ -468,7 +468,12 @@ export class AuthenticatedStructures {
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
if (organizations.get(registration.organizationId)?.active || (Context.auth.organization && Context.auth.organization.active && registration.organizationId === Context.auth.organization.id) || await Context.auth.hasFullAccess(registration.organizationId)) {
|
|
471
|
+
/* if ( // Causes issues with bundle discount calculations
|
|
472
|
+
registration.group.settings.implicitlyAllowViewRegistrations
|
|
473
|
+
|| await Context.auth.canAccessRegistration(registration, PermissionLevel.Read)
|
|
474
|
+
) { */
|
|
471
475
|
filtered.push(registration);
|
|
476
|
+
// }
|
|
472
477
|
}
|
|
473
478
|
}
|
|
474
479
|
member.registrations = filtered;
|
|
@@ -619,7 +624,7 @@ export class AuthenticatedStructures {
|
|
|
619
624
|
const balancesPermission = member ? (await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id)) : false;
|
|
620
625
|
r = registration.getStructure();
|
|
621
626
|
r.balances = balancesPermission
|
|
622
|
-
? ((await CachedBalance.getForObjects([registration.id], null)).map((b) => {
|
|
627
|
+
? ((await CachedBalance.getForObjects([registration.id], null, ReceivableBalanceType.registration)).map((b) => {
|
|
623
628
|
return GenericBalance.create(b);
|
|
624
629
|
}))
|
|
625
630
|
: [];
|
|
@@ -863,7 +868,7 @@ export class AuthenticatedStructures {
|
|
|
863
868
|
const members = memberIds.length > 0 ? await Member.getByIDs(...memberIds) : [];
|
|
864
869
|
|
|
865
870
|
const userIds = Formatter.uniqueArray([
|
|
866
|
-
...balances.filter(b => b.objectType === ReceivableBalanceType.user).map(b => b.objectId),
|
|
871
|
+
...balances.filter(b => b.objectType === ReceivableBalanceType.user || b.objectType === ReceivableBalanceType.userWithoutMembers).map(b => b.objectId),
|
|
867
872
|
]);
|
|
868
873
|
const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
|
|
869
874
|
|
|
@@ -988,7 +993,7 @@ export class AuthenticatedStructures {
|
|
|
988
993
|
});
|
|
989
994
|
}
|
|
990
995
|
}
|
|
991
|
-
else if (balance.objectType === ReceivableBalanceType.user) {
|
|
996
|
+
else if (balance.objectType === ReceivableBalanceType.user || balance.objectType === ReceivableBalanceType.userWithoutMembers) {
|
|
992
997
|
const user = users.find(m => m.id === balance.objectId) ?? null;
|
|
993
998
|
if (user) {
|
|
994
999
|
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
@@ -1024,7 +1029,7 @@ export class AuthenticatedStructures {
|
|
|
1024
1029
|
const results: DetailedReceivableBalance[] = [];
|
|
1025
1030
|
|
|
1026
1031
|
for (const { balance, object } of items) {
|
|
1027
|
-
const balanceItems = await CachedBalance.balanceForObjects(organizationId, [balance.objectId], balance.objectType
|
|
1032
|
+
const balanceItems = await CachedBalance.balanceForObjects(organizationId, [balance.objectId], balance.objectType);
|
|
1028
1033
|
const balanceItemsWithPayments = await BalanceItem.getStructureWithPayments(balanceItems);
|
|
1029
1034
|
|
|
1030
1035
|
const result = DetailedReceivableBalance.create({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CachedBalance, Member, MemberResponsibilityRecord, MemberWithUsers, Organization, Platform, User } from '@stamhoofd/models';
|
|
2
2
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
3
3
|
import { SQL } from '@stamhoofd/sql';
|
|
4
|
-
import { AuditLogSource, MemberDetails, PermissionRole, Permissions, UserPermissions } from '@stamhoofd/structures';
|
|
4
|
+
import { AuditLogSource, MemberDetails, PermissionRole, Permissions, ReceivableBalanceType, UserPermissions } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import basex from 'base-x';
|
|
7
7
|
import crypto from 'crypto';
|
|
@@ -416,7 +416,7 @@ export class MemberUserSyncerStatic {
|
|
|
416
416
|
*/
|
|
417
417
|
async updateUserBalance(userId: string, memberId: string) {
|
|
418
418
|
// Update balance of this user, as it could have changed
|
|
419
|
-
const memberBalances = await CachedBalance.getForObjects([memberId]);
|
|
419
|
+
const memberBalances = await CachedBalance.getForObjects([memberId], null, ReceivableBalanceType.member);
|
|
420
420
|
if (memberBalances.length > 0) {
|
|
421
421
|
const organizationIds = Formatter.uniqueArray(memberBalances.map(b => b.organizationId));
|
|
422
422
|
for (const organizationId of organizationIds) {
|
|
@@ -163,6 +163,18 @@ export const BalanceItemService = {
|
|
|
163
163
|
}
|
|
164
164
|
},
|
|
165
165
|
|
|
166
|
+
scheduleUserUpdate(organizationId: string, userId: string) {
|
|
167
|
+
userUpdateQueue.addItem(organizationId, userId);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
scheduleMemberUpdate(organizationId: string, memberId: string) {
|
|
171
|
+
memberUpdateQueue.addItem(organizationId, memberId);
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
scheduleOrganizationUpdate(organizationId: string, payingOrganizationId: string) {
|
|
175
|
+
organizationUpdateQueue.addItem(organizationId, payingOrganizationId);
|
|
176
|
+
},
|
|
177
|
+
|
|
166
178
|
async markDue(balanceItem: BalanceItem) {
|
|
167
179
|
if (balanceItem.status === BalanceItemStatus.Hidden) {
|
|
168
180
|
balanceItem.status = BalanceItemStatus.Due;
|
|
@@ -1,6 +1,59 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
1
2
|
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
|
|
3
|
+
import { FilterWrapperMarker, PermissionLevel, StamhoofdFilter, unwrapFilter } from '@stamhoofd/structures';
|
|
4
|
+
import { Context } from '../helpers/Context';
|
|
2
5
|
import { organizationFilterCompilers } from './organizations';
|
|
3
6
|
|
|
7
|
+
async function checkGroupIdFilterAccess(filter: StamhoofdFilter, permissionLevel: PermissionLevel) {
|
|
8
|
+
const groupIds = typeof filter === 'string'
|
|
9
|
+
? [filter]
|
|
10
|
+
: unwrapFilter(filter as StamhoofdFilter, {
|
|
11
|
+
$in: FilterWrapperMarker,
|
|
12
|
+
})?.markerValue;
|
|
13
|
+
|
|
14
|
+
if (!Array.isArray(groupIds)) {
|
|
15
|
+
throw new SimpleError({
|
|
16
|
+
code: 'invalid_field',
|
|
17
|
+
field: 'filter',
|
|
18
|
+
message: 'You must filter on a group of the organization you are trying to access',
|
|
19
|
+
human: $t(`d0ef2e12-dfa2-4d2a-9ee7-793e52e6b94f`),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (groupIds.length === 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const groupId of groupIds) {
|
|
28
|
+
if (typeof groupId !== 'string') {
|
|
29
|
+
throw new SimpleError({
|
|
30
|
+
code: 'invalid_field',
|
|
31
|
+
field: 'filter',
|
|
32
|
+
message: 'Invalid group ID in filter',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const groups = await Context.auth.getGroups(groupIds as string[]);
|
|
38
|
+
|
|
39
|
+
console.log('Filtering registrations on groups', groups.map(g => g.settings.name.toString()));
|
|
40
|
+
|
|
41
|
+
for (const group of groups) {
|
|
42
|
+
if (!await Context.auth.canAccessGroup(group, permissionLevel)) {
|
|
43
|
+
if (permissionLevel === PermissionLevel.Read && group.settings.implicitlyAllowViewRegistrations) {
|
|
44
|
+
// Allowed to filter on this group, since we have view access.
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw Context.auth.error({
|
|
48
|
+
message: 'You do not have access to this group',
|
|
49
|
+
human: $t(`45eedf49-0f0a-442c-a0bd-7881c2682698`, { groupName: group.settings.name }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
4
57
|
export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
5
58
|
...baseSQLFilterCompilers,
|
|
6
59
|
id: createColumnFilter({
|
|
@@ -38,6 +91,9 @@ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
38
91
|
expression: SQL.column('groupId'),
|
|
39
92
|
type: SQLValueType.String,
|
|
40
93
|
nullable: false,
|
|
94
|
+
async checkPermission(filter) {
|
|
95
|
+
await checkGroupIdFilterAccess(filter, PermissionLevel.Read);
|
|
96
|
+
},
|
|
41
97
|
}),
|
|
42
98
|
registeredAt: createColumnFilter({
|
|
43
99
|
expression: SQL.column('registeredAt'),
|
|
@@ -60,6 +116,9 @@ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
60
116
|
expression: SQL.column('groupId'),
|
|
61
117
|
type: SQLValueType.String,
|
|
62
118
|
nullable: false,
|
|
119
|
+
async checkPermission(filter) {
|
|
120
|
+
await checkGroupIdFilterAccess(filter, PermissionLevel.Read);
|
|
121
|
+
},
|
|
63
122
|
}),
|
|
64
123
|
type: createColumnFilter({
|
|
65
124
|
expression: SQL.column('groups', 'type'),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLFilterDefinitions, SQLValueType, SQLScalar } from '@stamhoofd/sql';
|
|
1
|
+
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLFilterDefinitions, SQLValueType, SQLScalar, createExistsFilter, createWildcardColumnFilter, SQLJsonExtract } from '@stamhoofd/sql';
|
|
2
2
|
|
|
3
3
|
export const orderFilterCompilers: SQLFilterDefinitions = {
|
|
4
4
|
...baseSQLFilterCompilers,
|
|
@@ -106,4 +106,100 @@ export const orderFilterCompilers: SQLFilterDefinitions = {
|
|
|
106
106
|
type: SQLValueType.Datetime,
|
|
107
107
|
nullable: true,
|
|
108
108
|
}),
|
|
109
|
+
|
|
110
|
+
items: createExistsFilter(
|
|
111
|
+
/**
|
|
112
|
+
* There is a bug in MySQL 8 that is fixed in 9.3
|
|
113
|
+
* where EXISTS (select * from json_table(...)) does not work
|
|
114
|
+
* To fix this, we do a double select with join inside the select
|
|
115
|
+
* It is a bit slower, but it works for now.
|
|
116
|
+
*/
|
|
117
|
+
SQL.select()
|
|
118
|
+
.from('webshop_orders', 'innerOrders')
|
|
119
|
+
.join(
|
|
120
|
+
SQL.join(
|
|
121
|
+
SQL.jsonTable(
|
|
122
|
+
SQL.jsonValue(SQL.column('innerOrders', 'data'), '$.value.cart.items'),
|
|
123
|
+
'items',
|
|
124
|
+
)
|
|
125
|
+
.addColumn(
|
|
126
|
+
'amount',
|
|
127
|
+
'INT',
|
|
128
|
+
'$.amount',
|
|
129
|
+
)
|
|
130
|
+
.addColumn(
|
|
131
|
+
'productId',
|
|
132
|
+
'TEXT',
|
|
133
|
+
'$.product.id',
|
|
134
|
+
)
|
|
135
|
+
.addColumn(
|
|
136
|
+
'productPriceId',
|
|
137
|
+
'TEXT',
|
|
138
|
+
'$.productPrice.id',
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
.where(SQL.column('innerOrders', 'id'), SQL.column('webshop_orders', 'id')),
|
|
143
|
+
{
|
|
144
|
+
...baseSQLFilterCompilers,
|
|
145
|
+
amount: createColumnFilter({
|
|
146
|
+
expression: SQL.column('items', 'amount'),
|
|
147
|
+
type: SQLValueType.Number,
|
|
148
|
+
nullable: false,
|
|
149
|
+
}),
|
|
150
|
+
product: {
|
|
151
|
+
...baseSQLFilterCompilers,
|
|
152
|
+
id: createColumnFilter({
|
|
153
|
+
expression: SQL.column('items', 'productId'),
|
|
154
|
+
type: SQLValueType.String,
|
|
155
|
+
nullable: false,
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
productPrice: {
|
|
159
|
+
...baseSQLFilterCompilers,
|
|
160
|
+
id: createColumnFilter({
|
|
161
|
+
expression: SQL.column('items', 'productPriceId'),
|
|
162
|
+
type: SQLValueType.String,
|
|
163
|
+
nullable: false,
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
),
|
|
168
|
+
|
|
169
|
+
recordAnswers: createWildcardColumnFilter(
|
|
170
|
+
(key: string) => ({
|
|
171
|
+
expression: SQL.jsonValue(SQL.column('data'), `$.value.recordAnswers.${SQLJsonExtract.escapePathComponent(key)}`, true),
|
|
172
|
+
type: SQLValueType.JSONObject,
|
|
173
|
+
nullable: true,
|
|
174
|
+
}),
|
|
175
|
+
(key: string) => ({
|
|
176
|
+
...baseSQLFilterCompilers,
|
|
177
|
+
selected: createColumnFilter({
|
|
178
|
+
expression: SQL.jsonValue(SQL.column('data'), `$.value.recordAnswers.${SQLJsonExtract.escapePathComponent(key)}.selected`, true),
|
|
179
|
+
type: SQLValueType.JSONBoolean,
|
|
180
|
+
nullable: true,
|
|
181
|
+
}),
|
|
182
|
+
selectedChoice: {
|
|
183
|
+
...baseSQLFilterCompilers,
|
|
184
|
+
id: createColumnFilter({
|
|
185
|
+
expression: SQL.jsonValue(SQL.column('data'), `$.value.recordAnswers.${SQLJsonExtract.escapePathComponent(key)}.selectedChoice.id`, true),
|
|
186
|
+
type: SQLValueType.JSONString,
|
|
187
|
+
nullable: true,
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
selectedChoices: {
|
|
191
|
+
...baseSQLFilterCompilers,
|
|
192
|
+
id: createColumnFilter({
|
|
193
|
+
expression: SQL.jsonValue(SQL.column('data'), `$.value.recordAnswers.${SQLJsonExtract.escapePathComponent(key)}.selectedChoices[*].id`, true),
|
|
194
|
+
type: SQLValueType.JSONArray,
|
|
195
|
+
nullable: true,
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
value: createColumnFilter({
|
|
199
|
+
expression: SQL.jsonValue(SQL.column('data'), `$.value.recordAnswers.${SQLJsonExtract.escapePathComponent(key)}.value`, true),
|
|
200
|
+
type: SQLValueType.JSONString,
|
|
201
|
+
nullable: true,
|
|
202
|
+
}),
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
109
205
|
};
|
|
@@ -97,7 +97,8 @@ export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
|
|
|
97
97
|
SQL.column('cached_outstanding_balances', 'objectId'),
|
|
98
98
|
).where(
|
|
99
99
|
SQL.column('cached_outstanding_balances', 'objectType'),
|
|
100
|
-
'user'
|
|
100
|
+
['user', 'userWithoutMembers'],
|
|
101
|
+
),
|
|
101
102
|
{
|
|
102
103
|
...baseSQLFilterCompilers,
|
|
103
104
|
name: createColumnFilter({
|
|
@@ -109,6 +110,11 @@ export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
|
|
|
109
110
|
type: SQLValueType.String,
|
|
110
111
|
nullable: false,
|
|
111
112
|
}),
|
|
113
|
+
email: createColumnFilter({
|
|
114
|
+
expression: SQL.column('email'),
|
|
115
|
+
type: SQLValueType.String,
|
|
116
|
+
nullable: false,
|
|
117
|
+
}),
|
|
112
118
|
},
|
|
113
119
|
),
|
|
114
120
|
|