@stamhoofd/backend 2.94.0 → 2.95.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.
@@ -0,0 +1,715 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { Group, Organization, OrganizationRegistrationPeriod, Registration, RegistrationPeriod } from '@stamhoofd/models';
3
+ import { CycleInformation, GroupCategory, GroupCategorySettings, GroupPrivateSettings, GroupSettings, GroupStatus, GroupType, RegistrationPeriodSettings, TranslatedString } from '@stamhoofd/structures';
4
+
5
+ export default new Migration(async () => {
6
+ if (STAMHOOFD.environment === 'test') {
7
+ console.log('skipped in tests');
8
+ return;
9
+ }
10
+
11
+ if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
12
+ console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
13
+ return;
14
+ }
15
+
16
+ const dryRun = false;
17
+ await start(dryRun);
18
+
19
+ if (dryRun) {
20
+ throw new Error('Migration did not finish because of dryRun');
21
+ }
22
+ });
23
+
24
+ const cycleIfMigrated = -99;
25
+
26
+ async function start(dryRun: boolean) {
27
+ for await (const organization of Organization.select().all()) {
28
+ const groups: Group[] = await Group.select().where('organizationId', organization.id).fetch();
29
+
30
+ if (groups.length === 0) {
31
+ await createDefaultRegistrationPeriod(organization, dryRun);
32
+ continue;
33
+ }
34
+
35
+ if (groups.some(g => g.cycle === cycleIfMigrated)) {
36
+ continue;
37
+ }
38
+
39
+ console.log('Organization: ' + organization.name);
40
+
41
+ // sort groups by start date
42
+ groups.sort((a, b) => a.settings.startDate.getTime() - b.settings.startDate.getTime());
43
+
44
+ const bestCurrentPeriodSpan = await calculateBestCurrentPeriodSpan(groups);
45
+
46
+ const allGroups: Group[] = await migrateGroups({ groups, organization, periodSpan: bestCurrentPeriodSpan }, dryRun);
47
+
48
+ // cleanup
49
+ for (const group of allGroups) {
50
+ await cleanupGroup(group, dryRun);
51
+ }
52
+ }
53
+ }
54
+
55
+ async function cleanupGroup(group: Group, dryRun: boolean) {
56
+ group.settings.cycleSettings = new Map();
57
+ group.cycle = cycleIfMigrated;
58
+ if (group.status === GroupStatus.Archived) {
59
+ group.status = GroupStatus.Closed;
60
+ }
61
+
62
+ if (!dryRun) {
63
+ await group.updateOccupancy();
64
+ await group.save();
65
+ }
66
+ }
67
+
68
+ async function migrateGroups({ groups, organization, periodSpan }: { groups: Group[]; organization: Organization; periodSpan: { startDate: Date; endDate: Date } }, dryRun: boolean) {
69
+ // #region create periods
70
+ const previousYearStartDate = new Date(periodSpan.startDate);
71
+ previousYearStartDate.setFullYear(previousYearStartDate.getFullYear() - 1);
72
+
73
+ const archivePeriod = await createRegistrationPeriod({
74
+ // todo: what should be the start date?
75
+ startDate: previousYearStartDate,
76
+ endDate: new Date(periodSpan.startDate.getTime() - 1),
77
+ locked: true,
78
+ organization,
79
+ previousPeriodId: undefined,
80
+ }, dryRun);
81
+
82
+ const archiveOrganizationPeriod = await createOrganizationRegistrationPeriod({
83
+ organization,
84
+ period: archivePeriod,
85
+ }, dryRun);
86
+
87
+ const currentPeriod = await createRegistrationPeriod({
88
+ startDate: periodSpan.startDate,
89
+ endDate: periodSpan.endDate,
90
+ locked: false,
91
+ previousPeriodId: archivePeriod.id,
92
+ organization,
93
+ }, dryRun);
94
+
95
+ organization.periodId = currentPeriod.id;
96
+
97
+ if (!dryRun) {
98
+ await organization.save();
99
+ }
100
+
101
+ const currentOrganizationRegistrationPeriod = await createOrganizationRegistrationPeriod({
102
+ organization,
103
+ period: currentPeriod,
104
+ }, dryRun);
105
+ // #endregion
106
+
107
+ // (to add to categories later)
108
+ const groupMap = new Map<
109
+ // original group id
110
+ string,
111
+ // groups in previous cycles
112
+ Group[]>();
113
+
114
+ for (const originalGroup of groups) {
115
+ // archived groups should be migrated to the archive period
116
+ const currentGroupPeriod = originalGroup.status === GroupStatus.Archived ? archivePeriod : currentPeriod;
117
+ const currentCycle = originalGroup.cycle;
118
+ const originalGroupId: string = originalGroup.id;
119
+ originalGroup.periodId = currentGroupPeriod.id;
120
+
121
+ // first migrate registrations for the current cycle
122
+ await migrateRegistrations({ organization, period: currentGroupPeriod, originalGroup, newGroup: originalGroup, cycle: currentCycle }, dryRun);
123
+
124
+ const cycleSettingEntries = [...originalGroup.settings.cycleSettings.entries()].filter(([cycle]) => {
125
+ return cycle !== currentCycle;
126
+ });
127
+
128
+ // most recent cycle first (highest cycle number)
129
+ cycleSettingEntries.sort((a, b) => b[0] - a[0]);
130
+
131
+ // create groups for the previous cycles and migrate registrations
132
+ for (let previousPeriodIndex = 0; previousPeriodIndex < cycleSettingEntries.length; previousPeriodIndex++) {
133
+ const [cycle, cycleInformation] = cycleSettingEntries[previousPeriodIndex];
134
+ const newGroup = createPreviousGroup({ originalGroup, period: archivePeriod, cycleInformation, index: previousPeriodIndex });
135
+
136
+ const registrationCount = await Registration.select()
137
+ .where('groupId', originalGroup.id)
138
+ .andWhere('cycle', cycle)
139
+ .count();
140
+
141
+ // only create group if there are registrations
142
+ if (registrationCount > 0) {
143
+ if (!dryRun) {
144
+ await newGroup.save();
145
+ }
146
+
147
+ await migrateRegistrations({ organization, period: archivePeriod, originalGroup, newGroup, cycle }, dryRun);
148
+
149
+ const allPreviousGroups = groupMap.get(originalGroupId);
150
+ if (allPreviousGroups) {
151
+ allPreviousGroups.push(newGroup);
152
+ }
153
+ else {
154
+ groupMap.set(originalGroupId, [newGroup]);
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ // #region create categories for current period
161
+ const nonArchivedGroupIds = [...new Set(groups.filter(g => g.status !== GroupStatus.Archived).map(g => g.id))];
162
+ const newCategoriesData = organization.meta.categories.map((c: GroupCategory) => {
163
+ const category = GroupCategory.create({
164
+ settings: GroupCategorySettings.create({
165
+ ...c.settings,
166
+ }),
167
+ groupIds: [...new Set(c.groupIds.filter(id => nonArchivedGroupIds.includes(id)))],
168
+ });
169
+ return {
170
+ category,
171
+ originalCategory: c,
172
+ };
173
+ });
174
+
175
+ newCategoriesData.forEach((c) => {
176
+ const newCategoryIds = [...new Set(c.originalCategory.categoryIds.flatMap((id) => {
177
+ const result = newCategoriesData.find(c => c.originalCategory.id === id);
178
+ if (result) {
179
+ return [result.category.id];
180
+ }
181
+ return [];
182
+ }))];
183
+ c.category.categoryIds = newCategoryIds;
184
+ });
185
+
186
+ const originalRootCategoryId = organization.meta.rootCategoryId;
187
+ const rootCategoryData = newCategoriesData.find(c => c.originalCategory.id === originalRootCategoryId);
188
+ if (!rootCategoryData) {
189
+ throw new Error('No root category found');
190
+ }
191
+
192
+ currentOrganizationRegistrationPeriod.settings.rootCategoryId = rootCategoryData.category.id;
193
+
194
+ const currentPeriodCategories = newCategoriesData.map(c => c.category);
195
+ currentOrganizationRegistrationPeriod.settings.categories = currentPeriodCategories;
196
+
197
+ if (currentOrganizationRegistrationPeriod.settings.categories.length === 0) {
198
+ throw new Error('No categories found');
199
+ }
200
+
201
+ if (!dryRun) {
202
+ await currentOrganizationRegistrationPeriod.save();
203
+ }
204
+ // #endregion
205
+
206
+ // #region archived category
207
+ const archivedGroupIds = [...new Set(groups.filter(g => g.status === GroupStatus.Archived).map(g => g.id))];
208
+ const hasArchivedGroups = archivedGroupIds.length > 0;
209
+
210
+ const archiveSubCategories = archivedGroupIds.map((originalGroupId) => {
211
+ const originalGroup = groups.find(g => g.id === originalGroupId)!;
212
+ const childGroups = groupMap.get(originalGroupId) ?? [];
213
+ const groupIds = [originalGroup, ...childGroups].map(g => g.id);
214
+
215
+ return GroupCategory.create({
216
+ settings: GroupCategorySettings.create({
217
+ name: originalGroup!.settings.name.toString(),
218
+ public: false,
219
+ maximumRegistrations: null,
220
+ }),
221
+ groupIds,
222
+ });
223
+ });
224
+
225
+ const archiveCategory = GroupCategory.create({
226
+ settings: GroupCategorySettings.create({
227
+ name: 'Archief',
228
+ description: 'Gearchiveerde groepen',
229
+ public: false,
230
+ }),
231
+ categoryIds: archiveSubCategories.map(c => c.id),
232
+ });
233
+ // #endregion
234
+
235
+ // #region create categories for previous periods
236
+ const allPreviousPeriodCategoriesData: { category: GroupCategory; originalCategory: GroupCategory | null }[] = currentPeriodCategories.flatMap((originalCategory: GroupCategory) => {
237
+ // important: do not set category ids on cloned category yet (because the new category ids are unknown still)
238
+ const clonedCategory = GroupCategory.create({
239
+ settings: GroupCategorySettings.create({
240
+ ...originalCategory.settings,
241
+ }),
242
+ });
243
+
244
+ if (originalCategory.groupIds.length) {
245
+ // create category for each original group
246
+ const newCategories = originalCategory.groupIds.flatMap((originalGroupId: string) => {
247
+ const childGroups = groupMap.get(originalGroupId);
248
+
249
+ // if(originalCategory)
250
+ if (childGroups && childGroups.length) {
251
+ const originalGroup = groups.find(g => g.id === originalGroupId)!;
252
+ const groupIds = childGroups.map(g => g.id);
253
+ const newCategory = GroupCategory.create({
254
+ settings: GroupCategorySettings.create({
255
+ name: originalGroup.settings.name.toString(),
256
+ public: false,
257
+ maximumRegistrations: null,
258
+ }),
259
+ groupIds,
260
+ });
261
+
262
+ return [newCategory];
263
+ }
264
+
265
+ return [];
266
+ });
267
+
268
+ clonedCategory.groupIds = [];
269
+ // set category ids because the ids are correct
270
+ clonedCategory.categoryIds = newCategories.map(c => c.id);
271
+
272
+ return [{ category: clonedCategory, originalCategory }, ...newCategories.map(c => ({ category: c, originalCategory: null }))];
273
+ }
274
+
275
+ // important: do not set category ids on cloned category yet (because the new category ids are unknown still)
276
+ return [{ category: clonedCategory, originalCategory }];
277
+ });
278
+
279
+ // update the category ids
280
+ const allPreviousPeriodCategories = allPreviousPeriodCategoriesData.map((c) => {
281
+ if (!c.originalCategory) {
282
+ return c.category;
283
+ }
284
+
285
+ const category = c.category;
286
+
287
+ // update category ids, if the category ids are not empty this means the ids are correct already
288
+ if (category.categoryIds.length === 0) {
289
+ category.categoryIds = c.originalCategory.categoryIds.flatMap((originalCategoryId) => {
290
+ const newCategoryData = allPreviousPeriodCategoriesData.find(c => c.originalCategory && c.originalCategory.id === originalCategoryId);
291
+ if (newCategoryData) {
292
+ return [newCategoryData.category.id];
293
+ }
294
+ return [];
295
+ });
296
+ }
297
+
298
+ return category;
299
+ });
300
+
301
+ const previousRootCategoryId = currentOrganizationRegistrationPeriod.settings.rootCategoryId;
302
+ const previousRootCategoryData = allPreviousPeriodCategoriesData.find(c => c.originalCategory && c.originalCategory.id === previousRootCategoryId);
303
+ if (!previousRootCategoryData) {
304
+ throw new Error(`No root category found for archive period (${previousRootCategoryId})`);
305
+ }
306
+
307
+ // add archive category to root category
308
+ if (hasArchivedGroups) {
309
+ previousRootCategoryData.category.categoryIds.push(archiveCategory.id);
310
+ }
311
+
312
+ archiveOrganizationPeriod.settings.rootCategoryId = previousRootCategoryData.category.id;
313
+ archiveOrganizationPeriod.settings.categories = hasArchivedGroups ? [...allPreviousPeriodCategories, archiveCategory, ...archiveSubCategories] : allPreviousPeriodCategories;
314
+
315
+ if (archiveOrganizationPeriod.settings.categories.length === 0) {
316
+ throw new Error('No categories found for archive period');
317
+ }
318
+
319
+ if (!dryRun) {
320
+ await archiveOrganizationPeriod.save();
321
+ }
322
+ // #endregion
323
+
324
+ const result: Group[] = [...groups, ...[...groupMap.values()].flat()];
325
+ return result;
326
+ }
327
+
328
+ async function migrateRegistrations({ organization, period, originalGroup, newGroup, cycle }: { organization: Organization; period: RegistrationPeriod; originalGroup: Group; newGroup: Group; cycle: number }, dryRun: boolean) {
329
+ // what for waiting lists of archive groups (previous cycles)?
330
+ let waitingList: Group | null = null;
331
+
332
+ const getOrCreateWaitingList = async () => {
333
+ if (newGroup.waitingListId) {
334
+ if (waitingList !== null) {
335
+ return waitingList;
336
+ }
337
+ const fetchedWaitingList = await Group.getByID(newGroup.waitingListId);
338
+
339
+ if (!fetchedWaitingList) {
340
+ throw new Error(`Waiting list not found: (waitingListId: ${newGroup.waitingListId}, groupId: ${newGroup.id})`);
341
+ }
342
+ }
343
+
344
+ const newWaitingList = new Group();
345
+ newWaitingList.cycle = cycleIfMigrated;
346
+ newWaitingList.type = GroupType.WaitingList;
347
+ newWaitingList.organizationId = organization.id;
348
+ newWaitingList.periodId = period.id;
349
+ newWaitingList.settings = GroupSettings.create({
350
+ name: TranslatedString.create($t(`c1f1d9d0-3fa1-4633-8e14-8c4fc98b4f0f`) + ' ' + newGroup.settings.name.toString()),
351
+ });
352
+
353
+ if (!dryRun) {
354
+ await newWaitingList.save();
355
+ }
356
+
357
+ waitingList = newWaitingList;
358
+ return newWaitingList;
359
+ };
360
+
361
+ const registrations = await Registration.select()
362
+ .where('groupId', originalGroup.id)
363
+ .andWhere('cycle', cycle)
364
+ .fetch();
365
+
366
+ for (const registration of registrations) {
367
+ if (registration.waitingList) {
368
+ const waitingList = await getOrCreateWaitingList();
369
+ if (newGroup.waitingListId !== waitingList.id) {
370
+ newGroup.waitingListId = waitingList.id;
371
+
372
+ if (!dryRun) {
373
+ await newGroup.save();
374
+ }
375
+ }
376
+
377
+ registration.groupId = waitingList.id;
378
+ }
379
+ else {
380
+ registration.groupId = newGroup.id;
381
+ }
382
+
383
+ registration.periodId = period.id;
384
+ registration.cycle = cycle;
385
+
386
+ if (!dryRun) {
387
+ await registration.save();
388
+ }
389
+ }
390
+ }
391
+
392
+ // #region period spans
393
+ type PeriodSpan = {
394
+ startMonth: number;
395
+ span: number;
396
+ };
397
+
398
+ function getPeriodSpans() {
399
+ const results: PeriodSpan[] = [];
400
+ const monthSpans: number[] = [12];
401
+
402
+ for (const monthSpan of monthSpans) {
403
+ for (let i = 0; i < monthSpan; i++) {
404
+ results.push({ startMonth: i, span: monthSpan });
405
+ }
406
+ }
407
+
408
+ return results;
409
+ }
410
+
411
+ const periodSpans = getPeriodSpans();
412
+
413
+ const defaultYear = 2025;
414
+ const defaultPeriodSpan = periodSpans.find(periodSpan => periodSpan.span === 12 && periodSpan.startMonth === 8)!;
415
+
416
+ function findBestPeriodMatch(cyclePeriod: { startDate: Date; endDate: Date }): { year: number; periodSpan: PeriodSpan } {
417
+ // best match = most days overlap and least days missing
418
+ const startYear = cyclePeriod.startDate.getFullYear();
419
+ const endYear = cyclePeriod.endDate.getFullYear();
420
+
421
+ const yearsToLoop = new Set([startYear - 1, startYear, endYear, endYear + 1]);
422
+ const cycleInfo = {
423
+ startDate: cyclePeriod.startDate,
424
+ endDate: cyclePeriod.endDate,
425
+ };
426
+
427
+ let bestMatch: {
428
+ year: number;
429
+ periodSpan: PeriodSpan;
430
+ daysMissing: number;
431
+ daysOverlapping: number;
432
+ } | null = null;
433
+
434
+ for (const periodSpan of periodSpans) {
435
+ const repeat = 12 / periodSpan.span;
436
+
437
+ for (let i = 0; i < repeat; i++) {
438
+ const startMonth = i * periodSpan.span + periodSpan.startMonth;
439
+
440
+ for (const year of yearsToLoop) {
441
+ const { daysMissing, daysOverlapping } = getDaysMissingAndOverlap(
442
+ cycleInfo,
443
+ {
444
+ startDate: new Date(year, startMonth, 1),
445
+ endDate: new Date(new Date(year, startMonth + periodSpan.span, 1).getTime() - 1),
446
+ },
447
+ );
448
+
449
+ if (bestMatch === null) {
450
+ bestMatch = {
451
+ year,
452
+ periodSpan,
453
+ daysMissing,
454
+ daysOverlapping,
455
+ };
456
+ continue;
457
+ }
458
+
459
+ if (daysOverlapping < 0) {
460
+ continue;
461
+ }
462
+
463
+ if (daysOverlapping > bestMatch.daysOverlapping) {
464
+ bestMatch = {
465
+ year,
466
+ periodSpan,
467
+ daysMissing,
468
+ daysOverlapping,
469
+ };
470
+ continue;
471
+ }
472
+
473
+ if (daysOverlapping === bestMatch.daysOverlapping && daysMissing < bestMatch.daysMissing) {
474
+ bestMatch = {
475
+ year,
476
+ periodSpan,
477
+ daysMissing,
478
+ daysOverlapping,
479
+ };
480
+ continue;
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ if (bestMatch) {
487
+ return { year: bestMatch.year, periodSpan: bestMatch.periodSpan };
488
+ }
489
+
490
+ return { year: defaultYear, periodSpan: periodSpans[0] };
491
+ }
492
+
493
+ /**
494
+ * returns the number of days that the periods overlap and the number of days that are missing from the longest period
495
+ * @param period
496
+ * @param targetPeriod
497
+ */
498
+ export function getDaysMissingAndOverlap(period1: { startDate: Date; endDate: Date }, period2: { startDate: Date; endDate: Date }): { daysMissing: number; daysOverlapping: number; longestPeriodInDays: number } {
499
+ const totalDays1 = differenceInDays(period1.startDate, period1.endDate);
500
+ const totalDays2 = differenceInDays(period2.startDate, period2.endDate);
501
+
502
+ const targetPeriod = totalDays1 > totalDays2 ? period1 : period2;
503
+ const otherPeriod = totalDays1 > totalDays2 ? period2 : period1;
504
+
505
+ const startOverlap = targetPeriod.startDate.getTime() > otherPeriod.startDate.getTime() ? targetPeriod.startDate : otherPeriod.startDate;
506
+ const endOverlap = targetPeriod.endDate.getTime() < otherPeriod.endDate.getTime() ? targetPeriod.endDate : otherPeriod.endDate;
507
+
508
+ const daysOverlapping = startOverlap.getTime() > endOverlap.getTime() ? 0 : differenceInDays(startOverlap, endOverlap);
509
+
510
+ const daysMissing = totalDays1 > totalDays2 ? totalDays1 - daysOverlapping : totalDays2 - daysOverlapping;
511
+
512
+ return {
513
+ daysMissing,
514
+ daysOverlapping,
515
+ longestPeriodInDays: totalDays1 > totalDays2 ? totalDays1 : totalDays2,
516
+ };
517
+ }
518
+ // #endregion
519
+
520
+ async function calculateBestCurrentPeriodSpan(groups: Group[]): Promise<{ startDate: Date; endDate: Date }> {
521
+ // #region separate short and long groups
522
+ const shortGroups: Group[] = [];
523
+ const longGroups: Group[] = [];
524
+
525
+ for (const group of groups) {
526
+ const { startDate, endDate } = group.settings as { startDate: Date; endDate: Date };
527
+ if (startDate && endDate && differenceInDays(startDate, endDate) < 150) {
528
+ shortGroups.push(group);
529
+ }
530
+ else {
531
+ longGroups.push(group);
532
+ }
533
+ }
534
+ // #endregion
535
+
536
+ // #region search the best period span
537
+ const periodSpanCounts = new Map<PeriodSpan, { year: number; group: Group }[]>();
538
+ let topPeriodSpan: PeriodSpan = defaultPeriodSpan;
539
+ let topCount: number = 0;
540
+
541
+ /**
542
+ * Use the long groups to calculate the best period span.
543
+ * If there are no long groups, use the short groups.
544
+ * If there are no short groups, use the default period span.
545
+ */
546
+ for (const group of (longGroups.length ? longGroups : shortGroups)) {
547
+ const { startDate, endDate } = group.settings as { startDate: Date; endDate: Date };
548
+
549
+ // month start, month end, span
550
+ const { periodSpan: bestPeriod, year } = findBestPeriodMatch({
551
+ startDate,
552
+ endDate });
553
+
554
+ const groups = periodSpanCounts.get(bestPeriod) ?? [];
555
+ // also keep track of the year to find the best year later
556
+ groups.push({ group, year });
557
+
558
+ if (groups.length > topCount) {
559
+ topCount = groups.length;
560
+ topPeriodSpan = bestPeriod;
561
+ }
562
+ periodSpanCounts.set(bestPeriod, groups);
563
+ }
564
+ // #endregion
565
+
566
+ // #region find the best year
567
+ let topYear = defaultYear;
568
+ const groupsInTopPeriodSpan = periodSpanCounts.get(topPeriodSpan) ?? [];
569
+
570
+ // calculate the number of groups in each year
571
+ const yearMap = new Map<number, Group[]>();
572
+ for (const { group, year } of groupsInTopPeriodSpan) {
573
+ const groups = yearMap.get(year) ?? [];
574
+ groups.push(group);
575
+ yearMap.set(year, groups);
576
+ }
577
+
578
+ // get the year with the most groups
579
+ let topYearCount = 0;
580
+ for (const [year, groups] of yearMap.entries()) {
581
+ if (groups.length > topYearCount) {
582
+ topYearCount = groups.length;
583
+ topYear = year;
584
+ }
585
+ }
586
+ // #endregion
587
+
588
+ const startDate = new Date(topYear, topPeriodSpan.startMonth, 1);
589
+ const endDate = new Date((new Date(topYear, topPeriodSpan.startMonth + topPeriodSpan.span)).getTime() - 1);
590
+
591
+ return { startDate, endDate };
592
+ }
593
+
594
+ // #region helpers
595
+ function differenceInDays(date1: Date, date2: Date) {
596
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
597
+ return timeToDays(diffTime);
598
+ }
599
+
600
+ function timeToDays(time: number): number {
601
+ return Math.ceil(time / (1000 * 60 * 60 * 24));
602
+ }
603
+
604
+ // #endregion
605
+
606
+ // #region factories
607
+ async function createDefaultRegistrationPeriod(organization: Organization, dryRun: boolean) {
608
+ // first check if already has a registration period for the organization
609
+ const registrationPeriod = await RegistrationPeriod.getByID(organization.periodId); ;
610
+ if (registrationPeriod && registrationPeriod.organizationId === organization.id) {
611
+ return;
612
+ }
613
+
614
+ // create new registration period, every organization should have a registration period with an organization id
615
+ const period = await createRegistrationPeriod({
616
+ organization,
617
+ startDate: new Date(2025, 0, 1, 0, 0, 0, 0),
618
+ endDate: new Date(2025, 11, 31, 59, 59, 59, 999),
619
+ }, dryRun);
620
+
621
+ organization.periodId = period.id;
622
+
623
+ if (!dryRun) {
624
+ await organization.save();
625
+ }
626
+ }
627
+
628
+ async function createOrganizationRegistrationPeriod(options: {
629
+ organization: Organization;
630
+ period: RegistrationPeriod;
631
+ }, dryRun: boolean) {
632
+ const organizationRegistrationPeriod = new OrganizationRegistrationPeriod();
633
+
634
+ organizationRegistrationPeriod.organizationId = options.organization.id;
635
+ organizationRegistrationPeriod.periodId = options.period.id;
636
+
637
+ if (!dryRun) {
638
+ await organizationRegistrationPeriod.save();
639
+ }
640
+
641
+ return organizationRegistrationPeriod;
642
+ }
643
+
644
+ async function createRegistrationPeriod(options: {
645
+ startDate?: Date;
646
+ endDate?: Date;
647
+ previousPeriodId?: string;
648
+ locked?: boolean;
649
+ organization?: Organization;
650
+ }, dryRun: boolean) {
651
+ const period = new RegistrationPeriod();
652
+
653
+ period.organizationId = options.organization ? options.organization.id : null;
654
+ period.startDate = options.startDate ?? new Date(2024, 0, 1, 0, 0, 0, 0);
655
+ period.endDate = options.endDate ?? new Date(2024, 11, 31, 59, 59, 59, 999);
656
+ if (options.previousPeriodId) {
657
+ period.previousPeriodId = options.previousPeriodId;
658
+ }
659
+ period.settings = RegistrationPeriodSettings.create({});
660
+ period.locked = options.locked ?? false;
661
+
662
+ if (!dryRun) {
663
+ await period.save();
664
+ }
665
+
666
+ return period;
667
+ }
668
+
669
+ function createPreviousGroup({ originalGroup, period, cycleInformation, index }: { originalGroup: Group; period: RegistrationPeriod; cycleInformation: CycleInformation; index: number }) {
670
+ const newGroup = new Group();
671
+ newGroup.organizationId = originalGroup.organizationId;
672
+ newGroup.periodId = period.id;
673
+ newGroup.status = originalGroup.status;
674
+ newGroup.createdAt = originalGroup.createdAt;
675
+ newGroup.deletedAt = originalGroup.deletedAt;
676
+
677
+ const originalPrivateSettings: GroupPrivateSettings = originalGroup.privateSettings;
678
+
679
+ // todo: should group ids in permissions get updated?
680
+ newGroup.privateSettings = GroupPrivateSettings.create({
681
+ ...originalPrivateSettings,
682
+ });
683
+
684
+ newGroup.cycle = cycleIfMigrated;
685
+
686
+ const originalSettings: GroupSettings = originalGroup.settings;
687
+
688
+ const periodStartDate: Date = period.startDate;
689
+ const periodEndDate: Date = period.endDate;
690
+
691
+ // todo: how to choose start and end dates?
692
+ let startDate = new Date(periodStartDate);
693
+ let endDate = new Date(periodEndDate);
694
+
695
+ if (cycleInformation.startDate && cycleInformation.endDate) {
696
+ startDate = new Date(cycleInformation.startDate);
697
+ endDate = new Date(cycleInformation.endDate);
698
+ }
699
+
700
+ const isPlural = index > 0;
701
+ const extraName = isPlural ? `${index + 1} periodes geleden` : `${index + 1} periode geleden`;
702
+
703
+ newGroup.settings = GroupSettings
704
+ .create({
705
+ ...originalSettings,
706
+ cycleSettings: new Map(),
707
+ name: new TranslatedString(`${originalSettings.name.toString()} (${extraName})`),
708
+ startDate,
709
+ endDate,
710
+ });
711
+ newGroup.type = originalGroup.type;
712
+
713
+ return newGroup;
714
+ }
715
+ // #endregion