@stamhoofd/backend 2.125.0 → 2.125.2

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.125.0",
3
+ "version": "2.125.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -57,20 +57,20 @@
57
57
  "@simonbackx/simple-endpoints": "1.21.1",
58
58
  "@simonbackx/simple-errors": "1.5.0",
59
59
  "@simonbackx/simple-logging": "1.0.1",
60
- "@stamhoofd/backend-env": "2.125.0",
61
- "@stamhoofd/backend-i18n": "2.125.0",
62
- "@stamhoofd/backend-middleware": "2.125.0",
63
- "@stamhoofd/crons": "2.125.0",
64
- "@stamhoofd/email": "2.125.0",
65
- "@stamhoofd/excel-writer": "2.125.0",
66
- "@stamhoofd/logging": "2.125.0",
67
- "@stamhoofd/models": "2.125.0",
68
- "@stamhoofd/object-differ": "2.125.0",
69
- "@stamhoofd/queues": "2.125.0",
70
- "@stamhoofd/sql": "2.125.0",
71
- "@stamhoofd/structures": "2.125.0",
72
- "@stamhoofd/types": "2.125.0",
73
- "@stamhoofd/utility": "2.125.0",
60
+ "@stamhoofd/backend-env": "2.125.2",
61
+ "@stamhoofd/backend-i18n": "2.125.2",
62
+ "@stamhoofd/backend-middleware": "2.125.2",
63
+ "@stamhoofd/crons": "2.125.2",
64
+ "@stamhoofd/email": "2.125.2",
65
+ "@stamhoofd/excel-writer": "2.125.2",
66
+ "@stamhoofd/logging": "2.125.2",
67
+ "@stamhoofd/models": "2.125.2",
68
+ "@stamhoofd/object-differ": "2.125.2",
69
+ "@stamhoofd/queues": "2.125.2",
70
+ "@stamhoofd/sql": "2.125.2",
71
+ "@stamhoofd/structures": "2.125.2",
72
+ "@stamhoofd/types": "2.125.2",
73
+ "@stamhoofd/utility": "2.125.2",
74
74
  "archiver": "7.0.1",
75
75
  "axios": "1.16.0",
76
76
  "base-x": "3.0.11",
@@ -91,7 +91,7 @@
91
91
  "stripe": "16.12.0"
92
92
  },
93
93
  "devDependencies": {
94
- "@stamhoofd/test-utils": "2.125.0",
94
+ "@stamhoofd/test-utils": "2.125.2",
95
95
  "@types/cookie": "0.6.0",
96
96
  "@types/luxon": "3.7.1",
97
97
  "@types/mailparser": "3.4.6",
@@ -107,5 +107,5 @@
107
107
  "publishConfig": {
108
108
  "access": "public"
109
109
  },
110
- "gitHead": "8fc26e2a47fee0ff97992a55f80a921880f33077"
110
+ "gitHead": "bda32cee30660a98c7563c405ce29dc0da7cab65"
111
111
  }
@@ -2,6 +2,7 @@ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
2
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
3
  import { DetailedReceivableBalance, PaymentStatus, PermissionLevel, ReceivableBalanceType } from '@stamhoofd/structures';
4
4
 
5
+ import type { MemberWithUsersAndRegistrations } from '@stamhoofd/models';
5
6
  import { BalanceItem, BalanceItemPayment, CachedBalance, Member, MemberUser, Payment, Registration } from '@stamhoofd/models';
6
7
  import { Context } from '../../../../helpers/Context.js';
7
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
@@ -36,20 +37,18 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
36
37
  await Context.authenticate();
37
38
 
38
39
  if (!await Context.auth.canManageFinances(organization.id)) {
39
- // Group-level financial access is sufficient for member and registration balance views
40
- if (request.params.type === ReceivableBalanceType.member) {
41
- const member = await Member.getByIdWithUsersAndRegistrations(request.params.id);
42
- if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
43
- throw Context.auth.error();
44
- }
45
- } else if (request.params.type === ReceivableBalanceType.registration) {
40
+ let member: MemberWithUsersAndRegistrations | null = null;
41
+ if (request.params.type === ReceivableBalanceType.registration) {
46
42
  const registration = await Registration.select().where('id', request.params.id).first(false);
47
43
  if (!registration) throw Context.auth.error();
48
- const member = await Member.getByIdWithUsersAndRegistrations(registration.memberId);
44
+ member = await Member.getByIdWithUsersAndRegistrations(registration.memberId);
45
+ } else if (request.params.type === ReceivableBalanceType.member) {
46
+ member = await Member.getByIdWithUsersAndRegistrations(request.params.id);
49
47
  if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
50
48
  throw Context.auth.error();
51
49
  }
52
- } else {
50
+ }
51
+ if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
53
52
  throw Context.auth.error();
