@stamhoofd/backend 2.62.0 → 2.64.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 (33) hide show
  1. package/index.ts +8 -6
  2. package/package.json +11 -11
  3. package/src/audit-logs/PaymentLogger.ts +1 -1
  4. package/src/crons/index.ts +1 -0
  5. package/src/crons/update-cached-balances.ts +39 -0
  6. package/src/email-recipient-loaders/receivable-balances.ts +5 -0
  7. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
  8. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
  9. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +85 -25
  11. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
  12. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +89 -30
  13. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +62 -17
  14. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +52 -2
  15. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +8 -2
  16. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
  17. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +19 -8
  18. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +10 -5
  19. package/src/helpers/AdminPermissionChecker.ts +3 -2
  20. package/src/helpers/AuthenticatedStructures.ts +127 -9
  21. package/src/helpers/MembershipCharger.ts +4 -0
  22. package/src/helpers/OrganizationCharger.ts +4 -0
  23. package/src/seeds/1733994455-balance-item-status-open.ts +30 -0
  24. package/src/seeds/1734596144-fill-previous-period-id.ts +55 -0
  25. package/src/seeds/1734700082-update-cached-outstanding-balance-from-items.ts +40 -0
  26. package/src/services/BalanceItemPaymentService.ts +8 -4
  27. package/src/services/BalanceItemService.ts +22 -3
  28. package/src/services/PaymentReallocationService.test.ts +746 -0
  29. package/src/services/PaymentReallocationService.ts +339 -0
  30. package/src/services/PaymentService.ts +13 -0
  31. package/src/services/PlatformMembershipService.ts +167 -137
  32. package/src/sql-filters/receivable-balances.ts +2 -1
  33. package/src/sql-sorters/receivable-balances.ts +3 -3
@@ -6,6 +6,7 @@ import { QueueHandler } from '@stamhoofd/queues';
6
6
  import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../../../helpers/Context';
9
+ import { BalanceItemService } from '../../../../services/BalanceItemService';
9
10
 
10
11
  type Params = Record<string, never>;
11
12
  type Query = undefined;
@@ -36,17 +37,13 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
36
37
  throw Context.auth.error();
37
38
  }
38
39
 
