@stamhoofd/backend 2.63.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 (27) hide show
  1. package/index.ts +8 -6
  2. package/package.json +10 -10
  3. package/src/crons/index.ts +1 -0
  4. package/src/crons/update-cached-balances.ts +39 -0
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
  6. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
  7. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +48 -3
  9. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
  10. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -2
  11. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -0
  12. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +22 -1
  13. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +6 -8
  14. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
  15. package/src/helpers/AdminPermissionChecker.ts +2 -1
  16. package/src/helpers/AuthenticatedStructures.ts +73 -3
  17. package/src/helpers/MembershipCharger.ts +4 -0
  18. package/src/helpers/OrganizationCharger.ts +4 -0
  19. package/src/seeds/1734596144-fill-previous-period-id.ts +55 -0
  20. package/src/services/BalanceItemService.ts +22 -3
  21. package/src/services/PaymentReallocationService.test.ts +746 -0
  22. package/src/services/PaymentReallocationService.ts +339 -0
  23. package/src/services/PaymentService.ts +13 -0
  24. package/src/services/PlatformMembershipService.ts +167 -137
  25. package/src/sql-filters/receivable-balances.ts +1 -1
  26. package/src/sql-sorters/receivable-balances.ts +3 -3
  27. /package/src/seeds/{1733996431-update-cached-outstanding-balance-from-items.ts → 1734700082-update-cached-outstanding-balance-from-items.ts} +0 -0
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import backendEnv from '@stamhoofd/backend-env';
2
2
  backendEnv.load();
3
3
 
4
- import { Column, Database, Migration, Model } from '@simonbackx/simple-database';
4
+ import { Column, Database, Migration } from '@simonbackx/simple-database';
5
5
  import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
6
6
  import { I18n } from '@stamhoofd/backend-i18n';
7
7
  import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
@@ -10,14 +10,14 @@ import { loadLogger } from '@stamhoofd/logging';
10
10
  import { Version } from '@stamhoofd/structures';
11
11
  import { sleep } from '@stamhoofd/utility';
12
12
 
13
- import { stopCrons, startCrons, waitForCrons } from '@stamhoofd/crons';
13
+ import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
14
+ import { Platform } from '@stamhoofd/models';
14
15
  import { resumeEmails } from './src/helpers/EmailResumer';
16
+ import { SetupStepUpdater } from './src/helpers/SetupStepUpdater';
15
17
  import { ContextMiddleware } from './src/middleware/ContextMiddleware';
16
- import { Platform } from '@stamhoofd/models';
17
18
  import { AuditLogService } from './src/services/AuditLogService';
18
- import { PlatformMembershipService } from './src/services/PlatformMembershipService';
19
19
  import { DocumentService } from './src/services/DocumentService';
20
- import { SetupStepUpdater } from './src/helpers/SetupStepUpdater';
20
+ import { PlatformMembershipService } from './src/services/PlatformMembershipService';
21
21
 
