@stamhoofd/backend 2.72.0 → 2.73.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 (34) hide show
  1. package/index.ts +1 -0
  2. package/package.json +10 -10
  3. package/src/email-recipient-loaders/orders.ts +1 -1
  4. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
  5. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -7
  6. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +23 -3
  7. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +3 -3
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +6 -6
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +36 -4
  10. package/src/endpoints/global/members/GetMembersEndpoint.ts +4 -4
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +24 -14
  12. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +34 -0
  13. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -1
  14. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +20 -12
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -3
  16. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
  17. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -6
  18. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
  19. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +5 -5
  20. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +51 -9
  21. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
  22. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +6 -6
  23. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +6 -6
  24. package/src/excel-loaders/members.ts +8 -0
  25. package/src/excel-loaders/receivable-balances.ts +294 -0
  26. package/src/helpers/AuthenticatedStructures.ts +32 -6
  27. package/src/helpers/SetupStepUpdater.ts +10 -4
  28. package/src/helpers/xlsxAddressTransformerColumnFactory.ts +8 -0
  29. package/src/services/PaymentReallocationService.ts +3 -2
  30. package/src/services/PaymentService.ts +17 -1
  31. package/src/sql-filters/members.ts +20 -1
  32. package/src/sql-filters/organizations.ts +1 -0
  33. package/src/sql-filters/receivable-balances.ts +53 -1
  34. package/src/sql-sorters/organizations.ts +11 -0
package/index.ts CHANGED
@@ -97,6 +97,7 @@ const start = async () => {
97
97
  await import('./src/excel-loaders/members');
98
98
  await import('./src/excel-loaders/payments');
99
99
  await import('./src/excel-loaders/organizations');
100
+ await import('./src/excel-loaders/receivable-balances');
100
101
 
101
102
  // Register Email Recipient loaders
102
103
  await import('./src/email-recipient-loaders/members');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.72.0",
3
+ "version": "2.73.1",
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.72.0",
41
- "@stamhoofd/backend-middleware": "2.72.0",
42
- "@stamhoofd/email": "2.72.0",
43
- "@stamhoofd/models": "2.72.0",
44
- "@stamhoofd/queues": "2.72.0",
45
- "@stamhoofd/sql": "2.72.0",
46
- "@stamhoofd/structures": "2.72.0",
47
- "@stamhoofd/utility": "2.72.0",
40
+ "@stamhoofd/backend-i18n": "2.73.1",
41
+ "@stamhoofd/backend-middleware": "2.73.1",
42
+ "@stamhoofd/email": "2.73.1",
43
+ "@stamhoofd/models": "2.73.1",
44
+ "@stamhoofd/queues": "2.73.1",
45
+ "@stamhoofd/sql": "2.73.1",
46
+ "@stamhoofd/structures": "2.73.1",
47
+ "@stamhoofd/utility": "2.73.1",
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": "9ca2f0fff00eafff2a398dbfdbfb032367473d1c"
67
+ "gitHead": "af8fb928f0d8fdfe07dcf1433d99a79a0cc44ea7"
68
68
  }
@@ -54,7 +54,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.Orders, {
54
54
  $neq: null,
55
55
  },
56
56
  }]);
57
- const q = GetWebshopOrdersEndpoint.buildQuery(query);
57
+ const q = await GetWebshopOrdersEndpoint.buildQuery(query);
58
58
  return await q.count();
59
59
  },
60
60
  });
@@ -28,7 +28,7 @@ export class GetOrganizationsCountEndpoint extends Endpoint<Params, Query, Body,
28
28
 