39
- if (request.body.changes.length == 0) {
40
+ if (request.body.changes.length === 0) {
40
41
  return new Response([]);
41
42
  }
42
43
 
43
44
  const returnedModels: BalanceItem[] = [];
44
45
  const updateOutstandingBalance: BalanceItem[] = [];
45
46
 
46
- // Keep track of updates
47
- const memberIds: string[] = [];
48
- const registrationIds: string[] = [];
49
-
50
47
  await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
51
48
  for (const { put } of request.body.getPuts()) {
52
49
  // Create a new balance item
@@ -58,7 +55,8 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
58
55
  model.amount = put.amount;
59
56
  model.organizationId = organization.id;
60
57
  model.createdAt = put.createdAt;
61
- model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
58
+ model.dueAt = put.dueAt;
59
+ model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Due;
62
60
 
63
61
  if (put.userId) {
64
62
  model.userId = (await this.validateUserId(model, put.userId)).id;
@@ -66,10 +64,43 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
66
64
 
67
65
  if (put.memberId) {
68
66
  model.memberId = (await this.validateMemberId(put.memberId)).id;
69
- memberIds.push(model.memberId);
70
67
  }
71
68
 
72
- if (!model.userId && !model.memberId) {
69
+ if (put.payingOrganizationId) {
70
+ // Not allowed if not full admin
71
+ if (!Context.auth.hasPlatformFullAccess()) {
72
+ throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
73
+ }
74
+ if (put.payingOrganizationId === model.organizationId) {
75
+ throw new SimpleError({
76
+ code: 'invalid_field',
77
+ message: 'payingOrganizationId cannot be the same as organizationId',
78
+ human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
79
+ field: 'payingOrganizationId',
80
+ });
81
+ }
82
+
83
+ model.payingOrganizationId = put.payingOrganizationId;
84
+ }
85
+
86
+ if (model.dueAt && model.price < 0) {
87
+ throw new SimpleError({
88
+ code: 'invalid_price',
89
+ message: 'Cannot create negative balance in the future',
90
+ human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
91
+ });
92
+ }
93
+
94
+ if (model.createdAt > new Date()) {
95
+ throw new SimpleError({
96
+ code: 'invalid_field',
97
+ message: 'createdAt cannot be in the future',
98
+ human: 'De datum kan niet in de toekomst liggen',
99
+ field: 'createdAt',
100
+ });
101
+ }
102
+
103
+ if (!model.userId && !model.memberId && !model.payingOrganizationId) {
73
104
  throw new SimpleError({
74
105
  code: 'invalid_field',
75
106
  message: 'No user or member provided',
@@ -93,39 +124,53 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
93
124
  });
94
125
  }
95
126
 
96
- if (patch.unitPrice !== undefined) {
97
- // throw new SimpleError({
98
- // code: 'invalid_field',
99
- // message: 'You cannot change the unit price of a balance item',
100
- // 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.',
101
- // });
102
- }
127
+ if (patch.payingOrganizationId !== undefined) {
128
+ // Not allowed if not full admin
129
+ if (!Context.auth.hasPlatformFullAccess()) {
130
+ throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
131
+ }
132
+ if (patch.payingOrganizationId === model.organizationId) {
133
+ throw new SimpleError({
134
+ code: 'invalid_field',
135
+ message: 'payingOrganizationId cannot be the same as organizationId',
136
+ human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
137
+ field: 'payingOrganizationId',
138
+ });
139
+ }
103
140
 
104
- // Check permissions
105
- if (model.memberId) {
106
- // Update old
107
- memberIds.push(model.memberId);
141
+ model.payingOrganizationId = patch.payingOrganizationId;
108
142
  }
109
143
 
110
144
  if (patch.memberId) {
111
145
  model.memberId = (await this.validateMemberId(patch.memberId)).id;
112
-
113
- // Update new
114
- memberIds.push(model.memberId);
115
- }
116
-
117
- if (model.registrationId) {
118
- // Update old
119
- registrationIds.push(model.registrationId);
120
146
  }
121
147
 
122
148
  if (patch.createdAt) {
123
149
  model.createdAt = patch.createdAt;
150
+
151
+ if (model.createdAt > new Date()) {
152
+ throw new SimpleError({
153
+ code: 'invalid_field',
154
+ message: 'createdAt cannot be in the future',
155
+ human: 'De datum kan niet in de toekomst liggen',
156
+ field: 'createdAt',
157
+ });
158
+ }
124
159
  }
125
160
 
126
161
  model.description = patch.description ?? model.description;
127
162
  model.unitPrice = patch.unitPrice ?? model.unitPrice;
128
163
  model.amount = patch.amount ?? model.amount;
164
+ model.dueAt = patch.dueAt === undefined ? model.dueAt : patch.dueAt;
165
+
166
+ if ((patch.dueAt !== undefined || patch.unitPrice !== undefined) && model.dueAt && model.price < 0) {
167
+ throw new SimpleError({
168
+ code: 'invalid_price',
169
+ message: 'Cannot create negative balance in the future',
170
+ human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
171
+ field: 'dueAt',
172
+ });
173
+ }
129
174
 
130
175
  if (model.orderId) {
131
176
  // Not allowed to change this
@@ -142,13 +187,21 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
142
187
  }
143
188
  }
144
189
  else if (patch.status) {
145
- model.status = model.pricePaid >= model.price ? BalanceItemStatus.Paid : BalanceItemStatus.Pending;
190
+ model.status = patch.status;
191
+ }
192
+
193
+ if (!model.userId && !model.memberId && !model.payingOrganizationId) {
194
+ throw new SimpleError({
195
+ code: 'invalid_field',
196
+ message: 'No user or member provided',
197
+ field: 'userId',
198
+ });
146
199
  }
147
200
 
148
201
  await model.save();
149
202
  returnedModels.push(model);
150
203
 
151
- if (patch.unitPrice || patch.amount || patch.status) {
204
+ if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined) {
152
205
  updateOutstandingBalance.push(model);
153
206
  }
154
207
  }
@@ -156,8 +209,14 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
156
209
 
157
210
  await BalanceItem.updateOutstanding(updateOutstandingBalance);
158
211
 
212
+ // Reallocate
213
+ await BalanceItemService.reallocate(updateOutstandingBalance, organization.id);
214
+
215
+ // Reload returnedModels
216
+ const returnedModelsReloaded = await BalanceItem.getByIDs(...returnedModels.map(m => m.id));
217
+
159
218
  return new Response(
160
- await BalanceItem.getStructureWithPayments(returnedModels),
219
+ await BalanceItem.getStructureWithPayments(returnedModelsReloaded),
161
220
  );
162
221
  }
163
222
 
@@ -1,15 +1,14 @@
1
1
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { BalanceItem, BalanceItemPayment, Payment, Token } from '@stamhoofd/models';
4
+ import { BalanceItem, BalanceItemPayment, Payment } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { Payment as PaymentStruct, PaymentGeneral, PaymentMethod, PaymentStatus, PermissionLevel } from '@stamhoofd/structures';
6
+ import { PaymentGeneral, PaymentMethod, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel } from '@stamhoofd/structures';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
9
  import { Context } from '../../../../helpers/Context';
10
- import { ExchangePaymentEndpoint } from '../../shared/ExchangePaymentEndpoint';
11
- import { PaymentService } from '../../../../services/PaymentService';
12
10
  import { BalanceItemService } from '../../../../services/BalanceItemService';
11
+ import { PaymentService } from '../../../../services/PaymentService';
13
12
 
14
13
  type Params = Record<string, never>;
15
14
  type Query = undefined;
@@ -49,11 +48,11 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
49
48
  // Modify payments
50
49
  for (const { put } of request.body.getPuts()) {
51
50
  // Create a new payment
52
- if (put.balanceItemPayments.length == 0) {
51
+ if (put.balanceItemPayments.length === 0) {
53
52
  throw new SimpleError({
54
53
  code: 'invalid_field',
55
54
  message: 'You need to add at least one balance item payment',
56
- human: 'Een betaling moet ten minste één afrekening bevatten voor een openstaande rekening.',
55
+ human: 'Een betaling moet ten minste één item bestaan',
57
56
  field: 'balanceItemPayments',
58
57
  });
59
58
  }
@@ -72,8 +71,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
72
71
  payment.status = PaymentStatus.Created;
73
72
  payment.method = put.method;
74
73
  payment.customer = put.customer;
74
+ payment.type = put.type;
75
75
 
76
- if (payment.method == PaymentMethod.Transfer) {
76
+ if (payment.method === PaymentMethod.Transfer) {
77
77
  if (!put.transferSettings) {
78
78
  throw new SimpleError({
79
79
  code: 'invalid_field',
@@ -112,7 +112,11 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
112
112
  balanceItemPayment.organizationId = organization.id;
113
113
  balanceItemPayment.balanceItemId = balanceItem.id;
114
114
  balanceItemPayment.price = item.price;
115
- balanceItemPayments.push(balanceItemPayment);
115
+
116
+ if (item.price !== 0) {
117
+ // Otherwise skip
118
+ balanceItemPayments.push(balanceItemPayment);
119
+ }
116
120
  }
117
121
 
118
122
  // Check permissions
@@ -122,17 +126,55 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
122
126
 
123
127
  // Check total price
124
128
  const totalPrice = balanceItemPayments.reduce((total, item) => total + item.price, 0);
129
+ payment.price = totalPrice;
125
130
 
126
- if (totalPrice !== put.price) {
127
- throw new SimpleError({
128
- code: 'invalid_field',
129
- message: "Total price doesn't match",
130
- human: 'De totale prijs komt niet overeen met de som van de afrekeningen',
131
- field: 'price',
132
- });
133
- }
131
+ switch (payment.type) {
132
+ case PaymentType.Payment: {
133
+ if (totalPrice <= 0) {
134
+ throw new SimpleError({
135
+ code: 'invalid_field',
136
+ message: 'The price should be greater than zero',
137
+ human: 'Het totaalbedrag moet groter zijn dan 0 euro',
138
+ field: 'price',
139
+ });
140
+ }
141
+ break;
142
+ }
134
143
 
135
- payment.price = totalPrice;
144
+ case PaymentType.Chargeback:
145
+ case PaymentType.Refund: {
146
+ if (totalPrice >= 0) {
147
+ throw new SimpleError({
148
+ code: 'invalid_field',
149
+ message: 'The price should be smaller than zero',
150
+ human: 'Het totaalbedrag moet kleiner zijn dan 0 euro',
151
+ field: 'price',
152
+ });
153
+ }
154
+ break;
155
+ }
156
+
157
+ case PaymentType.Reallocation:
158
+ {
159
+ if (totalPrice !== 0) {
160
+ throw new SimpleError({
161
+ code: 'invalid_field',
162
+ message: 'Total price should be zero',
163
+ human: 'Het totaalbedrag moet 0 euro zijn',
164
+ field: 'price',
165
+ });
166
+ }
167
+
168
+ if (balanceItemPayments.length < 2) {
169
+ throw new SimpleError({
170
+ code: 'missing_items',
171
+ message: 'At least two items are required for a reallocation',
172
+ human: 'Er moeten minstens twee items in een verrekening zitten',
173
+ });
174
+ }
175
+ break;
176
+ }
177
+ }
136
178
 
137
179
  // Save payment
138
180
  await payment.save();
@@ -158,6 +200,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
158
200
  }
159
201
 
160
202
  await BalanceItem.updateOutstanding(balanceItems);
203
+
204
+ // Reallocate
205
+ await BalanceItemService.reallocate(balanceItems, organization.id);
161
206
  }
162
207
 
163
208
  changedPayments.push(payment);
@@ -44,10 +44,19 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
44
44
  case ReceivableBalanceType.organization: {
45
45
  paymentModels = await Payment.select()
46
46
  .where('organizationId', organization.id)
47
- .where('payingOrganizationId', request.params.id)
48
47
  .andWhere(
49
48
  SQL.whereNot('status', PaymentStatus.Failed),
50
49
  )
50
+ .join(
51
+ SQL.join(BalanceItemPayment.table)
52
+ .where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
53
+ )
54
+ .join(
55
+ SQL.join(BalanceItem.table)
56
+ .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
57
+ )
58
+ .where(SQL.column(BalanceItem.table, 'payingOrganizationId'), request.params.id)
59
+ .groupBy(SQL.column(Payment.table, 'id'))
51
60
  .fetch();
52
61
  break;
53
62
  }
@@ -71,6 +80,46 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
71
80
  .fetch();
72
81
  break;
73
82
  }
83
+
84
+ case ReceivableBalanceType.user: {
85
+ paymentModels = await Payment.select()
86
+ .where('organizationId', organization.id)
87
+ .join(
88
+ SQL.join(BalanceItemPayment.table)
89
+ .where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
90
+ )
91
+ .join(
92
+ SQL.join(BalanceItem.table)
93
+ .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
94
+ )
95
+ .where(SQL.column(BalanceItem.table, 'userId'), request.params.id)
96
+ .andWhere(
97
+ SQL.whereNot('status', PaymentStatus.Failed),
98
+ )
99
+ .groupBy(SQL.column(Payment.table, 'id'))
100
+ .fetch();
101
+ break;
102
+ }
103
+
104
+ case ReceivableBalanceType.registration: {
105
+ paymentModels = await Payment.select()
106
+ .where('organizationId', organization.id)
107
+ .join(
108
+ SQL.join(BalanceItemPayment.table)
109
+ .where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
110
+ )
111
+ .join(
112
+ SQL.join(BalanceItem.table)
113
+ .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
114
+ )
115
+ .where(SQL.column(BalanceItem.table, 'registrationId'), request.params.id)
116
+ .andWhere(
117
+ SQL.whereNot('status', PaymentStatus.Failed),
118
+ )
119
+ .groupBy(SQL.column(Payment.table, 'id'))
120
+ .fetch();
121
+ break;
122
+ }
74
123
  }
75
124
 
76
125
  const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
@@ -79,8 +128,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
79
128
  const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
80
129
 
81
130
  const created = new CachedBalance();
82
- created.amount = 0;
131
+ created.amountOpen = 0;
83
132
  created.amountPending = 0;
133
+ created.amountPaid = 0;
84
134
  created.organizationId = organization.id;
85
135
  created.objectId = request.params.id;
86
136
  created.objectType = request.params.type;
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { CachedBalance } from '@stamhoofd/models';
5
5
  import { compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
6
- import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
6
+ import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter, ReceivableBalanceType } from '@stamhoofd/structures';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
9
  import { Context } from '../../../../helpers/Context';
@@ -49,8 +49,14 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
49
49
  scopeFilter = {
50
50
  organizationId: organization.id,
51
51
  $or: {
52
- amount: { $neq: 0 },
52
+ amountOpen: { $neq: 0 },
53
53
  amountPending: { $neq: 0 },
54
+ nextDueAt: { $neq: null },
55
+ },
56
+ $not: {
57
+ objectType: {
58
+ $in: Context.auth.hasSomePlatformAccess() ? [ReceivableBalanceType.registration] : [ReceivableBalanceType.organization, ReceivableBalanceType.registration],
59
+ },
54
60
  },
55
61
  };
56
62
 
@@ -346,7 +346,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
346
346
  model.settings.period = period.getBaseStructure();
347
347
 
348
348
  if (model.type !== GroupType.EventRegistration) {
349
- model.settings.startDate = period.startDate;
349
+ // Note: start date is curomizable, as long as it stays between period start and end
350
+ if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
351
+ model.settings.startDate = period.startDate;
352
+ }
353
+
350
354
  model.settings.endDate = period.endDate;
351
355
  }
352
356
  }
@@ -446,9 +450,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
446
450
  model.status = struct.status;
447
451
  model.type = struct.type;
448
452
  model.settings.period = period.getBaseStructure();
449
- model.settings.startDate = period.startDate;
450
453
  model.settings.endDate = period.endDate;
451
454
 
455
+ // Note: start date is curomizable, as long as it stays between period start and end
456
+ if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
457
+ model.settings.startDate = period.startDate;
458
+ }
459
+
452
460
  if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
453
461
  // Create a temporary permission role for this user
454
462
  const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId);