22
22
  process.on('unhandledRejection', (error: Error) => {
23
23
  console.error('unhandledRejection');
@@ -39,7 +39,9 @@ if (new Date().getTimezoneOffset() !== 0) {
39
39
  const seeds = async () => {
40
40
  try {
41
41
  // Internal
42
- await Migration.runAll(__dirname + '/src/seeds');
42
+ await AuditLogService.disable(async () => {
43
+ await Migration.runAll(__dirname + '/src/seeds');
44
+ });
43
45
  }
44
46
  catch (e) {
45
47
  console.error('Failed to run seeds:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.63.0",
3
+ "version": "2.64.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -37,14 +37,14 @@
37
37
  "@simonbackx/simple-encoding": "2.19.0",
38
38
  "@simonbackx/simple-endpoints": "1.15.0",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.63.0",
41
- "@stamhoofd/backend-middleware": "2.63.0",
42
- "@stamhoofd/email": "2.63.0",
43
- "@stamhoofd/models": "2.63.0",
44
- "@stamhoofd/queues": "2.63.0",
45
- "@stamhoofd/sql": "2.63.0",
46
- "@stamhoofd/structures": "2.63.0",
47
- "@stamhoofd/utility": "2.63.0",
40
+ "@stamhoofd/backend-i18n": "2.64.0",
41
+ "@stamhoofd/backend-middleware": "2.64.0",
42
+ "@stamhoofd/email": "2.64.0",
43
+ "@stamhoofd/models": "2.64.0",
44
+ "@stamhoofd/queues": "2.64.0",
45
+ "@stamhoofd/sql": "2.64.0",
46
+ "@stamhoofd/structures": "2.64.0",
47
+ "@stamhoofd/utility": "2.64.0",
48
48
  "archiver": "^7.0.1",
49
49
  "aws-sdk": "^2.885.0",
50
50
  "axios": "1.6.8",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "0dd71a1869c5931dcf844dea1ecd2167819dc5c6"
67
+ "gitHead": "80eebbb0b4fb84ac979041c3f8c7762863622367"
68
68
  }
@@ -2,3 +2,4 @@ import './amazon-ses.js';
2
2
  import './clearExcelCache.js';
3
3
  import './endFunctionsOfUsersWithoutRegistration.js';
4
4
  import './postmark.js';
5
+ import './update-cached-balances.js';
@@ -0,0 +1,39 @@
1
+ import { CachedBalance } from '@stamhoofd/models';
2
+ import { registerCron } from '@stamhoofd/crons';
3
+
4
+ registerCron('updateCachedBalances', updateCachedBalances);
5
+
6
+ async function updateCachedBalances() {
7
+ // Check if between 3 - 6 AM
8
+ if ((new Date().getHours() > 6 || new Date().getHours() < 3) && STAMHOOFD.environment !== 'development') {
9
+ console.log('Not between 3 and 6 AM, skipping.');
10
+ return;
11
+ }
12
+
13
+ const balances = await CachedBalance.select().where(
14
+ CachedBalance.whereNeedsUpdate(),
15
+ ).limit(100).fetch();
16
+
17
+ // Group by object type and by organization id
18
+ const grouped = new Map<string, CachedBalance[]>();
19
+
20
+ for (const balance of balances) {
21
+ const key = balance.objectType + '-' + balance.organizationId;
22
+ const arr = grouped.get(key);
23
+
24
+ if (!arr) {
25
+ grouped.set(key, [balance]);
26
+ continue;
27
+ }
28
+
29
+ arr.push(balance);
30
+ }
31
+
32
+ for (const [_, balances] of grouped) {
33
+ const balance = balances[0];
34
+
35
+ const ids = balances.map(b => b.objectId);
36
+ console.log('Updating', ids.length, balance.objectType, 'for', balance.organizationId);
37
+ await CachedBalance.updateForObjects(balance.organizationId, ids, balance.objectType);
38
+ }
39
+ }
@@ -2,7 +2,7 @@ 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, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
5
+ import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
6
  import { GroupType, MembersBlob, MemberWithRegistrationsBlob, PermissionLevel } from '@stamhoofd/structures';
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
@@ -218,7 +218,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
218
218
  });
219
219
  }
220
220
 
221
- const platform = await Platform.getShared();
222
221
  const responsibility = platform.config.responsibilities.find(r => r.id === patchResponsibility.responsibilityId);
223
222
 
224
223
  if (responsibility && !responsibility.organizationBased && !Context.auth.hasPlatformFullAccess()) {
@@ -261,7 +260,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
261
260
  throw Context.auth.error('Je hebt niet voldoende rechten om functies van leden aan te passen');
262
261
  }
263
262
 
264
- const platform = await Platform.getShared();
265
263
  const platformResponsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId);
266
264
  const org = organization ?? (put.organizationId ? await Organization.getByID(put.organizationId) : null);
267
265
 
@@ -414,12 +412,25 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
414
412
  // Add platform memberships
415
413
  for (const { put } of patch.platformMemberships.getPuts()) {
416
414
  if (put.periodId !== platform.periodId) {
417
- throw new SimpleError({
418
- code: 'invalid_field',
419
- message: 'Invalid period',
420
- human: 'Je kan geen aansluitingen maken voor een andere werkjaar dan het actieve werkjaar',
421
- field: 'periodId',
422
- });
415
+ const period = await RegistrationPeriod.getByID(put.periodId);
416
+
417
+ if (!period) {
418
+ throw new SimpleError({
419
+ code: 'invalid_field',
420
+ message: 'Invalid period',
421
+ human: Context.i18n.$t(`Je kan geen aansluitingen meer toevoegen in dit werkjaar`),
422
+ field: 'periodId',
423
+ });
424
+ }
425
+
426
+ if (period?.locked) {
427
+ throw new SimpleError({
428
+ code: 'invalid_field',
429
+ message: 'Invalid period',
430
+ human: Context.i18n.$t(`Je kan geen aansluitingen meer toevoegen in {period} (vergrendeld)`, { period: period?.getBaseStructure().name }),
431
+ field: 'periodId',
432
+ });
433
+ }
423
434
  }
424
435
 
425
436
  if (organization && put.organizationId !== organization.id) {
@@ -449,6 +460,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
449
460
  .where('memberId', member.id)
450
461
  .where('membershipTypeId', put.membershipTypeId)
451
462
  .where('periodId', put.periodId)
463
+ .where('deletedAt', null)
452
464
  .where(
453
465
  SQL.where(
454
466
  SQL.where('startDate', SQLWhereSign.LessEqual, put.startDate)
@@ -481,7 +493,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
481
493
  membership.organizationId = put.organizationId;
482
494
  membership.periodId = put.periodId;
483
495
 
484
- membership.startDate = put.startDate;
496
+ membership.startDate = new Date(Math.max(Date.now(), put.startDate.getTime()));
485
497
  membership.endDate = put.endDate;
486
498
  membership.expireDate = put.expireDate;
487
499
 
@@ -509,12 +521,25 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
509
521
  }
510
522
 
511
523
  if (membership.periodId !== platform.periodId) {
512
- throw new SimpleError({
513
- code: 'invalid_field',
514
- message: 'Invalid period',
515
- human: 'Je kan geen aansluitingen meer verwijderen voor een ander werkjaar dan het actieve werkjaar',
516
- field: 'periodId',
517
- });
524
+ const period = await RegistrationPeriod.getByID(membership.periodId);
525
+
526
+ if (!period) {
527
+ throw new SimpleError({
528
+ code: 'invalid_field',
529
+ message: 'Invalid period',
530
+ human: Context.i18n.$t(`Je kan geen aansluitingen meer verwijderen in dit werkjaar`),
531
+ field: 'periodId',
532
+ });
533
+ }
534
+
535
+ if (period?.locked) {
536
+ throw new SimpleError({
537
+ code: 'invalid_field',
538
+ message: 'Invalid period',
539
+ human: Context.i18n.$t(`Je kan geen aansluitingen meer verwijderen in {period} (vergrendeld)`, { period: period?.getBaseStructure().name }),
540
+ field: 'periodId',
541
+ });
542
+ }
518
543
  }
519
544
 
520
545
  if (!membership.canDelete() && !Context.auth.hasPlatformFullAccess()) {
@@ -157,6 +157,8 @@ export class PatchPlatformEndpoint extends Endpoint<
157
157
  platform.periodId = period.id;
158
158
  shouldUpdateSetupSteps = true;
159
159
  shouldMoveToPeriod = period;
160
+
161
+ await platform.setPreviousPeriodId();
160
162
  }
161
163
 
162
164
  if (request.body.membershipOrganizationId !== undefined) {
@@ -62,18 +62,21 @@ export class GetUserPayableBalanceEndpoint extends Endpoint<Params, Query, Body,
62
62
  for (const organization of authenticatedOrganizations) {
63
63
  const items = receivableBalances.filter(b => b.organizationId === organization.id);
64
64
 
65
- let amount = 0;
65
+ let amountOpen = 0;
66
66
  let amountPending = 0;
67
+ let amountPaid = 0;
67
68
 
68
69
  for (const item of items) {
69
- amount += item.amount;
70
+ amountOpen += item.amountOpen;
70
71
  amountPending += item.amountPending;
72
+ amountPaid += item.amountPaid;
71
73
  }
72
74
 
73
75
  billingStatus.organizations.push(PayableBalance.create({
74
76
  organization,
75
- amount,
77
+ amountOpen,
76
78
  amountPending,
79
+ amountPaid,
77
80
  }));
78
81
  }
79
82
 
@@ -242,7 +242,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
242
242
  }
243
243
 
244
244
  // Check if this member is already registered in this group?
245
- const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle });
245
+ const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle, periodId: group.periodId, registeredAt: { sign: '!=', value: null } });
246
246
 
