@stamhoofd/backend 2.36.2 → 2.37.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.36.2",
3
+ "version": "2.37.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -36,14 +36,14 @@
36
36
  "@simonbackx/simple-encoding": "2.15.1",
37
37
  "@simonbackx/simple-endpoints": "1.14.0",
38
38
  "@simonbackx/simple-logging": "^1.0.1",
39
- "@stamhoofd/backend-i18n": "2.36.2",
40
- "@stamhoofd/backend-middleware": "2.36.2",
41
- "@stamhoofd/email": "2.36.2",
42
- "@stamhoofd/models": "2.36.2",
43
- "@stamhoofd/queues": "2.36.2",
44
- "@stamhoofd/sql": "2.36.2",
45
- "@stamhoofd/structures": "2.36.2",
46
- "@stamhoofd/utility": "2.36.2",
39
+ "@stamhoofd/backend-i18n": "2.37.0",
40
+ "@stamhoofd/backend-middleware": "2.37.0",
41
+ "@stamhoofd/email": "2.37.0",
42
+ "@stamhoofd/models": "2.37.0",
43
+ "@stamhoofd/queues": "2.37.0",
44
+ "@stamhoofd/sql": "2.37.0",
45
+ "@stamhoofd/structures": "2.37.0",
46
+ "@stamhoofd/utility": "2.37.0",
47
47
  "archiver": "^7.0.1",
48
48
  "aws-sdk": "^2.885.0",
49
49
  "axios": "1.6.8",
@@ -60,5 +60,5 @@
60
60
  "postmark": "^4.0.5",
61
61
  "stripe": "^16.6.0"
62
62
  },
63
- "gitHead": "7802a792419ab01fd96ac9e0db86f7553b3e6619"
63
+ "gitHead": "c01fc82d46188cc4e35a7edb90ee595ff7129e6f"
64
64
  }
@@ -85,6 +85,18 @@ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, Respons
85
85
  if (StringCompare.typoCount(request.query.query, "Nederland") < 3) {
86
86
  countries.push(Country.Netherlands)
87
87
  }
88
+
89
+ if (StringCompare.typoCount(request.query.query, "Luxemburg") < 3) {
90
+ countries.push(Country.Luxembourg)
91
+ }
92
+
93
+ if (StringCompare.typoCount(request.query.query, "Duitsland") < 3) {
94
+ countries.push(Country.Germany)
95
+ }
96
+
97
+ if (StringCompare.typoCount(request.query.query, "Frankrijk") < 3) {
98
+ countries.push(Country.France)
99
+ }
88
100
 
89
101
  return new Response(SearchRegions.create({
90
102
  cities: loadedCities.map(c => CityStruct.create(Object.assign({...c}, { province: ProvinceStruct.create(c.province) }))),
@@ -83,6 +83,8 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
83
83
  model.fromAddress = request.body.fromAddress;
84
84
  model.fromName = request.body.fromName;
85
85
 
86
+ model.validateAttachments()
87
+
86
88
  // Check default
87
89
  if (JSON.stringify(model.json).length < 3 && model.recipientFilter.filters[0].type && EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)) {
88
90
  const type = EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)
@@ -89,6 +89,12 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
89
89
  rebuild = true;
90
90
  }
91
91
 
92
+ // Attachments
93
+ if (request.body.attachments !== undefined) {
94
+ model.attachments = patchObject(model.attachments, request.body.attachments);
95
+ model.validateAttachments()
96
+ }
97
+
92
98
  await model.save();
93
99
 
94
100
  if (rebuild) {
@@ -71,8 +71,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
71
71
  }
72
72
  return null
73
73
  }
74
- const updateGroups = new Map<string, Group>();
75
- const updateRegistrations = new Map<string, Registration>();
74
+
76
75
  const updateMembershipMemberIds = new Set<string>()
77
76
 
78
77
  // Loop all members one by one
@@ -489,8 +488,30 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
489
488
  }
490
489
  }
491
490
 