@@ -169,7 +169,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
169
169
  balanceItem.description = webshop.meta.name;
170
170
  balanceItem.pricePaid = 0;
171
171
  balanceItem.organizationId = organization.id;
172
- balanceItem.status = BalanceItemStatus.Pending;
172
+ balanceItem.status = BalanceItemStatus.Due;
173
173
  balanceItem.relations = new Map([
174
174
  [
175
175
  BalanceItemRelationType.Webshop,
@@ -271,13 +271,23 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
271
271
  const items = await BalanceItem.where({ orderId: model.id });
272
272
  if (items.length >= 1) {
273
273
  model.markUpdated();
274
- items[0].unitPrice = model.totalToPay;
275
- items[0].description = model.generateBalanceDescription(webshop);
276
- items[0].updateStatus();
277
- await items[0].save();
274
+
275
+ const paidItem = items.find(i => i.status === BalanceItemStatus.Due && i.pricePaid !== 0) ?? items[0];
276
+
277
+ paidItem.unitPrice = model.data.totalPrice;
278
+ paidItem.amount = 1;
279
+
280
+ if (model.isDue) {
281
+ paidItem.status = BalanceItemStatus.Due;
282
+ }
283
+ else {
284
+ paidItem.status = BalanceItemStatus.Canceled;
285
+ }
286
+ paidItem.description = model.generateBalanceDescription(webshop);
287
+ await paidItem.save();
278
288
 
279
289
  // Zero out the other items
280
- const otherItems = items.slice(1);
290
+ const otherItems = items.filter(i => i.id !== paidItem.id);
281
291
  await BalanceItem.deleteItems(otherItems);
282
292
  }
283
293
  else if (items.length === 0
@@ -285,11 +295,12 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
285
295
  model.markUpdated();
286
296
  const balanceItem = new BalanceItem();
287
297
  balanceItem.orderId = model.id;
288
- balanceItem.unitPrice = model.totalToPay;
298
+ balanceItem.unitPrice = model.data.totalPrice;
299
+ balanceItem.amount = 1;
300
+ balanceItem.status = BalanceItemStatus.Due;
289
301
  balanceItem.description = model.generateBalanceDescription(webshop);
290
302
  balanceItem.pricePaid = 0;
291
303
  balanceItem.organizationId = organization.id;
292
- balanceItem.status = BalanceItemStatus.Pending;
293
304
  await balanceItem.save();
294
305
  }
295
306
  }
@@ -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, BalanceItemType, BalanceItemRelationType, BalanceItemRelation, AuditLogSource } 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, AuditLogSource, PaymentCustomer } from '@stamhoofd/structures';
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
 