247
247
  for (const existingRegistration of existingRegistrations) {
248
248
  if (item.replaceRegistrations.some(r => r.id === existingRegistration.id)) {
@@ -263,7 +263,25 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
263
263
  }
264
264
  }
265
265
 
266
- const registration = new Registration()
266
+ let reuseRegistration: Registration | null = null;
267
+
268
+ if (item.replaceRegistrations.length === 1) {
269
+ // Try to reuse this specific one
270
+ reuseRegistration = (await Registration.getByID(item.replaceRegistrations[0].id)) ?? null;
271
+ }
272
+
273
+ let startDate = item.calculatedStartDate;
274
+
275
+ if (!reuseRegistration) {
276
+ // Otherwise try to reuse a registration in the same period, for the same group that has been deactived for less than 7 days since the start of the new registration
277
+ reuseRegistration = existingRegistrations.find(r => r.deactivatedAt !== null && r.deactivatedAt.getTime() > startDate.getTime() - 7 * 24 * 60 * 60 * 1000) ?? null;
278
+
279
+ if (reuseRegistration && reuseRegistration.startDate && reuseRegistration.startDate < startDate && reuseRegistration.startDate >= group.settings.startDate && !item.trial) {
280
+ startDate = reuseRegistration.startDate;
281
+ }
282
+ }
283
+
284
+ const registration = (reuseRegistration ?? new Registration())
267
285
  .setRelation(registrationMemberRelation, member as Member)
