@stamhoofd/backend 2.1.3 → 2.2.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.1.3",
3
+ "version": "2.2.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -50,5 +50,5 @@
50
50
  "postmark": "4.0.2",
51
51
  "stripe": "^11.5.0"
52
52
  },
53
- "gitHead": "b5773c547dceaae52b9c2a098b222417fcd42e4f"
53
+ "gitHead": "d67b2ac7c63880195902d29df682a2f2a709b82b"
54
54
  }
@@ -0,0 +1,47 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../helpers/Context';
6
+ import { GetInvoicesEndpoint } from './GetInvoicesEndpoint';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetInvoicesCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method != "GET") {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, "/admin/invoices/count", {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ await Context.authenticate()
31
+
32
+ if (!Context.auth.hasPlatformFullAccess()) {
33
+ throw Context.auth.error()
34
+ }
35
+
36
+ const query = GetInvoicesEndpoint.buildQuery(request.query)
37
+
38
+ const count = await query
39
+ .count();
40
+
41
+ return new Response(
42
+ CountResponse.create({
43
+ count
44
+ })
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,185 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ import { Decoder } from '@simonbackx/simple-encoding';
3
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
4
+ import { SimpleError } from '@simonbackx/simple-errors';
5
+ import { Organization, Payment, STInvoice } from '@stamhoofd/models';
6
+ import { SQL, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLExpressionFilterCompiler } from "@stamhoofd/sql";
7
+ import { CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, Payment as PaymentStruct, STInvoicePrivate, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
8
+
9
+ import { Context } from '../../../helpers/Context';
10
+
11
+ type Params = Record<string, never>;
12
+ type Query = LimitedFilteredRequest;
13
+ type Body = undefined;
14
+ type ResponseBody = PaginatedResponse<STInvoicePrivate[], LimitedFilteredRequest>
15
+
16
+ export const filterCompilers: SQLFilterDefinitions = {
17
+ ...baseSQLFilterCompilers,
18
+ id: createSQLExpressionFilterCompiler(
19
+ SQL.column('stamhoofd_invoices', 'id')
20
+ ),
21
+ number: createSQLExpressionFilterCompiler(
22
+ SQL.column('stamhoofd_invoices', 'number')
23
+ )
24
+ }
25
+
26
+ const sorters: SQLSortDefinitions<STInvoice> = {
27
+ 'id': {
28
+ getValue(a) {
29
+ return a.id
30
+ },
31
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
32
+ return new SQLOrderBy({
33
+ column: SQL.column('id'),
34
+ direction
35
+ })
36
+ }
37
+ },
38
+ 'number': {
39
+ getValue(a) {
40
+ return a.number
41
+ },
42
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
43
+ return new SQLOrderBy({
44
+ column: SQL.column('number'),
45
+ direction
46
+ })
47
+ }
48
+ }
49
+ }
50
+
51
+ export class GetInvoicesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
52
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
53
+
54
+ protected doesMatch(request: Request): [true, Params] | [false] {
55
+ if (request.method != "GET") {
56
+ return [false];
57
+ }
58
+
59
+ const params = Endpoint.parseParameters(request.url, "/admin/invoices", {});
60
+
61
+ if (params) {
62
+ return [true, params as Params];
63
+ }
64
+ return [false];
65
+ }
66
+
67
+ static buildQuery(q: CountFilteredRequest|LimitedFilteredRequest) {
68
+ const query = SQL
69
+ .select(
70
+ SQL.wildcard('stamhoofd_invoices')
71
+ )
72
+ .from(
73
+ SQL.table('stamhoofd_invoices')
74
+ )
75
+ .whereNot(SQL.column('stamhoofd_invoices', 'number'), null);
76
+
77
+
78
+ if (q.filter) {
79
+ query.where(compileToSQLFilter(q.filter, filterCompilers))
80
+ }
81
+
82
+ if (q.search) {
83
+ let searchFilter: StamhoofdFilter|null = null
84
+
85
+ // todo: auto detect e-mailaddresses and search on admins
86
+ searchFilter = {
87
+ name: {
88
+ $contains: q.search
89
+ }
90
+ }
91
+
92
+ if (searchFilter) {
93
+ query.where(compileToSQLFilter(searchFilter, filterCompilers))
94
+ }
95
+ }
96
+
97
+ if (q instanceof LimitedFilteredRequest) {
98
+ if (q.pageFilter) {
99
+ query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
100
+ }
101
+
102
+ query.orderBy(compileToSQLSorter(q.sort, sorters))
103
+ query.limit(q.limit)
104
+ }
105
+
106
+ return query
107
+ }
108
+
109
+ async handle(request: DecodedRequest<Params, Query, Body>) {
110
+ await Context.authenticate()
111
+
112
+ if (!Context.auth.hasPlatformFullAccess()) {
113
+ throw Context.auth.error()
114
+ }
115
+
116
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
117
+
118
+ if (request.query.limit > maxLimit) {
119
+ throw new SimpleError({
120
+ code: 'invalid_field',
121
+ field: 'limit',
122
+ message: 'Limit can not be more than ' + maxLimit
123
+ })
124
+ }
125
+
126
+ if (request.query.limit < 1) {
127
+ throw new SimpleError({
128
+ code: 'invalid_field',
129
+ field: 'limit',
130
+ message: 'Limit can not be less than 1'
131
+ })
132
+ }
133
+
134
+ const data = await GetInvoicesEndpoint.buildQuery(request.query).fetch()
135
+ const invoices = STInvoice.fromRows(data, 'stamhoofd_invoices');
136
+
137
+ let next: LimitedFilteredRequest|undefined;
138
+
139
+ if (invoices.length >= request.query.limit) {
140
+ const lastObject = invoices[invoices.length - 1];
141
+ const nextFilter = getSortFilter(lastObject, sorters, request.query.sort);
142
+
143
+ next = new LimitedFilteredRequest({
144
+ filter: request.query.filter,
145
+ pageFilter: nextFilter,
146
+ sort: request.query.sort,
147
+ limit: request.query.limit,
148
+ search: request.query.search
149
+ })
150
+
151
+ if (JSON.stringify(nextFilter) === JSON.stringify(request.query.pageFilter)) {
152
+ console.error('Found infinite loading loop for', request.query);
153
+ next = undefined;
154
+ }
155
+ }
156
+
157
+ // Get payments + organizations
158
+ const paymentIds = invoices.flatMap(i => i.paymentId ? [i.paymentId] : [])
159
+ const organizationIds = invoices.flatMap(i => i.organizationId ? [i.organizationId] : [])
160
+
161
+ const payments = await Payment.getByIDs(...paymentIds)
162
+ const organizations = await Organization.getByIDs(...organizationIds)
163
+
164
+ const structures: STInvoicePrivate[] = []
165
+ for (const invoice of invoices) {
166
+ const payment = payments.find(p => p.id === invoice.paymentId)
167
+ const organization = organizations.find(p => p.id === invoice.organizationId)
168
+ structures.push(
169
+ STInvoicePrivate.create({
170
+ ...invoice,
171
+ payment: payment ? PaymentStruct.create(payment) : null,
172
+ organization: organization ? (await organization.getStructure({emptyGroups: true})) : undefined,
173
+ settlement: payment?.settlement ?? null,
174
+ })
175
+ )
176
+ }
177
+
178
+ return new Response(
179
+ new PaginatedResponse<STInvoicePrivate[], LimitedFilteredRequest>({
180
+ results: structures,
181
+ next
182
+ })
183
+ );
184
+ }
185
+ }
@@ -0,0 +1,88 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { SQL, SQLAlias, SQLSum, SQLCount, SQLDistinct, SQLSelectAs } from '@stamhoofd/sql';
3
+ import { ChargeMembershipsSummary } from '@stamhoofd/structures';
4
+ import { Context } from '../../../helpers/Context';
5
+
6
+
7
+ type Params = Record<string, never>;
8
+ type Query = Record<string, never>;
9
+ type Body = undefined;
10
+ type ResponseBody = ChargeMembershipsSummary;
11
+
12
+ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
+ protected doesMatch(request: Request): [true, Params] | [false] {
14
+ if (request.method != "GET") {
15
+ return [false];
16
+ }
17
+
18
+ const params = Endpoint.parseParameters(request.url, "/admin/charge-memberships/summary", {});
19
+
20
+ if (params) {
21
+ return [true, params as Params];
22
+ }
23
+ return [false];
24
+ }
25
+
26
+ async handle(request: DecodedRequest<Params, Query, Body>) {
27
+ await Context.authenticate()
28
+
29
+ if (!Context.auth.hasPlatformFullAccess()) {
30
+ throw Context.auth.error()
31
+ }
32
+
33
+ const query = SQL
34
+ .select(
35
+ new SQLSelectAs(
36
+ new SQLCount(
37
+ new SQLDistinct(
38
+ SQL.column('member_platform_memberships', 'id')
39
+ )
40
+ ),
41
+ new SQLAlias('data__memberships')
42
+ ),
43
+ new SQLSelectAs(
44
+ new SQLCount(
45
+ new SQLDistinct(
46
+ SQL.column('member_platform_memberships', 'memberId')
47
+ )
48
+ ),
49
+ new SQLAlias('data__members')
50
+ ),
51
+ new SQLSelectAs(
52
+ new SQLCount(
53
+ new SQLDistinct(
54
+ SQL.column('member_platform_memberships', 'organizationId')
55
+ )
56
+ ),
57
+ new SQLAlias('data__organizations')
58
+ ),
59
+ new SQLSelectAs(
60
+ new SQLSum(
61
+ SQL.column('member_platform_memberships', 'price')
62
+ ),
63
+ new SQLAlias('data__price')
64
+ )
65
+ )
66
+ .from(
67
+ SQL.table('member_platform_memberships')
68
+ )
69
+ .where(SQL.column('invoiceId'), null)
70
+ .andWhere(SQL.column('invoiceItemDetailId'), null);
71
+
72
+
73
+ const result = await query.fetch();
74
+ const members = result[0]['data']['members'] as number;
75
+ const memberships = result[0]['data']['memberships'] as number;
76
+ const organizations = result[0]['data']['organizations'] as number;
77
+ const price = result[0]['data']['price'] as number;
78
+
79
+ return new Response(
80
+ ChargeMembershipsSummary.create({
81
+ memberships: memberships ?? 0,
82
+ members: members ?? 0,
83
+ price: price ?? 0,
84
+ organizations: organizations ?? 0
85
+ })
86
+ );
87
+ }
88
+ }
@@ -271,11 +271,13 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
271
271
  async handle(request: DecodedRequest<Params, Query, Body>) {
272
272
  await Context.authenticate()
273
273
 
274
- if (request.query.limit > 100) {
274
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
275
+
276
+ if (request.query.limit > maxLimit) {
275
277
  throw new SimpleError({
276
278
  code: 'invalid_field',
277
279
  field: 'limit',
278
- message: 'Limit can not be more than 100'
280
+ message: 'Limit can not be more than ' + maxLimit
279
281
  })
280
282
  }
281
283
 
@@ -136,6 +136,7 @@ const filterCompilers: SQLFilterDefinitions = {
136
136
  SQL.column('members', 'id'),
137
137
  ),
138
138
  {
139
+ ...baseSQLFilterCompilers,
139
140
  // Alias for responsibilityId
140
141
  "id": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'responsibilityId')),
141
142
  "responsibilityId": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'responsibilityId')),
