@stamhoofd/backend 2.71.0 → 2.73.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 (44) hide show
  1. package/index.ts +1 -0
  2. package/package.json +10 -10
  3. package/src/audit-logs/OrganizationLogger.ts +1 -1
  4. package/src/audit-logs/PlatformLogger.ts +1 -0
  5. package/src/email-recipient-loaders/orders.ts +1 -1
  6. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
  7. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -7
  8. package/src/endpoints/auth/CreateTokenEndpoint.ts +11 -1
  9. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +26 -2
  10. package/src/endpoints/auth/PatchUserEndpoint.ts +24 -2
  11. package/src/endpoints/auth/SignupEndpoint.ts +1 -1
  12. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +23 -3
  13. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +3 -3
  14. package/src/endpoints/global/events/GetEventsEndpoint.ts +6 -6
  15. package/src/endpoints/global/events/PatchEventsEndpoint.ts +36 -4
  16. package/src/endpoints/global/members/GetMembersEndpoint.ts +9 -7
  17. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +24 -14
  18. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +34 -0
  19. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -1
  20. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +20 -12
  21. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -3
  22. package/src/endpoints/global/sso/GetSSOEndpoint.ts +8 -1
  23. package/src/endpoints/global/sso/SetSSOEndpoint.ts +4 -0
  24. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
  25. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -6
  26. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
  27. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +5 -5
  28. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +51 -9
  29. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
  30. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +6 -6
  31. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +6 -6
  32. package/src/excel-loaders/members.ts +8 -0
  33. package/src/excel-loaders/receivable-balances.ts +294 -0
  34. package/src/helpers/AdminPermissionChecker.ts +4 -3
  35. package/src/helpers/AuthenticatedStructures.ts +32 -6
  36. package/src/helpers/SetupStepUpdater.ts +10 -4
  37. package/src/helpers/xlsxAddressTransformerColumnFactory.ts +8 -0
  38. package/src/services/PaymentReallocationService.ts +3 -2
  39. package/src/services/PaymentService.ts +17 -1
  40. package/src/services/SSOService.ts +68 -4
  41. package/src/sql-filters/members.ts +20 -1
  42. package/src/sql-filters/organizations.ts +1 -0
  43. package/src/sql-filters/receivable-balances.ts +53 -1
  44. 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.71.0",
3
+ "version": "2.73.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.71.0",
41
- "@stamhoofd/backend-middleware": "2.71.0",
42
- "@stamhoofd/email": "2.71.0",
43
- "@stamhoofd/models": "2.71.0",
44
- "@stamhoofd/queues": "2.71.0",
45
- "@stamhoofd/sql": "2.71.0",
46
- "@stamhoofd/structures": "2.71.0",
47
- "@stamhoofd/utility": "2.71.0",
40
+ "@stamhoofd/backend-i18n": "2.73.0",
41
+ "@stamhoofd/backend-middleware": "2.73.0",
42
+ "@stamhoofd/email": "2.73.0",
43
+ "@stamhoofd/models": "2.73.0",
44
+ "@stamhoofd/queues": "2.73.0",
45
+ "@stamhoofd/sql": "2.73.0",
46
+ "@stamhoofd/structures": "2.73.0",
47
+ "@stamhoofd/utility": "2.73.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": "8b35d36e694303905aa27cf9bff5539777d6ace1"
67
+ "gitHead": "fee6c6f01c19d15010807f2bac6a3ab5ee14019e"
68
68
  }
@@ -3,7 +3,7 @@ import { AuditLogType } from '@stamhoofd/structures';
3
3
  import { getDefaultGenerator, ModelLogger } from './ModelLogger';
4
4
 