268
286
  .setRelation(Registration.group, group);
269
287
  registration.organizationId = organization.id;
@@ -275,6 +293,20 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
275
293
  registration.groupPrice = item.groupPrice;
276
294
  registration.options = item.options;
277
295
  registration.recordAnswers = item.recordAnswers;
296
+ registration.startDate = startDate;
297
+
298
+ // Clear if we are reusing an existing registration
299
+ registration.trialUntil = null;
300
+ registration.pricePaid = 0;
301
+ registration.payingOrganizationId = null;
302
+
303
+ // NOTE: we don't reset deactivatedAt - registeredAt, because those will get reset when markValid is called later on (while keeping the original registeredAt date)
304
+ // registration.deactivatedAt = null;
305
+ // registration.registeredAt = null; // this is required to trigger platform membership updates
306
+
307
+ if (item.trial) {
308
+ registration.trialUntil = item.calculatedTrialUntil;
309
+ }
278
310
 
279
311
  if (whoWillPayNow === 'organization' && request.body.asOrganizationId) {
280
312
  registration.payingOrganizationId = request.body.asOrganizationId;
@@ -428,6 +460,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
428
460
  // Who is responsible for payment?
429
461
  balanceItem2.memberId = registration.memberId;
430
462
 
463
+ if (registration.trialUntil) {
464
+ balanceItem2.dueAt = registration.trialUntil;
465
+ }
466
+
431
467
  // If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
432
468
  balanceItem2.status = BalanceItemStatus.Hidden;
433
469
  await balanceItem2.save();
@@ -446,6 +482,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
446
482
  // Connect the 'pay back' balance item to this balance item. As soon as this balance item is paid, we'll mark the other one as pending so the outstanding balance for the member increases
447
483
  balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null;
448
484
 
485
+ if (registration.trialUntil) {
486
+ balanceItem.dueAt = registration.trialUntil;
487
+ }
488
+
449
489
  await balanceItem.save();
450
490
  createdBalanceItems.push(balanceItem);
451
491
  }
@@ -613,7 +653,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
613
653
  const mappedBalanceItems = new Map<BalanceItem, number>();
614
654
 
615
655
  for (const item of createdBalanceItems) {
616
- mappedBalanceItems.set(item, item.price);
656
+ if (item.dueAt === null) {
657
+ mappedBalanceItems.set(item, item.price);
658
+ }
617
659
  }
618
660
 
