@stamhoofd/backend 2.115.1 → 2.116.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 (32) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/ModelLogger.ts +2 -3
  3. package/src/boot.ts +3 -4
  4. package/src/crons/delete-archived-data.ts +47 -0
  5. package/src/crons/index.ts +1 -0
  6. package/src/debug.ts +230 -0
  7. package/src/email-recipient-loaders/documents.ts +1 -1
  8. package/src/email-recipient-loaders/payments.ts +1 -1
  9. package/src/endpoints/auth/PatchUserEndpoint.ts +4 -4
  10. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +4 -4
  11. package/src/endpoints/global/members/GetMembersEndpoint.ts +4 -16
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +8 -7
  13. package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +2 -2
  14. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +2 -2
  16. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -7
  17. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +2 -2
  18. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +7 -4
  19. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +1 -1
  20. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +12 -12
  21. package/src/helpers/AdminPermissionChecker.ts +20 -17
  22. package/src/helpers/AuthenticatedStructures.ts +177 -107
  23. package/src/helpers/Context.ts +2 -0
  24. package/src/helpers/PeriodHelper.ts +5 -45
  25. package/src/helpers/SetupStepUpdater.ts +5 -4
  26. package/src/helpers/outstandingBalanceJoin.ts +3 -1
  27. package/src/services/PaymentService.ts +12 -0
  28. package/src/services/PlatformMembershipService.ts +3 -3
  29. package/src/sql-filters/members.ts +1 -1
  30. package/src/sql-sorters/members.ts +2 -2
  31. package/tests/e2e/register.test.ts +10 -10
  32. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +0 -67
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.115.1",
3
+ "version": "2.116.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -55,14 +55,14 @@
55
55
  "@simonbackx/simple-encoding": "2.23.1",
56
56
  "@simonbackx/simple-endpoints": "1.20.1",
57
57
  "@simonbackx/simple-logging": "^1.0.1",
58
- "@stamhoofd/backend-i18n": "2.115.1",
59
- "@stamhoofd/backend-middleware": "2.115.1",
60
- "@stamhoofd/email": "2.115.1",
61
- "@stamhoofd/models": "2.115.1",
62
- "@stamhoofd/queues": "2.115.1",
63
- "@stamhoofd/sql": "2.115.1",
64
- "@stamhoofd/structures": "2.115.1",
65
- "@stamhoofd/utility": "2.115.1",
58
+ "@stamhoofd/backend-i18n": "2.116.0",
59
+ "@stamhoofd/backend-middleware": "2.116.0",
60
+ "@stamhoofd/email": "2.116.0",
61
+ "@stamhoofd/models": "2.116.0",
62
+ "@stamhoofd/queues": "2.116.0",
63
+ "@stamhoofd/sql": "2.116.0",
64
+ "@stamhoofd/structures": "2.116.0",
65
+ "@stamhoofd/utility": "2.116.0",
66
66
  "archiver": "^7.0.1",
67
67
  "axios": "^1.13.2",
68
68
  "cookie": "^0.7.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "4b692d62820d095e00524e3c0214e8f73a69506e"
83
+ "gitHead": "842040d2dcbde452fb25178f3586c8d5293b974f"
84
84
  }
@@ -2,8 +2,8 @@ import { Model, ModelEvent } from '@simonbackx/simple-database';
2
2
  import { AuditLog } from '@stamhoofd/models';
3
3
  import { ObjectDiffer } from '@stamhoofd/object-differ';
4
4
  import { AuditLogPatchItem, AuditLogPatchItemType, AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
5
- import { ContextInstance } from '../helpers/Context';
6
- import { AuditLogService } from '../services/AuditLogService';
5
+ import { ContextInstance } from '../helpers/Context.js';
6
+ import { AuditLogService } from '../services/AuditLogService.js';
7
7
 
8
8
  export type ModelEventLogOptions<D> = {
9
9
  type: AuditLogType;
@@ -197,7 +197,6 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
197
197
  }
198
198
  }
199
199
  else {
200
- console.log('No changes');
201
200
  return false;
202
201
  }
203
202
  }
