@stamhoofd/backend 2.91.0 → 2.93.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.
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +6 -6
- package/src/crons/amazon-ses.ts +100 -4
- package/src/crons/balance-emails.ts +1 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/email-recipient-loaders/receivable-balances.ts +3 -1
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +37 -7
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +205 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +81 -26
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +47 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +225 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +164 -0
- package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +64 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +5 -1
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +19 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -5
- package/src/helpers/EmailResumer.ts +2 -2
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -0
- package/src/seeds/1755876819-remove-duplicate-members.ts +145 -0
- package/src/seeds/1756115432-remove-old-drafts.ts +16 -0
- package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +30 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/email-recipients.ts +59 -0
- package/src/sql-filters/emails.ts +95 -0
- package/src/sql-filters/members.ts +42 -1
- package/src/sql-filters/registration-periods.ts +5 -0
- package/src/sql-sorters/email-recipients.ts +69 -0
- package/src/sql-sorters/emails.ts +47 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
2
|
+
import { GetEmailRecipientsEndpoint } from './GetEmailRecipientsEndpoint';
|
|
3
|
+
import { AccessRight, EmailStatus, LimitedFilteredRequest, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
|
|
4
|
+
import { Email, EmailRecipient, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
|
|
5
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
6
|
+
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
7
|
+
|
|
8
|
+
const baseUrl = `/email-recipients`;
|
|
9
|
+
|
|
10
|
+
describe('Endpoint.GetEmailRecipients', () => {
|
|
11
|
+
const endpoint = new GetEmailRecipientsEndpoint();
|
|
12
|
+
let period: RegistrationPeriod;
|
|
13
|
+
let organization: Organization;
|
|
14
|
+
let token: Token;
|
|
15
|
+
let user: User;
|
|
16
|
+
let sender: OrganizationEmail;
|
|
17
|
+
let sender2: OrganizationEmail;
|
|
18
|
+
|
|
19
|
+
let token2: Token;
|
|
20
|
+
let user2: User;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
TestUtils.setPermanentEnvironment('userMode', 'platform');
|
|
24
|
+
period = await new RegistrationPeriodFactory({
|
|
25
|
+
startDate: new Date(2023, 0, 1),
|
|
26
|
+
endDate: new Date(2023, 11, 31),
|
|
27
|
+
}).create();
|
|
28
|
+
|
|
29
|
+
organization = await new OrganizationFactory({ period })
|
|
30
|
+
.create();
|
|
31
|
+
|
|
32
|
+
sender = OrganizationEmail.create({
|
|
33
|
+
email: 'groepsleiding@voorbeeld.com',
|
|
34
|
+
name: 'Groepsleiding',
|
|
35
|
+
});
|
|
36
|
+
sender2 = OrganizationEmail.create({
|
|
37
|
+
email: 'kapoenen@voorbeeld.com',
|
|
38
|
+
name: 'Kapoenen',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
organization.privateMeta.emails.push(sender);
|
|
42
|
+
organization.privateMeta.emails.push(sender2);
|
|
43
|
+
await organization.save();
|
|
44
|
+
|
|
45
|
+
user = await new UserFactory({
|
|
46
|
+
organization,
|
|
47
|
+
permissions: Permissions.create({
|
|
48
|
+
level: PermissionLevel.None,
|
|
49
|
+
resources: new Map([
|
|
50
|
+
[PermissionsResourceType.Senders, new Map([['', ResourcePermissions.create({
|
|
51
|
+
resourceName: sender.name!,
|
|
52
|
+
level: PermissionLevel.Read,
|
|
53
|
+
})]])],
|
|
54
|
+
]),
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
57
|
+
.create();
|
|
58
|
+
|
|
59
|
+
token = await Token.createToken(user);
|
|
60
|
+
|
|
61
|
+
user2 = await new UserFactory({
|
|
62
|
+
organization,
|
|
63
|
+
permissions: Permissions.create({
|
|
64
|
+
level: PermissionLevel.None,
|
|
65
|
+
resources: new Map([
|
|
66
|
+
[PermissionsResourceType.Senders, new Map([[sender2.id, ResourcePermissions.create({
|
|
67
|
+
resourceName: sender.name!,
|
|
68
|
+
level: PermissionLevel.Read,
|
|
69
|
+
})]])],
|
|
70
|
+
]),
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
.create();
|
|
74
|
+
|
|
75
|
+
token2 = await Token.createToken(user2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('It can request all email recipients if read permission for all senders', async () => {
|
|
79
|
+
const email = new Email();
|
|
80
|
+
email.subject = 'test subject';
|
|
81
|
+
email.status = EmailStatus.Draft;
|
|
82
|
+
email.text = 'test email';
|
|
83
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
84
|
+
email.json = {};
|
|
85
|
+
email.organizationId = organization.id;
|
|
86
|
+
email.senderId = sender.id;
|
|
87
|
+
await email.save();
|
|
88
|
+
|
|
89
|
+
const emailRecipient = new EmailRecipient();
|
|
90
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
91
|
+
emailRecipient.firstName = 'Jan';
|
|
92
|
+
emailRecipient.lastName = 'Janssens';
|
|
93
|
+
emailRecipient.emailId = email.id;
|
|
94
|
+
emailRecipient.organizationId = organization.id;
|
|
95
|
+
await emailRecipient.save();
|
|
96
|
+
|
|
97
|
+
const request = Request.get({
|
|
98
|
+
path: baseUrl,
|
|
99
|
+
host: organization.getApiHost(),
|
|
100
|
+
query: new LimitedFilteredRequest({
|
|
101
|
+
filter: {},
|
|
102
|
+
limit: 10,
|
|
103
|
+
}),
|
|
104
|
+
headers: {
|
|
105
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const result = await testServer.test(endpoint, request);
|
|
109
|
+
expect(result.body.results).toHaveLength(1);
|
|
110
|
+
expect(result.body.results[0]).toMatchObject({
|
|
111
|
+
id: emailRecipient.id,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('It can not request all email recipients if not read permission for all senders', async () => {
|
|
116
|
+
const email = new Email();
|
|
117
|
+
email.subject = 'test subject';
|
|
118
|
+
email.status = EmailStatus.Draft;
|
|
119
|
+
email.text = 'test email';
|
|
120
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
121
|
+
email.json = {};
|
|
122
|
+
email.organizationId = organization.id;
|
|
123
|
+
email.senderId = sender2.id;
|
|
124
|
+
await email.save();
|
|
125
|
+
|
|
126
|
+
const emailRecipient = new EmailRecipient();
|
|
127
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
128
|
+
emailRecipient.firstName = 'Jan';
|
|
129
|
+
emailRecipient.lastName = 'Janssens';
|
|
130
|
+
emailRecipient.emailId = email.id;
|
|
131
|
+
emailRecipient.organizationId = organization.id;
|
|
132
|
+
await emailRecipient.save();
|
|
133
|
+
|
|
134
|
+
const request = Request.get({
|
|
135
|
+
path: baseUrl,
|
|
136
|
+
host: organization.getApiHost(),
|
|
137
|
+
query: new LimitedFilteredRequest({
|
|
138
|
+
filter: {},
|
|
139
|
+
limit: 10,
|
|
140
|
+
}),
|
|
141
|
+
headers: {
|
|
142
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
await expect(testServer.test(endpoint, request))
|
|
146
|
+
.rejects
|
|
147
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('It request all email recipients of a single email if read permission for that sender', async () => {
|
|
151
|
+
const email = new Email();
|
|
152
|
+
email.subject = 'test subject';
|
|
153
|
+
email.status = EmailStatus.Draft;
|
|
154
|
+
email.text = 'test email';
|
|
155
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
156
|
+
email.json = {};
|
|
157
|
+
email.organizationId = organization.id;
|
|
158
|
+
email.senderId = sender2.id;
|
|
159
|
+
await email.save();
|
|
160
|
+
|
|
161
|
+
const emailRecipient = new EmailRecipient();
|
|
162
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
163
|
+
emailRecipient.firstName = 'Jan';
|
|
164
|
+
emailRecipient.lastName = 'Janssens';
|
|
165
|
+
emailRecipient.emailId = email.id;
|
|
166
|
+
emailRecipient.organizationId = organization.id;
|
|
167
|
+
await emailRecipient.save();
|
|
168
|
+
|
|
169
|
+
const request = Request.get({
|
|
170
|
+
path: baseUrl,
|
|
171
|
+
host: organization.getApiHost(),
|
|
172
|
+
query: new LimitedFilteredRequest({
|
|
173
|
+
filter: {
|
|
174
|
+
emailId: email.id,
|
|
175
|
+
},
|
|
176
|
+
limit: 10,
|
|
177
|
+
}),
|
|
178
|
+
headers: {
|
|
179
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const result = await testServer.test(endpoint, request);
|
|
183
|
+
expect(result.body.results).toHaveLength(1);
|
|
184
|
+
expect(result.body.results[0]).toMatchObject({
|
|
185
|
+
id: emailRecipient.id,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('It cannot request all email recipients of a single email if read permission for another sender', async () => {
|
|
190
|
+
const email = new Email();
|
|
191
|
+
email.subject = 'test subject';
|
|
192
|
+
email.status = EmailStatus.Draft;
|
|
193
|
+
email.text = 'test email';
|
|
194
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
195
|
+
email.json = {};
|
|
196
|
+
email.organizationId = organization.id;
|
|
197
|
+
email.senderId = sender.id;
|
|
198
|
+
await email.save();
|
|
199
|
+
|
|
200
|
+
const emailRecipient = new EmailRecipient();
|
|
201
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
202
|
+
emailRecipient.firstName = 'Jan';
|
|
203
|
+
emailRecipient.lastName = 'Janssens';
|
|
204
|
+
emailRecipient.emailId = email.id;
|
|
205
|
+
emailRecipient.organizationId = organization.id;
|
|
206
|
+
await emailRecipient.save();
|
|
207
|
+
|
|
208
|
+
const request = Request.get({
|
|
209
|
+
path: baseUrl,
|
|
210
|
+
host: organization.getApiHost(),
|
|
211
|
+
query: new LimitedFilteredRequest({
|
|
212
|
+
filter: {
|
|
213
|
+
emailId: email.id,
|
|
214
|
+
},
|
|
215
|
+
limit: 10,
|
|
216
|
+
}),
|
|
217
|
+
headers: {
|
|
218
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
await expect(testServer.test(endpoint, request))
|
|
222
|
+
.rejects
|
|
223
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { assertSort, CountFilteredRequest, EmailRecipient as EmailRecipientStruct, getSortFilter, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
5
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import { EmailRecipient } from '@stamhoofd/models';
|
|
7
|
+
import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
8
|
+
import { Context } from '../../../helpers/Context';
|
|
9
|
+
import { emailRecipientsFilterCompilers } from '../../../sql-filters/email-recipients';
|
|
10
|
+
import { emailRecipientSorters } from '../../../sql-sorters/email-recipients';
|
|
11
|
+
import { validateEmailRecipientFilter } from './helpers/validateEmailRecipientFilter';
|
|
12
|
+
|
|
13
|
+
type Params = Record<string, never>;
|
|
14
|
+
type Query = LimitedFilteredRequest;
|
|
15
|
+
type Body = undefined;
|
|
16
|
+
type ResponseBody = PaginatedResponse<EmailRecipientStruct[], LimitedFilteredRequest>;
|
|
17
|
+
|
|
18
|
+
const filterCompilers: SQLFilterDefinitions = emailRecipientsFilterCompilers;
|
|
19
|
+
const sorters: SQLSortDefinitions<EmailRecipient> = emailRecipientSorters;
|
|
20
|
+
|
|
21
|
+
export class GetEmailRecipientsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
22
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
23
|
+
|
|
24
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
25
|
+
if (request.method !== 'GET') {
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const params = Endpoint.parseParameters(request.url, '/email-recipients', {});
|
|
30
|
+
|
|
31
|
+
if (params) {
|
|
32
|
+
return [true, params as Params];
|
|
33
|
+
}
|
|
34
|
+
return [false];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
38
|
+
const organization = Context.organization;
|
|
39
|
+
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
40
|
+
|
|
41
|
+
if (organization) {
|
|
42
|
+
scopeFilter = {
|
|
43
|
+
organizationId: organization.id,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const canReadAllEmails = await Context.auth.canReadAllEmails(organization ?? null);
|
|
47
|
+
|
|
48
|
+
if (!canReadAllEmails) {
|
|
49
|
+
// Check if scope is correctly limited to a single email, otherwise throw an error.
|
|
50
|
+
if (!await validateEmailRecipientFilter({ filter: q.filter, permissionLevel: PermissionLevel.Read })) {
|
|
51
|
+
throw Context.auth.error({
|
|
52
|
+
message: 'You do not have sufficient permissions to view all email recipients',
|
|
53
|
+
human: $t(`Je hebt niet voldoende toegangsrechten om alle email ontvangers te bekijken. Filter op één specifieke e-mail.`),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const query = EmailRecipient.select();
|
|
59
|
+
|
|
60
|
+
if (scopeFilter) {
|
|
61
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (q.filter) {
|
|
65
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (q.search) {
|
|
69
|
+
let searchFilter: StamhoofdFilter | null = null;
|
|
70
|
+
|
|
71
|
+
searchFilter = {
|
|
72
|
+
$or: [
|
|
73
|
+
{
|
|
74
|
+
email: {
|
|
75
|
+
$contains: q.search,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: {
|
|
80
|
+
$contains: q.search,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (searchFilter) {
|
|
87
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
92
|
+
if (q.pageFilter) {
|
|
93
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
97
|
+
applySQLSorter(query, q.sort, sorters);
|
|
98
|
+
query.limit(q.limit);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return query;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
105
|
+
const query = await GetEmailRecipientsEndpoint.buildQuery(requestQuery);
|
|
106
|
+
const recipients = await query.fetch();
|
|
107
|
+
|
|
108
|
+
let next: LimitedFilteredRequest | undefined;
|
|
109
|
+
|
|
110
|
+
if (recipients.length >= requestQuery.limit) {
|
|
111
|
+
const lastObject = recipients[recipients.length - 1];
|
|
112
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
113
|
+
|
|
114
|
+
next = new LimitedFilteredRequest({
|
|
115
|
+
filter: requestQuery.filter,
|
|
116
|
+
pageFilter: nextFilter,
|
|
117
|
+
sort: requestQuery.sort,
|
|
118
|
+
limit: requestQuery.limit,
|
|
119
|
+
search: requestQuery.search,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
123
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
124
|
+
next = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new PaginatedResponse<EmailRecipientStruct[], LimitedFilteredRequest>({
|
|
129
|
+
results: recipients.map(r => r.getStructure()),
|
|
130
|
+
next,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
135
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
136
|
+
await Context.authenticate();
|
|
137
|
+
|
|
138
|
+
if (!await Context.auth.canReadEmails(organization)) {
|
|
139
|
+
throw Context.auth.error();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
143
|
+
|
|
144
|
+
if (request.query.limit > maxLimit) {
|
|
145
|
+
throw new SimpleError({
|
|
146
|
+
code: 'invalid_field',
|
|
147
|
+
field: 'limit',
|
|
148
|
+
message: 'Limit can not be more than ' + maxLimit,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (request.query.limit < 1) {
|
|
153
|
+
throw new SimpleError({
|
|
154
|
+
code: 'invalid_field',
|
|
155
|
+
field: 'limit',
|
|
156
|
+
message: 'Limit can not be less than 1',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return new Response(
|
|
161
|
+
await GetEmailRecipientsEndpoint.buildData(request.query),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { Email } from '@stamhoofd/models';
|
|
3
|
+
import { FilterWrapperMarker, PermissionLevel, StamhoofdFilter, unwrapFilter, WrapperFilter } from '@stamhoofd/structures';
|
|
4
|
+
import { Context } from '../../../../helpers/Context';
|
|
5
|
+
|
|
6
|
+
export async function validateEmailRecipientFilter({ filter, permissionLevel }: { filter: StamhoofdFilter; permissionLevel: PermissionLevel }) {
|
|
7
|
+
// Require presence of a filter
|
|
8
|
+
const requiredFilter: WrapperFilter = {
|
|
9
|
+
emailId: FilterWrapperMarker,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const unwrapped = unwrapFilter(filter, requiredFilter);
|
|
13
|
+
if (!unwrapped.match) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const emailIds = typeof unwrapped.markerValue === 'string'
|
|
18
|
+
? [unwrapped.markerValue]
|
|
19
|
+
: unwrapFilter(unwrapped.markerValue as StamhoofdFilter, {
|
|
20
|
+
$in: FilterWrapperMarker,
|
|
21
|
+
})?.markerValue;
|
|
22
|
+
|
|
23
|
+
if (!Array.isArray(emailIds)) {
|
|
24
|
+
throw new SimpleError({
|
|
25
|
+
code: 'invalid_field',
|
|
26
|
+
field: 'filter',
|
|
27
|
+
message: 'You must filter on an email id of the email recipients you are trying to access',
|
|
28
|
+
human: $t(`Je hebt niet voldoende toegangsrechten om alle email ontvangers te bekijken.`),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (emailIds.length === 0) {
|
|
33
|
+
throw new SimpleError({
|
|
34
|
+
code: 'invalid_field',
|
|
35
|
+
field: 'filter',
|
|
36
|
+
message: 'Filtering on an empty list of email ids is not supported',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const emailId of emailIds) {
|
|
41
|
+
if (typeof emailId !== 'string') {
|
|
42
|
+
throw new SimpleError({
|
|
43
|
+
code: 'invalid_field',
|
|
44
|
+
field: 'filter',
|
|
45
|
+
message: 'Invalid email ID in filter',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const emails = await Email.getByIDs(...emailIds as string[]);
|
|
51
|
+
|
|
52
|
+
console.log('Fetching recipients for emails', emails.map(g => g.subject));
|
|
53
|
+
|
|
54
|
+
for (const email of emails) {
|
|
55
|
+
if (!await Context.auth.canAccessEmail(email, permissionLevel)) {
|
|
56
|
+
throw Context.auth.error({
|
|
57
|
+
message: 'You do not have access to this email',
|
|
58
|
+
human: $t(`Je hebt geen toegangsrechten tot de ontvangers van deze email`),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
@@ -966,7 +966,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
966
966
|
}
|
|
967
967
|
|
|
968
968
|
static shouldCheckIfMemberIsDuplicate(put: Member): boolean {
|
|
969
|
-
if (put.details.firstName
|
|
969
|
+
if (put.details.firstName === '???') {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (put.details.name.length <= 3) {
|
|
970
974
|
return false;
|
|
971
975
|
}
|
|
972
976
|
|
|
@@ -5,6 +5,7 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { RegistrationPeriod } from '@stamhoofd/models';
|
|
7
7
|
import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
8
|
+
import { Context } from '../../../helpers/Context';
|
|
8
9
|
import { registrationPeriodFilterCompilers } from '../../../sql-filters/registration-periods';
|
|
9
10
|
import { registrationPeriodSorters } from '../../../sql-sorters/registration-periods';
|
|
10
11
|
|
|
@@ -33,9 +34,24 @@ export class GetRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
36
|
-
|
|
37
|
+
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
37
38
|
const query = RegistrationPeriod.select();
|
|
38
39
|
|
|
40
|
+
if (STAMHOOFD.userMode === 'organization') {
|
|
41
|
+
const organization = Context.organization;
|
|
42
|
+
|
|
43
|
+
if (!organization) {
|
|
44
|
+
throw new SimpleError({
|
|
45
|
+
code: 'no_organization',
|
|
46
|
+
message: 'Organization is undefined on Context',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
scopeFilter = {
|
|
51
|
+
organizationId: organization.id,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
if (scopeFilter) {
|
|
40
56
|
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
41
57
|
}
|
|
@@ -127,6 +143,8 @@ export class GetRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body
|
|
|
127
143
|
});
|
|
128
144
|
}
|
|
129
145
|
|
|
146
|
+
await Context.setUserOrganizationScope();
|
|
147
|
+
|
|
130
148
|
return new Response(
|
|
131
149
|
await GetRegistrationPeriodsEndpoint.buildData(request.query),
|
|
132
150
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
2
|
import { BalanceItem, Order, Webshop } from '@stamhoofd/models';
|
|
4
3
|
import { PermissionLevel } from '@stamhoofd/structures';
|
|
5
4
|
|
|
6
5
|
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
8
|
|
|
8
9
|
type Params = { id: string };
|
|
9
10
|
type Query = undefined;
|
|
@@ -42,6 +43,14 @@ export class DeleteWebshopEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
42
43
|
throw Context.auth.notFoundOrNoAccess();
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
if (await UitpasService.areThereRegisteredTicketSales(webshop.id)) {
|
|
47
|
+
throw new SimpleError({
|
|
48
|
+
code: 'webshop_has_registered_ticket_sales',
|
|
49
|
+
message: `Webshop ${webshop.id} has registered ticket sales`,
|
|
50
|
+
human: $t(`0b3d6ea1-a70b-428c-9ba4-cc0c327ed415`),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
const orders = await Order.where({ webshopId: webshop.id });
|
|
46
55
|
await BalanceItem.deleteForDeletedOrders(orders.map(o => o.id));
|
|
47
56
|
await webshop.delete();
|
|
@@ -7,6 +7,7 @@ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceIt
|
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
import { AuditLogService } from '../../../../services/AuditLogService';
|
|
10
|
+
import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
10
11
|
|
|
11
12
|
type Params = { id: string };
|
|
12
13
|
type Query = undefined;
|
|
@@ -132,6 +133,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
132
133
|
|
|
133
134
|
// TODO: validate before updating stock
|
|
134
135
|
order.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
136
|
+
order.data.cart = await UitpasService.validateCart(organization.id, webshop.id, order.data.cart);
|
|
135
137
|
|
|
136
138
|
try {
|
|
137
139
|
await order.updateStock(null, true);
|
|
@@ -230,6 +232,8 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
230
232
|
const previousToPay = model.totalToPay;
|
|
231
233
|
const previousStatus = model.status;
|
|
232
234
|
|
|
235
|
+
const shouldReserveBefore = shouldReserveUitpasNumbers(model.status);
|
|
236
|
+
|
|
233
237
|
model.status = patch.status ?? model.status;
|
|
234
238
|
|
|
235
239
|
// For now, we don't invalidate tickets, because they will get invalidated at scan time (the order status is checked)
|
|
@@ -240,13 +244,16 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
240
244
|
const previousData = model.data.clone();
|
|
241
245
|
if (patch.data) {
|
|
242
246
|
model.data.patchOrPut(patch.data);
|
|
243
|
-
|
|
244
247
|
if (model.status !== OrderStatus.Deleted) {
|
|
245
248
|
// Make sure all data is up to date and validated (= possible corrections happen here too)
|
|
246
249
|
model.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
if ((patch.data || !shouldReserveBefore) && shouldReserveUitpasNumbers(model.status)) {
|
|
254
|
+
model.data.cart = await UitpasService.validateCart(organization.id, webshop.id, model.data.cart, model.id);
|
|
255
|
+
}
|
|
256
|
+
|
|
250
257
|
if (model.status === OrderStatus.Deleted) {
|
|
251
258
|
model.data.removePersonalData();
|
|
252
259
|
|
|
@@ -3,7 +3,7 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Email } from '@stamhoofd/email';
|
|
6
|
-
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode
|
|
6
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
7
7
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
8
|
import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
@@ -133,72 +133,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
133
133
|
request.body.validate(webshopStruct, organization.meta, request.i18n, false, Context.user?.getStructure());
|
|
134
134
|
request.body.update(webshopStruct);
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
const articlesWithUitpasSocialTariff = request.body.cart.items.filter(item => item.productPrice.uitpasBaseProductPriceId !== null);
|
|
138
|
-
for (const item of articlesWithUitpasSocialTariff) {
|
|
139
|
-
const uitpasNumbersOnly = item.uitpasNumbers.map(p => p.uitpasNumber);
|
|
140
|
-
|
|
141
|
-
// verify the amount of UiTPAS numbers
|
|
142
|
-
if (uitpasNumbersOnly.length !== item.amount) {
|
|
143
|
-
throw new SimpleError({
|
|
144
|
-
code: 'amount_of_uitpas_numbers_mismatch',
|
|
145
|
-
message: 'The number of UiTPAS numbers and items with UiTPAS social tariff does not match',
|
|
146
|
-
human: $t('6140c642-69b2-43d6-80ba-2af4915c5837'),
|
|
147
|
-
field: 'cart.items.uitpasNumbers',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// verify the UiTPAS numbers are unique (within the order)
|
|
152
|
-
if (uitpasNumbersOnly.length !== Formatter.uniqueArray(uitpasNumbersOnly).length) {
|
|
153
|
-
throw new SimpleError({
|
|
154
|
-
code: 'duplicate_uitpas_numbers',
|
|
155
|
-
message: 'Duplicate uitpas numbers used',
|
|
156
|
-
human: $t('d9ec27f3-dafa-41e8-bcfb-9da564a4a675'),
|
|
157
|
-
field: 'cart.items.uitpasNumbers',
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// verify the UiTPAS numbers are not already used for this product
|
|
162
|
-
const hasBeenUsed = await WebshopUitpasNumber.areUitpasNumbersUsed(webshop.id, item.product.id, uitpasNumbersOnly);
|
|
163
|
-
if (hasBeenUsed) {
|
|
164
|
-
throw new SimpleError({
|
|
165
|
-
code: 'uitpas_number_already_used',
|
|
166
|
-
message: 'One or more uitpas numbers are already used',
|
|
167
|
-
human: $t('1ef059c2-e758-4cfa-bc2b-16a581029450'),
|
|
168
|
-
field: 'cart.items.uitpasNumbers',
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// verify the UiTPAS numbers are valid for social tariff (static check + API call to UiTPAS)
|
|
173
|
-
if (item.product.uitpasEvent) {
|
|
174
|
-
const basePrice = item.product.prices.find(p => p.id === item.productPrice.uitpasBaseProductPriceId)?.price ?? 0;
|
|
175
|
-
const reducedPrices = await UitpasService.getSocialTariffForUitpasNumbers(organization.id, uitpasNumbersOnly, basePrice, item.product.uitpasEvent.url);
|
|
176
|
-
const expectedReducedPrices = item.uitpasNumbers;
|
|
177
|
-
if (reducedPrices.length < expectedReducedPrices.length) {
|
|
178
|
-
// should not happen
|
|
179
|
-
throw new SimpleError({
|
|
180
|
-
code: 'uitpas_social_tariff_price_mismatch',
|
|
181
|
-
message: 'UiTPAS wrong number of prices retruned',
|
|
182
|
-
human: $t('2d1983fa-2224-422f-9ea0-fdae77cb4914'),
|
|
183
|
-
field: 'cart.items.uitpasNumbers',
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
for (let i = 0; i < expectedReducedPrices.length; i++) {
|
|
187
|
-
if (reducedPrices[i].price !== expectedReducedPrices[i].price) {
|
|
188
|
-
throw new SimpleError({
|
|
189
|
-
code: 'uitpas_social_tariff_price_mismatch',
|
|
190
|
-
message: 'UiTPAS social tariff have a different price',
|
|
191
|
-
human: $t('2f4b9572-4b9c-42e0-91f1-b0984624d225', { correctPrice: Formatter.price(reducedPrices[i].price), orderPrice: Formatter.price(expectedReducedPrices[i].price) }),
|
|
192
|
-
field: 'uitpasNumbers.' + i.toString(),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
item.uitpasNumbers[i].uitpasTariffId = reducedPrices[i].uitpasTariffId;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
await UitpasService.checkUitpasNumbers(uitpasNumbersOnly); // Throws if invalid
|
|
200
|
-
}
|
|
201
|
-
}
|
|
136
|
+
request.body.cart = await UitpasService.validateCart(organization.id, webshop.id, request.body.cart);
|
|
202
137
|
|
|
203
138
|
const order = new Order().setRelation(Order.webshop, webshop);
|
|
204
139
|
order.data = request.body; // TODO: validate
|