@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.
- package/index.ts +1 -0
- package/package.json +10 -10
- package/src/email-recipient-loaders/orders.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -7
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +23 -3
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +6 -6
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +36 -4
- package/src/endpoints/global/members/GetMembersEndpoint.ts +4 -4
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +24 -14
- package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +34 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -1
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +20 -12
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -3
- package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +51 -9
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +6 -6
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +6 -6
- package/src/excel-loaders/members.ts +8 -0
- package/src/excel-loaders/receivable-balances.ts +294 -0
- package/src/helpers/AuthenticatedStructures.ts +32 -6
- package/src/helpers/SetupStepUpdater.ts +10 -4
- package/src/helpers/xlsxAddressTransformerColumnFactory.ts +8 -0
- package/src/services/PaymentReallocationService.ts +3 -2
- package/src/services/PaymentService.ts +17 -1
- package/src/sql-filters/members.ts +20 -1
- package/src/sql-filters/organizations.ts +1 -0
- package/src/sql-filters/receivable-balances.ts +53 -1
- 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.
|
|
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.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
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": "
|
|
67
|
+
"gitHead": "af8fb928f0d8fdfe07dcf1433d99a79a0cc44ea7"
|
|
68
68
|
}
|
|
@@ -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
|
|
35
|
-
|
|
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
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
111
|
-
|
|
111
|
+
if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
|
|
112
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
|
|
113
|
+
if (duplicate) {
|
|
112
114
|
// Merge data
|
|
113
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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: '
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
114
|
-
|
|
119
|
+
if (shouldCheckDuplicate) {
|
|
120
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
|
|
121
|
+
if (duplicate) {
|
|
115
122
|
// Remove the member from the list
|
|
116
|
-
|
|
123
|
+
members.splice(members.findIndex(m => m.id === member.id), 1);
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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();
|