29
29
  async handle(request: DecodedRequest<Params, Query, Body>) {
30
30
  await Context.authenticate();
31
- const query = GetOrganizationsEndpoint.buildQuery(request.query);
31
+ const query = await GetOrganizationsEndpoint.buildQuery(request.query);
32
32
 
33
33
  const count = await query
34
34
  .count();
@@ -5,11 +5,11 @@ import { Organization } from '@stamhoofd/models';
5
5
  import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
6
6
  import { CountFilteredRequest, LimitedFilteredRequest, Organization as OrganizationStruct, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
 
8
+ import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
8
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
9
10
  import { Context } from '../../../helpers/Context';
10
11
  import { organizationFilterCompilers } from '../../../sql-filters/organizations';
11
12
  import { organizationSorters } from '../../../sql-sorters/organizations';
12
- import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
13
13
 
14
14
  type Params = Record<string, never>;
15
15
  type Query = LimitedFilteredRequest;
@@ -35,7 +35,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
35
35
  return [false];
36
36
  }
37
37
 
38
- static buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
38
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
39
39
  const tags = Context.auth.getPlatformAccessibleOrganizationTags(PermissionLevel.Read);
40
40
  if (tags !== 'all' && tags.length === 0) {
41
41
  throw Context.auth.error();
@@ -62,11 +62,11 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
62
62
  );
63
63
 
64
64
  if (scopeFilter) {
65
- query.where(compileToSQLFilter(scopeFilter, filterCompilers));
65
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
66
66
  }
67
67
 
68
68
  if (q.filter) {
69
- query.where(compileToSQLFilter(q.filter, filterCompilers));
69
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
70
70
  }
71
71
 
72
72
  if (q.search) {
@@ -80,13 +80,13 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
80
80
  };
81
81
 
82
82
  if (searchFilter) {
83
- query.where(compileToSQLFilter(searchFilter, filterCompilers));
83
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
84
84
  }
85
85
  }
86
86
 
87
87
  if (q instanceof LimitedFilteredRequest) {
88
88
  if (q.pageFilter) {
89
- query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
89
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
90
90
  }
91
91
 
92
92
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
@@ -116,7 +116,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
116
116
  });
117
117
  }
118
118
 
119
- const query = GetOrganizationsEndpoint.buildQuery(requestQuery);
119
+ const query = await GetOrganizationsEndpoint.buildQuery(requestQuery);
120
120
  let data: SQLResultNamespacedRow[];
121
121
 
