@stamhoofd/backend 2.91.0 → 2.92.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.
@@ -1,9 +1,10 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { Email, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
3
- import { EmailStatus, Email as EmailStruct, PermissionLevel, Permissions, Version } from '@stamhoofd/structures';
4
- import { TestUtils } from '@stamhoofd/test-utils';
3
+ import { AccessRight, EmailStatus, Email as EmailStruct, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
4
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
5
5
  import { testServer } from '../../../../tests/helpers/TestServer';
6
6
  import { PatchEmailEndpoint } from './PatchEmailEndpoint';
7
+ import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
7
8
 
8
9
  const baseUrl = `/v${Version}/email`;
9
10
 
@@ -13,8 +14,13 @@ describe('Endpoint.PatchEmailEndpoint', () => {
13
14
  let organization: Organization;
14
15
  let token: Token;
15
16
  let user: User;
17
+ let sender: OrganizationEmail;
18
+ let sender2: OrganizationEmail;
16
19
 
17
- const patchEmail = async (email: EmailStruct, token: Token, organization?: Organization) => {
20
+ let token2: Token;
21
+ let user2: User;
22
+
23
+ const patchEmail = async (email: AutoEncoderPatchType<EmailStruct>, token: Token, organization?: Organization) => {
18
24
  const id = email.id;
19
25
  const request = Request.buildJson('PATCH', `${baseUrl}/${id}`, organization?.getApiHost(), email);
20
26
  request.headers.authorization = 'Bearer ' + token.accessToken;
@@ -34,15 +40,360 @@ describe('Endpoint.PatchEmailEndpoint', () => {
34
40
  organization = await new OrganizationFactory({ period })
35
41
  .create();
36
42
 
43
+ sender = OrganizationEmail.create({
44
+ email: 'groepsleiding@voorbeeld.com',
45
+ name: 'Groepsleiding',
46
+ });
47
+ sender2 = OrganizationEmail.create({
48
+ email: 'kapoenen@voorbeeld.com',
49
+ name: 'Kapoenen',
50
+ });
51
+
52
+ organization.privateMeta.emails.push(sender);
53
+ organization.privateMeta.emails.push(sender2);
54
+ await organization.save();
55
+
37
56
  user = await new UserFactory({
38
57
  organization,
39
58
  permissions: Permissions.create({
40
- level: PermissionLevel.Read,
59
+ level: PermissionLevel.None,
60
+ resources: new Map([
61
+ [PermissionsResourceType.Senders, new Map([[sender.id, ResourcePermissions.create({
62
+ resourceName: sender.name!,
63
+ level: PermissionLevel.None,
64
+ accessRights: [AccessRight.SendMessages],
65
+ })]])],
66
+ ]),
41
67
  }),
42
68
  })
43
69
  .create();
44
70
 
45
71
  token = await Token.createToken(user);
72
+
73
+ user2 = await new UserFactory({
74
+ organization,
75
+ permissions: Permissions.create({
76
+ level: PermissionLevel.None,
77
+ resources: new Map([
78
+ [PermissionsResourceType.Senders, new Map([[sender2.id, ResourcePermissions.create({
79
+ resourceName: sender.name!,
80
+ level: PermissionLevel.Write,
81
+ accessRights: [AccessRight.SendMessages],
82
+ })]])],
83
+ ]),
84
+ }),
85
+ })
86
+ .create();
87
+
88
+ token2 = await Token.createToken(user2);
89
+ });
90
+
91
+ test('Should throw for invalid senderId', async () => {
92
+ const email = new Email();
93
+ email.subject = 'test subject';
94
+ email.status = EmailStatus.Draft;
95
+ email.text = 'test email {{unsubscribeUrl}}';
96
+ email.html = `<!DOCTYPE html>
97
+ <html>
98
+
99
+ <head>
100
+ <meta charset="utf-8" />
101
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
102
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
103
+ <title>test</title>
104
+ </head>
105
+
106
+ <body>
107
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
108
+
109
+ {{unsubscribeUrl}}
110
+ </body>
111
+
112
+ </html>`;
113
+ email.json = {
114
+ content: [
115
+ {
116
+ content: [
117
+ {
118
+ text: 'test email',
119
+ type: 'text',
120
+ },
121
+ ],
122
+ type: 'paragraph',
123
+ },
124
+ ],
125
+ type: 'doc',
126
+ };
127
+ email.userId = user.id;
128
+ email.organizationId = organization.id;
129
+
130
+ await email.save();
131
+
132
+ const body = EmailStruct.patch({
133
+ id: email.id,
134
+ status: EmailStatus.Sending,
135
+ senderId: 'invalid-sender-id',
136
+ });
137
+
138
+ await expect(async () => await patchEmail(body, token, organization))
139
+ .rejects
140
+ .toThrow(STExpect.errorWithCode('invalid_sender'));
141
+ });
142
+
143
+ test('Should throw when patching other users email without sender id', async () => {
144
+ const email = new Email();
145
+ email.subject = 'test subject';
146
+ email.status = EmailStatus.Draft;
147
+ email.text = 'test email {{unsubscribeUrl}}';
148
+ email.html = `<!DOCTYPE html>
149
+ <html>
150
+
151
+ <head>
152
+ <meta charset="utf-8" />
153
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
154
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
155
+ <title>test</title>
156
+ </head>
157
+
158
+ <body>
159
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
160
+
161
+ {{unsubscribeUrl}}
162
+ </body>
163
+
164
+ </html>`;
165
+ email.json = {
166
+ content: [
167
+ {
168
+ content: [
169
+ {
170
+ text: 'test email',
171
+ type: 'text',
172
+ },
173
+ ],
174
+ type: 'paragraph',
175
+ },
176
+ ],
177
+ type: 'doc',
178
+ };
179
+ email.userId = user2.id;
180
+ email.organizationId = organization.id;
181
+
182
+ await email.save();
183
+
184
+ const body = EmailStruct.patch({
185
+ id: email.id,
186
+ subject: 'new subject',
187
+ });
188
+
189
+ await expect(async () => await patchEmail(body, token, organization))
190
+ .rejects
191
+ .toThrow(STExpect.errorWithCode('permission_denied'));
192
+ });
193
+
194
+ test('Should throw when patching other users email even when matching sender', async () => {
195
+ const email = new Email();
196
+ email.subject = 'test subject';
197
+ email.status = EmailStatus.Draft;
198
+ email.text = 'test email {{unsubscribeUrl}}';
199
+ email.html = `<!DOCTYPE html>
200
+ <html>
201
+
202
+ <head>
203
+ <meta charset="utf-8" />
204
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
205
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
206
+ <title>test</title>
207
+ </head>
208
+
209
+ <body>
210
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
211
+
212
+ {{unsubscribeUrl}}
213
+ </body>
214
+
215
+ </html>`;
216
+ email.json = {
217
+ content: [
218
+ {
219
+ content: [
220
+ {
221
+ text: 'test email',
222
+ type: 'text',
223
+ },
224
+ ],
225
+ type: 'paragraph',
226
+ },
227
+ ],
228
+ type: 'doc',
229
+ };
230
+ email.userId = user2.id;
231
+ email.organizationId = organization.id;
232
+ email.senderId = sender.id;
233
+
234
+ await email.save();
235
+
236
+ const body = EmailStruct.patch({
237
+ id: email.id,
238
+ subject: 'new subject',
239
+ });
240
+
241
+ await expect(async () => await patchEmail(body, token, organization))
242
+ .rejects
243
+ .toThrow(STExpect.errorWithCode('permission_denied'));
244
+ });
245
+
246
+ test('Should not throw when patching other users email when having write access to sender', async () => {
247
+ const email = new Email();
248
+ email.subject = 'test subject';
249
+ email.status = EmailStatus.Draft;
250
+ email.text = 'test email {{unsubscribeUrl}}';
251
+ email.html = `<!DOCTYPE html>
252
+ <html>
253
+
254
+ <head>
255
+ <meta charset="utf-8" />
256
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
257
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
258
+ <title>test</title>
259
+ </head>
260
+
261
+ <body>
262
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
263
+
264
+ {{unsubscribeUrl}}
265
+ </body>
266
+
267
+ </html>`;
268
+ email.json = {
269
+ content: [
270
+ {
271
+ content: [
272
+ {
273
+ text: 'test email',
274
+ type: 'text',
275
+ },
276
+ ],
277
+ type: 'paragraph',
278
+ },
279
+ ],
280
+ type: 'doc',
281
+ };
282
+ email.userId = user.id; // other user
283
+ email.organizationId = organization.id;
284
+ email.senderId = sender2.id; // write access to this sender
285
+
286
+ await email.save();
287
+
288
+ const body = EmailStruct.patch({
289
+ id: email.id,
290
+ subject: 'new subject',
291
+ });
292
+
293
+ await expect(patchEmail(body, token2, organization)).toResolve();
294
+ });
295
+
296
+ test('Should throw when patching if no permission for sender', async () => {
297
+ const email = new Email();
298
+ email.subject = 'test subject';
299
+ email.status = EmailStatus.Draft;
300
+ email.text = 'test email {{unsubscribeUrl}}';
301
+ email.html = `<!DOCTYPE html>
302
+ <html>
303
+
304
+ <head>
305
+ <meta charset="utf-8" />
306
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
307
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
308
+ <title>test</title>
309
+ </head>
310
+
311
+ <body>
312
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
313
+
314
+ {{unsubscribeUrl}}
315
+ </body>
316
+
317
+ </html>`;
318
+ email.json = {
319
+ content: [
320
+ {
321
+ content: [
322
+ {
323
+ text: 'test email',
324
+ type: 'text',
325
+ },
326
+ ],
327
+ type: 'paragraph',
328
+ },
329
+ ],
330
+ type: 'doc',
331
+ };
332
+ email.userId = user.id;
333
+ email.organizationId = organization.id;
334
+
335
+ await email.save();
336
+
337
+ const body = EmailStruct.patch({
338
+ id: email.id,
339
+ senderId: sender2.id,
340
+ });
341
+
342
+ await expect(async () => await patchEmail(body, token, organization))
343
+ .rejects
344
+ .toThrow(STExpect.errorWithCode('permission_denied'));
345
+ });
346
+
347
+ test('Should throw when sending if no permission for sender', async () => {
348
+ const email = new Email();
349
+ email.subject = 'test subject';
350
+ email.status = EmailStatus.Draft;
351
+ email.text = 'test email {{unsubscribeUrl}}';
352
+ email.html = `<!DOCTYPE html>
353
+ <html>
354
+
355
+ <head>
356
+ <meta charset="utf-8" />
357
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
358
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
359
+ <title>test</title>
360
+ </head>
361
+
362
+ <body>
363
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
364
+
365
+ {{unsubscribeUrl}}
366
+ </body>
367
+
368
+ </html>`;
369
+ email.json = {
370
+ content: [
371
+ {
372
+ content: [
373
+ {
374
+ text: 'test email',
375
+ type: 'text',
376
+ },
377
+ ],
378
+ type: 'paragraph',
379
+ },
380
+ ],
381
+ type: 'doc',
382
+ };
383
+ email.userId = user.id;
384
+ email.organizationId = organization.id;
385
+ email.senderId = sender2.id;
386
+
387
+ await email.save();
388
+
389
+ const body = EmailStruct.patch({
390
+ id: email.id,
391
+ status: EmailStatus.Sending,
392
+ });
393
+
394
+ await expect(async () => await patchEmail(body, token, organization))
395
+ .rejects
396
+ .toThrow(STExpect.errorWithCode('permission_denied'));
46
397
  });
47
398
 
48
399
  test('Should throw error if no unsubscribe button in email html', async () => {
@@ -84,11 +435,11 @@ describe('Endpoint.PatchEmailEndpoint', () => {
84
435
 
85
436
  await email.save();
86
437
 
87
- const body = EmailStruct.create({ ...email, fromAddress: 'test@test.be', status: EmailStatus.Sending });
438
+ const body = EmailStruct.patch({ id: email.id, senderId: sender.id, status: EmailStatus.Sending });
88
439
 
89
440
  await expect(async () => await patchEmail(body, token, organization))
90
441
  .rejects
91
- .toThrow('Missing unsubscribe button');
442
+ .toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
92
443
  });
93
444
 
94
445
  test('Should throw error if no unsubscribe button in email text', async () => {
@@ -130,10 +481,55 @@ describe('Endpoint.PatchEmailEndpoint', () => {
130
481
 
131
482
  await email.save();
132
483
 
133
- const body = EmailStruct.create({ ...email, fromAddress: 'test@test.be', status: EmailStatus.Sending });
484
+ const body = EmailStruct.patch({ id: email.id, senderId: sender.id, status: EmailStatus.Sending });
134
485
 
135
486
  await expect(async () => await patchEmail(body, token, organization))
136
487
  .rejects
137
- .toThrow('Missing unsubscribe button');
488
+ .toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
489
+ });
490
+
491
+ test('Can send an email', async () => {
492
+ const email = new Email();
493
+ email.subject = 'test subject';
494
+ email.status = EmailStatus.Draft;
495
+ email.text = 'test email {{unsubscribeUrl}}';
496
+ email.html = `<!DOCTYPE html>
497
+ <html>
498
+
499
+ <head>
500
+ <meta charset="utf-8" />
501
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
502
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
503
+ <title>test</title>
504
+ </head>
505
+
506
+ <body>
507
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email {{unsubscribeUrl}}</p>
508
+ </body>
509
+
510
+ </html>`;
511
+ email.json = {
512
+ content: [
513
+ {
514
+ content: [
515
+ {
516
+ text: 'test email',
517
+ type: 'text',
518
+ },
519
+ ],
520
+ type: 'paragraph',
521
+ },
522
+ ],
523
+ type: 'doc',
524
+ };
525
+ email.userId = user.id;
526
+ email.organizationId = organization.id;
527
+ email.senderId = sender.id;
528
+
529
+ await email.save();
530
+
531
+ const body = EmailStruct.patch({ id: email.id, status: EmailStatus.Sending });
532
+
533
+ await expect(patchEmail(body, token, organization)).toResolve();
138
534
  });
139
535
  });
@@ -1,6 +1,6 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { Email } from '@stamhoofd/models';
3
- import { EmailPreview, EmailStatus, Email as EmailStruct } from '@stamhoofd/structures';
2
+ import { Email, Platform } from '@stamhoofd/models';
3
+ import { EmailPreview, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
4
4
 
5
5
  import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
6
6
  import { SimpleError } from '@simonbackx/simple-errors';
@@ -29,14 +29,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
29
29
 
30
30
  async handle(request: DecodedRequest<Params, Query, Body>) {
31
31
  const organization = await Context.setOptionalOrganizationScope();
32
- const { user } = await Context.authenticate();
32
+ await Context.authenticate();
33
33
 
34
- if (!Context.auth.canSendEmails()) {
34
+ if (!await Context.auth.canReadEmails(organization)) {
35
+ // Fast fail before query
35
36
  throw Context.auth.error();
36
37
  }
37
38
 
38
39
  const model = await Email.getByID(request.params.id);
39
- if (!model || model.userId !== user.id || (model.organizationId !== (organization?.id ?? null))) {
40
+ if (!model || (model.organizationId !== (organization?.id ?? null))) {
40
41
  throw new SimpleError({
41
42
  code: 'not_found',
42
43
  human: 'Email not found',
@@ -45,6 +46,10 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
45
46
  });
46
47
  }
47
48
 
49
+ if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
50
+ throw Context.auth.error();
51
+ }
52
+
48
53
  if (model.status !== EmailStatus.Draft) {
49
54
  throw new SimpleError({
50
55
  code: 'not_draft',
@@ -60,6 +65,47 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
60
65
  model.subject = request.body.subject;
61
66
  }
62
67
 
68
+ if (request.body.senderId !== undefined) {
69
+ const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
70
+ const sender = list.find(e => e.id === request.body.senderId);
71
+ if (sender) {
72
+ if (!await Context.auth.canSendEmailsFrom(organization, sender.id)) {
73
+ throw Context.auth.error({
74
+ message: 'Cannot send emails from this sender',
75
+ human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
76
+ });
77
+ }
78
+ model.senderId = sender.id;
79
+ model.fromAddress = sender.email;
80
+ model.fromName = sender.name;
81
+ }
82
+ else {
83
+ throw new SimpleError({
84
+ code: 'invalid_sender',
85
+ human: 'Sender not found',
86
+ message: $t(`94adb4e0-2ef1-4ee8-9f02-5a76efa51c1d`),
87
+ statusCode: 400,
88
+ });
89
+ }
90
+ }
91
+ else if (model.senderId) {
92
+ // Update data, to avoid sending from an old address
93
+ const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
94
+ const sender = list.find(e => e.id === model.senderId);
95
+ if (sender) {
96
+ model.fromAddress = sender.email;
97
+ model.fromName = sender.name;
98
+ }
99
+ else {
100
+ throw new SimpleError({
101
+ code: 'invalid_sender',
102
+ human: 'Sender not found',
103
+ message: $t(`f08cccb3-faf9-473f-b729-16120fadec9c`),
104
+ statusCode: 400,
105
+ });
106
+ }
107
+ }
108
+
63
109
  if (request.body.html !== undefined) {
64
110
  model.html = request.body.html;
65
111
  }
@@ -72,14 +118,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
72
118
  model.json = request.body.json;
73
119
  }
74
120
 
75
- if (request.body.fromAddress !== undefined) {
76
- model.fromAddress = request.body.fromAddress;
77
- }
78
-
79
- if (request.body.fromName !== undefined) {
80
- model.fromName = request.body.fromName;
81
- }
82
-
83
121
  if (request.body.recipientFilter) {
84
122
  model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
85
123
  rebuild = true;
@@ -102,19 +140,26 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
102
140
  }
103
141
 
104
142
  if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
143
+ if (!await Context.auth.canSendEmail(model)) {
144
+ throw Context.auth.error({
145
+ message: 'Cannot send emails from this sender',
146
+ human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
147
+ });
148
+ }
149
+
105
150
  model.throwIfNotReadyToSend();
106
151
 
107
152
  const replacement = '{{unsubscribeUrl}}';
108
-
153
+
109
154
  if (model.html) {
110
155
  // Check email contains an unsubscribe button
111
156
  if (!model.html.includes(replacement)) {
112
157
  throw new SimpleError({
113
- code: "missing_unsubscribe_button",
114
- message: "Missing unsubscribe button",
158
+ code: 'missing_unsubscribe_button',
159
+ message: 'Missing unsubscribe button',
115
160
  human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
116
- field: "html"
117
- })
161
+ field: 'html',
162
+ });
118
163
  }
119
164
  }
120
165
 
@@ -122,11 +167,11 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
122
167
  // Check email contains an unsubscribe button
123
168
  if (!model.text.includes(replacement)) {
124
169
  throw new SimpleError({
125
- code: "missing_unsubscribe_button",
126
- message: "Missing unsubscribe button",
170
+ code: 'missing_unsubscribe_button',
171
+ message: 'Missing unsubscribe button',
127
172
  human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
128
- field: "text"
129
- })
173
+ field: 'text',
174
+ });
130
175
  }
131
176
  }
132
177
 
@@ -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