@stamhoofd/backend 2.101.0 → 2.103.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 (29) hide show
  1. package/package.json +10 -10
  2. package/src/crons.ts +21 -26
  3. package/src/email-recipient-loaders/receivable-balances.ts +18 -11
  4. package/src/endpoints/global/events/PatchEventsEndpoint.ts +1 -0
  5. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +12 -8
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +30 -7
  7. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +4 -4
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +382 -31
  9. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +47 -23
  10. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
  11. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +60 -8
  12. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +6 -1
  13. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +12 -9
  14. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -2
  15. package/src/excel-loaders/receivable-balances.ts +2 -2
  16. package/src/helpers/AdminPermissionChecker.ts +68 -10
  17. package/src/helpers/AuthenticatedStructures.ts +18 -10
  18. package/src/helpers/MemberUserSyncer.ts +2 -2
  19. package/src/seeds/{1735577912-update-cached-outstanding-balance-from-items.ts → 1760702454-update-cached-outstanding-balance-from-items.ts} +2 -0
  20. package/src/services/BalanceItemService.ts +12 -0
  21. package/src/services/PaymentService.ts +3 -0
  22. package/src/sql-filters/base-registration-filter-compilers.ts +59 -0
  23. package/src/sql-filters/orders.ts +97 -1
  24. package/src/sql-filters/receivable-balances.ts +7 -1
  25. package/tests/e2e/bundle-discounts.test.ts +327 -1
  26. package/tests/helpers/PayconiqMocker.ts +22 -0
  27. package/tests/init/index.ts +1 -0
  28. package/tests/init/initAdmin.ts +2 -2
  29. package/tests/init/initPermissionRole.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.101.0",
3
+ "version": "2.103.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -45,14 +45,14 @@
45
45
  "@simonbackx/simple-encoding": "2.22.0",
46
46
  "@simonbackx/simple-endpoints": "1.20.1",
47
47
  "@simonbackx/simple-logging": "^1.0.1",
48
- "@stamhoofd/backend-i18n": "2.101.0",
49
- "@stamhoofd/backend-middleware": "2.101.0",
50
- "@stamhoofd/email": "2.101.0",
51
- "@stamhoofd/models": "2.101.0",
52
- "@stamhoofd/queues": "2.101.0",
53
- "@stamhoofd/sql": "2.101.0",
54
- "@stamhoofd/structures": "2.101.0",
55
- "@stamhoofd/utility": "2.101.0",
48
+ "@stamhoofd/backend-i18n": "2.103.0",
49
+ "@stamhoofd/backend-middleware": "2.103.0",
50
+ "@stamhoofd/email": "2.103.0",
51
+ "@stamhoofd/models": "2.103.0",
52
+ "@stamhoofd/queues": "2.103.0",
53
+ "@stamhoofd/sql": "2.103.0",
54
+ "@stamhoofd/structures": "2.103.0",
55
+ "@stamhoofd/utility": "2.103.0",
56
56
  "archiver": "^7.0.1",
57
57
  "axios": "^1.8.2",
58
58
  "cookie": "^0.7.0",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "1eeeabc23e86e699d54f09fb385b5c32b4618723"
73
+ "gitHead": "81c0e08f64f5c9a32a513cf6cfd2d48e3601f1a8"
74
74
  }
package/src/crons.ts CHANGED
@@ -2,7 +2,7 @@ import { Database } from '@simonbackx/simple-database';
2
2
  import { Group, Organization, Payment, Registration, STPackage, Webshop } from '@stamhoofd/models';
3
3
  import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
4
4
  import { Formatter } from '@stamhoofd/utility';
5
-
5
+ import { SQL } from '@stamhoofd/sql';
6
6
  import { registerCron } from '@stamhoofd/crons';
7
7
  import { checkSettlements } from './helpers/CheckSettlements';
8
8
  import { PaymentService } from './services/PaymentService';