122
122
  try {
@@ -31,8 +31,28 @@ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, Respons
31
31
 
32
32
  async handle(request: DecodedRequest<Params, Query, Body>) {
33
33
  // Escape query
34
- const query = request.query.query.replace(/([-+><()~*"@\s]+)/g, ' ').replace(/[^\w\d]+$/, '');
35
- if (query.length == 0) {
34
+ const rawQuery = request.query.query.replace(/([-+><()~*"@\s]+)/g, ' ');
35
+ const words = rawQuery.split(' ').filter(w => w.length > 0);
36
+
37
+ // Escape words
38
+ const cleanedWords: string[] = [];
39
+ for (const [index, word] of words.entries()) {
40
+ // If contains special char (non a-zA-Z) - escape with " character
41
+ if (/^[a-zA-Z0-9]*$/.test(word)) {
42
+ if (index === words.length - 1) {
43
+ cleanedWords.push('+' + word + '*');
44
+ }
45
+ else {
46
+ cleanedWords.push('+' + word);
47
+ }
48
+ }
49
+ else {
50
+ cleanedWords.push('+"' + word + '"');
51
+ }
52
+ }
53
+ const query = cleanedWords.join(' ');
54
+
55
+ if (query.length === 0) {
36
56
  // Do not try searching...
37
57
  return new Response(SearchRegions.create({
38
58
  cities: [],
@@ -43,7 +63,7 @@ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, Respons
43
63
 
44
64
  const match = {
45
65
  sign: 'MATCH',
46
- value: query + '*', // We replace special operators in boolean mode with spaces since special characters aren't indexed anyway
66
+ value: query, // We replace special operators in boolean mode with spaces since special characters aren't indexed anyway
47
67
  mode: 'BOOLEAN',
48
68
  };
49
69
 
@@ -61,11 +61,11 @@ export class GetAuditLogsEndpoint extends Endpoint<Params, Query, Body, Response
61
61
  );
62
62
 
63
63
  if (scopeFilter) {
64
- query.where(compileToSQLFilter(scopeFilter, filterCompilers));
64
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
65
65
  }
66
66
 
67
67
  if (q.filter) {
68
- query.where(compileToSQLFilter(q.filter, filterCompilers));
68
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
69
69
  }
70
70
 
71
71
  if (q.search) {
@@ -78,7 +78,7 @@ export class GetAuditLogsEndpoint extends Endpoint<Params, Query, Body, Response
78
78
 
79
79
  if (q instanceof LimitedFilteredRequest) {
80
80
  if (q.pageFilter) {
81
- query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
81
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
82
82
  }
83
83
 
84
84
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
@@ -34,7 +34,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
34
34
  return [false];
35
35
  }
36
36
 
37
- static buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
37
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
38
38
  const organization = Context.organization;
39
39
  let scopeFilter: StamhoofdFilter | undefined = undefined;
40
40
 
@@ -60,11 +60,11 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
60
60
  );
61
61
 
62
62
  if (scopeFilter) {
63
- query.where(compileToSQLFilter(scopeFilter, filterCompilers));
63
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
64
64
  }
65
65
 
66
66
  if (q.filter) {
67
- query.where(compileToSQLFilter(q.filter, filterCompilers));
67
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
68
68
  }
69
69
 
70
70
  if (q.search) {
@@ -78,13 +78,13 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
78
78
  };
79
79
 
80
80
  if (searchFilter) {
81
- query.where(compileToSQLFilter(searchFilter, filterCompilers));
81
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
82
82
  }
83
83
  }
84
84
 
85
85
  if (q instanceof LimitedFilteredRequest) {
86
86
  if (q.pageFilter) {
87
- query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
87
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
88
88
  }
89
89
 
90
90
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
@@ -96,7 +96,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
96
96
  }
97
97
 
98
98
  static async buildData(requestQuery: LimitedFilteredRequest) {
99
- const query = GetEventsEndpoint.buildQuery(requestQuery);
99
+ const query = await GetEventsEndpoint.buildQuery(requestQuery);
100
100
  const data = await query.fetch();
101
101
 
102
102
  const events = Event.fromRows(data, Event.table);
@@ -1,15 +1,15 @@
1
1
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { Event, Group, Platform, RegistrationPeriod } from '@stamhoofd/models';
4
- import { Event as EventStruct, GroupType, NamedObject, Group as GroupStruct, AuditLogType, AuditLogSource } from '@stamhoofd/structures';
4
+ import { AuditLogSource, Event as EventStruct, Group as GroupStruct, GroupType, NamedObject } from '@stamhoofd/structures';
5
5
 
6
6
  import { SimpleError } from '@simonbackx/simple-errors';
7
7
  import { SQL, SQLWhereSign } from '@stamhoofd/sql';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { Context } from '../../../helpers/Context';
11
- import { PatchOrganizationRegistrationPeriodsEndpoint } from '../../organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
12
11
  import { AuditLogService } from '../../../services/AuditLogService';
12
+ import { PatchOrganizationRegistrationPeriodsEndpoint } from '../../organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
13
13
 
14
14
  type Params = { id: string };
15
15
  type Query = undefined;
@@ -67,6 +67,22 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
67
67
  });
68
68
  }
69
69
 
70
+ if (event.meta.defaultAgeGroupIds && event.meta.defaultAgeGroupIds.length === 0) {
71
+ throw new SimpleError({
72
+ code: 'invalid_field',
73
+ message: 'Empty default age groups',
74
+ human: 'Kies minstens één standaard leeftijdsgroep',
75
+ });
76
+ }
77
+
78
+ if (event.meta.organizationTagIds && event.meta.organizationTagIds.length === 0) {
79
+ throw new SimpleError({
80
+ code: 'invalid_field',
81
+ message: 'Empty organization tag ids',
82
+ human: 'Kies minstens één tag',
83
+ });
84
+ }
85
+
70
86
  const eventOrganization = await this.checkEventAccess(event);
71
87
  event.id = put.id;
72
88
  event.name = put.name;
@@ -85,7 +101,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
85
101
  throw new SimpleError({
86
102
  code: 'invalid_period',
87
103
  message: 'No period found for this start date',
88
- human: 'Oeps, je kan nog geen evenementen met inschrijvingen aanmaken in deze periode. Dit werkjaar is nog niet aangemaakt in het systeem.',
104
+ human: Context.i18n.$t('5959a6a9-064a-413c-871f-c74a145ed569'),
89
105
  field: 'startDate',
90
106
  });
91
107
  }
@@ -154,6 +170,22 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
154
170
  });
155
171
  }
156
172
 
173
+ if (event.meta.defaultAgeGroupIds && event.meta.defaultAgeGroupIds.length === 0) {
174
+ throw new SimpleError({
175
+ code: 'invalid_field',
176
+ message: 'Empty default age groups',
177
+ human: 'Kies minstens één standaard leeftijdsgroep',
178
+ });
179
+ }
180
+
181
+ if (event.meta.organizationTagIds && event.meta.organizationTagIds.length === 0) {
182
+ throw new SimpleError({
183
+ code: 'invalid_field',
184
+ message: 'Empty organization tag ids',
185
+ human: 'Kies minstens één tag',
186
+ });
187
+ }
188
+
157
189
  const eventOrganization = await this.checkEventAccess(event);
158
190
  if (eventOrganization) {
159
191
  event.meta.organizationCache = NamedObject.create({ id: eventOrganization.id, name: eventOrganization.name });
@@ -210,7 +242,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
210
242
  throw new SimpleError({
211
243
  code: 'invalid_period',
212
244
  message: 'No period found for this start date',
213
- human: 'Oeps, je kan nog geen evenementen met inschrijvingen aanmaken in deze periode. Dit werkjaar is nog niet aangemaakt in het systeem.',
245
+ human: Context.i18n.$t('5959a6a9-064a-413c-871f-c74a145ed569'),
214
246
  field: 'startDate',
215
247
  });
216
248
  }
@@ -127,11 +127,11 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
127
127
  );
128
128
 
129
129
  if (scopeFilter) {
130
- query.where(compileToSQLFilter(scopeFilter, filterCompilers));
130
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
131
131
  }
132
132
 
133
133
  if (q.filter) {
134
- query.where(compileToSQLFilter(q.filter, filterCompilers));
134
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
135
135
  }
136
136
 
137
137
  if (q.search) {
@@ -221,13 +221,13 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
221
221
  // todo: Address search detection
222
222
 
223
223
  if (searchFilter) {
224
- query.where(compileToSQLFilter(searchFilter, filterCompilers));
224
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
225
225
  }
226
226
  }
227
227
 
228
228
  if (q instanceof LimitedFilteredRequest) {
229
229
  if (q.pageFilter) {
230
- query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
230
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
231
231
  }
232
232
 
233
233
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
@@ -16,6 +16,7 @@ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
16
16
  import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
17
17
  import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
18
18
  import { RegistrationService } from '../../../services/RegistrationService';
19
+ import { shouldCheckIfMemberIsDuplicateForPatch, shouldCheckIfMemberIsDuplicateForPut } from './shouldCheckIfMemberIsDuplicate';
19
20
 
20
21
  type Params = Record<string, never>;
21
22
  type Query = undefined;
@@ -107,10 +108,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
107
108
  struct.details.cleanData();
108
109
  member.details = struct.details;
109
110
 
110
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
111
- if (duplicate) {
111
+ if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
112
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
113
+ if (duplicate) {
112
114
  // Merge data
113
- member = duplicate;
115
+ member = duplicate;
116
+ }
114
117
  }
115
118
 
116
119
  // We risk creating a new member without being able to access it manually afterwards
@@ -166,6 +169,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
166
169
  patch = await Context.auth.filterMemberPatch(member, patch);
167
170
  const originalDetails = member.details.clone();
168
171
 
172
+ let shouldCheckDuplicate = false;
173
+
169
174
  if (patch.details) {
170
175
  if (patch.details.isPut()) {
171
176
  throw new SimpleError({
@@ -176,6 +181,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
176
181
  });
177
182
  }
178
183
 
184
+ shouldCheckDuplicate = shouldCheckIfMemberIsDuplicateForPatch(patch, originalDetails);
185
+
179
186
  const wasReduced = member.details.shouldApplyReducedPrice;
180
187
  member.details.patchOrPut(patch.details);
181
188
  member.details.cleanData();
@@ -185,17 +192,20 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
185
192
  }
186
193
  }
187
194
 
188
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
189
- if (duplicate) {
190
- // Remove the member from the list
191
- const iii = members.findIndex(m => m.id === member.id);
192
- if (iii !== -1) {
193
- members.splice(iii, 1);
194
- }
195
+ if (shouldCheckDuplicate) {
196
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
195
197
 
196
- // Add new
197
- members.push(duplicate);
198
- continue;
198
+ if (duplicate) {
199
+ // Remove the member from the list
200
+ const iii = members.findIndex(m => m.id === member.id);
201
+ if (iii !== -1) {
202
+ members.splice(iii, 1);
203
+ }
204
+
205
+ // Add new
206
+ members.push(duplicate);
207
+ continue;
208
+ }
199
209
  }
200
210
 
201
211
  await member.save();
@@ -315,7 +325,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
315
325
  throw new SimpleError({
316
326
  code: 'invalid_field',
317
327
  message: 'Invalid organization',
318
- human: 'Je kan een functie enkel toekennen aan leden die zijn ingeschreven in het huidige werkjaar',
328
+ human: Context.i18n.$t('d41cdbe3-57e3-4a2e-83bc-cb9e65c9c840'),
319
329
  });
320
330
  }
321
331
 
@@ -0,0 +1,34 @@
1
+ import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
2
+ import { MemberDetails, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
3
+
4
+ export function shouldCheckIfMemberIsDuplicateForPatch(patch: { details: MemberDetails | AutoEncoderPatchType<MemberDetails> | undefined }, originalDetails: MemberDetails): boolean {
5
+ if (patch.details === undefined) {
6
+ return false;
7
+ }
8
+
9
+ return (
10
+ // has long first name
11
+ ((patch.details.firstName !== undefined && patch.details.firstName.length > 3) || (patch.details.firstName === undefined && originalDetails.firstName.length > 3))
12
+ // or has long last name
13
+ || ((patch.details.lastName !== undefined && patch.details.lastName.length > 3) || (patch.details.lastName === undefined && originalDetails.lastName.length > 3))
14
+ )
15
+ // has name change or birthday change
16
+ && (
17
+ // has first name change
18
+ (patch.details.firstName !== undefined && patch.details.firstName !== originalDetails.firstName)
19
+ // has last name change
20
+ || (patch.details.lastName !== undefined && patch.details.lastName !== originalDetails.lastName)
21
+ // has birth day change
22
+ || (patch.details.birthDay !== undefined && patch.details.birthDay?.getTime() !== originalDetails.birthDay?.getTime())
23
+ );
24
+ }
25
+
26
+ export function shouldCheckIfMemberIsDuplicateForPut(put: MemberWithRegistrationsBlob): boolean {
27
+ if (put.details.firstName.length <= 3 && put.details.lastName.length <= 3) {
28
+ return false;
29
+ }
30
+
31
+ const age = put.details.age;
32
+ // do not check if member is duplicate for historical members
33
+ return age !== null && age < 81;
34
+ }
@@ -8,9 +8,9 @@ import { QueueHandler } from '@stamhoofd/queues';
8
8
  import { Context } from '../../../helpers/Context';
9
9
  import { MembershipCharger } from '../../../helpers/MembershipCharger';
10
10
  import { PeriodHelper } from '../../../helpers/PeriodHelper';
11
+ import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
11
12
  import { TagHelper } from '../../../helpers/TagHelper';
12
13
  import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
13
- import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
14
14
 
15
15
  type Params = Record<string, never>;
16
16
  type Query = undefined;
@@ -265,6 +265,16 @@ export class PatchPlatformEndpoint extends Endpoint<
265
265
  newPremiseTypes: PlatformPremiseType[],
266
266
  oldPremiseTypes: PlatformPremiseType[],
267
267
  ) {
268
+ // should be updated because the step will be removed
269
+ if (newPremiseTypes.length === 0 && oldPremiseTypes.length !== 0) {
270
+ return true;
271
+ }
272
+
273
+ // should be updated because the step will be added
274
+ if (newPremiseTypes.length !== 0 && oldPremiseTypes.length === 0) {
275
+ return true;
276
+ }
277
+
268
278
  for (const premiseType of newPremiseTypes) {
269
279
  const id = premiseType.id;
270
280
  const oldVersion = oldPremiseTypes.find(x => x.id === id);
@@ -2,13 +2,13 @@ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArra
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Document, Member, RateLimiter } from '@stamhoofd/models';
5
- import { AuditLogType, MemberDetails, MembersBlob, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
5
+ import { MemberDetails, MembersBlob, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
6
6
 
7
7
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
8
8
  import { Context } from '../../../helpers/Context';
9
9
  import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
10
10
  import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
11
- import { AuditLogService } from '../../../services/AuditLogService';
11
+ import { shouldCheckIfMemberIsDuplicateForPatch, shouldCheckIfMemberIsDuplicateForPut } from '../members/shouldCheckIfMemberIsDuplicate';
12
12
  type Params = Record<string, never>;
13
13
  type Query = undefined;
14
14
  type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
@@ -61,10 +61,12 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
61
61
 
62
62
  this.throwIfInvalidDetails(member.details);
63
63
 
64
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
65
- if (duplicate) {
66
- addedMembers.push(duplicate);
67
- continue;
64
+ if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
65
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
66
+ if (duplicate) {
67
+ addedMembers.push(duplicate);
68
+ continue;
69
+ }
68
70
  }
69
71
 
70
72
  await member.save();
@@ -86,6 +88,8 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
86
88
  const securityCode = struct.details?.securityCode; // will get cleared after the filter
87
89
  struct = await Context.auth.filterMemberPatch(member, struct);
88
90
 
91
+ let shouldCheckDuplicate = false;
92
+
89
93
  if (struct.details) {
90
94
  if (struct.details.isPut()) {
91
95
  throw new SimpleError({
@@ -96,6 +100,8 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
96
100
  });
97
101
  }
98
102
 
103
+ shouldCheckDuplicate = shouldCheckIfMemberIsDuplicateForPatch(struct, member.details);
104
+
99
105
  member.details.patchOrPut(struct.details);
100
106
  member.details.cleanData();
101
107
  this.throwIfInvalidDetails(member.details);
@@ -110,14 +116,16 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
110
116
  });
111
117
  }
112
118
 
113
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
114
- if (duplicate) {
119
+ if (shouldCheckDuplicate) {
120
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
121
+ if (duplicate) {
115
122
  // Remove the member from the list
116
- members.splice(members.findIndex(m => m.id === member.id), 1);
123
+ members.splice(members.findIndex(m => m.id === member.id), 1);
117
124
 
118
- // Add new
119
- addedMembers.push(duplicate);
120
- continue;
125
+ // Add new
126
+ addedMembers.push(duplicate);
127
+ continue;
128
+ }
121
129
  }
122
130
 
123
131
  await member.save();
@@ -359,10 +359,14 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
359
359
 
360
360
  const createdBalanceItems: BalanceItem[] = [];
361
361
  const unrelatedCreatedBalanceItems: BalanceItem[] = [];
362
+ const deletedBalanceItems: BalanceItem[] = [];
362
363
  const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale || checkout.paymentMethod === PaymentMethod.Unknown;
363
364
 
364
365
  // Create negative balance items
365
- for (const registrationStruct of [...checkout.cart.deleteRegistrations, ...checkout.cart.items.flatMap(i => i.replaceRegistrations)]) {
366
+ for (const { registration: registrationStruct, deleted } of [
367
+ ...checkout.cart.deleteRegistrations.map(r => ({ registration: r, deleted: true })),
368
+ ...checkout.cart.items.flatMap(i => i.replaceRegistrations).map(r => ({ registration: r, deleted: false })),
369
+ ]) {
366
370
  if (whoWillPayNow !== 'nobody') {
367
371
  // this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
368
372
  throw new SimpleError({
@@ -398,7 +402,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
398
402
 
399
403
  // We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
400
404
  // Find all balance items of this registration and set them to zero
401
- await BalanceItem.deleteForDeletedRegistration(existingRegistration.id);
405
+ deletedBalanceItems.push(...(await BalanceItem.deleteForDeletedRegistration(existingRegistration.id, {
406
+ cancellationFeePercentage: deleted ? checkout.cancellationFeePercentage : 0,
407
+ })));
408
+
409
+ // todo: add cancelation fee
402
410
 
403
411
  // Clear the registration
404
412
  let group = groups.find(g => g.id === existingRegistration.groupId);
@@ -695,7 +703,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
695
703
  }
696
704
 
697
705
  // Reallocate
698
- await BalanceItemService.reallocate([...createdBalanceItems, ...unrelatedCreatedBalanceItems], organization.id);
706
+ await BalanceItemService.reallocate([...createdBalanceItems, ...unrelatedCreatedBalanceItems, ...deletedBalanceItems], organization.id);
699
707
 
700
708
  // Update occupancy
701
709
  for (const group of groups) {
@@ -35,7 +35,7 @@ export class GetDocumentsCountEndpoint extends Endpoint<Params, Query, Body, Res
35
35
  throw Context.auth.error();
36
36
  }
37
37
 
38
- const query = GetDocumentsEndpoint.buildQuery(request.query);
38
+ const query = await GetDocumentsEndpoint.buildQuery(request.query);
39
39
 
40
40
  const count = await query
41
41
  .count();