@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.
Files changed (32) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +3 -3
  3. package/src/crons.ts +3 -3
  4. package/src/decoders/StringArrayDecoder.ts +24 -0
  5. package/src/decoders/StringNullableDecoder.ts +18 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +14 -0
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  9. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  10. package/src/endpoints/global/members/GetMembersEndpoint.ts +0 -31
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +34 -367
  12. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  13. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -11
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +205 -110
  15. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -3
  16. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  17. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  18. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -2
  19. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -40
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  24. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  25. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  26. package/src/helpers/AdminPermissionChecker.ts +35 -24
  27. package/src/helpers/AuthenticatedStructures.ts +16 -7
  28. package/src/helpers/Context.ts +21 -0
  29. package/src/helpers/EmailResumer.ts +22 -2
  30. package/src/helpers/MemberUserSyncer.ts +42 -14
  31. package/src/seeds/1722344160-update-membership.ts +19 -22
  32. 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 { MemberBalanceItem } from "@stamhoofd/structures";
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 = MemberBalanceItem[]
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.getMemberStructure(balanceItems)
45
+ await BalanceItem.getStructureWithPayments(balanceItems)
46
46
  );
47
47
  }
48
48
  }
@@ -1,54 +1,17 @@
1
- import { AutoEncoder, Data, DateDecoder, Decoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
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, MemberBalanceItem, PermissionLevel } from "@stamhoofd/structures";
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<MemberBalanceItem>
15
- type ResponseBody = MemberBalanceItem[]
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(MemberBalanceItem as Decoder<MemberBalanceItem>, MemberBalanceItem.patchType() as Decoder<AutoEncoderPatchType<MemberBalanceItem>>, StringDecoder)
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.price = put.price;
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.price = patch.price ?? model.price;
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.price = order.totalToPay
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.getMemberStructure(returnedModels)
155
+ await BalanceItem.getStructureWithPayments(returnedModels)
171
156
  );
172
157
  }
173
158
 
@@ -274,6 +274,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
274
274
  }
275
275
  }
276
276
  }
277
+
277
278
 
278
279
  await model.updateOccupancy()
279
280
  await model.save();
@@ -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.price = totalPrice
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].price = model.totalToPay
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.price = model.totalToPay
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.price = totalPrice
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 (const [id] of organization.meta.recordsConfiguration.inheritedRecordCategories) {
792
- if (recordCategories.find(c => c.id === id)) {
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
- const category = this.platform.config.recordsConfiguration.recordCategories.find(c => c.id === id)
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, groups} = await Payment.loadBalanceItemRelations(balanceItems);
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 await Payment.getGeneralStructureFromRelations({
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)
@@ -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
- await email.send()
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 = [...member.details.alternativeEmails]
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
- // Remove access of users that are not in this list
33
- for (const user of member.users) {
34
- if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
35
- await this.unlinkUser(user, member)
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: 100, sort: ['id']});
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