@stamhoofd/backend 2.103.1 → 2.104.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/endpoints/global/events/PatchEventsEndpoint.test.ts +264 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +15 -4
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +1 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +130 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +17 -3
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +486 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +130 -10
- package/src/helpers/AdminPermissionChecker.ts +15 -7
- package/src/helpers/PeriodHelper.ts +1 -1
- package/src/helpers/fetchToAsyncIterator.ts +7 -0
- package/tests/assertions/assertBalances.ts +13 -1
- package/tests/jest.setup.ts +0 -1
|
@@ -3,7 +3,8 @@ import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegi
|
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
-
import { Event, Group, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
6
|
+
import { Event, Group, Organization, OrganizationRegistrationPeriod, Platform, Registration, RegistrationPeriod } from '@stamhoofd/models';
|
|
7
|
+
import { SQL } from '@stamhoofd/sql';
|
|
7
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
9
|
import { Context } from '../../../../helpers/Context';
|
|
9
10
|
import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
|
|
@@ -75,7 +76,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
if (period.locked) {
|
|
79
|
+
if ((!period.organizationId && period.locked) || (period.locked && period.organizationId && !await Context.auth.hasFullAccess(period.organizationId))) {
|
|
79
80
|
throw new SimpleError({
|
|
80
81
|
code: 'locked_period',
|
|
81
82
|
message: 'This period is locked',
|
|
@@ -375,7 +376,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null) {
|
|
379
|
+
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null, options: { allowPatchWaitingListPeriod?: boolean; isPatchingEvent?: boolean } = {}) {
|
|
380
|
+
const { allowPatchWaitingListPeriod = false, isPatchingEvent = false } = options;
|
|
381
|
+
|
|
379
382
|
const model = await Group.getByID(struct.id);
|
|
380
383
|
|
|
381
384
|
if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
@@ -417,16 +420,87 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
417
420
|
|
|
418
421
|
if (!period && !model.settings.period) {
|
|
419
422
|
period = await RegistrationPeriod.getByID(model.periodId);
|
|
420
|
-
}
|
|
421
423
|
|
|
422
|
-
|
|
423
|
-
if (STAMHOOFD.userMode === 'organization' && period.organizationId !== model.organizationId) {
|
|
424
|
+
if (!period) {
|
|
424
425
|
throw new SimpleError({
|
|
425
|
-
code: '
|
|
426
|
-
message: '
|
|
427
|
-
statusCode:
|
|
426
|
+
code: 'missing_required_relation',
|
|
427
|
+
message: 'Relation periodId is missing on group ' + model.id,
|
|
428
|
+
statusCode: 500,
|
|
428
429
|
});
|
|
429
430
|
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let shouldUpdatePeriodIds = false;
|
|
434
|
+
|
|
435
|
+
if (period) {
|
|
436
|
+
shouldUpdatePeriodIds = period.id !== model.periodId;
|
|
437
|
+
|
|
438
|
+
if (shouldUpdatePeriodIds && period.locked) {
|
|
439
|
+
if (period.organizationId === null || !await Context.auth.hasFullAccess(period.organizationId)) {
|
|
440
|
+
throw new SimpleError({
|
|
441
|
+
code: 'locked_period',
|
|
442
|
+
message: 'This period is locked',
|
|
443
|
+
human: Context.i18n.$t('f544b972-416c-471d-8836-d7f3b16f947d'),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (shouldUpdatePeriodIds) {
|
|
449
|
+
if (!isPatchingEvent && struct.periodId === undefined) {
|
|
450
|
+
throw new SimpleError({
|
|
451
|
+
code: 'invalid_field',
|
|
452
|
+
field: 'periodId',
|
|
453
|
+
message: 'Setting periodId is required when moving a group to a new period',
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (STAMHOOFD.userMode === 'organization' && period.organizationId !== model.organizationId) {
|
|
458
|
+
throw new SimpleError({
|
|
459
|
+
code: 'invalid_period',
|
|
460
|
+
message: 'Period has different organization id',
|
|
461
|
+
statusCode: 400,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Check current period is locked
|
|
466
|
+
const currentPeriod = await RegistrationPeriod.getByID(model.periodId);
|
|
467
|
+
|
|
468
|
+
if (!currentPeriod) {
|
|
469
|
+
throw new SimpleError({
|
|
470
|
+
code: 'missing_required_relation',
|
|
471
|
+
message: 'Relation periodId is missing on group ' + model.id,
|
|
472
|
+
statusCode: 500,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (currentPeriod.locked) {
|
|
477
|
+
if (currentPeriod.organizationId === null || !await Context.auth.hasFullAccess(currentPeriod.organizationId)) {
|
|
478
|
+
throw new SimpleError({
|
|
479
|
+
code: 'locked_period',
|
|
480
|
+
message: 'This period is locked',
|
|
481
|
+
human: $t('e115a9d1-11a6-42b9-a781-c6ab9d8a4b9c'),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if ((!isPatchingEvent && model.type === GroupType.EventRegistration) || (model.type === GroupType.WaitingList && !allowPatchWaitingListPeriod)) {
|
|
487
|
+
throw new SimpleError({
|
|
488
|
+
code: 'not_supported',
|
|
489
|
+
message: `Moving group with type ${model.type} to a different period is not supported`,
|
|
490
|
+
statusCode: 400,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!isPatchingEvent) {
|
|
496
|
+
if (struct.periodId !== undefined && struct.periodId !== period.id) {
|
|
497
|
+
throw new SimpleError({
|
|
498
|
+
code: 'invalid_field',
|
|
499
|
+
field: 'periodId',
|
|
500
|
+
message: 'Cannot patch periodId to a different period then the one being patched',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
430
504
|
|
|
431
505
|
model.periodId = period.id;
|
|
432
506
|
model.settings.period = period.getBaseStructure();
|
|
@@ -447,6 +521,27 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
447
521
|
}
|
|
448
522
|
}
|
|
449
523
|
|
|
524
|
+
/**
|
|
525
|
+
* A group should only be able to be moved to another period if the waiting list is not shared with other groups in the current period.
|
|
526
|
+
*/
|
|
527
|
+
const throwIfUpdateWaitingListPeriodWithMultipleGroups = async (waitingListId: string) => {
|
|
528
|
+
if (shouldUpdatePeriodIds) {
|
|
529
|
+
const groupCount = await Group.select()
|
|
530
|
+
.where('waitingListId', waitingListId)
|
|
531
|
+
.whereNot('id', model.id)
|
|
532
|
+
.whereNot('periodId', model.periodId).limit(1).count();
|
|
533
|
+
|
|
534
|
+
if (groupCount > 0) {
|
|
535
|
+
throw new SimpleError({
|
|
536
|
+
code: 'invalid_field',
|
|
537
|
+
field: 'periodId',
|
|
538
|
+
message: 'Group has waiting list with other groups in the current period',
|
|
539
|
+
human: $t('753684e1-94aa-4663-a81c-9656a51283ae'),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
450
545
|
const patch = struct;
|
|
451
546
|
if (patch.waitingList !== undefined) {
|
|
452
547
|
if (patch.waitingList === null) {
|
|
@@ -467,7 +562,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
467
562
|
}
|
|
468
563
|
patch.waitingList.id = model.waitingListId;
|
|
469
564
|
patch.waitingList.type = GroupType.WaitingList;
|
|
470
|
-
await
|
|
565
|
+
await throwIfUpdateWaitingListPeriodWithMultipleGroups(patch.waitingList.id);
|
|
566
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList, period, {
|
|
567
|
+
allowPatchWaitingListPeriod: shouldUpdatePeriodIds,
|
|
568
|
+
isPatchingEvent,
|
|
569
|
+
});
|
|
471
570
|
}
|
|
472
571
|
else {
|
|
473
572
|
if (model.waitingListId) {
|
|
@@ -525,6 +624,27 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
525
624
|
}
|
|
526
625
|
}
|
|
527
626
|
}
|
|
627
|
+
else if (shouldUpdatePeriodIds && model.waitingListId && period) {
|
|
628
|
+
// update waiting list period
|
|
629
|
+
await throwIfUpdateWaitingListPeriodWithMultipleGroups(model.waitingListId);
|
|
630
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(
|
|
631
|
+
GroupStruct.patch({
|
|
632
|
+
id: model.waitingListId,
|
|
633
|
+
periodId: period.id,
|
|
634
|
+
}),
|
|
635
|
+
period,
|
|
636
|
+
{
|
|
637
|
+
allowPatchWaitingListPeriod: shouldUpdatePeriodIds,
|
|
638
|
+
isPatchingEvent,
|
|
639
|
+
},
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (shouldUpdatePeriodIds) {
|
|
644
|
+
// change the period ids of the registrations in the group
|
|
645
|
+
const { changedRows } = await SQL.update(Registration.table).set('periodId', model.periodId).where('groupId', model.id).update();
|
|
646
|
+
console.log(`Moved ${changedRows} registrations to period ${model.periodId}`);
|
|
647
|
+
}
|
|
528
648
|
|
|
529
649
|
model.settings.throwIfInvalidPrices();
|
|
530
650
|
await model.updateOccupancy();
|
|
@@ -216,20 +216,28 @@ export class AdminPermissionChecker {
|
|
|
216
216
|
return true;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
async canAccessGroupsInPeriod(periodId: string, organizationId: string) {
|
|
220
|
+
const organization = await this.getOrganization(organizationId);
|
|
221
|
+
if (periodId !== organization.periodId) {
|
|
222
|
+
if (STAMHOOFD.userMode === 'organization' || periodId !== this.platform.period.id) {
|
|
223
|
+
if (!await this.hasFullAccess(organization.id)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
219
231
|
async canAccessGroup(group: Group, permissionLevel: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
220
232
|
// Check permissions aren't scoped to a specific organization, and they mismatch
|
|
221
233
|
if (!this.checkScope(group.organizationId)) {
|
|
222
234
|
// return false;
|
|
223
235
|
}
|
|
224
|
-
const organization = await this.getOrganization(group.organizationId);
|
|
225
236
|
|
|
226
|
-
if (group.periodId
|
|
227
|
-
|
|
228
|
-
if (!await this.hasFullAccess(group.organizationId)) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
237
|
+
if (!await this.canAccessGroupsInPeriod(group.periodId, group.organizationId)) {
|
|
238
|
+
return false;
|
|
232
239
|
}
|
|
240
|
+
const organization = await this.getOrganization(group.organizationId);
|
|
233
241
|
|
|
234
242
|
if (group.deletedAt || group.status === GroupStatus.Archived) {
|
|
235
243
|
return await this.canAccessArchivedGroups(group.organizationId);
|
|
@@ -155,7 +155,7 @@ export class PeriodHelper {
|
|
|
155
155
|
await QueueHandler.schedule(tag, async () => {
|
|
156
156
|
await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
|
|
157
157
|
for await (const group of Group.select().where('periodId', period.id).all()) {
|
|
158
|
-
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: group.id }), period);
|
|
158
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: group.id, periodId: period.id }), period);
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
161
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { IPaginatedResponse, LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
2
|
+
import { FileSignService } from '../services/FileSignService';
|
|
2
3
|
|
|
3
4
|
export function fetchToAsyncIterator<T>(
|
|
4
5
|
initialFilter: LimitedFilteredRequest,
|
|
5
6
|
loader: {
|
|
6
7
|
fetch(request: LimitedFilteredRequest): Promise<IPaginatedResponse<T, LimitedFilteredRequest>>;
|
|
7
8
|
},
|
|
9
|
+
options?: {
|
|
10
|
+
signFiles?: boolean;
|
|
11
|
+
},
|
|
8
12
|
): AsyncIterable<T> {
|
|
9
13
|
return {
|
|
10
14
|
[Symbol.asyncIterator]: function () {
|
|
@@ -20,6 +24,9 @@ export function fetchToAsyncIterator<T>(
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
const response = await loader.fetch(request);
|
|
27
|
+
if (options?.signFiles) {
|
|
28
|
+
await FileSignService.fillSignedUrlsForStruct(response.results);
|
|
29
|
+
}
|
|
23
30
|
request = response.next ?? null;
|
|
24
31
|
|
|
25
32
|
return {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { BalanceItem } from '@stamhoofd/models';
|
|
2
2
|
import { BalanceItemService } from '../../src/services/BalanceItemService';
|
|
3
3
|
|
|
4
|
-
export async function assertBalances(
|
|
4
|
+
export async function assertBalances(
|
|
5
|
+
selector:
|
|
6
|
+
{ user: { id: string | null } }
|
|
7
|
+
| { member: { id: string | null } }
|
|
8
|
+
| { organization: { id: string | null } }
|
|
9
|
+
| { payingOrganization: { id: string | null } },
|
|
10
|
+
balances: Partial<BalanceItem>[]) {
|
|
5
11
|
await BalanceItemService.flushAll();
|
|
6
12
|
|
|
7
13
|
// Fetch all user balances
|
|
@@ -12,6 +18,12 @@ export async function assertBalances(selector: { user: { id: string | null } } |
|
|
|
12
18
|
else if ('member' in selector && selector.member.id) {
|
|
13
19
|
q.where('memberId', selector.member.id);
|
|
14
20
|
}
|
|
21
|
+
else if ('organization' in selector && selector.organization.id) {
|
|
22
|
+
q.where('organizationId', selector.organization.id);
|
|
23
|
+
}
|
|
24
|
+
else if ('payingOrganization' in selector && selector.payingOrganization.id) {
|
|
25
|
+
q.where('payingOrganizationId', selector.payingOrganization.id);
|
|
26
|
+
}
|
|
15
27
|
else {
|
|
16
28
|
throw new Error('Selector must contain either user or member with an id');
|
|
17
29
|
}
|
package/tests/jest.setup.ts
CHANGED
|
@@ -51,7 +51,6 @@ beforeAll(async () => {
|
|
|
51
51
|
await Database.delete('DELETE FROM `webshops`');
|
|
52
52
|
await Database.delete('DELETE FROM `groups`');
|
|
53
53
|
await Database.delete('DELETE FROM `email_addresses`');
|
|
54
|
-
|
|
55
54
|
await Database.update('UPDATE registration_periods set organizationId = null, customName = ? where organizationId is not null', ['delete']);
|
|
56
55
|
await Database.delete('DELETE FROM `organizations`');
|
|
57
56
|
await Database.delete('DELETE FROM `registration_periods` where customName = ?', ['delete']);
|