12
12
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
@@ -169,6 +169,11 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
169
169
  payment.status = PaymentStatus.Created;
170
170
  payment.price = totalPrice;
171
171
  payment.paidAt = null;
172
+ payment.customer = PaymentCustomer.create({
173
+ firstName: request.body.customer.firstName,
174
+ lastName: request.body.customer.lastName,
175
+ email: request.body.customer.email,
176
+ });
172
177
 
173
178
  // Determine the payment provider
174
179
  // Throws if invalid
@@ -219,18 +224,18 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
219
224
  let paymentUrl: string | null = null;
220
225
  const description = webshop.meta.name + ' - ' + payment.id;
221
226
 
222
- if (payment.method == PaymentMethod.Transfer) {
227
+ if (payment.method === PaymentMethod.Transfer) {
223
228
  await order.markValid(payment, []);
224
229
 
225
230
  if (order.number) {
226
231
  balanceItem.description = order.generateBalanceDescription(webshop);
227
232
  }
228
233
 
229
- balanceItem.status = BalanceItemStatus.Pending;
234
+ balanceItem.status = BalanceItemStatus.Due;
230
235
  await balanceItem.save();
231
236
  await payment.save();
232
237
  }
233
- else if (payment.method == PaymentMethod.PointOfSale) {
238
+ else if (payment.method === PaymentMethod.PointOfSale) {
234
239
  // Not really paid, but needed to create the tickets if needed
235
240
  await order.markPaid(payment, organization, webshop);
236
241
 
@@ -238,7 +243,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
238
243
  balanceItem.description = order.generateBalanceDescription(webshop);
239
244
  }
240
245
 
241
- balanceItem.status = BalanceItemStatus.Pending;
246
+ balanceItem.status = BalanceItemStatus.Due;
242
247
  await balanceItem.save();
243
248
  await payment.save();
244
249
  }
@@ -263,7 +263,7 @@ export class AdminPermissionChecker {
263
263
  }
264
264
 
265
265
  const cachedBalance = await CachedBalance.getForObjects([member.id]);
266
- if (cachedBalance.length === 0 || (cachedBalance[0].amount === 0 && cachedBalance[0].amountPending === 0)) {
266
+ if (cachedBalance.length === 0 || (cachedBalance[0].amountOpen === 0 && cachedBalance[0].amountPending === 0)) {
267
267
  return true;
268
268
  }
269
269
  }
@@ -1063,6 +1063,7 @@ export class AdminPermissionChecker {
1063
1063
  for (const registration of cloned.registrations) {
1064
1064
  registration.price = 0;
1065
1065
  registration.pricePaid = 0;
1066
+ registration.balances = [];
1066
1067
  }
1067
1068
  }
1068
1069
 
@@ -1310,7 +1311,7 @@ export class AdminPermissionChecker {
1310
1311
  }
1311
1312
 
1312
1313
  hasSomePlatformAccess(): boolean {
1313
- return !!this.platformPermissions;
1314
+ return !!this.platformPermissions && !this.platformPermissions.isEmpty;
1314
1315
  }
1315
1316
 
1316
1317
  canManagePlatformAdmins() {