@stamhoofd/backend 2.101.0 → 2.103.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 +21 -26
- 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.test.ts +382 -31
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +47 -23
- 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/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -2
- package/src/excel-loaders/receivable-balances.ts +2 -2
- package/src/helpers/AdminPermissionChecker.ts +68 -10
- package/src/helpers/AuthenticatedStructures.ts +18 -10
- package/src/helpers/MemberUserSyncer.ts +2 -2
- package/src/seeds/{1735577912-update-cached-outstanding-balance-from-items.ts → 1760702454-update-cached-outstanding-balance-from-items.ts} +2 -0
- package/src/services/BalanceItemService.ts +12 -0
- package/src/services/PaymentService.ts +3 -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/tests/e2e/bundle-discounts.test.ts +327 -1
- package/tests/helpers/PayconiqMocker.ts +22 -0
- package/tests/init/index.ts +1 -0
- package/tests/init/initAdmin.ts +2 -2
- package/tests/init/initPermissionRole.ts +12 -0
|
@@ -388,7 +388,7 @@ export class AuthenticatedStructures {
|
|
|
388
388
|
return (await this.membersBlob(members, false)).members;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User): Promise<MembersBlob> {
|
|
391
|
+
static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User, options?: { forAdminCartCalculation?: boolean }): Promise<MembersBlob> {
|
|
392
392
|
if (members.length === 0 && !includeUser) {
|
|
393
393
|
return MembersBlob.create({ members: [], organizations: [] });
|
|
394
394
|
}
|
|
@@ -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) {
|
|
@@ -459,6 +459,7 @@ export class AuthenticatedStructures {
|
|
|
459
459
|
const filtered: (Registration & {
|
|
460
460
|
group: Group;
|
|
461
461
|
})[] = [];
|
|
462
|
+
const userManager = Context.auth.isUserManager(member);
|
|
462
463
|
for (const registration of member.registrations) {
|
|
463
464
|
if (includeContextOrganization || registration.organizationId !== Context.auth.organization?.id) {
|
|
464
465
|
const found = organizations.get(registration.id);
|
|
@@ -468,11 +469,18 @@ export class AuthenticatedStructures {
|
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
471
|
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
|
-
|
|
472
|
+
if (
|
|
473
|
+
!!options?.forAdminCartCalculation
|
|
474
|
+
|| registration.group.settings.implicitlyAllowViewRegistrations
|
|
475
|
+
|| userManager
|
|
476
|
+
|| await Context.auth.canAccessRegistration(registration, PermissionLevel.Read, member)
|
|
477
|
+
) {
|
|
478
|
+
filtered.push(registration);
|
|
479
|
+
}
|
|
472
480
|
}
|
|
473
481
|
}
|
|
474
482
|
member.registrations = filtered;
|
|
475
|
-
const balancesPermission = await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id);
|
|
483
|
+
const balancesPermission = (!!options?.forAdminCartCalculation) || await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id);
|
|
476
484
|
|
|
477
485
|
let memberBalances: GenericBalance[] = [];
|
|
478
486
|
|
|
@@ -507,7 +515,7 @@ export class AuthenticatedStructures {
|
|
|
507
515
|
});
|
|
508
516
|
|
|
509
517
|
memberBlobs.push(
|
|
510
|
-
await Context.auth.filterMemberData(member, blob),
|
|
518
|
+
await Context.auth.filterMemberData(member, blob, { forAdminCartCalculation: options?.forAdminCartCalculation ?? false }),
|
|
511
519
|
);
|
|
512
520
|
}
|
|
513
521
|
|
|
@@ -619,7 +627,7 @@ export class AuthenticatedStructures {
|
|
|
619
627
|
const balancesPermission = member ? (await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id)) : false;
|
|
620
628
|
r = registration.getStructure();
|
|
621
629
|
r.balances = balancesPermission
|
|
622
|
-
? ((await CachedBalance.getForObjects([registration.id], null)).map((b) => {
|
|
630
|
+
? ((await CachedBalance.getForObjects([registration.id], null, ReceivableBalanceType.registration)).map((b) => {
|
|
623
631
|
return GenericBalance.create(b);
|
|
624
632
|
}))
|
|
625
633
|
: [];
|
|
@@ -863,7 +871,7 @@ export class AuthenticatedStructures {
|
|
|
863
871
|
const members = memberIds.length > 0 ? await Member.getByIDs(...memberIds) : [];
|
|
864
872
|
|
|
865
873
|
const userIds = Formatter.uniqueArray([
|
|
866
|
-
...balances.filter(b => b.objectType === ReceivableBalanceType.user).map(b => b.objectId),
|
|
874
|
+
...balances.filter(b => b.objectType === ReceivableBalanceType.user || b.objectType === ReceivableBalanceType.userWithoutMembers).map(b => b.objectId),
|
|
867
875
|
]);
|
|
868
876
|
const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
|
|
869
877
|
|
|
@@ -988,7 +996,7 @@ export class AuthenticatedStructures {
|
|
|
988
996
|
});
|
|
989
997
|
}
|
|
990
998
|
}
|
|
991
|
-
else if (balance.objectType === ReceivableBalanceType.user) {
|
|
999
|
+
else if (balance.objectType === ReceivableBalanceType.user || balance.objectType === ReceivableBalanceType.userWithoutMembers) {
|
|
992
1000
|
const user = users.find(m => m.id === balance.objectId) ?? null;
|
|
993
1001
|
if (user) {
|
|
994
1002
|
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
@@ -1024,7 +1032,7 @@ export class AuthenticatedStructures {
|
|
|
1024
1032
|
const results: DetailedReceivableBalance[] = [];
|
|
1025
1033
|
|
|
1026
1034
|
for (const { balance, object } of items) {
|
|
1027
|
-
const balanceItems = await CachedBalance.balanceForObjects(organizationId, [balance.objectId], balance.objectType
|
|
1035
|
+
const balanceItems = await CachedBalance.balanceForObjects(organizationId, [balance.objectId], balance.objectType);
|
|
1028
1036
|
const balanceItemsWithPayments = await BalanceItem.getStructureWithPayments(balanceItems);
|
|
1029
1037
|
|
|
1030
1038
|
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;
|
|
@@ -260,6 +260,9 @@ export const PaymentService = {
|
|
|
260
260
|
if (await payconiqPayment.cancel(organization)) {
|
|
261
261
|
status = PaymentStatus.Failed;
|
|
262
262
|
}
|
|
263
|
+
else {
|
|
264
|
+
console.error('Failed to manually cancel payment');
|
|
265
|
+
}
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
if (this.isManualExpired(status, payment)) {
|
|
@@ -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
|
|