@@ -124,36 +124,31 @@ async function checkWebshopDNS() {
124
124
  // Keep checking pending paymetns for 3 days
125
125
  async function checkPayments() {
126
126
  if (STAMHOOFD.environment === 'development') {
127
- return;
127
+ // return;
128
128
  }
129
129
 
130
130
  const timeout = 60 * 1000 * 31;
131
131
 
132
132
  // TODO: only select the ID + organizationId
133
- const payments = await Payment.where({
134
- status: {
135
- sign: 'IN',
136
- value: [PaymentStatus.Created, PaymentStatus.Pending],
137
- },
138
- method: {
139
- sign: 'IN',
140
- value: [PaymentMethod.Bancontact, PaymentMethod.iDEAL, PaymentMethod.Payconiq, PaymentMethod.CreditCard],
141
- },
142
- // Check all payments that are 11 minutes old and are still pending
143
- createdAt: {
144
- sign: '<',
145
- value: new Date(new Date().getTime() - timeout),
146
- },
147
- }, {
148
- limit: 100,
149
-
150
- // Return oldest payments first
151
- // If at some point, they are still pending after 1 day, their status should change to failed
152
- sort: [{
153
- column: 'createdAt',
154
- direction: 'ASC',
155
- }],
156
- });
133
+ const payments = await Payment.select()
134
+ .where(
135
+ SQL.where('method', [
136
+ PaymentMethod.Bancontact, PaymentMethod.iDEAL, PaymentMethod.Payconiq, PaymentMethod.CreditCard,
137
+ ])
138
+ .and('status', [PaymentStatus.Created, PaymentStatus.Pending])
139
+ .and('createdAt', '<', new Date(new Date().getTime() - timeout)),
140
+ )
141
+ // For payconiq payments, we have a shorter timeout of 1 minute if they are still in the 'created' state (not scanned)
142
+ .orWhere(
143
+ SQL.where('method', [
144
+ PaymentMethod.Payconiq,
145
+ ])
146
+ .and('status', [PaymentStatus.Created])
147
+ .and('createdAt', '<', new Date(new Date().getTime() - 60 * 1000)),
148
+ )
149
+ .orderBy('createdAt', 'ASC')
150
+ .limit(200)
151
+ .fetch();
157
152
 
158
153
  console.log('[DELAYED PAYMENTS] Checking pending payments: ' + payments.length);
159
154
 
@@ -14,7 +14,7 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
14
14
 
15
15
  const recipients: EmailRecipient[] = [];
16
16
  for (const balance of result.results) {
17
- const balanceItemModels = await CachedBalance.balanceForObjects(balance.organizationId, [balance.object.id], balance.objectType, true);
17
+ const balanceItemModels = balance.objectType === ReceivableBalanceType.organization ? (await CachedBalance.balanceForObjects(balance.organizationId, [balance.object.id], balance.objectType)) : [];
18
18
  const balanceItems = balanceItemModels.map(i => i.getStructure());
19
19
 
20
20
  const filteredContacts = balance.object.contacts.filter(c => compiledFilter(c));
@@ -22,7 +22,7 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
22
22
  for (const email of contact.emails) {
23
23
  const recipient = EmailRecipient.create({
24
24
  objectId: balance.id, // Note: not set member, user or organization id here - should be the queryable balance id
25
- userId: balance.objectType === ReceivableBalanceType.user ? balance.object.id : null,
25
+ userId: balance.objectType === ReceivableBalanceType.user || balance.objectType === ReceivableBalanceType.userWithoutMembers ? balance.object.id : null,
26
26
  memberId: balance.objectType === ReceivableBalanceType.member ? balance.object.id : null,
27
27
  firstName: contact.firstName,
28
28
  lastName: contact.lastName,
@@ -32,15 +32,22 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
32
32
  token: 'objectName',
33
33
  value: balance.object.name,
34
34
  }),
35
- Replacement.create({
36
- token: 'outstandingBalance',
37
- value: Formatter.price(balance.amountOpen),
38
- }),
39
- Replacement.create({
40
- token: 'balanceTable',
41
- value: '',
42
- html: BalanceItemStruct.getDetailsHTMLTable(balanceItems),
43
- }),
35
+ ...(
36
+ balance.objectType === ReceivableBalanceType.organization
37
+ ? [
38
+ Replacement.create({
39
+ token: 'outstandingBalance',
40
+ value: Formatter.price(balance.amountOpen),
41
+ }),
42
+ Replacement.create({
43
+ token: 'balanceTable',
44
+ value: '',
45
+ html: BalanceItemStruct.getDetailsHTMLTable(balanceItems),
46
+ }),
47
+ ]
48
+ : []
49
+ ),
50
+
44
51
  ...(contact.meta && contact.meta.url && typeof contact.meta.url === 'string'
45
52
  ? [Replacement.create({
46
53
  token: 'paymentUrl',
@@ -59,6 +59,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
59
59
  putGroup,
60
60
  putGroup.organizationId,
61
61
  period,
62
+ { allowedIds: [putGroup.id] },
62
63
  );
63
64
  }
64
65
  else {
@@ -1,5 +1,4 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { Group } from '@stamhoofd/models';
3
2
  import { FilterWrapperMarker, PermissionLevel, StamhoofdFilter, unwrapFilter, WrapperFilter } from '@stamhoofd/structures';
4
3
  import { Context } from '../../../../helpers/Context';
5
4
 
@@ -55,17 +54,22 @@ export async function validateGroupFilter({ filter, permissionLevel, key }: { fi
55
54
  }
56
55
  }
57
56
 
58
- const groups = await Group.getByIDs(...groupIds as string[]);
59
- Context.auth.cacheGroups(groups);
57
+ const groups = await Context.auth.getGroups(groupIds as string[]);
60
58
 
61
- console.log('Fetching members for groups', groups.map(g => g.settings.name.toString()));
59
+ console.log('Fetching members for groups before', groups.map(g => g.settings.name.toString()));
62
60
 
63
61
  for (const group of groups) {
64
62
  if (!await Context.auth.canAccessGroup(group, permissionLevel)) {
65
- throw Context.auth.error({
66
- message: 'You do not have access to this group',
67
- human: $t(`45eedf49-0f0a-442c-a0bd-7881c2682698`, { groupName: group.settings.name }),
68
- });
63
+ if (permissionLevel !== PermissionLevel.Read || !group.settings.implicitlyAllowViewRegistrations) {
64
+ throw Context.auth.error({
65
+ message: 'You do not have access to this group',
66
+ human: $t(`45eedf49-0f0a-442c-a0bd-7881c2682698`, { groupName: group.settings.name }),
67
+ });
68
+ }
69
+ else {
70
+ // Return false so we add additional scope filters (only view overlap)
71
+ return false;
72
+ }
69
73
  }
70
74
  }
71
75
 
@@ -99,14 +99,26 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
99
99
  if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
100
100
  // Can access full history for now
101
101
  scopeFilter = {
102
- organizationId: organization.id,
102
+ member: {
103
+ registrations: {
104
+ $elemMatch: {
105
+ organizationId: organization.id,
106
+ },
107
+ },
108
+ },
103
109
  };
104
110
  }
105
111
  else {
106
112
  // Can only access current period
107
113
  scopeFilter = {
108
- organizationId: organization.id,
109
- periodId: organization.periodId,
114
+ member: {
115
+ registrations: {
116
+ $elemMatch: {
117
+ organizationId: organization.id,
118
+ periodId: organization.periodId,
119
+ },
120
+ },
121
+ },
110
122
  };
111
123
  }
112
124
  }
@@ -130,8 +142,14 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
130
142
  }
131
143
 
132
144
  scopeFilter = {
133
- groupId: {
134
- $in: groupIds,
145
+ member: {
146
+ registrations: {
147
+ $elemMatch: {
148
+ groupId: {
149
+ $in: groupIds,
150
+ },
151
+ },
152
+ },
135
153
  },
136
154
  };
137
155
  }
@@ -196,8 +214,13 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
196
214
  }
197
215
 
198
216
  for (const registration of data) {
199
- if (!await Context.auth.canAccessRegistration(registration, permissionLevel)) {
200
- throw Context.auth.error();
217
+ if (registration.group.settings.implicitlyAllowViewRegistrations) {
218
+ // okay, only need to check if we can access the members (next step)
219
+ }
220
+ else {
221
+ if (!await Context.auth.canAccessRegistration(registration, permissionLevel)) {
222
+ throw Context.auth.error();
223
+ }
201
224
  }
202
225
  }
203
226
 
@@ -1,6 +1,6 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { CachedBalance, Organization } from '@stamhoofd/models';
3
- import { PayableBalance, PayableBalanceCollection } from '@stamhoofd/structures';
3
+ import { PayableBalance, PayableBalanceCollection, ReceivableBalanceType } from '@stamhoofd/structures';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -31,12 +31,12 @@ export class GetUserPayableBalanceEndpoint extends Endpoint<Params, Query, Body,
31
31
  const organization = await Context.setUserOrganizationScope();
32
32
  const { user } = await Context.authenticate();
33
33
 
34
- return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([user.id], organization));
34
+ return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([user.id], organization, ReceivableBalanceType.user));
35
35
  }
36
36
 
37
- static async getBillingStatusForObjects(objectIds: string[], organization?: Organization | null) {
37
+ static async getBillingStatusForObjects(objectIds: string[], organization: Organization | null, objectType: ReceivableBalanceType) {
38
38
  // Load cached balances
39
- const receivableBalances = await CachedBalance.getForObjects(objectIds, organization?.id);
39
+ const receivableBalances = await CachedBalance.getForObjects(objectIds, organization?.id, objectType);
40
40
 
41
41
  const organizationIds = Formatter.uniqueArray(receivableBalances.map(b => b.organizationId));
42
42