package/src/boot.ts CHANGED
@@ -16,13 +16,12 @@ import { SetupStepUpdater } from './helpers/SetupStepUpdater.js';
16
16
  import { ContextMiddleware } from './middleware/ContextMiddleware.js';
17
17
  import { AuditLogService } from './services/AuditLogService.js';
18
18
  import { BalanceItemService } from './services/BalanceItemService.js';
19
+ import { CpuService } from './services/CpuService.js';
19
20
  import { DocumentService } from './services/DocumentService.js';
20
21
  import { FileSignService } from './services/FileSignService.js';
21
22
  import { PlatformMembershipService } from './services/PlatformMembershipService.js';
22
23
  import { UitpasService } from './services/uitpas/UitpasService.js';
23
24
  import { UniqueUserService } from './services/UniqueUserService.js';
24
- import { CpuService } from './services/CpuService.js';
25
- import { SQLLogger } from '@stamhoofd/sql';
26
25
 
27
26
  process.on('unhandledRejection', (error: Error) => {
28
27
  console.error('unhandledRejection');
@@ -149,8 +148,8 @@ export const boot = async (options: { killProcess: boolean }) => {
149
148
  CpuService.startMonitoring();
150
149
  }
151
150
  else if (STAMHOOFD.environment === 'development') {
152
- SQLLogger.slowQueryThresholdMs = 200;
153
- SQLLogger.explainAllAndLogInefficient = true;
151
+ const { loadDebugFunctions } = await import('./debug.js');
152
+ loadDebugFunctions({ routerServer });
154
153
  }
155
154
 
156
155
  if (routerServer.server) {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * We cannot keep deletedAt data forever. We only keep it that way to debug things or be able to easily restore data.
3
+ * After 1 month of deletion, we should delete it permanently.
4
+ */
5
+
6
+ import { registerCron } from '@stamhoofd/crons';
7
+ import { Group } from '@stamhoofd/models';
8
+
9
+ let lastRunDate: number | null = null;
10
+ const keepDeletedDataForDays = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
11
+
12
+ registerCron('delete-archived-data', deleteArchivedData);
13
+
14
+ function shouldRun() {
15
+ const now = new Date();
16
+
17
+ if (now.getDate() === lastRunDate) {
18
+ return false;
19
+ }
20
+
21
+ const hour = now.getHours();
22
+
23
+ // between 4 and 5 AM - except in development
24
+ if (hour !== 4 && STAMHOOFD.environment !== 'development') {
25
+ return false;
26
+ }
27
+
28
+ return true;
29
+ }
30
+
31
+ async function deleteArchivedData() {
32
+ if (!shouldRun()) {
33
+ return;
34
+ }
35
+
36
+ const now = new Date();
37
+ lastRunDate = now.getDate();
38
+
39
+ console.log('Deleting archived data...');
40
+
41
+ const result = await Group.delete()
42
+ .where('deletedAt', '<', new Date(now.getTime() - keepDeletedDataForDays))
43
+ .delete();
44
+ console.log('Deleted groups:', result.affectedRows);
45
+
46
+ // For now, we don't delete orders yet (can 'fuck' up pagination because deleted orders are still used in the frontend to remove cached orders)
47
+ }
@@ -4,3 +4,4 @@ import './endFunctionsOfUsersWithoutRegistration.js';
4
4
  import './update-cached-balances.js';
5
5
  import './balance-emails.js';
6
6
  import './delete-old-email-drafts.js';
7
+ import './delete-archived-data.js';
package/src/debug.ts ADDED
@@ -0,0 +1,230 @@
1
+ import { Column } from '@simonbackx/simple-database';
2
+ import { EncodedResponse, ResponseMiddleware, Router, RouterServer } from '@simonbackx/simple-endpoints';
3
+ import { logger } from '@simonbackx/simple-logging';
4
+ import { requestPrefix } from '@stamhoofd/backend-middleware';
5
+
6
+ import { AutoEncoder } from '@simonbackx/simple-encoding';
7
+ import { Request, Response } from '@simonbackx/simple-endpoints';
8
+ import { SQLLogger } from '@stamhoofd/sql';
9
+ import { ContextInstance } from './helpers/Context.js';
10
+
11
+ export function loadDebugFunctions({ routerServer }: { routerServer: RouterServer }) {
12
+ SQLLogger.slowQueryThresholdMs = 75;
13
+ SQLLogger.explainAllAndLogInefficient = true;
14
+
15
+ if (process.env.LOG_REQUEST_QUERIES) {
16
+ loadRequestQueries({ routerServer });
17
+ }
18
+
19
+ if (process.env.LOG_TIMERS) {
20
+ loadTimers({ routerServer });
21
+ }
22
+ }
23
+
24
+ export function loadRequestQueries({ routerServer }: { routerServer: RouterServer }) {
25
+ SQLLogger.customLoggers.push((query, params, elapsedTimeMs) => {
26
+ const context = ContextInstance.optional;
27
+ if (context) {
28
+ context.queries.push({ query, time: elapsedTimeMs });
29
+ }
30
+ });
31
+ const LogQueriesMiddleware: ResponseMiddleware = {
32
+ handleResponse(request: Request, response: Response, error?: Error) {
33
+ const prefix = !error ? [] : requestPrefix(request, 'error');
34
+ const context = (request as any)._context as ContextInstance | undefined;
35
+
36
+ if (context && request.method !== 'OPTIONS') {
37
+ logger.log(
38
+ ...prefix,
39
+ ' - Queries ' + context.queries.map(q => `\n- ${q.query} (${q.time?.toFixed(2) ?? '-'}ms)`).join(''),
40
+ );
41
+ }
42
+ },
43
+ };
44
+ routerServer.addResponseMiddleware(LogQueriesMiddleware);
45
+ }
46
+
47
+ export function loadTimers({ routerServer }: { routerServer: RouterServer }) {
48
+ const jsonStringify = JSON.stringify;
49
+ JSON.stringify = function (...args) {
50
+ const startTime = process.hrtime.bigint();
51
+ const result = jsonStringify.apply(this, args);
52
+ const elapsedTime = process.hrtime.bigint() - startTime;
53
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
54
+
55
+ if (elapsedTimeMs > 1) {
56
+ console.log('JSON.stringify took ' + elapsedTimeMs.toFixed(2) + 'ms');
57
+ }
58
+ // Count total request
59
+ const context = ContextInstance.optional;
60
+ if (context) {
61
+ context.timers.set('JSON.stringify', (context.timers.get('JSON.stringify') ?? 0) + elapsedTimeMs);
62
+ }
63
+ return result;
64
+ };
65
+
66
+ const jsonParse = JSON.parse;
67
+ JSON.parse = function (...args) {
68
+ const startTime = process.hrtime.bigint();
69
+ const result = jsonParse.apply(this, args);
70
+ const elapsedTime = process.hrtime.bigint() - startTime;
71
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
72
+
73
+ if (elapsedTimeMs > 1) {
74
+ console.log('JSON.parse took ' + elapsedTimeMs.toFixed(2) + 'ms');
75
+ }
76
+
77
+ // Count total request
78
+ const context = ContextInstance.optional;
79
+ if (context) {
80
+ context.timers.set('JSON.parse', (context.timers.get('JSON.parse') ?? 0) + elapsedTimeMs);
81
+ }
82
+ return result;
83
+ };
84
+
85
+ const routerDecode = Router.prototype.decode;
86
+ Router.prototype.decode = function (...args) {
87
+ const startTime = process.hrtime.bigint();
88
+ const result = routerDecode.apply(this, args);
89
+ const elapsedTime = process.hrtime.bigint() - startTime;
90
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
91
+
92
+ if (elapsedTimeMs > 1) {
93
+ console.log('Router.decode took ' + elapsedTimeMs.toFixed(2) + 'ms');
94
+ }
95
+
96
+ // Count total request
97
+ const context = ContextInstance.optional;
98
+ if (context) {
99
+ context.timers.set('Router.decode', (context.timers.get('Router.decode') ?? 0) + elapsedTimeMs);
100
+ }
101
+ return result;
102
+ };
103
+
104
+ const encodedResponseEncode = EncodedResponse.encode;
105
+ EncodedResponse.encode = function (...args) {
106
+ const startTime = process.hrtime.bigint();
107
+ const result = encodedResponseEncode.apply(this, args);
108
+ const elapsedTime = process.hrtime.bigint() - startTime;
109
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
110
+
111
+ if (elapsedTimeMs > 1) {
112
+ console.log('EncodedResponse.encode took ' + elapsedTimeMs.toFixed(2) + 'ms');
113
+ }
114
+
115
+ // Count total request
116
+ const context = ContextInstance.optional;
117
+ if (context) {
118
+ context.timers.set('EncodedResponse.encode', (context.timers.get('EncodedResponse.encode') ?? 0) + elapsedTimeMs);
119
+ }
120
+
121
+ return result;
122
+ };
123
+
124
+ // Change
125
+ const columnTo = Column.prototype.to;
126
+ Column.prototype.to = function (this: Column, value: any) {
127
+ const startTime = process.hrtime.bigint();
128
+ const result = columnTo.apply(this, [value]);
129
+ const elapsedTime = process.hrtime.bigint() - startTime;
130
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
131
+
132
+ if (elapsedTimeMs > 1) {
133
+ console.log(`Column.to for ${this.constructor.name} took ${elapsedTimeMs.toFixed(2)}ms`);
134
+ }
135
+
136
+ // Count total request
137
+ const context = ContextInstance.optional;
138
+ if (context) {
139
+ context.timers.set(`Column.to`, (context.timers.get(`Column.to`) ?? 0) + elapsedTimeMs);
140
+ }
141
+ return result;
142
+ };
143
+
144
+ const columnFrom = Column.prototype.from;
145
+ Column.prototype.from = function (this: Column, value: any) {
146
+ const startTime = process.hrtime.bigint();
147
+ const result = columnFrom.apply(this, [value]);
148
+ const elapsedTime = process.hrtime.bigint() - startTime;
149
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
150
+
151
+ if (elapsedTimeMs > 1) {
152
+ console.log(`Column.from for ${this.constructor.name} took ${elapsedTimeMs.toFixed(2)}ms`);
153
+ }
154
+
155
+ // Count total request
156
+ const context = ContextInstance.optional;
157
+ if (context) {
158
+ context.timers.set(`Column.from`, (context.timers.get(`Column.from`) ?? 0) + elapsedTimeMs);
159
+ }
160
+ return result;
161
+ };
162
+
163
+ const decode = AutoEncoder.decode;
164
+ AutoEncoder.decode = function (...args) {
165
+ const c = args[0].context as any;
166
+ if (c._didTrack) {
167
+ return decode.apply(this, args);
168
+ }
169
+ c._didTrack = true;
170
+
171
+ const startTime = process.hrtime.bigint();
172
+ const result = decode.apply(this, args);
173
+ const elapsedTime = process.hrtime.bigint() - startTime;
174
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
175
+
176
+ if (elapsedTimeMs > 1) {
177
+ console.log('AutoEncoder.decode took ' + elapsedTimeMs.toFixed(2) + 'ms');
178
+ }
179
+
180
+ const context = ContextInstance.optional;
181
+ if (context) {
182
+ context.timers.set('AutoEncoder.decode', (context.timers.get('AutoEncoder.decode') ?? 0) + elapsedTimeMs);
183
+ }
184
+
185
+ return result;
186
+ };
187
+
188
+ const encode = AutoEncoder.prototype.encode;
189
+ AutoEncoder.prototype.encode = function (this: AutoEncoder, ...args) {
190
+ const c = args[0] as any;
191
+ if (c._didTrack) {
192
+ return encode.apply(this, args);
193
+ }
194
+ c._didTrack = true;
195
+
196
+ const startTime = process.hrtime.bigint();
197
+ const result = encode.apply(this, args);
198
+ const elapsedTime = process.hrtime.bigint() - startTime;
199
+ const elapsedTimeMs = Number(elapsedTime) / 1000 / 1000;
200
+
201
+ if (elapsedTimeMs > 1) {
202
+ console.log('AutoEncoder.encode took ' + elapsedTimeMs.toFixed(2) + 'ms');
203
+ }
204
+
205
+ const context = ContextInstance.optional;
206
+ if (context) {
207
+ context.timers.set('AutoEncoder.encode', (context.timers.get('AutoEncoder.encode') ?? 0) + elapsedTimeMs);
208
+ }
209
+
210
+ return result;
211
+ };
212
+
213
+ // Add middleware to log timers
214
+ const LogTimersMiddleware: ResponseMiddleware = {
215
+ handleResponse(request: Request, response: Response, error?: Error) {
216
+ const prefix = !error ? [] : requestPrefix(request, 'error');
217
+ const context = (request as any)._context as ContextInstance | undefined;
218
+
219
+ if (context && request.method !== 'OPTIONS') {
220
+ logger.log(
221
+ ...prefix,
222
+ ' - Timers: ' + Array.from(context.timers.entries()).sort((a, b) => {
223
+ return b[1] - a[1];
224
+ }).map(([key, value]) => `\n- ${key}: ${value.toFixed(2)}ms`).join(''),
225
+ );
226
+ }
227
+ },
228
+ };
229
+ routerServer.addResponseMiddleware(LogTimersMiddleware);
230
+ }
@@ -9,7 +9,7 @@ async function fetch(query: LimitedFilteredRequest) {
9
9
  const recipients: EmailRecipient[] = [];
10
10
  const memberIds = new Set(result.results.map(doc => doc.memberId).filter(id => id !== null)); // silently skip null memberIds
11
11
 
12
- const members = await Member.getBlobByIds(...memberIds);
12
+ const members = await Member.getByIdsWithUsers(...memberIds);
13
13
  for (const member of members) {
14
14
  const emails = member.details.getNotificationEmails();
15
15
  for (const user of member.users) {
@@ -254,7 +254,7 @@ async function getMemberRecipients(ids: { memberId: string; payment: PaymentGene
254
254
  }
255
255
 
256
256
  const allMemberIds = Formatter.uniqueArray(ids.map(i => i.memberId));
257
- const members = await Member.getBlobByIds(...allMemberIds);
257
+ const members = await Member.getByIdsWithUsers(...allMemberIds);
258
258
 
259
259
  const results: EmailRecipient[] = [];
260
260
 
@@ -4,9 +4,9 @@ import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { EmailVerificationCode, Member, PasswordToken, Platform, Token, User } from '@stamhoofd/models';
5
5
  import { LoginMethod, NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
6
6
 
7
- import { Context } from '../../helpers/Context';
8
- import { MemberUserSyncer } from '../../helpers/MemberUserSyncer';
9
- import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures';
7
+ import { Context } from '../../helpers/Context.js';
8
+ import { MemberUserSyncer } from '../../helpers/MemberUserSyncer.js';
9
+ import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures.js';
10
10
 
11
11
  type Params = { id: string };
12
12
  type Query = undefined;
@@ -49,7 +49,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
49
49
 
50
50
  if (await Context.auth.canEditUserName(editUser)) {
51
51
  if (editUser.memberId) {
52
- const member = await Member.getWithRegistrations(editUser.memberId);
52
+ const member = await Member.getByID(editUser.memberId);
53
53
  if (member) {
54
54
  member.details.firstName = request.body.firstName ?? member.details.firstName;
55
55
  member.details.lastName = request.body.lastName ?? member.details.lastName;
@@ -1,9 +1,9 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { Member, MemberWithRegistrations } from '@stamhoofd/models';
2
+ import { Member, MemberWithUsersRegistrationsAndGroups } from '@stamhoofd/models';
3
3
 
4
4
  import { MembersBlob } from '@stamhoofd/structures';
5
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
6
- import { Context } from '../../../helpers/Context';
5
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
6
+ import { Context } from '../../../helpers/Context.js';
7
7
  type Params = { id: string };
8
8
  type Query = undefined;
9
9
  type Body = undefined;
@@ -47,7 +47,7 @@ export class GetMemberFamilyEndpoint extends Endpoint<Params, Query, Body, Respo
47
47
 
48
48
  let foundMember = false;
49
49
 
50
- const validatedMembers: MemberWithRegistrations[] = [];
50
+ const validatedMembers: MemberWithUsersRegistrationsAndGroups[] = [];
51
51
 
52
52
  for (const member of members) {
53
53
  if (member.id === request.params.id) {
@@ -126,10 +126,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
126
126
  }
127
127
  }
128
128
 
129
- const query = SQL
130
- .select(
131
- SQL.column('members', 'id'),
132
- )
129
+ const query = Member.select()
133
130
  .setMaxExecutionTime(15 * 1000)
134
131
  .from(
135
132
  SQL.table('members'),
@@ -257,7 +254,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
257
254
 
258
255
  static async buildData(requestQuery: LimitedFilteredRequest, permissionLevel = PermissionLevel.Read) {
259
256
  const query = await GetMembersEndpoint.buildQuery(requestQuery, permissionLevel);
260
- let data: SQLResultNamespacedRow[];
257
+ let data: Member[];
261
258
 
262
259
  try {
263
260
  data = await query.fetch();
@@ -273,16 +270,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
273
270
  throw error;
274
271
  }
275
272
 
276
- const memberIds = data.map((r) => {
277
- if (typeof r.members.id === 'string') {
278
- return r.members.id;
279
- }
280
- throw new Error('Expected string');
281
- });
282
-
283
- const _members = await Member.getBlobByIds(...memberIds);
284
- // Make sure members is in same order as memberIds
285
- const members = memberIds.map(id => _members.find(m => m.id === id)!);
273
+ const members = await Member.loadRegistrationsAndUsers(data, true);
286
274
 
287
275
  for (const member of members) {
288
276
  if (!await Context.auth.canAccessMember(member, permissionLevel)) {
@@ -293,7 +281,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
293
281
 
294
282
  let next: LimitedFilteredRequest | undefined;
295
283
 
296
- if (memberIds.length >= requestQuery.limit) {
284
+ if (members.length >= requestQuery.limit) {
297
285
  const lastObject = members[members.length - 1];
298
286
  const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
299
287
 
@@ -2,7 +2,7 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
2
2
  import { AutoEncoderPatchType, ConvertArrayToPatchableArray, Decoder, isEmptyPatch, isPatchableArray, PatchableArray, 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 { AuditLog, BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
5
+ import { AuditLog, BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, MemberWithUsersAndRegistrations, MemberWithUsersRegistrationsAndGroups, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
6
  import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, EmergencyContact, GroupType, MemberDetails, MemberResponsibility, MembersBlob, MemberWithRegistrationsBlob, Parent, PermissionLevel, SetupStepType } from '@stamhoofd/structures';
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
@@ -72,7 +72,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
72
72
  }
73
73
  }
74
74
 
75
- const members: MemberWithRegistrations[] = [];
75
+ const members: MemberWithUsersRegistrationsAndGroups[] = [];
76
76
 
77
77
  const platform = await Platform.getShared();
78
78
 
@@ -717,7 +717,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
717
717
 
718
718
  // Loop all members one by one
719
719
  for (const id of ids) {
720
- const member = await Member.getWithRegistrations(id);
720
+ const member = await Member.getByIdWithUsersAndRegistrations(id);
721
721
  if (!member || !await Context.auth.canDeleteMember(member)) {
722
722
  throw Context.auth.error($t(`39f5696c-3755-429f-b0da-a0ca920ed11e`));
723
723
  }
@@ -754,9 +754,10 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
754
754
  }
755
755
  }
756
756
 
757
- static async mergeDuplicateRelations(member: MemberWithRegistrations, patch: AutoEncoderPatchType<MemberDetails> | MemberDetails) {
758
- const _familyMembers = await Member.getFamilyWithRegistrations(member.id);
759
- const familyMembers: typeof _familyMembers = [];
757
+ static async mergeDuplicateRelations(member: Member, patch: AutoEncoderPatchType<MemberDetails> | MemberDetails) {
758
+ const __familyMembers = await Member.getFamily(member.id);
759
+ const _familyMembers = await Member.loadRegistrationsAndUsers(__familyMembers);
760
+ const familyMembers: Member[] = [];
760
761
  // Only modify members if we have write access to them (this avoids issues with overriding data)
761
762
  for (const member of _familyMembers) {
762
763
  if (await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
@@ -901,7 +902,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
901
902
  }
902
903
  }
903
904
 
904
- static async checkSecurityCode(member: MemberWithRegistrations, securityCode: string | null | undefined, type: 'put' | 'patch') {
905
+ static async checkSecurityCode(member: MemberWithUsersRegistrationsAndGroups, securityCode: string | null | undefined, type: 'put' | 'patch') {
905
906
  if ((type === 'put' && await member.isSafeToMergeDuplicateWithoutSecurityCode()) || await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
906
907
  console.log('checkSecurityCode: without security code: allowed for ' + member.id);
907
908
  }
@@ -3,8 +3,8 @@ import { BalanceItem, Member, Organization, Payment } from '@stamhoofd/models';
3
3
  import { DetailedPayableBalanceCollection, DetailedPayableBalance } from '@stamhoofd/structures';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
7
- import { Context } from '../../../helpers/Context';
6
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
7
+ import { Context } from '../../../helpers/Context.js';
8
8
 
9
9
  type Params = Record<string, never>;
10
10
  type Query = undefined;
@@ -3,7 +3,7 @@ import { Document, DocumentTemplate, Member } from '@stamhoofd/models';
3
3
  import { Document as DocumentStruct, DocumentStatus } from '@stamhoofd/structures';
4
4
  import { Sorter } from '@stamhoofd/utility';
5
5
 
6
- import { Context } from '../../../helpers/Context';
6
+ import { Context } from '../../../helpers/Context.js';
7
7
  type Params = Record<string, never>;
8
8
  type Query = undefined;
9
9
  type Body = undefined;
@@ -31,7 +31,7 @@ export class GetUserDocumentsEndpoint extends Endpoint<Params, Query, Body, Resp
31
31
  const organization = await Context.setUserOrganizationScope();
32
32
  const { user } = await Context.authenticate();
33
33
 
34
- const members = await Member.getMembersWithRegistrationForUser(user);
34
+ const members = await Member.getMembersForUser(user);
35
35
  let templates = organization ? await DocumentTemplate.where({ status: 'Published', organizationId: organization.id }) : null;
36
36
  const memberIds = members.map(m => m.id);
37
37
  const templateIds = templates ? templates.map(t => t.id) : null;
@@ -1,7 +1,7 @@
1
1
  import { PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { Request } from '@simonbackx/simple-endpoints';
3
3
  import { EmailMocker } from '@stamhoofd/email';
4
- import { BalanceItemFactory, Group, GroupFactory, Member, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import { BalanceItemFactory, Group, GroupFactory, Member, MemberFactory, MemberWithUsersRegistrationsAndGroups, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
5
5
  import { AccessRight, BalanceItemCartItem, BalanceItemStatus, BalanceItemType, BooleanStatus, Company, GroupOption, GroupOptionMenu, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, OrganizationPackages, PaymentCustomer, PaymentMethod, PermissionLevel, Permissions, PermissionsResourceType, ReduceablePrice, RegisterItemOption, ResourcePermissions, STPackageStatus, STPackageType, UitpasNumberDetails, UitpasSocialTariff, UitpasSocialTariffStatus, UserPermissions, Version } from '@stamhoofd/structures';
6
6
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
7
7
  import { v4 as uuidv4 } from 'uuid';
@@ -80,7 +80,7 @@ describe('Endpoint.RegisterMembers', () => {
80
80
  const member = await new MemberFactory({ organization, user: linkMembersToUser ? user : undefined })
81
81
  .create();
82
82
 
83
- const otherMembers: MemberWithRegistrations[] = [];
83
+ const otherMembers: MemberWithUsersRegistrationsAndGroups[] = [];
84
84
 
85
85
  for (let i = 0; i < otherMemberAmount; i++) {
86
86
  otherMembers.push(await new MemberFactory({ organization, user: linkMembersToUser ? user : undefined })
@@ -4,9 +4,9 @@ import { Decoder } from '@simonbackx/simple-encoding';
4
4
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
6
  import { Email } from '@stamhoofd/email';
7
- import { BalanceItem, BalanceItemPayment, CachedBalance, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
7
+ import { BalanceItem, BalanceItemPayment, CachedBalance, Group, Member, MemberWithUsersRegistrationsAndGroups, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
8
8
  import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, ReceivableBalanceType, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
9
- import { Formatter } from '@stamhoofd/utility';
9
+ import { Formatter, sleep } from '@stamhoofd/utility';
10
10
 
11
11
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
12
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
@@ -145,7 +145,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
145
145
  memberBalanceItemsStructs = balanceItemsModels.map(i => i.getStructure());
146
146
  }
147
147
 
148
- let members: MemberWithRegistrations[] = [];
148
+ let members: MemberWithUsersRegistrationsAndGroups[] = [];
149
149
  if (request.body.asOrganizationId) {
150
150
  const memberIds = Formatter.uniqueArray(
151
151
  [...request.body.memberIds, ...deleteRegistrationModels.map(i => i.memberId), ...balanceItemsModels.map(i => i.memberId).filter(m => m !== null)],
@@ -900,21 +900,26 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
900
900
  }
901
901
  }
902
902
 
903
- const updatedMembers = await Member.getBlobByIds(...members.map(m => m.id));
903
+ // Flush caches so data is up to date in response
904
+ await BalanceItemService.flushCaches(organization.id);
905
+
906
+ // Force reload registrations and group data
907
+ Member.unloadRegistrations(members);
908
+ await Member.loadRegistrations(members, true);
904
909
 
905
910
  return new Response(RegisterResponse.create({
906
911
  payment: payment ? PaymentStruct.create(payment) : null,
907
- members: await AuthenticatedStructures.membersBlob(updatedMembers),
912
+ members: await AuthenticatedStructures.membersBlob(members),
908
913
  registrations: registrations.map(r => Member.getRegistrationWithTinyMemberStructure(r)),
909
914
  paymentUrl,
910
915
  paymentQRCode,
911
916
  }));
912
917
  }
913
918
 
914
- async createPayment({ balanceItems, organization, user, checkout, members }: { balanceItems: Map<BalanceItem, number>; organization: Organization; user: User; checkout: IDRegisterCheckout; members: MemberWithRegistrations[] }) {
919
+ async createPayment({ balanceItems, organization, user, checkout, members }: { balanceItems: Map<BalanceItem, number>; organization: Organization; user: User; checkout: IDRegisterCheckout; members: MemberWithUsersRegistrationsAndGroups[] }) {
915
920
  // Calculate total price to pay
916
921
  let totalPrice = 0;
917
- const payMembers: MemberWithRegistrations[] = [];
922
+ const payMembers: MemberWithUsersRegistrationsAndGroups[] = [];
918
923
  let hasNegative = false;
919
924
 
920
925
  for (const [balanceItem, price] of balanceItems) {
@@ -4,7 +4,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Document, DocumentTemplate, Member, Registration } from '@stamhoofd/models';
5
5
  import { DocumentStatus, Document as DocumentStruct, PermissionLevel } from '@stamhoofd/structures';
6
6
 
7
- import { Context } from '../../../../helpers/Context';
7
+ import { Context } from '../../../../helpers/Context.js';
8
8
 
9
9
  type Params = Record<string, never>;
10
10
  type Query = undefined;
@@ -104,7 +104,7 @@ export class PatchDocumentEndpoint extends Endpoint<Params, Query, Body, Respons
104
104
  put.memberId = registration.memberId;
105
105
  }
106
106
  if (put.memberId) {
107
- const member = await Member.getWithRegistrations(put.memberId);
107
+ const member = await Member.getByIdWithUsersAndRegistrations(put.memberId);
108
108
  if (!member || !await Context.auth.canAccessMember(member, PermissionLevel.Read)) {
109
109
  throw new SimpleError({
110
110
  code: 'not_found',