@stamhoofd/backend 2.106.1 → 2.107.1

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 (30) hide show
  1. package/index.ts +6 -1
  2. package/package.json +17 -11
  3. package/src/boot.ts +28 -22
  4. package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +89 -0
  5. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +30 -0
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +3 -2
  7. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -3
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +109 -109
  9. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +7 -0
  10. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -7
  11. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +7 -0
  12. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +9 -2
  13. package/src/excel-loaders/payments.ts +5 -5
  14. package/src/excel-loaders/receivable-balances.ts +7 -7
  15. package/src/helpers/AdminPermissionChecker.ts +20 -0
  16. package/src/helpers/BuckarooHelper.ts +1 -1
  17. package/src/helpers/ServiceFeeHelper.ts +8 -4
  18. package/src/helpers/StripeHelper.ts +20 -35
  19. package/src/seeds/1752848561-groups-registration-periods.ts +35 -0
  20. package/src/services/BalanceItemService.ts +15 -2
  21. package/src/services/PaymentReallocationService.test.ts +298 -128
  22. package/src/services/PaymentReallocationService.ts +46 -16
  23. package/src/services/PaymentService.ts +49 -2
  24. package/src/services/uitpas/getSocialTariffForEvent.ts +2 -2
  25. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +2 -2
  26. package/src/services/uitpas/registerTicketSales.ts +2 -2
  27. package/tests/e2e/bundle-discounts.test.ts +415 -391
  28. package/tests/e2e/documents.test.ts +21 -21
  29. package/tests/e2e/register.test.ts +93 -93
  30. package/tests/e2e/stock.test.ts +4 -4
@@ -277,6 +277,13 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
277
277
  }
278
278
  }
279
279
 
280
+ if (totalPrice % 100 !== 0) {
281
+ throw new SimpleError({
282
+ code: 'more_than_2_decimal_places',
283
+ message: 'Unexpected total price. The total price should be rounded to maximum 2 decimal places',
284
+ });
285
+ }
286
+
280
287
  const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
281
288
  registrationMemberRelation.foreignKey = 'memberId';
282
289
 
@@ -42,7 +42,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
42
42
  }
43
43
 
44
44
  const returnedModels: BalanceItem[] = [];
45
- const updateOutstandingBalance: BalanceItem[] = [];
46
45
 
47
46
  // Tracking changes
48
47
  const additionalItems: { memberId: string; organizationId: string }[] = [];
@@ -60,6 +59,9 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
60
59
  model.createdAt = put.createdAt;
61
60
  model.dueAt = put.dueAt;
62
61
  model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Due;
62
+ model.VATIncluded = put.VATIncluded;
63
+ model.VATPercentage = put.VATPercentage;
64
+ model.VATExcempt = put.VATExcempt;
63
65
 
64
66
  if (put.userId) {
65
67
  model.userId = (await this.validateUserId(model, put.userId)).id;
@@ -113,8 +115,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
113
115
 
114
116
  await model.save();
115
117
  returnedModels.push(model);
116
-
117
- updateOutstandingBalance.push(model);
118
118
  }
119
119
 
120
120
  for (const patch of request.body.getPatches()) {
@@ -186,6 +186,9 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
186
186
  model.unitPrice = patch.unitPrice ?? model.unitPrice;
187
187
  model.amount = patch.amount ?? model.amount;
188
188
  model.dueAt = patch.dueAt === undefined ? model.dueAt : patch.dueAt;
189
+ model.VATIncluded = patch.VATIncluded === undefined ? model.VATIncluded : patch.VATIncluded;
190
+ model.VATPercentage = patch.VATPercentage === undefined ? model.VATPercentage : patch.VATPercentage;
191
+ model.VATExcempt = patch.VATExcempt === undefined ? model.VATExcempt : patch.VATExcempt;
189
192
 
190
193
  if ((patch.dueAt !== undefined || patch.unitPrice !== undefined) && model.dueAt && model.price < 0) {
191
194
  throw new SimpleError({
@@ -224,13 +227,12 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
224
227
 
225
228
  await model.save();
226
229
  returnedModels.push(model);
227
-
228
- if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined || patch.memberId || patch.userId) {
229
- updateOutstandingBalance.push(model);
230
- }
231
230
  }
232
231
  });
