@stamhoofd/backend 2.7.0 → 2.9.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/.env.template.json +3 -1
- package/package.json +3 -3
- package/src/crons.ts +3 -3
- package/src/decoders/StringArrayDecoder.ts +24 -0
- package/src/decoders/StringNullableDecoder.ts +18 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +14 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +0 -31
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +34 -367
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -11
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +205 -110
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -40
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
- package/src/helpers/AdminPermissionChecker.ts +35 -24
- package/src/helpers/AuthenticatedStructures.ts +16 -7
- package/src/helpers/Context.ts +21 -0
- package/src/helpers/EmailResumer.ts +22 -2
- package/src/helpers/MemberUserSyncer.ts +42 -14
- package/src/seeds/1722344160-update-membership.ts +19 -22
- package/src/seeds/1722344161-sync-member-users.ts +60 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
2
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
3
3
|
import { BalanceItem, Group, Member } from "@stamhoofd/models";
|
|
4
|
-
import {
|
|
4
|
+
import { BalanceItemWithPayments } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
6
|
import { Context } from "../../../../helpers/Context";
|
|
7
7
|
|
|
8
8
|
type Params = { id: string };
|
|
9
9
|
type Query = undefined
|
|
10
10
|
type Body = undefined
|
|
11
|
-
type ResponseBody =
|
|
11
|
+
type ResponseBody = BalanceItemWithPayments[]
|
|
12
12
|
|
|
13
13
|
export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
14
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
@@ -42,7 +42,7 @@ export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
42
42
|
const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization.id, member.users.map(u => u.id), [member.id])
|
|
43
43
|
|
|
44
44
|
return new Response(
|
|
45
|
-
await BalanceItem.
|
|
45
|
+
await BalanceItem.getStructureWithPayments(balanceItems)
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -1,54 +1,17 @@
|
|
|
1
|
-
import { AutoEncoder,
|
|
1
|
+
import { AutoEncoder, DateDecoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { Organization, Payment } from "@stamhoofd/models";
|
|
4
4
|
import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
|
+
import { StringArrayDecoder } from "../../../../decoders/StringArrayDecoder";
|
|
6
7
|
import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
|
|
7
8
|
import { Context } from "../../../../helpers/Context";
|
|
9
|
+
import { StringNullableDecoder } from "../../../../decoders/StringNullableDecoder";
|
|
8
10
|
|
|
9
11
|
type Params = Record<string, never>;
|
|
10
12
|
type Body = undefined
|
|
11
13
|
type ResponseBody = PaymentGeneral[]
|
|
12
14
|
|
|
13
|
-
export class StringArrayDecoder<T> implements Decoder<T[]> {
|
|
14
|
-
decoder: Decoder<T>;
|
|
15
|
-
|
|
16
|
-
constructor(decoder: Decoder<T>) {
|
|
17
|
-
this.decoder = decoder;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
decode(data: Data): T[] {
|
|
21
|
-
const strValue = data.string;
|
|
22
|
-
|
|
23
|
-
// Split on comma
|
|
24
|
-
const parts = strValue.split(",");
|
|
25
|
-
return parts
|
|
26
|
-
.map((v, index) => {
|
|
27
|
-
return data.clone({
|
|
28
|
-
data: v,
|
|
29
|
-
context: data.context,
|
|
30
|
-
field: data.addToCurrentField(index)
|
|
31
|
-
}).decode(this.decoder)
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class StringNullableDecoder<T> implements Decoder<T | null> {
|
|
37
|
-
decoder: Decoder<T>;
|
|
38
|
-
|
|
39
|
-
constructor(decoder: Decoder<T>) {
|
|
40
|
-
this.decoder = decoder;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
decode(data: Data): T | null {
|
|
44
|
-
if (data.value === 'null') {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return data.decode(this.decoder);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
15
|
|
|
53
16
|
class Query extends AutoEncoder {
|
|
54
17
|
/**
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
4
|
import { BalanceItem, Member, Order, Registration, User } from '@stamhoofd/models';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
-
import { BalanceItemStatus,
|
|
6
|
+
import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from "@stamhoofd/structures";
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
@@ -11,11 +11,11 @@ import { Context } from '../../../../helpers/Context';
|
|
|
11
11
|
|
|
12
12
|
type Params = Record<string, never>;
|
|
13
13
|
type Query = undefined;
|
|
14
|
-
type Body = PatchableArrayAutoEncoder<
|
|
15
|
-
type ResponseBody =
|
|
14
|
+
type Body = PatchableArrayAutoEncoder<BalanceItemWithPayments>
|
|
15
|
+
type ResponseBody = BalanceItemWithPayments[]
|
|
16
16
|
|
|
17
17
|
export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
-
bodyDecoder = new PatchableArrayDecoder(
|
|
18
|
+
bodyDecoder = new PatchableArrayDecoder(BalanceItemWithPayments as Decoder<BalanceItemWithPayments>, BalanceItemWithPayments.patchType() as Decoder<AutoEncoderPatchType<BalanceItemWithPayments>>, StringDecoder)
|
|
19
19
|
|
|
20
20
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
21
21
|
if (request.method != "PATCH") {
|
|
@@ -53,7 +53,10 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
53
53
|
// Create a new balance item
|
|
54
54
|
const model = new BalanceItem();
|
|
55
55
|
model.description = put.description;
|
|
56
|
-
model.
|
|
56
|
+
model.amount = put.amount;
|
|
57
|
+
model.type = BalanceItemType.Other
|
|
58
|
+
model.unitPrice = put.unitPrice;
|
|
59
|
+
model.amount = put.amount;
|
|
57
60
|
model.organizationId = organization.id;
|
|
58
61
|
model.createdAt = put.createdAt;
|
|
59
62
|
model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
|
|
@@ -75,19 +78,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
75
78
|
})
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
if (put.registration) {
|
|
79
|
-
const registration = await Registration.getByID(put.registration.id)
|
|
80
|
-
if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
|
|
81
|
-
throw new SimpleError({
|
|
82
|
-
code: 'invalid_field',
|
|
83
|
-
message: 'Registration not found',
|
|
84
|
-
field: 'registration'
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
model.registrationId = registration.id
|
|
88
|
-
registrationIds.push(registration.id)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
await model.save();
|
|
92
82
|
returnedModels.push(model);
|
|
93
83
|
}
|
|
@@ -101,6 +91,15 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
101
91
|
message: 'BalanceItem not found'
|
|
102
92
|
})
|
|
103
93
|
}
|
|
94
|
+
|
|
95
|
+
if (patch.unitPrice !== undefined) {
|
|
96
|
+
throw new SimpleError({
|
|
97
|
+
code: 'invalid_field',
|
|
98
|
+
message: 'You cannot change the unit price of a balance item',
|
|
99
|
+
human: 'Het is niet mogelijk om de eenheidsprijs van een openstaande schuld te wijzigen. Je kan de openstaande schuld verwijderen en opnieuw aanmaken indien noodzakelijk.'
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
104
103
|
// Check permissions
|
|
105
104
|
if (model.memberId) {
|
|
106
105
|
// Update old
|
|
@@ -123,30 +122,16 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
123
122
|
model.createdAt = patch.createdAt
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
if (patch.registration) {
|
|
127
|
-
const registration = await Registration.getByID(patch.registration.id)
|
|
128
|
-
if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
|
|
129
|
-
throw new SimpleError({
|
|
130
|
-
code: 'invalid_field',
|
|
131
|
-
message: 'Registration not found',
|
|
132
|
-
field: 'registration'
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
model.registrationId = registration.id
|
|
136
|
-
|
|
137
|
-
// Update new
|
|
138
|
-
registrationIds.push(model.registrationId)
|
|
139
|
-
} else if (patch.registration === null) {
|
|
140
|
-
model.registrationId = null
|
|
141
|
-
}
|
|
142
125
|
model.description = patch.description ?? model.description;
|
|
143
|
-
model.
|
|
126
|
+
model.unitPrice = patch.unitPrice ?? model.unitPrice;
|
|
127
|
+
model.amount = patch.amount ?? model.amount;
|
|
144
128
|
|
|
145
129
|
if (model.orderId) {
|
|
146
130
|
// Not allowed to change this
|
|
147
131
|
const order = await Order.getByID(model.orderId)
|
|
148
132
|
if (order) {
|
|
149
|
-
model.
|
|
133
|
+
model.unitPrice = order.totalToPay
|
|
134
|
+
model.amount = 1
|
|
150
135
|
}
|
|
151
136
|
}
|
|
152
137
|
|
|
@@ -167,7 +152,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
167
152
|
await Registration.updateOutstandingBalance(Formatter.uniqueArray(registrationIds), organization.id)
|
|
168
153
|
|
|
169
154
|
return new Response(
|
|
170
|
-
await BalanceItem.
|
|
155
|
+
await BalanceItem.getStructureWithPayments(returnedModels)
|
|
171
156
|
);
|
|
172
157
|
}
|
|
173
158
|
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
4
|
import { BalanceItem, BalanceItemPayment, Order, Payment, Token, Webshop } from '@stamhoofd/models';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
-
import { BalanceItemStatus, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
|
|
6
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
|
|
@@ -157,11 +157,21 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
157
157
|
// Create balance item
|
|
158
158
|
const balanceItem = new BalanceItem();
|
|
159
159
|
balanceItem.orderId = order.id;
|
|
160
|
-
balanceItem.
|
|
160
|
+
balanceItem.type = BalanceItemType.Order;
|
|
161
|
+
balanceItem.unitPrice = totalPrice
|
|
161
162
|
balanceItem.description = webshop.meta.name
|
|
162
163
|
balanceItem.pricePaid = 0
|
|
163
164
|
balanceItem.organizationId = organization.id;
|
|
164
165
|
balanceItem.status = BalanceItemStatus.Pending;
|
|
166
|
+
balanceItem.relations = new Map([
|
|
167
|
+
[
|
|
168
|
+
BalanceItemRelationType.Webshop,
|
|
169
|
+
BalanceItemRelation.create({
|
|
170
|
+
id: webshop.id,
|
|
171
|
+
name: webshop.meta.name,
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
])
|
|
165
175
|
await balanceItem.save();
|
|
166
176
|
|
|
167
177
|
// Create one balance item payment to pay it in one payment
|
|
@@ -245,7 +255,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
245
255
|
const items = await BalanceItem.where({ orderId: model.id })
|
|
246
256
|
if (items.length >= 1) {
|
|
247
257
|
model.markUpdated()
|
|
248
|
-
items[0].
|
|
258
|
+
items[0].unitPrice = model.totalToPay
|
|
249
259
|
items[0].description = model.generateBalanceDescription(webshop)
|
|
250
260
|
items[0].updateStatus();
|
|
251
261
|
await items[0].save()
|
|
@@ -258,7 +268,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
258
268
|
model.markUpdated()
|
|
259
269
|
const balanceItem = new BalanceItem();
|
|
260
270
|
balanceItem.orderId = model.id;
|
|
261
|
-
balanceItem.
|
|
271
|
+
balanceItem.unitPrice = model.totalToPay
|
|
262
272
|
balanceItem.description = model.generateBalanceDescription(webshop)
|
|
263
273
|
balanceItem.pricePaid = 0
|
|
264
274
|
balanceItem.organizationId = organization.id;
|
|
@@ -6,7 +6,7 @@ import { I18n } from '@stamhoofd/backend-i18n';
|
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
7
|
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
8
8
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
|
-
import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct,WebshopAuthType } from "@stamhoofd/structures";
|
|
9
|
+
import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct,WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation } from "@stamhoofd/structures";
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
|
|
12
12
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
@@ -182,12 +182,22 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
182
182
|
|
|
183
183
|
// Create balance item
|
|
184
184
|
const balanceItem = new BalanceItem();
|
|
185
|
+
balanceItem.type = BalanceItemType.Order;
|
|
185
186
|
balanceItem.orderId = order.id;
|
|
186
|
-
balanceItem.
|
|
187
|
+
balanceItem.unitPrice = totalPrice
|
|
187
188
|
balanceItem.description = webshop.meta.name
|
|
188
189
|
balanceItem.pricePaid = 0
|
|
189
190
|
balanceItem.organizationId = organization.id;
|
|
190
191
|
balanceItem.status = BalanceItemStatus.Hidden;
|
|
192
|
+
balanceItem.relations = new Map([
|
|
193
|
+
[
|
|
194
|
+
BalanceItemRelationType.Webshop,
|
|
195
|
+
BalanceItemRelation.create({
|
|
196
|
+
id: webshop.id,
|
|
197
|
+
name: webshop.meta.name,
|
|
198
|
+
})
|
|
199
|
+
]
|
|
200
|
+
])
|
|
191
201
|
await balanceItem.save();
|
|
192
202
|
|
|
193
203
|
// Create one balance item payment to pay it in one payment
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
|
|
2
2
|
import { SimpleError } from "@simonbackx/simple-errors"
|
|
3
3
|
import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
|
|
4
|
-
import { AccessRight, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
|
|
4
|
+
import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
|
|
5
5
|
import { Formatter } from "@stamhoofd/utility"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -207,10 +207,11 @@ export class AdminPermissionChecker {
|
|
|
207
207
|
return await this.hasFullAccess(organizationId)
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
/**
|
|
211
|
-
* Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
|
|
212
|
-
*/
|
|
213
210
|
async canAccessMember(member: MemberWithRegistrations, permissionLevel: PermissionLevel = PermissionLevel.Read) {
|
|
211
|
+
if (this.isUserManager(member) && permissionLevel !== PermissionLevel.Full) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
214
215
|
// Check user has permissions
|
|
215
216
|
if (!this.user.permissions) {
|
|
216
217
|
return false
|
|
@@ -761,18 +762,6 @@ export class AdminPermissionChecker {
|
|
|
761
762
|
for (const category of organization.meta.recordsConfiguration.recordCategories) {
|
|
762
763
|
recordCategories.push(category)
|
|
763
764
|
}
|
|
764
|
-
|
|
765
|
-
for (const [id] of organization.meta.recordsConfiguration.inheritedRecordCategories) {
|
|
766
|
-
if (recordCategories.find(c => c.id === id)) {
|
|
767
|
-
// Already added
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const category = this.platform.config.recordsConfiguration.recordCategories.find(c => c.id === id)
|
|
772
|
-
if (category) {
|
|
773
|
-
recordCategories.push(category)
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
765
|
continue;
|
|
777
766
|
}
|
|
778
767
|
|
|
@@ -788,17 +777,15 @@ export class AdminPermissionChecker {
|
|
|
788
777
|
}
|
|
789
778
|
}
|
|
790
779
|
|
|
791
|
-
for
|
|
792
|
-
|
|
780
|
+
// Platform ones where we have been given permissions for in this organization
|
|
781
|
+
for (const category of this.platform.config.recordsConfiguration.recordCategories) {
|
|
782
|
+
if (recordCategories.find(c => c.id === category.id)) {
|
|
793
783
|
// Already added
|
|
794
784
|
continue;
|
|
795
785
|
}
|
|
796
786
|
|
|
797
|
-
if (permissions.hasResourceAccess(PermissionsResourceType.RecordCategories, id, level)) {
|
|
798
|
-
|
|
799
|
-
if (category) {
|
|
800
|
-
recordCategories.push(category)
|
|
801
|
-
}
|
|
787
|
+
if (permissions.hasResourceAccess(PermissionsResourceType.RecordCategories, category.id, level)) {
|
|
788
|
+
recordCategories.push(category)
|
|
802
789
|
}
|
|
803
790
|
}
|
|
804
791
|
}
|
|
@@ -936,6 +923,7 @@ export class AdminPermissionChecker {
|
|
|
936
923
|
// Has financial read access?
|
|
937
924
|
if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
|
|
938
925
|
cloned.details.requiresFinancialSupport = null
|
|
926
|
+
cloned.details.uitpasNumber = null
|
|
939
927
|
cloned.outstandingBalance = 0
|
|
940
928
|
|
|
941
929
|
for (const registration of cloned.registrations) {
|
|
@@ -961,8 +949,9 @@ export class AdminPermissionChecker {
|
|
|
961
949
|
|
|
962
950
|
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
963
951
|
const hasNotes = data.details.notes !== undefined;
|
|
952
|
+
const isSetFinancialSupportTrue = data.details.requiresFinancialSupport?.value === true;
|
|
964
953
|
|
|
965
|
-
if(hasRecordAnswers || hasNotes) {
|
|
954
|
+
if(hasRecordAnswers || hasNotes || isSetFinancialSupportTrue) {
|
|
966
955
|
const isUserManager = this.isUserManager(member);
|
|
967
956
|
|
|
968
957
|
if (hasRecordAnswers) {
|
|
@@ -1017,6 +1006,20 @@ export class AdminPermissionChecker {
|
|
|
1017
1006
|
statusCode: 400
|
|
1018
1007
|
})
|
|
1019
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
if(isSetFinancialSupportTrue) {
|
|
1011
|
+
const financialSupport = this.platform.config.recordsConfiguration.financialSupport;
|
|
1012
|
+
const preventSelfAssignment = financialSupport?.preventSelfAssignment === true;
|
|
1013
|
+
|
|
1014
|
+
if(preventSelfAssignment) {
|
|
1015
|
+
throw new SimpleError({
|
|
1016
|
+
code: 'permission_denied',
|
|
1017
|
+
message: 'Je hebt geen toegangsrechten om de financiële status van dit lid aan te passen',
|
|
1018
|
+
human: financialSupport.preventSelfAssignmentText ?? FinancialSupportSettings.defaultPreventSelfAssignmentText,
|
|
1019
|
+
statusCode: 400
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1020
1023
|
}
|
|
1021
1024
|
|
|
1022
1025
|
// Has financial write access?
|
|
@@ -1029,6 +1032,14 @@ export class AdminPermissionChecker {
|
|
|
1029
1032
|
})
|
|
1030
1033
|
}
|
|
1031
1034
|
|
|
1035
|
+
if (data.details.uitpasNumber) {
|
|
1036
|
+
throw new SimpleError({
|
|
1037
|
+
code: 'permission_denied',
|
|
1038
|
+
message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
|
|
1039
|
+
statusCode: 400
|
|
1040
|
+
})
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1032
1043
|
if (data.outstandingBalance) {
|
|
1033
1044
|
throw new SimpleError({
|
|
1034
1045
|
code: 'permission_denied',
|
|
@@ -26,7 +26,7 @@ export class AuthenticatedStructures {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
|
|
29
|
-
const {registrations, orders
|
|
29
|
+
const {registrations, orders} = await Payment.loadBalanceItemRelations(balanceItems);
|
|
30
30
|
|
|
31
31
|
if (checkPermissions) {
|
|
32
32
|
// Note: permission checking is moved here for performacne to avoid loading the data multiple times
|
|
@@ -41,13 +41,10 @@ export class AuthenticatedStructures {
|
|
|
41
41
|
|
|
42
42
|
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
|
|
43
43
|
|
|
44
|
-
return
|
|
44
|
+
return Payment.getGeneralStructureFromRelations({
|
|
45
45
|
payments,
|
|
46
46
|
balanceItemPayments,
|
|
47
|
-
balanceItems
|
|
48
|
-
registrations,
|
|
49
|
-
orders,
|
|
50
|
-
groups
|
|
47
|
+
balanceItems
|
|
51
48
|
}, includeSettlements)
|
|
52
49
|
}
|
|
53
50
|
|
|
@@ -154,6 +151,19 @@ export class AuthenticatedStructures {
|
|
|
154
151
|
})
|
|
155
152
|
}
|
|
156
153
|
|
|
154
|
+
static async organizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
|
|
155
|
+
// for now simple loop
|
|
156
|
+
if (organizations.length > 10) {
|
|
157
|
+
console.warn('Trying to load too many organizations at once: ' + organizations.length)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const structs: OrganizationStruct[] = [];
|
|
161
|
+
for (const organization of organizations) {
|
|
162
|
+
structs.push(await this.organization(organization))
|
|
163
|
+
}
|
|
164
|
+
return structs
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
|
|
158
168
|
const structs: OrganizationStruct[] = [];
|
|
159
169
|
|
|
@@ -228,7 +238,6 @@ export class AuthenticatedStructures {
|
|
|
228
238
|
}
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
|
|
232
241
|
const blob = member.getStructureWithRegistrations()
|
|
233
242
|
memberBlobs.push(
|
|
234
243
|
await Context.auth.filterMemberData(member, blob)
|
package/src/helpers/Context.ts
CHANGED
|
@@ -60,6 +60,27 @@ export class ContextInstance {
|
|
|
60
60
|
return c;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
static async startForUser<T>(user: User, organization: Organization|null, handler: () => Promise<T>): Promise<T> {
|
|
64
|
+
const request = new Request({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: '/',
|
|
67
|
+
host: ''
|
|
68
|
+
})
|
|
69
|
+
const context = new ContextInstance(request);
|
|
70
|
+
|
|
71
|
+
if (organization) {
|
|
72
|
+
context.organization = organization
|
|
73
|
+
context.i18n.switchToLocale({ country: organization.address.country })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
context.user = user
|
|
77
|
+
context.#auth = new AdminPermissionChecker(user, await Platform.getSharedPrivateStruct(), context.organization);
|
|
78
|
+
|
|
79
|
+
return await this.asyncLocalStorage.run(context, async () => {
|
|
80
|
+
return await handler()
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
static async start<T>(request: Request, handler: () => Promise<T>): Promise<T> {
|
|
64
85
|
const context = new ContextInstance(request);
|
|
65
86
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Email } from "@stamhoofd/models";
|
|
1
|
+
import { Email, Organization, User } from "@stamhoofd/models";
|
|
2
2
|
import { SQL } from "@stamhoofd/sql";
|
|
3
3
|
import { EmailStatus } from "@stamhoofd/structures";
|
|
4
|
+
import { ContextInstance } from "./Context";
|
|
4
5
|
|
|
5
6
|
export async function resumeEmails() {
|
|
6
7
|
const query = SQL.select()
|
|
@@ -11,7 +12,26 @@ export async function resumeEmails() {
|
|
|
11
12
|
const emails = Email.fromRows(result, Email.table);
|
|
12
13
|
|
|
13
14
|
for (const email of emails) {
|
|
15
|
+
if (!email.userId) {
|
|
16
|
+
console.warn('Cannot retry sending email because userId is not set - which is required for setting the scope', email.id)
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
14
19
|
console.log('Resuming email that has sending status on boot', email.id);
|
|
15
|
-
|
|
20
|
+
|
|
21
|
+
const user = await User.getByID(email.userId);
|
|
22
|
+
if (!user) {
|
|
23
|
+
console.warn('Cannot retry sending email because user not found', email.id)
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const organization = email.organizationId ? (await Organization.getByID(email.organizationId)) : null;
|
|
28
|
+
if (organization === undefined) {
|
|
29
|
+
console.warn('Cannot retry sending email because organization not found', email.id)
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await ContextInstance.startForUser(user, organization, async () => {
|
|
34
|
+
await email.send()
|
|
35
|
+
})
|
|
16
36
|
}
|
|
17
37
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from "@stamhoofd/models";
|
|
2
2
|
import { SQL } from "@stamhoofd/sql";
|
|
3
|
-
import { Permissions, UserPermissions } from "@stamhoofd/structures";
|
|
3
|
+
import { MemberDetails, Permissions, UserPermissions } from "@stamhoofd/structures";
|
|
4
4
|
|
|
5
5
|
export class MemberUserSyncerStatic {
|
|
6
6
|
/**
|
|
@@ -8,15 +8,8 @@ export class MemberUserSyncerStatic {
|
|
|
8
8
|
* - responsibilities have changed
|
|
9
9
|
* - email addresses have changed
|
|
10
10
|
*/
|
|
11
|
-
async onChangeMember(member: MemberWithRegistrations) {
|
|
12
|
-
const userEmails =
|
|
13
|
-
|
|
14
|
-
if (member.details.email) {
|
|
15
|
-
userEmails.push(member.details.email)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const unverifiedEmails: string[] = member.details.unverifiedEmails;
|
|
19
|
-
const parentAndUnverifiedEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : []
|
|
11
|
+
async onChangeMember(member: MemberWithRegistrations, unlinkUsers: boolean = false) {
|
|
12
|
+
const {userEmails, parentAndUnverifiedEmails} = this.getMemberAccessEmails(member.details)
|
|
20
13
|
|
|
21
14
|
// Make sure all these users have access to the member
|
|
22
15
|
for (const email of userEmails) {
|
|
@@ -29,14 +22,49 @@ export class MemberUserSyncerStatic {
|
|
|
29
22
|
await this.linkUser(email, member, true)
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
if (unlinkUsers && !member.details.parentsHaveAccess) {
|
|
26
|
+
// Remove access of users that are not in this list
|
|
27
|
+
// NOTE: we should only do this once a year (preferably on the birthday of the member)
|
|
28
|
+
// only once because otherwise users loose the access to a member during the creation of the member, or when they have changed their email address
|
|
29
|
+
// users can regain access to a member after they have lost control by using the normal verification flow when detecting duplicate members
|
|
30
|
+
|
|
31
|
+
for (const user of member.users) {
|
|
32
|
+
if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
|
|
33
|
+
await this.unlinkUser(user, member)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
// Only auto unlink users that do not have an account
|
|
38
|
+
for (const user of member.users) {
|
|
39
|
+
if (!user.hasAccount() && !userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
|
|
40
|
+
await this.unlinkUser(user, member)
|
|
41
|
+
}
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
getMemberAccessEmails(details: MemberDetails) {
|
|
47
|
+
const userEmails = [...details.alternativeEmails]
|
|
48
|
+
|
|
49
|
+
if (details.email) {
|
|
50
|
+
userEmails.push(details.email)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const unverifiedEmails: string[] = details.unverifiedEmails;
|
|
54
|
+
const parentAndUnverifiedEmails = details.parentsHaveAccess ? details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : []
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
userEmails,
|
|
58
|
+
parentAndUnverifiedEmails,
|
|
59
|
+
emails: userEmails.concat(parentAndUnverifiedEmails)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
doesEmailHaveAccess(details: MemberDetails, email: string) {
|
|
64
|
+
const {emails} = this.getMemberAccessEmails(details)
|
|
65
|
+
return emails.includes(email)
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
async onDeleteMember(member: MemberWithRegistrations) {
|
|
41
69
|
for (const u of member.users) {
|
|
42
70
|
console.log("Unlinking user "+u.email+" from deleted member "+member.id)
|
|
@@ -22,33 +22,30 @@ export default new Migration(async () => {
|
|
|
22
22
|
value: id,
|
|
23
23
|
sign: '>'
|
|
24
24
|
}
|
|
25
|
-
}, {limit:
|
|
26
|
-
|
|
27
|
-
// const members = await Member.getByIDs(...rawMembers.map(m => m.id));
|
|
28
|
-
|
|
29
|
-
for (const member of rawMembers) {
|
|
30
|
-
const memberWithRegistrations = await Member.getWithRegistrations(member.id);
|
|
31
|
-
if(memberWithRegistrations) {
|
|
32
|
-
await memberWithRegistrations.updateMemberships();
|
|
33
|
-
await memberWithRegistrations.save();
|
|
34
|
-
} else {
|
|
35
|
-
throw new Error("Member with registrations not found: " + member.id);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
c++;
|
|
39
|
-
|
|
40
|
-
if (c%1000 === 0) {
|
|
41
|
-
process.stdout.write('.');
|
|
42
|
-
}
|
|
43
|
-
if (c%10000 === 0) {
|
|
44
|
-
process.stdout.write('\n');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
25
|
+
}, {limit: 500, sort: ['id']});
|
|
47
26
|
|
|
48
27
|
if (rawMembers.length === 0) {
|
|
49
28
|
break;
|
|
50
29
|
}
|
|
51
30
|
|
|
31
|
+
const promises: Promise<any>[] = [];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
for (const member of rawMembers) {
|
|
35
|
+
promises.push((async () => {
|
|
36
|
+
await Member.updateMembershipsForId(member.id, true);
|
|
37
|
+
c++;
|
|
38
|
+
|
|
39
|
+
if (c%1000 === 0) {
|
|
40
|
+
process.stdout.write('.');
|
|
41
|
+
}
|
|
42
|
+
if (c%10000 === 0) {
|
|
43
|
+
process.stdout.write('\n');
|
|
44
|
+
}
|
|
45
|
+
})())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await Promise.all(promises);
|
|
52
49
|
id = rawMembers[rawMembers.length - 1].id;
|
|
53
50
|
}
|
|
54
51
|
|