5
5
  export const OrganizationLogger = new ModelLogger(Organization, {
6
- skipKeys: ['searchIndex'],
6
+ skipKeys: ['searchIndex', 'serverMeta'],
7
7
  optionsGenerator: getDefaultGenerator({
8
8
  created: AuditLogType.OrganizationAdded,
9
9
  updated: AuditLogType.OrganizationEdited,
@@ -3,6 +3,7 @@ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
3
3
  import { AuditLogType } from '@stamhoofd/structures';
4
4
 
5
5
  export const PlatformLogger = new ModelLogger(Platform, {
6
+ skipKeys: ['serverConfig'],
6
7
  optionsGenerator: getDefaultGenerator({
7
8
  updated: AuditLogType.PlatformSettingsChanged,
8
9
  }),
@@ -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 {
@@ -86,7 +86,8 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
86
86
 
87
87
  if (STAMHOOFD.userMode === 'platform') {
88
88
  const platform = await Platform.getShared();
89
- if (!platform.config.loginMethods.includes(LoginMethod.Password)) {
89
+ const config = platform.config.loginMethods.get(LoginMethod.Password);
90
+ if (!config) {
90
91
  throw new SimpleError({
91
92
  code: 'not_supported',
92
93
  message: 'This platform does not support password login',
@@ -94,6 +95,15 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
94
95
  statusCode: 400,
95
96
  });
96
97
  }
98
+
99
+ if (!config.isEnabledForEmail(request.body.username)) {
100
+ throw new SimpleError({
101
+ code: 'not_supported',
102
+ message: 'Login method not supported',
103
+ human: 'Je kan op dit account niet inloggen met een wachtwoord. Gebruik een andere methode om in te loggen.',
104
+ statusCode: 400,
105
+ });
106
+ }
97
107
  }
98
108
 
99
109
  const user = await User.login(organization?.id ?? null, request.body.username, request.body.password);
@@ -1,8 +1,9 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { PasswordToken, sendEmailTemplate, User } from '@stamhoofd/models';
4
- import { EmailTemplateType, ForgotPasswordRequest, Recipient, Replacement } from '@stamhoofd/structures';
3
+ import { PasswordToken, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
4
+ import { EmailTemplateType, ForgotPasswordRequest, LoginMethod, Recipient, Replacement } from '@stamhoofd/structures';
5
5
 
6
+ import { SimpleError } from '@simonbackx/simple-errors';
6
7
  import { Context } from '../../helpers/Context';
7
8
 
8
9
  type Params = Record<string, never>;
@@ -28,6 +29,29 @@ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, Respon
28
29
 
29
30
  async handle(request: DecodedRequest<Params, Query, Body>) {
30
31
  const organization = await Context.setOptionalOrganizationScope();
32
+
33
+ if (STAMHOOFD.userMode === 'platform') {
34
+ const platform = await Platform.getShared();
35
+ const config = platform.config.loginMethods.get(LoginMethod.Password);
36
+ if (!config) {
37
+ throw new SimpleError({
38
+ code: 'not_supported',
39
+ message: 'This platform does not support password login',
40
+ human: 'Dit platform ondersteunt geen wachtwoord login',
41
+ statusCode: 400,
42
+ });
43
+ }
44
+
45
+ if (!config.isEnabledForEmail(request.body.email)) {
46
+ throw new SimpleError({
47
+ code: 'not_supported',
48
+ message: 'Login method not supported',
49
+ human: 'Je kan op dit account geen wachtwoord gebruiken om in te loggen.',
50
+ statusCode: 400,
51
+ });
52
+ }
53
+ }
54
+
31
55
  const user = await User.getForAuthentication(organization?.id ?? null, request.body.email, { allowWithoutAccount: true });
32
56
 
33
57
  if (!user) {
@@ -1,8 +1,8 @@
1
1
  import { AutoEncoderPatchType, Decoder, isPatch } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { EmailVerificationCode, Member, PasswordToken, Token, User } from '@stamhoofd/models';
5
- import { NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
4
+ import { EmailVerificationCode, Member, PasswordToken, Platform, Token, User } from '@stamhoofd/models';
5
+ import { LoginMethod, NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
6
6
 
7
7
  import { Context } from '../../helpers/Context';
8
8
  import { MemberUserSyncer } from '../../helpers/MemberUserSyncer';
@@ -135,6 +135,28 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
135
135
  }
136
136
 
137
137
  if (editUser.id === user.id && request.body.password) {
138
+ if (STAMHOOFD.userMode === 'platform') {
139
+ const platform = await Platform.getShared();
140
+ const config = platform.config.loginMethods.get(LoginMethod.Password);
141
+ if (!config) {
142
+ throw new SimpleError({
143
+ code: 'not_supported',
144
+ message: 'This platform does not support password login',
145
+ human: 'Dit platform ondersteunt geen wachtwoord login',
146
+ statusCode: 400,
147
+ });
148
+ }
149
+
150
+ if (!config.isEnabledForEmail(editUser.email)) {
151
+ throw new SimpleError({
152
+ code: 'not_supported',
153
+ message: 'Login method not supported',
154
+ human: 'Je kan op dit account geen wachtwoord gebruiken om in te loggen.',
155
+ statusCode: 400,
156
+ });
157
+ }
158
+ }
159
+
138
160
  // password changes
139
161
  await editUser.changePassword(request.body.password);
140
162
  await PasswordToken.clearFor(editUser.id);
@@ -32,7 +32,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
32
32
 
33
33
  if (STAMHOOFD.userMode === 'platform') {
34
34
  const platform = await Platform.getShared();
35
- if (!platform.config.loginMethods.includes(LoginMethod.Password)) {
35
+ if (!platform.config.loginMethods.has(LoginMethod.Password)) {
36
36
  throw new SimpleError({
37
37
  code: 'not_supported',
38
38
  message: 'This platform does not support password login',
@@ -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
  }
@@ -3,15 +3,15 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Member, Platform } from '@stamhoofd/models';
5
5
  import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
6
- import { CountFilteredRequest, Country, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
6
+ import { CountFilteredRequest, Country, CountryCode, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
  import { DataValidator } from '@stamhoofd/utility';
8
8
 
9
+ import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
9
10
  import parsePhoneNumber from 'libphonenumber-js/max';
10
11
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
11
12
  import { Context } from '../../../helpers/Context';
12
13
  import { memberFilterCompilers } from '../../../sql-filters/members';
13
14
  import { memberSorters } from '../../../sql-sorters/members';
14
- import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
15
15
 
16
16
  type Params = Record<string, never>;
17
17
  type Query = LimitedFilteredRequest;
@@ -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) {
@@ -141,7 +141,9 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
141
141
  if (!searchFilter && q.search.match(/^\+?[0-9\s-]+$/)) {
142
142
  // Try to format as phone so we have 1:1 space matches
143
143
  try {
144
- const phoneNumber = parsePhoneNumber(q.search, (Context.i18n.country as Country) || Country.Belgium);
144
+ const country = (Context.i18n.country as CountryCode) || Country.Belgium;
145
+
146
+ const phoneNumber = parsePhoneNumber(q.search, country);
145
147
  if (phoneNumber && phoneNumber.isValid()) {
146
148
  const formatted = phoneNumber.formatInternational();
147
149
  searchFilter = {
@@ -219,13 +221,13 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
219
221
  // todo: Address search detection
220
222
 
221
223
  if (searchFilter) {
222
- query.where(compileToSQLFilter(searchFilter, filterCompilers));
224
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
223
225
  }
224
226
  }
225
227
 
226
228
  if (q instanceof LimitedFilteredRequest) {
227
229
  if (q.pageFilter) {
228
- query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
230
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
229
231
  }
230
232
 
231
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
+ }