491
+ await PatchOrganizationMembersEndpoint.deleteMembers(request.body.getDeletes())
492
+
493
+ for (const member of members) {
494
+ if (updateMembershipMemberIds.has(member.id)) {
495
+ await member.updateMemberships()
496
+ }
497
+ }
498
+
499
+ if(shouldUpdateSetupSteps && organization) {
500
+ SetupStepUpdater.updateForOrganization(organization).catch(console.error);
501
+ }
502
+
503
+ return new Response(
504
+ await AuthenticatedStructures.membersBlob(members)
505
+ );
506
+ }
507
+
508
+ static async deleteMembers(ids: string[]) {
509
+ const updateGroups = new Set<string>();
510
+ const updateRegistrations = new Map<string, Registration>();
511
+ const updateSteps = new Set<string>();
512
+
492
513
  // Loop all members one by one
493
- for (const id of request.body.getDeletes()) {
514
+ for (const id of ids) {
494
515
  const member = await Member.getWithRegistrations(id)
495
516
  if (!member || !await Context.auth.canDeleteMember(member)) {
496
517
  throw Context.auth.error("Je hebt niet voldoende rechten om dit lid te verwijderen")
@@ -500,16 +521,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
500
521
  await User.deleteForDeletedMember(member.id)
501
522
  await BalanceItem.deleteForDeletedMember(member.id)
502
523
  await member.delete()
503
- shouldUpdateSetupSteps = true
504
524
 
505
525
  for(const registration of member.registrations) {
506
526
  const groupId = registration.groupId;
507
- const group = await getGroup(groupId);
508
527
  updateRegistrations.set(registration.id, registration);
509
- if (group) {
510
- // We need to update this group occupancy because we moved one member away from it
511
- updateGroups.set(group.id, group)
512
- }
528
+ updateGroups.add(groupId);
529
+ updateSteps.add(registration.organizationId);
513
530
  }
514
531
  }
515
532
 
@@ -517,25 +534,19 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
517
534
  registration.scheduleStockUpdate();
518
535
  }
519
536
 
537
+ const groups = await Group.getByIDs(...Array.from(updateGroups));
538
+
520
539
  // Loop all groups and update occupancy if needed
521
- for (const group of updateGroups.values()) {
540
+ for (const group of groups) {
522
541
  await group.updateOccupancy()
523
542
  await group.save()
524
543
  }
525
-
526
- for (const member of members) {
527
- if (updateMembershipMemberIds.has(member.id)) {
528
- await member.updateMemberships()
529
- }
530
- }
531
544
 
532
- if(shouldUpdateSetupSteps && organization) {
545
+ const organizations = await Organization.getByIDs(...Array.from(updateSteps));
546
+
547
+ for (const organization of organizations) {
533
548
  SetupStepUpdater.updateForOrganization(organization).catch(console.error);
534
549
  }
535
-
536
- return new Response(
537
- await AuthenticatedStructures.membersBlob(members)
538
- );
539
550
  }
540
551
 
541
552
  static async checkDuplicate(member: Member) {
@@ -134,6 +134,8 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
134
134
  // Give access to created members
135
135
  await Member.users.reverse("members").link(user, addedMembers)
136
136
  }
137
+
138
+ await PatchOrganizationMembersEndpoint.deleteMembers(request.body.getDeletes())
137
139
 
138
140
  members = await Member.getMembersWithRegistrationForUser(user)
139
141
 
@@ -0,0 +1,453 @@
1
+ import { Request } from "@simonbackx/simple-endpoints";
2
+ import { Group, GroupFactory, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from "@stamhoofd/models";
3
+ import { GroupPrice, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, PayconiqAccount, PaymentMethod, PermissionLevel, Permissions, Version } from "@stamhoofd/structures";
4
+ import nock from "nock";
5
+ import { v4 as uuidv4 } from "uuid";
6
+ import { testServer } from "../../../../tests/helpers/TestServer";
7
+ import { RegisterMembersEndpoint } from "./RegisterMembersEndpoint";
8
+
9
+ const baseUrl = `/v${Version}/members/register`
10
+
11
+ describe("Endpoint.RegisterMembers", () => {
12
+ //#region global
13
+ const endpoint = new RegisterMembersEndpoint();
14
+ let period: RegistrationPeriod;
15
+ let organization: Organization;
16
+ let user: User;
17
+ let token: Token;
18
+ let member: MemberWithRegistrations;
19
+ let group1: Group;
20
+ let groupPrice1: GroupPrice;
21
+ let group2: Group;
22
+ let groupPrice2: GroupPrice;
23
+
24
+ //#region helpers
25
+ const post = async (body: IDRegisterCheckout) => {
26
+ const request = Request.buildJson("POST", baseUrl,organization.getApiHost(), body);
27
+ request.headers.authorization = "Bearer "+token.accessToken;
28
+ return await testServer.test(endpoint, request);
29
+ }
30
+ //#endregion
31
+
32
+ //#endregion
33
+
34
+ beforeAll(async () => {
35
+ period = await new RegistrationPeriodFactory({}).create();
36
+ organization = await new OrganizationFactory({ period }).create();
37
+ organization.meta.registrationPaymentConfiguration.paymentMethods = [PaymentMethod.PointOfSale, PaymentMethod.Payconiq];
38
+
39
+ organization.privateMeta.payconiqAccounts = [PayconiqAccount.create({
40
+ id: uuidv4(),
41
+ apiKey: 'test',
42
+ merchantId: 'test',
43
+ profileId: 'test',
44
+ name: 'test',
45
+ iban: 'BE56587127952688', // = random IBAN
46
+ callbackUrl: 'https://example.com'
47
+ })]
48
+
49
+ user = await new UserFactory({
50
+ organization,
51
+ permissions: Permissions.create({
52
+ level: PermissionLevel.Full
53
+ })
54
+ }).create();
55
+ token = await Token.createToken(user);
56
+ member = await new MemberFactory({ organization, user }).create();
57
+ });
58
+
59
+ beforeEach(async () => {
60
+ //#region groups
61
+ group1 = await new GroupFactory({
62
+ organization,
63
+ price: 25,
64
+ stock: 5
65
+ }).create();
66
+
67
+ groupPrice1 = group1.settings.prices[0];
68
+
69
+ group2 = await new GroupFactory({
70
+ organization,
71
+ price: 15,
72
+ stock: 4,
73
+ maxMembers: 1
74
+ }).create();
75
+
76
+ groupPrice2 = group2.settings.prices[0];
77
+ //#endregion
78
+ });
79
+
80
+ describe('Register member', () => {
81
+
82
+ test("Should update registered mmebers", async () => {
83
+ //#region arrange
84
+ const body = IDRegisterCheckout.create({
85
+ cart: IDRegisterCart.create({
86
+ items: [
87
+ IDRegisterItem.create({
88
+ id: uuidv4(),
89
+ replaceRegistrationIds: [],
90
+ options: [],
91
+ groupPrice: groupPrice1,
92
+ organizationId: organization.id,
93
+ groupId: group1.id,
94
+ memberId: member.id
95
+ })
96
+ ],
97
+ balanceItems: [],
98
+ deleteRegistrationIds: []
99
+ }),
100
+ administrationFee: 0,
101
+ freeContribution: 0,
102
+ paymentMethod: PaymentMethod.PointOfSale,
103
+ totalPrice: 25,
104
+ asOrganizationId: organization.id,
105
+ customer: null,
106
+ });
107
+ //#endregion
108
+
109
+ // act
110
+ const response = await post(body);
111
+
112
+ // assert
113
+ expect(response.body).toBeDefined();
114
+ expect(response.body.registrations.length).toBe(1);
115
+
116
+ const updatedGroup = await Group.getByID(group1.id);
117
+ expect(updatedGroup!.settings.registeredMembers).toBe(1);
118
+ expect(updatedGroup!.settings.reservedMembers).toBe(0);
119
+ })
120
+
121
+ test("Should update reserved members", async () => {
122
+ //#region arrange
123
+ const body = IDRegisterCheckout.create({
124
+ cart: IDRegisterCart.create({
125
+ items: [
126
+ IDRegisterItem.create({
127
+ id: uuidv4(),
128
+ replaceRegistrationIds: [],
129
+ options: [],
130
+ groupPrice: groupPrice2,
131
+ organizationId: organization.id,
132
+ groupId: group2.id,
133
+ memberId: member.id
134
+ })
135
+ ],
136
+ balanceItems: [],
137
+ deleteRegistrationIds: []
138
+ }),
139
+ administrationFee: 0,
140
+ freeContribution: 0,
141
+ paymentMethod: PaymentMethod.Payconiq,
142
+ redirectUrl: new URL("https://www.example.com"),
143
+ cancelUrl: new URL("https://www.example.com"),
144
+ totalPrice: 15,
145
+ customer: null,
146
+ });
147
+
148
+ nock('https://api.ext.payconiq.com')
149
+ .post('/v3/payments')
150
+ .reply(200, {
151
+ paymentId: 'testPaymentId',
152
+ _links: {
153
+ checkout: {
154
+ href: 'https://www.example.com'
155
+ }
156
+ }
157
+ });
158
+ //#endregion
159
+
160
+ // act
161
+ const response = await post(body);
162
+
163
+ // assert
164
+ expect(response.body).toBeDefined();
165
+ expect(response.body.registrations.length).toBe(1);
166
+
167
+ const updatedGroup = await Group.getByID(group2.id);
168
+ expect(updatedGroup!.settings.registeredMembers).toBe(0);
169
+ expect(updatedGroup!.settings.reservedMembers).toBe(1);
170
+ })
171
+ })
172
+
173
+ describe('Register member with replace registration', () => {
174
+
175
+ test("Should update registered members", async () => {
176
+ //#region arrange
177
+ const registration = await new RegistrationFactory({
178
+ member,
179
+ group: group1,
180
+ groupPrice: groupPrice1
181
+ })
182
+ .create();
183
+
184
+ const group = await new GroupFactory({
185
+ organization,
186
+ price: 30,
187
+ stock: 5
188
+ }).create();
189
+
190
+ const groupPrice = group.settings.prices[0];
191
+
192
+ const body = IDRegisterCheckout.create({
193
+ cart: IDRegisterCart.create({
194
+ items: [
195
+ IDRegisterItem.create({
196
+ id: uuidv4(),
197
+ replaceRegistrationIds: [registration.id],
198
+ options: [],
199
+ groupPrice,
200
+ organizationId: organization.id,
201
+ groupId: group.id,
202
+ memberId: member.id
203
+ })
204
+ ],
205
+ balanceItems: [],
206
+ deleteRegistrationIds: []
207
+ }),
208
+ administrationFee: 0,
209
+ freeContribution: 0,
210
+ paymentMethod: PaymentMethod.PointOfSale,
211
+ totalPrice: 5,
212
+ asOrganizationId: organization.id,
213
+ customer: null,
214
+ });
215
+ //#endregion
216
+
217
+ //#region act and assert
218
+
219
+ // update occupancy to be sure occupancy is 1
220
+ await group1.updateOccupancy();
221
+ expect(group1.settings.registeredMembers).toBe(1);
222
+
223
+ // send request and check occupancy
224
+ const response = await post(body);
225
+
226
+ expect(response.body).toBeDefined();
227
+ expect(response.body.registrations.length).toBe(1);
228
+
229
+ const updatedGroup = await Group.getByID(group.id);
230
+ expect(updatedGroup!.settings.registeredMembers).toBe(1);
231
+ expect(updatedGroup!.settings.reservedMembers).toBe(0);
232
+
233
+ const updatedGroup1After = await Group.getByID(group1.id);
234
+ // occupancy should go from 1 to 0 because the registration should be replaced
235
+ expect(updatedGroup1After!.settings.registeredMembers).toBe(0);
236
+ expect(updatedGroup1After!.settings.reservedMembers).toBe(0);
237
+ //#endregion
238
+ })
239
+
240
+ test("Should throw error if with payment", async () => {
241
+ //#region arrange
242
+ const registration = await new RegistrationFactory({
243
+ member,
244
+ group: group1,
245
+ groupPrice: groupPrice1
246
+ })
247
+ .create();
248
+
249
+ const group = await new GroupFactory({
250
+ organization,
251
+ price: 30,
252
+ stock: 5,
253
+ maxMembers: 1
254
+ }).create();
255
+
256
+ const groupPrice = group.settings.prices[0];
257
+
258
+ const body = IDRegisterCheckout.create({
259
+ cart: IDRegisterCart.create({
260
+ items: [
261
+ IDRegisterItem.create({
262
+ id: uuidv4(),
263
+ replaceRegistrationIds: [registration.id],
264
+ options: [],
265
+ groupPrice,
266
+ organizationId: organization.id,
267
+ groupId: group.id,
268
+ memberId: member.id
269
+ })
270
+ ],
271
+ balanceItems: [],
272
+ deleteRegistrationIds: []
273
+ }),
274
+ administrationFee: 0,
275
+ freeContribution: 0,
276
+ paymentMethod: PaymentMethod.Payconiq,
277
+ redirectUrl: new URL("https://www.example.com"),
278
+ cancelUrl: new URL("https://www.example.com"),
279
+ totalPrice: 5,
280
+ customer: null,
281
+ });
282
+ //#endregion
283
+
284
+ //#region act and assert
285
+
286
+ // update occupancy to be sure occupancy is 1
287
+ await group1.updateOccupancy();
288
+ expect(group1.settings.registeredMembers).toBe(1);
289
+
290
+ await expect(async () => await post(body)).rejects.toThrow("Not allowed to move registrations");
291
+ //#endregion
292
+ })
293
+ })
294
+
295
+ describe('Register member with delete registration', () => {
296
+
297
+ test("Should update registered members", async () => {
298
+ //#region arrange
299
+ const registration = await new RegistrationFactory({
300
+ member,
301
+ group: group1,
302
+ groupPrice: groupPrice1
303
+ })
304
+ .create();
305
+
306
+ const group = await new GroupFactory({
307
+ organization,
308
+ price: 30,
309
+ stock: 5
310
+ }).create();
311
+
312
+ const groupPrice = group.settings.prices[0];
313
+
314
+ const body = IDRegisterCheckout.create({
315
+ cart: IDRegisterCart.create({
316
+ items: [
317
+ IDRegisterItem.create({
318
+ id: uuidv4(),
319
+ replaceRegistrationIds: [],
320
+ options: [],
321
+ groupPrice,
322
+ organizationId: organization.id,
323
+ groupId: group.id,
324
+ memberId: member.id
325
+ })
326
+ ],
327
+ balanceItems: [],
328
+ deleteRegistrationIds: [registration.id]
329
+ }),
330
+ administrationFee: 0,
331
+ freeContribution: 0,
332
+ paymentMethod: PaymentMethod.PointOfSale,
333
+ totalPrice: 5,
334
+ asOrganizationId: organization.id,
335
+ customer: null,
336
+ });
337
+ //#endregion
338
+
339
+ //#region act and assert
340
+
341
+ // update occupancy to be sure occupancy is 1
342
+ await group1.updateOccupancy();
343
+ expect(group1.settings.registeredMembers).toBe(1);
344
+
345
+ // send request and check occupancy
346
+ const response = await post(body);
347
+
348
+ expect(response.body).toBeDefined();
349
+ expect(response.body.registrations.length).toBe(1);
350
+
351
+ const updatedGroup = await Group.getByID(group.id);
352
+ expect(updatedGroup!.settings.registeredMembers).toBe(1);
353
+ expect(updatedGroup!.settings.reservedMembers).toBe(0);
354
+
355
+ const updatedGroup1After = await Group.getByID(group1.id);
356
+ // occupancy should go from 1 to 0 because the registration should be deleted
357
+ expect(updatedGroup1After!.settings.registeredMembers).toBe(0);
358
+ expect(updatedGroup1After!.settings.reservedMembers).toBe(0);
359
+ //#endregion
360
+ })
361
+
362
+ test("Should throw error if with payment", async () => {
363
+ //#region arrange
364
+ const registration = await new RegistrationFactory({
365
+ member,
366
+ group: group1,
367
+ groupPrice: groupPrice1
368
+ })
369
+ .create();
370
+
371
+ const group = await new GroupFactory({
372
+ organization,
373
+ price: 30,
374
+ stock: 5,
375
+ maxMembers: 1
376
+ }).create();
377
+
378
+ const groupPrice = group.settings.prices[0];
379
+
380
+ const body = IDRegisterCheckout.create({
381
+ cart: IDRegisterCart.create({
382
+ items: [
383
+ IDRegisterItem.create({
384
+ id: uuidv4(),
385
+ replaceRegistrationIds: [],
386
+ options: [],
387
+ groupPrice,
388
+ organizationId: organization.id,
389
+ groupId: group.id,
390
+ memberId: member.id
391
+ })
392
+ ],
393
+ balanceItems: [],
394
+ deleteRegistrationIds: [registration.id]
395
+ }),
396
+ administrationFee: 0,
397
+ freeContribution: 0,
398
+ paymentMethod: PaymentMethod.Payconiq,
399
+ redirectUrl: new URL("https://www.example.com"),
400
+ cancelUrl: new URL("https://www.example.com"),
401
+ totalPrice: 5,
402
+ customer: null,
403
+ });
404
+ //#endregion
405
+
406
+ //#region act and assert
407
+
408
+ // update occupancy to be sure occupancy is 1
409
+ await group1.updateOccupancy();
410
+ expect(group1.settings.registeredMembers).toBe(1);
411
+
412
+ await expect(async () => await post(body)).rejects.toThrow("Permission denied: you are not allowed to delete registrations");
413
+ //#endregion
414
+ })
415
+ })
416
+
417
+ it('Register member that is already registered should throw error', async () => {
418
+ // create existing registration
419
+ await new RegistrationFactory({
420
+ member,
421
+ group: group1,
422
+ groupPrice: groupPrice1
423
+ })
424
+ .create();
425
+
426
+ // register again
427
+ const body = IDRegisterCheckout.create({
428
+ cart: IDRegisterCart.create({
429
+ items: [
430
+ IDRegisterItem.create({
431
+ id: uuidv4(),
432
+ replaceRegistrationIds: [],
433
+ options: [],
434
+ groupPrice: groupPrice1,
435
+ organizationId: organization.id,
436
+ groupId: group1.id,
437
+ memberId: member.id
438
+ })
439
+ ],
440
+ balanceItems: [],
441
+ deleteRegistrationIds: []
442
+ }),
443
+ administrationFee: 0,
444
+ freeContribution: 0,
445
+ paymentMethod: PaymentMethod.PointOfSale,
446
+ totalPrice: groupPrice1.price.price,
447
+ asOrganizationId: organization.id,
448
+ customer: null,
449
+ });
450
+
451
+ await expect(async () => await post(body)).rejects.toThrow("Already registered");
452
+ })
453
+ })
@@ -55,6 +55,17 @@ export class AddressValidatorStatic {
55
55
  })
56
56
  }
57
57
  }
58
+
59
+ if (address.country !== Country.Belgium && address.country !== Country.Netherlands) {
60
+ // No validation for other countries
61
+ return ValidatedAddress.create(Object.assign({ ... address }, {
62
+ postalCode: address.postalCode,
63
+ city: address.city,
64
+ cityId: 'unknown',
65
+ parentCityId: null,
66
+ provinceId: 'unknown',
67
+ }))
68
+ }
58
69
 
59
70
  const city = await PostalCode.getCity(postalCode, address.city, address.country)
60
71
 
@@ -1,6 +1,6 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
2
2
  import { SimpleError } from "@simonbackx/simple-errors"
3
- import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
3
+ import { BalanceItem, CachedOutstandingBalance, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
4
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
 
@@ -250,7 +250,20 @@ export class AdminPermissionChecker {
250
250
  * Only full admins can delete members permanently
251
251
  */
252
252
  async canDeleteMember(member: MemberWithRegistrations) {
253
+ if (member.registrations.length === 0 && this.isUserManager(member)) {
254
+ const platformMemberships = await MemberPlatformMembership.where({ memberId: member.id })
255
+ if (platformMemberships.length === 0) {
256
+ return true;
257
+ }
258
+
259
+ const cachedBalance = await CachedOutstandingBalance.getForObjects([member.id])
260
+ if (cachedBalance.length === 0 || (cachedBalance[0].amount === 0 && cachedBalance[0].amountPending === 0)) {
261
+ return true;
262
+ }
263
+ }
264
+
253
265
  if (member.organizationId) {
266
+ // Not a platform
254
267
  return await this.hasFullAccess(member.organizationId)
255
268
  }
256
269
  return this.hasPlatformFullAccess()
@@ -0,0 +1,40 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { logger } from '@simonbackx/simple-logging';
3
+ import { BalanceItem } from '@stamhoofd/models';
4
+
5
+ export default new Migration(async () => {
6
+ if (STAMHOOFD.environment == "test") {
7
+ console.log("skipped in tests")
8
+ return;
9
+ }
10
+
11
+ process.stdout.write('\n');
12
+ let c = 0;
13
+ let id: string = '';
14
+
15
+ await logger.setContext({tags: ['silent-seed', 'seed']}, async () => {
16
+ while(true) {
17
+ const items = await BalanceItem.where({
18
+ id: {
19
+ value: id,
20
+ sign: '>'
21
+ }
22
+ }, {limit: 1000, sort: ['id']});
23
+
24
+ await BalanceItem.updateOutstanding(items)
25
+
26
+ c += items.length;
27
+ process.stdout.write('.');
28
+
29
+ if (items.length < 1000) {
30
+ break;
31
+ }
32
+ id = items[items.length - 1].id;
33
+ }
34
+ })
35
+
36
+ console.log("Updated outstanding balance for " + c + " items")
37
+
38
+ // Do something here
39
+ return Promise.resolve()
40
+ })