619
661
  for (const item of checkout.cart.balanceItems) {
@@ -652,6 +694,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
652
694
  await BalanceItem.updateOutstanding([...createdBalanceItems, ...unrelatedCreatedBalanceItems]);
653
695
  }
654
696
 
697
+ // Reallocate
698
+ await BalanceItemService.reallocate([...createdBalanceItems, ...unrelatedCreatedBalanceItems], organization.id);
699
+
655
700
  // Update occupancy
656
701
  for (const group of groups) {
657
702
  if (registrations.some(r => r.groupId === group.id) || deactivatedRegistrationGroupIds.some(id => id === group.id)) {
@@ -1,6 +1,6 @@
1
1
  import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder, patchObject } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { AuditLogType, RegistrationPeriod as RegistrationPeriodStruct } from '@stamhoofd/structures';
3
+ import { AuditLogSource, AuditLogType, RegistrationPeriod as RegistrationPeriodStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
6
  import { Platform, RegistrationPeriod } from '@stamhoofd/models';
@@ -60,6 +60,7 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
60
60
  period.locked = put.locked;
61
61
  period.settings = put.settings;
62
62
  period.organizationId = organization?.id ?? null;
63
+ await period.setPreviousPeriodId();
63
64
 
64
65
  await period.save();
65
66
  periods.push(period);
@@ -91,6 +92,7 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
91
92
  model.settings = patchObject(model.settings, patch.settings);
92
93
  }
93
94
 
95
+ await model.setPreviousPeriodId();
94
96
  await model.save();
95
97
 
96
98
  // Schedule patch of all groups in this period
@@ -108,7 +110,19 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
108
110
  });
109
111
  }
110
112
 
113
+ // Get before deleting the model
114
+ const updateWhere = await RegistrationPeriod.where({ previousPeriodId: model.id });
115
+
116
+ // Now delete the model
111
117
  await model.delete();
118
+
119
+ // Update all previous period ids
120
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
121
+ for (const period of updateWhere) {
122
+ await period.setPreviousPeriodId();
123
+ await period.save();
124
+ }
125
+ });
112
126
  }
113
127
 
114
128
  // Clear platform cache
@@ -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,7 +37,7 @@ 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
 
@@ -208,8 +209,14 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
208
209
 
209
210
  await BalanceItem.updateOutstanding(updateOutstandingBalance);
210
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
+
211
218
  return new Response(
212
- await BalanceItem.getStructureWithPayments(returnedModels),
219
+ await BalanceItem.getStructureWithPayments(returnedModelsReloaded),
213
220
  );
214
221
  }
215
222
 
@@ -200,6 +200,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
200
200
  }
201
201
 
202
202
  await BalanceItem.updateOutstanding(balanceItems);
203
+
204
+ // Reallocate
205
+ await BalanceItemService.reallocate(balanceItems, organization.id);
203
206
  }
204
207
 
205
208
  changedPayments.push(payment);
@@ -100,6 +100,26 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
100
100
  .fetch();
101
101
  break;
102
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
+ }
103
123
  }
104
124
 
105
125
  const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
@@ -108,8 +128,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
108
128
  const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
109
129
 
110
130
  const created = new CachedBalance();
111
- created.amount = 0;
131
+ created.amountOpen = 0;
112
132
  created.amountPending = 0;
133
+ created.amountPaid = 0;
113
134
  created.organizationId = organization.id;
114
135
  created.objectId = request.params.id;
115
136
  created.objectType = request.params.type;
@@ -49,19 +49,17 @@ 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
54
  nextDueAt: { $neq: null },
55
55
  },
56
+ $not: {
57
+ objectType: {
58
+ $in: Context.auth.hasSomePlatformAccess() ? [ReceivableBalanceType.registration] : [ReceivableBalanceType.organization, ReceivableBalanceType.registration],
59
+ },
60
+ },
56
61
  };
57
62
 
58
- if (!Context.auth.hasSomePlatformAccess()) {
59
- // Cannot see debt between organizations
60
- scopeFilter.objectType = {
61
- $neq: ReceivableBalanceType.organization,
62
- };
63
- }
64
-
65
63
  const query = CachedBalance
66
64
  .select();
67
65
 
@@ -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);
@@ -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