@@ -145,6 +146,29 @@ const filterCompilers: SQLFilterDefinitions = {
145
146
  }
146
147
  ),
147
148
 
149
+ platformMemberships: createSQLRelationFilterCompiler(
150
+ SQL.select()
151
+ .from(
152
+ SQL.table('member_platform_memberships')
153
+ )
154
+ .where(
155
+ SQL.column('memberId'),
156
+ SQL.column('members', 'id'),
157
+ ),
158
+ {
159
+ ...baseSQLFilterCompilers,
160
+ "id": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'id')),
161
+ "membershipTypeId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'membershipTypeId')),
162
+ "organizationId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'organizationId')),
163
+ "periodId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'periodId')),
164
+ "price": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'price')),
165
+ "invoiceId": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'invoiceId')),
166
+ "startDate": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'startDate')),
167
+ "endDate": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'endDate')),
168
+ "expireDate": createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'expireDate')),
169
+ }
170
+ ),
171
+
148
172
  /**
149
173
  * @deprecated?
150
174
  */
@@ -388,11 +412,13 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
388
412
  await Context.setOptionalOrganizationScope();
389
413
  await Context.authenticate()
390
414
 
391
- if (request.query.limit > 100) {
415
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
416
+
417
+ if (request.query.limit > maxLimit) {
392
418
  throw new SimpleError({
393
419
  code: 'invalid_field',
394
420
  field: 'limit',
395
- message: 'Limit can not be more than 100'
421
+ message: 'Limit can not be more than ' + maxLimit
396
422
  })
397
423
  }
398
424
 
@@ -2,8 +2,8 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
2
2
  import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
4
  import { SimpleError } from "@simonbackx/simple-errors";
5
- import { BalanceItem, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
- import { BalanceItemStatus, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
5
+ import { BalanceItem, MemberPlatformMembership, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
+ import { BalanceItemStatus, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -52,6 +52,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
52
52
 
53
53
  const members: MemberWithRegistrations[] = []
54
54
 
55
+ const platform = await Platform.getShared()
56
+
55
57
  // Cache
56
58
  const groups: Group[] = []
57
59
 
@@ -71,6 +73,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
71
73
 
72
74
  const balanceItemMemberIds: string[] = []
73
75
  const balanceItemRegistrationIdsPerOrganization: Map<string, string[]> = new Map()
76
+ const updateMembershipMemberIds = new Set<string>()
74
77
 
75
78
  function addBalanceItemRegistrationId(organizationId: string, registrationId: string) {
76
79
  const existing = balanceItemRegistrationIdsPerOrganization.get(organizationId);
@@ -169,6 +172,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
169
172
  await member.save()
170
173
  members.push(member)
171
174
  balanceItemMemberIds.push(member.id)
175
+ updateMembershipMemberIds.add(member.id)
172
176
 
173
177
  // Add registrations
174
178
  for (const registrationStruct of struct.registrations) {
@@ -232,7 +236,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
232
236
  }
233
237
 
234
238
  let group: Group | null = null
235
-
239
+
240
+ console.log('Patch registration', patchRegistration)
241
+
242
+ if (patchRegistration.group) {
243
+ patchRegistration.groupId = patchRegistration.group.id
244
+ }
236
245
 
237
246
  if (patchRegistration.groupId) {
238
247
  group = await getGroup(patchRegistration.groupId)
@@ -291,6 +300,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
291
300
  }
292
301
  registration.cycle = patchRegistration.cycle ?? registration.cycle
293
302
  registration.groupId = patchRegistration.groupId ?? registration.groupId
303
+ registration.group = group
294
304
  registration.organizationId = patchRegistration.organizationId ?? registration.organizationId
295
305
 
296
306
  // Check if we should create a placeholder payment?
@@ -338,6 +348,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
338
348
  }
339
349
 
340
350
  await registration.save()
351
+ updateMembershipMemberIds.add(member.id)
341
352
  }
342
353
 
343
354
  for (const deleteId of patch.registrations.getDeletes()) {
@@ -362,7 +373,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
362
373
  })
363
374
  }
364
375
 
365
- balanceItemMemberIds.push(member.id)
376
+ balanceItemMemberIds.push(member.id)
377
+ updateMembershipMemberIds.add(member.id)
366
378
  await BalanceItem.deleteForDeletedRegistration(registration.id)
367
379
  await registration.delete()
368
380
  member.registrations = member.registrations.filter(r => r.id !== deleteId)
@@ -391,6 +403,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
391
403
 
392
404
  const reg = await this.addRegistration(member, struct, group)
393
405
  balanceItemMemberIds.push(member.id)
406
+ updateMembershipMemberIds.add(member.id)
394
407
  addBalanceItemRegistrationId(reg.organizationId, reg.id)
395
408
 
396
409
  // We need to update this group occupancy because we moved one member away from it
@@ -506,6 +519,97 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
506
519
  await PatchOrganizationMembersEndpoint.updateManagers(member)
507
520
  }
508
521
 
522
+ // Add platform memberships
523
+ for (const {put} of patch.platformMemberships.getPuts()) {
524
+ if (put.periodId !== platform.periodId) {
525
+ throw new SimpleError({
526
+ code: "invalid_field",
527
+ message: "Invalid period",
528
+ human: "Je kan geen aansluitingen maken voor een andere werkjaar dan het actieve werkjaar",
529
+ field: "periodId"
530
+ })
531
+ }
532
+
533
+ if (organization && put.organizationId !== organization.id) {
534
+ throw new SimpleError({
535
+ code: "invalid_field",
536
+ message: "Invalid organization",
537
+ human: "Je kan geen aansluitingen maken voor een andere vereniging",
538
+ field: "organizationId"
539
+ })
540
+ }
541
+
542
+ if (!await Context.auth.hasFullAccess(put.organizationId)) {
543
+ throw Context.auth.error("Je hebt niet voldoende rechten om deze aansluiting toe te voegen")
544
+ }
545
+
546
+ if (!platform.config.membershipTypes.find(t => t.id === put.membershipTypeId)) {
547
+ throw new SimpleError({
548
+ code: "invalid_field",
549
+ field: "membershipTypeId",
550
+ message: "Invalid membership type",
551
+ human: "Dit aansluitingstype bestaat niet"
552
+ })
553
+ }
554
+
555
+ // Check duplicate memberships
556
+
557
+ // Check dates
558
+
559
+ // Calculate prices
560
+
561
+ const membership = new MemberPlatformMembership()
562
+ membership.id = put.id
563
+ membership.memberId = member.id
564
+ membership.membershipTypeId = put.membershipTypeId
565
+ membership.organizationId = put.organizationId
566
+ membership.periodId = put.periodId
567
+
568
+ membership.startDate = put.startDate
569
+ membership.endDate = put.endDate
570
+ membership.expireDate = put.expireDate
571
+
572
+ await membership.calculatePrice()
573
+ await membership.save()
574
+ }
575
+
576
+ // Delete platform memberships
577
+ for (const id of patch.platformMemberships.getDeletes()) {
578
+ const membership = await MemberPlatformMembership.getByID(id)
579
+
580
+ if (!membership || membership.memberId !== member.id) {
581
+ throw new SimpleError({
582
+ code: "invalid_field",
583
+ field: "id",
584
+ message: "Invalid id",
585
+ human: "Deze aansluiting bestaat niet"
586
+ })
587
+ }
588
+
589
+ if (!await Context.auth.hasFullAccess(membership.organizationId)) {
590
+ throw Context.auth.error("Je hebt niet voldoende rechten om deze aansluiting te verwijderen")
591
+ }
592
+
593
+ if (membership.periodId !== platform.periodId) {
594
+ throw new SimpleError({
595
+ code: "invalid_field",
596
+ message: "Invalid period",
597
+ human: "Je kan geen aansluitingen meer verwijderen voor een ander werkjaar dan het actieve werkjaar",
598
+ field: "periodId"
599
+ })
600
+ }
601
+
602
+ if (membership.invoiceId || membership.invoiceItemDetailId) {
603
+ throw new SimpleError({
604
+ code: "invalid_field",
605
+ message: "Invalid invoice",
606
+ human: "Je kan geen aansluiting verwijderen die al werd gefactureerd",
607
+ })
608
+ }
609
+
610
+ await membership.delete()
611
+ }
612
+
509
613
  if (!members.find(m => m.id === member.id)) {
510
614
  members.push(member)
511
615
  }
@@ -553,6 +657,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
553
657
  }