54
53
  }
55
54
  }
@@ -316,7 +316,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
316
316
  }
317
317
 
318
318
  const maximumStart = 1000 * 60 * 60 * 24 * 31 * 8; // 8 months in advance
319
- if (period.startDate > new Date(Date.now() + maximumStart)) {
319
+ if (period.startDate > new Date(Date.now() + maximumStart) && STAMHOOFD.userMode === 'platform') {
320
320
  throw new SimpleError({
321
321
  code: 'invalid_field',
322
322
  message: 'Period start date is too far in the future',
@@ -326,7 +326,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
326
326
  }
327
327
 
328
328
  // Period has ended
329
- if (STAMHOOFD.environment !== 'development' && period.endDate < new Date()) {
329
+ if (STAMHOOFD.environment !== 'development' && period.endDate < new Date() && STAMHOOFD.userMode === 'platform') {
330
330
  throw new SimpleError({
331
331
  code: 'invalid_field',
332
332
  message: 'Period has ended',
@@ -565,29 +565,16 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
565
565
  if (patch.waitingList === null) {
566
566
  // delete
567
567
  if (model.waitingListId) {
568
- // for now don't delete, as waiting lists can be shared between multiple groups
569
- // await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
570
568
  model.waitingListId = null;
571
569
  }
572
570
  } else if (patch.waitingList.isPatch()) {
573
- if (!model.waitingListId) {
574
- throw new SimpleError({
575
- code: 'invalid_field',
576
- field: 'waitingList',
577
- message: 'Cannot patch waiting list before it is created',
578
- });
579
- }
580
- patch.waitingList.id = model.waitingListId;
581
- patch.waitingList.type = GroupType.WaitingList;
582
- await throwIfUpdateWaitingListPeriodWithMultipleGroups(patch.waitingList.id);
583
- await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList, period, {
584
- allowPatchWaitingListPeriod: shouldUpdatePeriodIds,
585
- isPatchingEvent,
571
+ throw new SimpleError({
572
+ code: 'invalid_field',
573
+ field: 'waitingList',
574
+ message: 'Cannot patch waitingList',
586
575
  });
587
576
  } else {
588
577
  if (model.waitingListId) {
589
- // for now don't delete, as waiting lists can be shared between multiple groups
590
- // await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
591
578
  model.waitingListId = null;
592
579
  }
593
580
  patch.waitingList.type = GroupType.WaitingList;
@@ -613,29 +600,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
613
600
 
614
601
  model.waitingListId = existing.id;
615
602
  } else {
616
- const requiredPeriod = period ?? await RegistrationPeriod.getByID(model.periodId);
617
-
618
- if (!requiredPeriod) {
619
- throw new Error('Unexpected missing period when creating waiting list');
620
- }
621
-
622
- if (STAMHOOFD.userMode === 'organization' && requiredPeriod.organizationId !== model.organizationId) {
623
- throw new SimpleError({
624
- code: 'invalid_period',
625
- message: 'Period has different organization id',
626
- statusCode: 400,
627
- });
628
- }
629
-
630
- const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
631
- patch.waitingList,
632
- model.organizationId,
633
- requiredPeriod,
634
- {
635
- allowedIds: [patch.waitingList.id],
636
- },
637
- );
638
- model.waitingListId = group.id;
603
+ throw new SimpleError({
604
+ code: 'invalid_waiting_list',
605
+ field: 'waitingList',
606
+ message: 'Waiting list not found',
607
+ human: $t('%ZZb'),
608
+ statusCode: 404,
609
+ });
639
610
  }
640
611
  }
641
612
  } else if (shouldUpdatePeriodIds && model.waitingListId && period) {
@@ -761,16 +732,24 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
761
732
  });
762
733
  }
763
734
 
735
+ if (existing.periodId !== model.periodId) {
736
+ throw new SimpleError({
737
+ code: 'invalid_field',
738
+ field: 'waitingList',
739
+ message: 'Waiting list group is already used in another period',
740
+ human: $t(`%F9`),
741
+ });
742
+ }
743
+
764
744
  model.waitingListId = existing.id;
765
745
  } else {
766
- struct.waitingList.type = GroupType.WaitingList;
767
- const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
768
- struct.waitingList,
769
- model.organizationId,
770
- period,
771
- { allowedIds: [struct.waitingList.id] },
772
- );
773
- model.waitingListId = group.id;
746
+ throw new SimpleError({
747
+ code: 'invalid_waiting_list',
748
+ field: 'waitingList',
749
+ message: 'Waiting list not found',
750
+ human: $t('%ZZb'),
751
+ statusCode: 404,
752
+ });
774
753
  }
775
754
  }