233
232
 
233
+ // Update balances before we return the up to date versions
234
+ await BalanceItemService.flushCaches(organization.id);
235
+
234
236
  // Reload returnedModels
235
237
  const returnedModelsReloaded = await BalanceItem.getByIDs(...returnedModels.map(m => m.id));
236
238
 
@@ -73,6 +73,10 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
73
73
  payment.customer = put.customer;
74
74
  payment.type = put.type;
75
75
 
76
+ if (payment.type === PaymentType.Reallocation) {
77
+ payment.method = PaymentMethod.Unknown;
78
+ }
79
+
76
80
  if (payment.method === PaymentMethod.Transfer) {
77
81
  if (!put.transferSettings) {
78
82
  throw new SimpleError({
@@ -273,6 +277,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
273
277
  });
274
278
  }
275
279
 
280
+ // Update balances before we return the up to date versions
281
+ await BalanceItemService.flushCaches(organization.id);
282
+
276
283
  return new Response(
277
284
  await AuthenticatedStructures.paymentsGeneral(changedPayments, true),
278
285
  );
@@ -156,8 +156,15 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
156
156
  // The order now is valid, the stock is reserved for now (until the payment fails or expires)
157
157
  const totalPrice = request.body.totalPrice;
158
158
 
159
+ if (totalPrice % 100 !== 0) {
160
+ throw new SimpleError({
161
+ code: 'more_than_2_decimal_places',
162
+ message: 'Unexpected total price. The total price should be rounded to maximum 2 decimal places',
163
+ });
164
+ }
165
+
159
166
  try {
160
- if (totalPrice == 0) {
167
+ if (totalPrice === 0) {
161
168
  // Force unknown payment method
162
169
  order.data.paymentMethod = PaymentMethod.Unknown;
163
170
 
@@ -306,7 +313,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
306
313
  const molliePayment = await mollieClient.payments.create({
307
314
  amount: {
308
315
  currency: 'EUR',
309
- value: (totalPrice / 100).toFixed(2),
316
+ value: (totalPrice / 10000).toFixed(2), // from 4 decimals to 0 decimals
310
317
  },
311
318
  method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
312
319
  testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
@@ -210,7 +210,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<PaymentWithItem>[] {
210
210
  name: $t(`7f7fdce2-1fcd-44c9-8c98-856aea11ffc3`),
211
211
  width: 20,
212
212
  getValue: (object: PaymentWithItem) => ({
213
- value: object.balanceItemPayment.unitPrice / 100,
213
+ value: object.balanceItemPayment.unitPrice / 1_0000,
214
214
  style: {
215
215
  numberFormat: {
216
216
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -223,7 +223,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<PaymentWithItem>[] {
223
223
  name: $t(`6f3104d4-9b8f-4946-8434-77202efae9f0`),
224
224
  width: 20,
225
225
  getValue: (object: PaymentWithItem) => ({
226
- value: object.balanceItemPayment.price / 100,
226
+ value: object.balanceItemPayment.price / 1_0000,
227
227
  style: {
228
228
  numberFormat: {
229
229
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -254,7 +254,7 @@ function getGeneralColumns(): XlsxTransformerConcreteColumn<PaymentGeneral>[] {
254
254
  name: $t(`61b7b9cb-287a-4655-bac2-bb2d0b83fe47`),
255
255
  width: 10,
256
256
  getValue: (object: PaymentGeneralWithStripeAccount) => ({
257
- value: object.price / 100,
257
+ value: object.price / 1_0000,
258
258
  style: {
259
259
  numberFormat: {
260
260
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -361,7 +361,7 @@ function getSettlementColumns(): XlsxTransformerColumn<PaymentGeneral>[] {
361
361
  width: 18,
362
362
  getValue: (object: PaymentGeneralWithStripeAccount) => {
363
363
  return {
364
- value: object.settlement?.amount !== undefined ? (object.settlement?.amount / 100) : null,
364
+ value: object.settlement?.amount !== undefined ? (object.settlement?.amount / 1_0000) : null,
365
365
  style: {
366
366
  numberFormat: {
367
367
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -381,7 +381,7 @@ function getStripeColumns(): XlsxTransformerColumn<PaymentGeneral>[] {
381
381
  width: 16,
382
382
  getValue: (object: PaymentGeneralWithStripeAccount) => {
383
383
  return {
384
- value: object.transferFee / 100,
384
+ value: object.transferFee / 1_0000,
385
385
  style: {
386
386
  numberFormat: {
387
387
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -108,7 +108,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<ReceivableBalanceWithIte
108
108
  name: $t(`7f7fdce2-1fcd-44c9-8c98-856aea11ffc3`),
109
109
  width: 20,
110
110
  getValue: (object: ReceivableBalanceWithItem) => ({
111
- value: object.balanceItem.unitPrice / 100,
111
+ value: object.balanceItem.unitPrice / 1_0000,
112
112
  style: {
113
113
  numberFormat: {
114
114
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -121,7 +121,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<ReceivableBalanceWithIte
121
121
  name: $t(`6f3104d4-9b8f-4946-8434-77202efae9f0`),
122
122
  width: 20,
123
123
  getValue: (object: ReceivableBalanceWithItem) => ({
124
- value: object.balanceItem.price / 100,
124
+ value: object.balanceItem.priceWithVAT / 1_0000,
125
125
  style: {
126
126
  numberFormat: {
127
127
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -134,7 +134,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<ReceivableBalanceWithIte
134
134
  name: $t(`dc9f65e0-19ce-4908-8830-da48235faa70`),
135
135
  width: 20,
136
136
  getValue: (object: ReceivableBalanceWithItem) => ({
137
- value: object.balanceItem.pricePaid / 100,
137
+ value: object.balanceItem.pricePaid / 1_0000,
138
138
  style: {
139
139
  numberFormat: {
140
140
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -147,7 +147,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<ReceivableBalanceWithIte
147
147
  name: $t(`5c75e9bf-1b64-4d28-a435-6e33247d5170`),
148
148
  width: 20,
149
149
  getValue: (object: ReceivableBalanceWithItem) => ({
150
- value: object.balanceItem.pricePending / 100,
150
+ value: object.balanceItem.pricePending / 1_0000,
151
151
  style: {
152
152
  numberFormat: {
153
153
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -160,7 +160,7 @@ function getBalanceItemColumns(): XlsxTransformerColumn<ReceivableBalanceWithIte
160
160
  name: $t(`eb0421f4-6ee9-4d81-b549-2bc4e16c4b63`),
161
161
  width: 20,
162
162
  getValue: (object: ReceivableBalanceWithItem) => ({
163
- value: object.balanceItem.priceOpen / 100,
163
+ value: object.balanceItem.priceOpen / 1_0000,
164
164
  style: {
165
165
  numberFormat: {
166
166
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -261,7 +261,7 @@ function getGeneralColumns(): XlsxTransformerConcreteColumn<ReceivableBalance>[]
261
261
  name: $t(`eb0421f4-6ee9-4d81-b549-2bc4e16c4b63`),
262
262
  width: 10,
263
263
  getValue: (object: ReceivableBalance) => ({
264
- value: object.amountOpen / 100,
264
+ value: object.amountOpen / 1_0000,
265
265
  style: {
266
266
  numberFormat: {
267
267
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -274,7 +274,7 @@ function getGeneralColumns(): XlsxTransformerConcreteColumn<ReceivableBalance>[]
274
274
  name: $t(`5c75e9bf-1b64-4d28-a435-6e33247d5170`),
275
275
  width: 18,
276
276
  getValue: (object: ReceivableBalance) => ({
277
- value: object.amountPending / 100,
277
+ value: object.amountPending / 1_0000,
278
278
  style: {
279
279
  numberFormat: {
280
280
  id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
@@ -1504,6 +1504,19 @@ export class AdminPermissionChecker {
1504
1504
  return cloned;
1505
1505
  }
1506
1506
 
1507
+ /**
1508
+ * Only for creating new members
1509
+ */
1510
+ filterMemberPut(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob, options: {asUserManager: boolean}) {
1511
+ if (options.asUserManager || STAMHOOFD.userMode === 'platform') {
1512
+ // A user manager cannot choose the member number + in platform mode, nobody can choose the member number
1513
+ data.details.memberNumber = null;
1514
+ }
1515
+
1516
+ // Do not allow setting the security code
1517
+ data.details.securityCode = null;
1518
+ }
1519
+
1507
1520
  async filterMemberPatch(member: MemberWithRegistrations, data: AutoEncoderPatchType<MemberWithRegistrationsBlob>): Promise<AutoEncoderPatchType<MemberWithRegistrationsBlob>> {
1508
1521
  if (!data.details) {
1509
1522
  return data;
@@ -1524,6 +1537,7 @@ export class AdminPermissionChecker {
1524
1537
  });
1525
1538
  }
1526
1539
 
1540
+
1527
1541
  const hasRecordAnswers = !!data.details.recordAnswers;
1528
1542
  const hasNotes = data.details.notes !== undefined;
1529
1543
  const isSetFinancialSupportTrue = data.details.shouldApplyReducedPrice;
@@ -1575,6 +1589,12 @@ export class AdminPermissionChecker {
1575
1589
 
1576
1590
  const isUserManager = this.isUserManager(member);
1577
1591
 
1592
+ // Do not allow setting the member number
1593
+ if (isUserManager || STAMHOOFD.userMode === 'platform') {
1594
+ // A user manager cannot choose the member number + in platform mode, nobody can choose the member number
1595
+ delete data.details.memberNumber;
1596
+ }
1597
+
1578
1598
  if (hasNotes && isUserManager && !(await this.canAccessMember(member, PermissionLevel.Full))) {
1579
1599
  throw new SimpleError({
1580
1600
  code: 'permission_denied',
@@ -166,7 +166,7 @@ export class BuckarooHelper {
166
166
 
167
167
  const data = {
168
168
  Currency: 'EUR',
169
- AmountDebit: (payment.price / 100).toFixed(2),
169
+ AmountDebit: (payment.price / 10000).toFixed(2), // 4 decimals to zero
170
170
  Invoice: 'ID ' + payment.id,
171
171
  ClientIP: {
172
172
  Type: 0, // 0 = ipv4, 1 = ipv6
@@ -18,16 +18,14 @@ export class ServiceFeeHelper {
18
18
  if (price === 0 && !minimumFee) {
19
19
  return 0;
20
20
  }
21
- let fee = Math.round(fixed + Math.max(1, price * percentageTimes100 / 100 / 100));
21
+ let fee = Math.round((fixed + Math.max(100, price * percentageTimes100 / 100 / 100)) / 100) * 100; // Round to 2 decimals, minimum 1 cent
22
22
  if (minimumFee !== null && fee < minimumFee) {
23
23
  fee = minimumFee;
24
24
  }
25
25
  if (maximumFee !== null && fee > maximumFee) {
26
26
  fee = maximumFee;
27
27
  }
28
- return Math.round(
29
- fee * (100 + vat) / 100,
30
- ); // € 0,21 + 0,2%
28
+ return fee;
31
29
  }
32
30
 
33
31
  let serviceFee = 0;
@@ -52,6 +50,12 @@ export class ServiceFeeHelper {
52
50
  }
53
51
  }
54
52
 
53
+ // Add VAT
54
+ serviceFee = serviceFee * (100 + vat) / 100;
55
+
56
+ // Round service fee to 2 decimal places
57
+ serviceFee = Math.round(serviceFee / 100) * 100;
58
+
55
59
  console.log('Service fee for payment', payment.id, type, 'is', serviceFee);
56
60
  if (payment.provider === PaymentProvider.Stripe && payment.stripeAccountId) {
57
61
  payment.serviceFeePayout = serviceFee;
@@ -219,7 +219,7 @@ export class StripeHelper {
219
219
  });
220
220
  }
221
221
 
222
- const totalPrice = payment.price;
222
+ const totalPrice = Math.round(payment.price / 100); // Convert from 4 decimal places to 2 decimal places
223
223
 
224
224
  if (totalPrice < 50) {
225
225
  throw new SimpleError({
@@ -252,14 +252,14 @@ export class StripeHelper {
252
252
  directCharge = true;
253
253
  }
254
254
 
255
- payment.transferFee = fee;
256
- const serviceFee = payment.serviceFeePayout;
255
+ payment.transferFee = fee * 100; // Convert back to 4 decimal places for storage
256
+ const serviceFee = Math.round(payment.serviceFeePayout / 100);
257
257
 
258
258
  const fullMetadata = {
259
259
  ...(metadata ?? {}),
260
260
  organizationVATNumber: organization.meta.VATNumber,
261
261
  transactionFee: fee,
262
- serviceFee: serviceFee,
262
+ serviceFee: serviceFee, // For historic reasons, this is stored in cents
263
263
  };
264
264
 
265
265
  const stripe = StripeHelper.getInstance(directCharge ? stripeAccount.accountId : null);
@@ -327,41 +327,24 @@ export class StripeHelper {
327
327
  await paymentIntentModel.save();
328
328
  }
329
329
  else {
330
- // Build Stripe line items
331
- const stripeLineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [];
332
- let lineItemsPrice = 0;
333
- for (const item of lineItems) {
334
- const stripeLineItem = {
335
- price_data: {
336
- currency: 'eur',
337
- unit_amount: item.price,
338
- product_data: {
339
- name: item.balanceItem.description,
340
- },
341
- },
342
- quantity: 1,
343
- };
344
- stripeLineItems.push(stripeLineItem);
345
- lineItemsPrice += item.price;
346
- }
347
-
348
- if (lineItemsPrice !== totalPrice) {
349
- console.error('Total price of line items does not match total price of payment', lineItemsPrice, totalPrice, payment.id);
350
- throw new SimpleError({
351
- code: 'invalid_price',
352
- message: 'De totale prijs van de betaling komt niet overeen met de prijs van de items',
353
- human: $t(`e66b54d3-70fb-4b40-b3e5-21bc795ba704`),
354
- statusCode: 500,
355
- });
356
- }
357
-
358
330
  // Use checkout flow
359
- const session = await stripe.checkout.sessions.create({
331
+ const data: Stripe.Checkout.SessionCreateParams = {
360
332
  mode: 'payment',
361
333
  success_url: redirectUrl,
362
334
  cancel_url: cancelUrl,
363
335
  payment_method_types: payment.method === PaymentMethod.DirectDebit ? ['sepa_debit'] : ['card'],
364
- line_items: stripeLineItems,
336
+ line_items: [
337
+ {
338
+ price_data: {
339
+ currency: 'eur',
340
+ unit_amount: totalPrice,
341
+ product_data: {
342
+ name: statementDescriptor,
343
+ },
344
+ },
345
+ quantity: 1,
346
+ },
347
+ ],
365
348
  currency: 'eur',
366
349
  locale: i18n.language as 'nl',
367
350
  payment_intent_data: {
@@ -385,7 +368,9 @@ export class StripeHelper {
385
368
  request_three_d_secure: 'challenge', // Force usage of string customer authentication for card payments
386
369
  },
387
370
  },
388
- });
371
+ };
372
+ console.log('Creating Stripe session', data);
373
+ const session = await stripe.checkout.sessions.create(data);
389
374
  console.log('Stripe session', session);
390
375
 
391
376
  if (!session.url) {
@@ -54,6 +54,8 @@ async function start(dryRun: boolean) {
54
54
 
55
55
  async function cleanupGroup(group: Group, dryRun: boolean) {
56
56
  group.settings.cycleSettings = new Map();
57
+ group.settings.preventPreviousGroupIds = [];
58
+ group.settings.requirePreviousGroupIds = [];
57
59
  group.cycle = cycleIfMigrated;
58
60
  if (group.status === GroupStatus.Archived) {
59
61
  group.status = GroupStatus.Closed;
@@ -157,6 +159,39 @@ async function migrateGroups({ groups, organization, periodSpan }: { groups: Gro
157
159
  }
158
160
  }
159
161
 
162
+ // migrate require and prevent group ids
163
+ for (const originalGroup of groups) {
164
+ // migrate requirePreviousGroupIds
165
+ const requirePreviousGroupIds = originalGroup.settings.requirePreviousGroupIds;
166
+ const requireIdSet = new Set<string>();
167
+
168
+ for (const groupIdToGetPreviousGroupOf of requirePreviousGroupIds) {
169
+ const previousGroups = groupMap.get(groupIdToGetPreviousGroupOf);
170
+ if (previousGroups && previousGroups.length > 0) {
171
+ // previous groups are already ordered
172
+ const firstPreviousGroup = previousGroups[0];
173
+ requireIdSet.add(firstPreviousGroup.id);
174
+ }
175
+ }
176
+
177
+ originalGroup.settings.requireGroupIds = [...requireIdSet];
178
+
179
+ // migrate preventPreviousGroupIds
180
+ const preventPreviousGroupIds = originalGroup.settings.preventPreviousGroupIds;
181
+ const preventIdSet = new Set<string>();
182
+
183
+ for (const groupIdToGetPreviousGroupOf of preventPreviousGroupIds) {
184
+ const previousGroups = groupMap.get(groupIdToGetPreviousGroupOf);
185
+ if (previousGroups && previousGroups.length > 0) {
186
+ // previous groups are already ordered
187
+ const firstPreviousGroup = previousGroups[0];
188
+ preventIdSet.add(firstPreviousGroup.id);
189
+ }
190
+ }
191
+
192
+ originalGroup.settings.preventGroupIds = [...preventIdSet];
193
+ }
194
+
160
195
  // #region create categories for current period
161
196
  const nonArchivedGroupIds = [...new Set(groups.filter(g => g.status !== GroupStatus.Archived).map(g => g.id))];
162
197
  const newCategoriesData = organization.meta.categories.map((c: GroupCategory) => {
@@ -70,9 +70,8 @@ export const BalanceItemService = {
70
70
  // status, unitPrice, dueAt, amount
71
71
  if (
72
72
  'status' in event.changedFields
73
- || 'unitPrice' in event.changedFields
74
73
  || 'dueAt' in event.changedFields
75
- || 'amount' in event.changedFields
74
+ || 'priceTotal' in event.changedFields
76
75
  || 'memberId' in event.changedFields
77
76
  || 'userId' in event.changedFields
78
77
  || 'payingOrganizationId' in event.changedFields
@@ -268,6 +267,13 @@ export const BalanceItemService = {
268
267
  await order.undoPaid(payment, organization);
269
268
  }
270
269
  }
270
+
271
+ // If a rounded payment was canceled, make sure the balance item is hidden again (will become visible again when marking paid)
272
+ if (this.type === BalanceItemType.Rounding && balanceItem.status !== BalanceItemStatus.Hidden) {
273
+ // Mark undue
274
+ balanceItem.status = BalanceItemStatus.Hidden;
275
+ await balanceItem.save();
276
+ }
271
277
  },
272
278
 
273
279
  async markFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
@@ -283,6 +289,13 @@ export const BalanceItemService = {
283
289
  }
284
290
  }
285
291
  }
292
+
293
+ // If a rounded payment was canceled, make sure the balance item is hidden again (will become visible again when marking paid)
294
+ if (this.type === BalanceItemType.Rounding && balanceItem.status !== BalanceItemStatus.Hidden) {
295
+ // Mark undue
296
+ balanceItem.status = BalanceItemStatus.Hidden;
297
+ await balanceItem.save();
298
+ }
286
299
  },
287
300
 
288
301
  async undoFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {