@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
|
@@ -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';
|
|
@@ -67,8 +67,25 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
67
67
|
const organization = await Context.setOrganizationScope();
|
|
68
68
|
const { user } = await Context.authenticate();
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Who is going to pay?
|
|
71
|
+
let whoWillPayNow: 'member' | 'organization' | 'nobody' = 'member'; // if this is set to 'organization', there will also be created separate balance items so the member can pay back the paying organization
|
|
72
|
+
|
|
73
|
+
if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
|
|
74
|
+
// Fast fail
|
|
75
|
+
if (!await Context.auth.hasSomeAccess(request.body.asOrganizationId)) {
|
|
76
|
+
throw new SimpleError({
|
|
77
|
+
code: 'forbidden',
|
|
78
|
+
message: 'No permission to register members manually',
|
|
79
|
+
human: $t(`62fe6e39-f6b0-4836-b0f7-dc84d22a81e3`),
|
|
80
|
+
statusCode: 403,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// We won't create a payment. The balance will get added to the outstanding amount of the member / already paying organization
|
|
85
|
+
whoWillPayNow = 'nobody';
|
|
86
|
+
}
|
|
87
|
+
else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
|
|
88
|
+
if (!await Context.auth.hasFullAccess(request.body.asOrganizationId)) {
|
|
72
89
|
throw new SimpleError({
|
|
73
90
|
code: 'forbidden',
|
|
74
91
|
message: 'No permission to register as this organization for a different organization',
|
|
@@ -76,6 +93,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
76
93
|
statusCode: 403,
|
|
77
94
|
});
|
|
78
95
|
}
|
|
96
|
+
|
|
97
|
+
// The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
|
|
98
|
+
whoWillPayNow = 'organization';
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
// For non paid organizations, limit amount of tests
|
|
@@ -145,7 +165,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
for (const member of members) {
|
|
148
|
-
if (!await Context.auth.canAccessMember(
|
|
168
|
+
if (!await Context.auth.canAccessMember(
|
|
169
|
+
member,
|
|
170
|
+
request.body.asOrganizationId ? PermissionLevel.Read : PermissionLevel.Write,
|
|
171
|
+
)) {
|
|
149
172
|
throw new SimpleError({
|
|
150
173
|
code: 'forbidden',
|
|
151
174
|
message: 'No permission to register this member',
|
|
@@ -170,7 +193,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
170
193
|
for (const memberId of memberIds) {
|
|
171
194
|
const familyMembers = (await Member.getFamilyWithRegistrations(memberId));
|
|
172
195
|
members.push(...familyMembers);
|
|
173
|
-
const blob = await AuthenticatedStructures.membersBlob(familyMembers, true);
|
|
196
|
+
const blob = await AuthenticatedStructures.membersBlob(familyMembers, true, undefined, { forAdminCartCalculation: true });
|
|
174
197
|
const family = PlatformFamily.create(blob, {
|
|
175
198
|
platform: platformStruct,
|
|
176
199
|
contextOrganization,
|
|
@@ -225,22 +248,18 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
225
248
|
const totalPrice = checkout.totalPrice;
|
|
226
249
|
|
|
227
250
|
if (totalPrice !== request.body.totalPrice) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
|
|
242
|
-
// The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
|
|
243
|
-
whoWillPayNow = 'organization';
|
|
251
|
+
if (whoWillPayNow === 'nobody') {
|
|
252
|
+
// Safe and important to ignore: we are only updating the outstanding amounts
|
|
253
|
+
// If we would throw here, that could leak personal data (e.g. that the user uses financial support)
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// when whoWillPay = organization/member, we should throw or the payment amount could be different / incorrect.
|
|
257
|
+
// This never leaks information because in this case the user already has full access to the organization (asOrganizationId) or the member
|
|
258
|
+
throw new SimpleError({
|
|
259
|
+
code: 'changed_price',
|
|
260
|
+
message: $t(`e424d549-2bb8-4103-9a14-ac4063d7d454`, { total: Formatter.price(totalPrice) }),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
244
263
|
}
|
|
245
264
|
|
|
246
265
|
const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
|
|
@@ -315,7 +334,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
315
334
|
);
|
|
316
335
|
|
|
317
336
|
// 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);
|
|
337
|
+
const balances = await CachedBalance.getForObjects(possibleReuseRegistrations.map(r => r.id), null, ReceivableBalanceType.registration);
|
|
319
338
|
|
|
320
339
|
reuseRegistration = possibleReuseRegistrations.find((r) => {
|
|
321
340
|
const balance = balances.filter(b => b.objectId === r.id).reduce((a, b) => a + b.amountOpen + b.amountPaid + b.amountPending, 0);
|
|
@@ -763,6 +782,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
763
782
|
}
|
|
764
783
|
|
|
765
784
|
let paymentUrl: string | null = null;
|
|
785
|
+
let paymentQRCode: string | null = null;
|
|
766
786
|
let payment: Payment | null = null;
|
|
767
787
|
|
|
768
788
|
// Delay marking as valid as late as possible so any errors will prevent creating valid balance items
|
|
@@ -814,6 +834,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
814
834
|
|
|
815
835
|
if (response) {
|
|
816
836
|
paymentUrl = response.paymentUrl;
|
|
837
|
+
paymentQRCode = response.paymentQRCode;
|
|
817
838
|
payment = response.payment;
|
|
818
839
|
}
|
|
819
840
|
}
|
|
@@ -841,6 +862,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
841
862
|
members: await AuthenticatedStructures.membersBlob(updatedMembers),
|
|
842
863
|
registrations: registrations.map(r => Member.getRegistrationWithTinyMemberStructure(r)),
|
|
843
864
|
paymentUrl,
|
|
865
|
+
paymentQRCode,
|
|
844
866
|
}));
|
|
845
867
|
}
|
|
846
868
|
|
|
@@ -1047,6 +1069,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
1047
1069
|
const description = $t(`33a926ea-9bc7-444e-becc-c0f2f70e1f0e`) + ' ' + organization.name;
|
|
1048
1070
|
|
|
1049
1071
|
let paymentUrl: string | null = null;
|
|
1072
|
+
let paymentQRCode: string | null = null;
|
|
1050
1073
|
|
|
1051
1074
|
try {
|
|
1052
1075
|
// Update balance items
|
|
@@ -1144,7 +1167,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
1144
1167
|
await dbPayment.save();
|
|
1145
1168
|
}
|
|
1146
1169
|
else if (payment.provider === PaymentProvider.Payconiq) {
|
|
1147
|
-
paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl);
|
|
1170
|
+
({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl));
|
|
1148
1171
|
}
|
|
1149
1172
|
else if (payment.provider == PaymentProvider.Buckaroo) {
|
|
1150
1173
|
// Increase request timeout because buckaroo is super slow (in development)
|
|
@@ -1175,6 +1198,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
1175
1198
|
provider,
|
|
1176
1199
|
stripeAccount,
|
|
1177
1200
|
paymentUrl,
|
|
1201
|
+
paymentQRCode,
|
|
1178
1202
|
};
|
|
1179
1203
|
}
|
|
1180
1204
|
}
|
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) {
|
|
@@ -224,7 +224,8 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
224
224
|
balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem));
|
|
225
225
|
|
|
226
226
|
let paymentUrl: string | null = null;
|
|
227
|
-
|
|
227
|
+
let paymentQRCode: string | null = null;
|
|
228
|
+
const description = webshop.meta.name + ' - ' + organization.name;
|
|
228
229
|
|
|
229
230
|
if (payment.method === PaymentMethod.Transfer) {
|
|
230
231
|
await order.markValid(payment, []);
|
|
@@ -324,7 +325,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
324
325
|
await dbPayment.save();
|
|
325
326
|
}
|
|
326
327
|
else if (payment.provider == PaymentProvider.Payconiq) {
|
|
327
|
-
paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, exchangeUrl);
|
|
328
|
+
({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, exchangeUrl));
|
|
328
329
|
}
|
|
329
330
|
else if (payment.provider == PaymentProvider.Buckaroo) {
|
|
330
331
|
// Increase request timeout because buckaroo is super slow
|
|
@@ -351,6 +352,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
351
352
|
|
|
352
353
|
return new Response(OrderResponse.create({
|
|
353
354
|
paymentUrl: paymentUrl,
|
|
355
|
+
paymentQRCode,
|
|
354
356
|
order: OrderStruct.create({ ...order, payment: PaymentStruct.create(payment) }),
|
|
355
357
|
}));
|
|
356
358
|
}
|
|
@@ -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: boolean | MemberWithRegistrations = 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 = checkMember === true ? await Member.getBlobByIds(registration.memberId) : [checkMember];
|
|
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,
|
|
@@ -1358,7 +1416,7 @@ export class AdminPermissionChecker {
|
|
|
1358
1416
|
/**
|
|
1359
1417
|
* Changes data inline
|
|
1360
1418
|
*/
|
|
1361
|
-
async filterMemberData(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob): Promise<MemberWithRegistrationsBlob> {
|
|
1419
|
+
async filterMemberData(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob, options?: { forAdminCartCalculation?: boolean }): Promise<MemberWithRegistrationsBlob> {
|
|
1362
1420
|
const cloned = data.clone();
|
|
1363
1421
|
|
|
1364
1422
|
for (const [key, value] of cloned.details.recordAnswers.entries()) {
|
|
@@ -1386,7 +1444,7 @@ export class AdminPermissionChecker {
|
|
|
1386
1444
|
}
|
|
1387
1445
|
|
|
1388
1446
|
// Has financial read access?
|
|
1389
|
-
if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
|
|
1447
|
+
if (!options?.forAdminCartCalculation && !await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
|
|
1390
1448
|
cloned.details.requiresFinancialSupport = null;
|
|
1391
1449
|
cloned.details.uitpasNumber = null;
|
|
1392
1450
|
cloned.outstandingBalance = 0;
|