554
658
  }
555
659
 
660
+ for (const member of members) {
661
+ if (updateMembershipMemberIds.has(member.id)) {
662
+ await member.updateMemberships()
663
+ }
664
+ }
665
+
556
666
  return new Response(
557
667
  await AuthenticatedStructures.membersBlob(members)
558
668
  );
@@ -200,11 +200,17 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
200
200
 
201
201
  if (item.waitingList) {
202
202
  registration.waitingList = true
203
+ registration.canRegister = false
203
204
  registration.reservedUntil = null
204
205
  await registration.save()
205
206
  } else {
206
- registration.waitingList = false
207
- registration.canRegister = false
207
+ if (registration.waitingList && registration.canRegister) {
208
+ // Keep data: otherwise people cannot retry if the payment fails
209
+ // We'll mark the registration as valid after the payment
210
+ } else {
211
+ registration.waitingList = false
212
+ registration.canRegister = false
213
+ }
208
214
  registration.price = item.calculatedPrice
209
215
  payRegistrations.push({
210
216
  registration,
@@ -286,6 +292,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
286
292
  // registration.paymentId = payment.id
287
293
 
288
294
  registration.reservedUntil = null
295
+ registration.canRegister = false
289
296
 
290
297
  if (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale || payment.status == PaymentStatus.Succeeded) {
291
298
  await registration.markValid()
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from "@simonbackx/simple-errors";
2
- import { Group, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
3
- import { OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, User as UserStruct, Group as GroupStruct, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, Webshop as WebshopStruct,WebshopPreview, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
2
+ import { Group, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
3
+ import { MemberPlatformMembership as MemberPlatformMembershipStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, User as UserStruct, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Context } from "./Context";
6
6
 
@@ -145,9 +145,11 @@ export class AuthenticatedStructures {
145
145
 
146
146
  // Load responsibilities
147
147
  const responsibilities = members.length > 0 ? await MemberResponsibilityRecord.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
148
+ const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ memberId: { sign: 'IN', value: members.map(m => m.id) } }) : []
148
149
 
149
150
  for (const blob of memberBlobs) {
150
151
  blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => MemberResponsibilityRecordStruct.create(r))
152
+ blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r))
151
153
  }
152
154
 
153
155
  return MembersBlob.create({