776
755
 
@@ -702,12 +702,6 @@ export class AdminPermissionChecker {
702
702
 
703
703
  if (this.user.permissions) {
704
704
  // We grant permission for a whole payment when the user has at least permission for a part of that payment.
705
- for (const registration of registrations) {
706
- if (await this.canAccessRegistration(registration, permissionLevel)) {
707
- return true;
708
- }
709
- }
710
-
711
705
  const webshopCache: Map<string, Webshop> = new Map();
712
706
 
713
707
  for (const order of orders) {
@@ -720,6 +714,23 @@ export class AdminPermissionChecker {
720
714
  }
721
715
  }
722
716
  }
717
+
718
+ for (const registration of registrations) {
719
+ if (await this.canAccessRegistration(registration, permissionLevel)) {
720
+ return true;
721
+ }
722
+ }
723
+
724
+ const seenMemberIds = new Set<string>();
725
+
726
+ for (const balanceItem of balanceItems) {
727
+ if (!balanceItem.memberId || seenMemberIds.has(balanceItem.memberId)) continue;
728
+ seenMemberIds.add(balanceItem.memberId);
729
+ const member = await Member.getByIdWithUsersAndRegistrations(balanceItem.memberId);
730
+ if (member && await this.hasFinancialMemberAccess(member, permissionLevel)) {
731
+ return true;
732
+ }
733
+ }
723
734
  }
724
735
 
725
736
  if (permissionLevel === PermissionLevel.Read) {
@@ -1188,7 +1199,8 @@ export class AdminPermissionChecker {
1188
1199
  return true;
1189
1200
  }
1190
1201
 
1191
- if (!await this.canAccessMember(member, level)) {
1202
+ // If you can't read the member, you can't have financial access
1203
+ if (!await this.canAccessMember(member, PermissionLevel.Read)) {
1192
1204
  return false;
1193
1205
  }
1194
1206
 
@@ -32,10 +32,8 @@ export class AuthenticatedStructures {
32
32
  const { balanceItemPayments, balanceItems } = await Payment.loadBalanceItems(payments);
33
33
 
34
34
  if (checkPermissions) {
35
- const { registrations, orders } = await Payment.loadBalanceItemRelations(balanceItems);
36
-
37
35
  // Note: permission checking is moved here for performacne to avoid loading the data multiple times
38
- if (!(await Context.optionalAuth?.canAccessBalanceItems(balanceItems, PermissionLevel.Read, { registrations, orders }))) {
36
+ if (!(await Context.optionalAuth?.canAccessBalanceItems(balanceItems, PermissionLevel.Read))) {
39
37
  throw new SimpleError({
40
38
  code: 'permission_denied',
41
39
  message: 'Permission denied',
@@ -1,7 +1,8 @@
1
1
  import { Payment } from '@stamhoofd/models';
2
2
  import type { SQLFilterDefinitions } from '@stamhoofd/sql';
3
- import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
3
+ import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createJoinedRelationFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
4
4
  import { balanceItemPaymentsCompilers } from './balance-item-payments.js';
5
+ import { organizationFilterCompilers } from './organizations.js';
5
6
 
6
7
  /**
7
8
  * Defines how to filter payments in the database from StamhoofdFilter objects
@@ -38,6 +39,10 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
38
39
  type: SQLValueType.String,
39
40
  nullable: true,
40
41
  }),
42
+ payingOrganization: createJoinedRelationFilter(
43
+ SQL.join('organizations').where(SQL.column('organizations', 'id'), SQL.column(Payment.table, 'payingOrganizationId')),
44
+ organizationFilterCompilers,
45
+ ),
41
46
  invoiceId: createColumnFilter({
42
47
  expression: SQL.column('invoiceId'),
43
48
  type: SQLValueType.String,