@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.
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +4 -4
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +29 -5
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -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 +67 -22
- 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/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/emails.ts +65 -0
- package/src/sql-sorters/emails.ts +47 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Group, Organization, OrganizationRegistrationPeriodFactory, Registration, RegistrationPeriod, RegistrationPeriodFactory } from '@stamhoofd/models';
|
|
3
|
+
import { CycleInformation, GroupCategory, GroupCategorySettings, GroupPrivateSettings, GroupSettings, GroupStatus, GroupType, TranslatedString } from '@stamhoofd/structures';
|
|
4
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
|
+
|
|
6
|
+
type CycleData = {
|
|
7
|
+
cycle: number;
|
|
8
|
+
startDate: Date | null;
|
|
9
|
+
endDate: Date | null;
|
|
10
|
+
groups: Group[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const cycleIfMigrated = -99;
|
|
14
|
+
|
|
15
|
+
function nullIfInvalidDate(date: Date) {
|
|
16
|
+
// some dates are very old and this throws an error otherwise
|
|
17
|
+
if (date.getFullYear() < 1950) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function limitDate(date: Date): Date {
|
|
24
|
+
// some dates are very old and this throws an error otherwise
|
|
25
|
+
if (date.getFullYear() < 1950) {
|
|
26
|
+
return new Date(1950, date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
27
|
+
}
|
|
28
|
+
return date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Often cycle settings contains information about various cycles without start and end date.
|
|
33
|
+
* Therefore a good start and end date should be calculated.
|
|
34
|
+
*/
|
|
35
|
+
function convertCycleSettings(cycleSettings: Map<number, CycleInformation>, startDate: Date, endDate: Date): Map<number, CycleInformation> {
|
|
36
|
+
if (endDate < startDate) {
|
|
37
|
+
// switch start and end dates
|
|
38
|
+
const originalStartDate = startDate;
|
|
39
|
+
const originalEndDate = endDate;
|
|
40
|
+
startDate = originalEndDate;
|
|
41
|
+
endDate = originalStartDate;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const array = [...cycleSettings.entries()].map((entry) => {
|
|
45
|
+
const cycle = entry[0];
|
|
46
|
+
const settings = entry[1];
|
|
47
|
+
return {
|
|
48
|
+
cycle,
|
|
49
|
+
settings,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// reversed
|
|
54
|
+
array.sort((a, b) => b.cycle - a.cycle);
|
|
55
|
+
|
|
56
|
+
let lastStartDate: Date | null = nullIfInvalidDate(startDate);
|
|
57
|
+
let lastEndDate: Date | null = nullIfInvalidDate(endDate);
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < array.length; i++) {
|
|
60
|
+
const item = array[i];
|
|
61
|
+
|
|
62
|
+
if (item.settings.startDate !== null) {
|
|
63
|
+
lastStartDate = item.settings.startDate;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (lastEndDate !== null && lastStartDate !== null) {
|
|
67
|
+
const dayDifference = differenceInDays(lastStartDate, lastEndDate);
|
|
68
|
+
|
|
69
|
+
const years = Math.ceil(dayDifference / 366);
|
|
70
|
+
|
|
71
|
+
const newStartDate = new Date(lastStartDate.getTime());
|
|
72
|
+
newStartDate.setFullYear(newStartDate.getFullYear() - years);
|
|
73
|
+
item.settings.startDate = newStartDate;
|
|
74
|
+
lastStartDate = newStartDate;
|
|
75
|
+
|
|
76
|
+
// const isMostRecent = i === 0;
|
|
77
|
+
|
|
78
|
+
// if (!isMostRecent && item.settings.endDate === null) {
|
|
79
|
+
// if (lastEndDate === null) {
|
|
80
|
+
// throw new Error('Cannot calculate new end date');
|
|
81
|
+
// }
|
|
82
|
+
// const newEndDate = new Date(lastStartDate.getTime() - 1);
|
|
83
|
+
// item.settings.endDate = newEndDate;
|
|
84
|
+
// }
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw new Error('Cannot calculate new start date');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (item.settings.endDate !== null) {
|
|
92
|
+
lastEndDate = item.settings.endDate;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const isOldest = i === array.length - 1;
|
|
96
|
+
if (isOldest) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (lastStartDate !== null) {
|
|
101
|
+
const newEndDate = new Date(lastStartDate.getTime() - 1);
|
|
102
|
+
item.settings.endDate = newEndDate;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// todo
|
|
106
|
+
throw Error('test todo');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return new Map([...array].reverse().map(item => [item.cycle, item.settings]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function startGroupCyclesToPeriodsMigration() {
|
|
115
|
+
for await (const organization of Organization.select().all()) {
|
|
116
|
+
const allCycles: CycleData[] = [];
|
|
117
|
+
|
|
118
|
+
const groups = await Group.select().where('organizationId', organization.id).fetch();
|
|
119
|
+
|
|
120
|
+
if (groups.some(g => g.cycle === cycleIfMigrated)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('Organization: ' + organization.name);
|
|
125
|
+
for (const group of groups) {
|
|
126
|
+
const cycle: number = group.cycle;
|
|
127
|
+
const startDate: Date = limitDate(group.settings.startDate);
|
|
128
|
+
const endDate: Date = limitDate(group.settings.endDate);
|
|
129
|
+
|
|
130
|
+
const addCycle = (cycle: number, startDate: Date | null, endDate: Date | null) => {
|
|
131
|
+
if (endDate && startDate && endDate < startDate) {
|
|
132
|
+
// switch start and end dates
|
|
133
|
+
const originalStartDate = startDate;
|
|
134
|
+
const originalEndDate = endDate;
|
|
135
|
+
startDate = originalEndDate;
|
|
136
|
+
endDate = originalStartDate;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const equalCycle = allCycles.find((data) => {
|
|
140
|
+
if (data.cycle === cycle && data.startDate === startDate && data.endDate === endDate) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (equalCycle) {
|
|
146
|
+
const hasGroup = equalCycle.groups.find(g => g.id === group.id);
|
|
147
|
+
if (!hasGroup) {
|
|
148
|
+
equalCycle.groups.push(group);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
allCycles.push({ cycle, startDate, endDate, groups: [group] });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
addCycle(cycle, startDate, endDate);
|
|
157
|
+
|
|
158
|
+
if (group.settings.cycleSettings && group.settings.cycleSettings.size) {
|
|
159
|
+
console.log('Group: ' + group.id + ' - ' + group.settings.name);
|
|
160
|
+
for (const entry of convertCycleSettings(group.settings.cycleSettings, startDate, endDate).entries()) {
|
|
161
|
+
const currentCycle: number = entry[0];
|
|
162
|
+
const cycleSettings = entry[1];
|
|
163
|
+
const cycleStartDate: Date | null = cycleSettings.startDate ? limitDate(cycleSettings.startDate) : null;
|
|
164
|
+
const cycleEndDate: Date | null = cycleSettings.endDate ? limitDate(cycleSettings.endDate) : null;
|
|
165
|
+
addCycle(currentCycle, cycleStartDate, cycleEndDate);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (allCycles.length === 0) {
|
|
171
|
+
// first check if already has a registration period for the organization
|
|
172
|
+
const registrationPeriod = await RegistrationPeriod.getByID(organization.periodId); ;
|
|
173
|
+
if (registrationPeriod && registrationPeriod.organizationId === organization.id) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// create new registration period, every organization should have a registration period with an organization id
|
|
178
|
+
const period = await new RegistrationPeriodFactory({
|
|
179
|
+
organization,
|
|
180
|
+
startDate: new Date(2025, 0, 1, 0, 0, 0, 0),
|
|
181
|
+
endDate: new Date(2025, 11, 31, 59, 59, 59, 999),
|
|
182
|
+
}).create();
|
|
183
|
+
|
|
184
|
+
organization.periodId = period.id;
|
|
185
|
+
await organization.save();
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const cycleGroups = groupCycles(allCycles);
|
|
190
|
+
|
|
191
|
+
cycleGroups.forEach((g) => {
|
|
192
|
+
if (g.startDate > g.endDate) {
|
|
193
|
+
throw new Error(`(${organization.name}) - Grouped startDate (${Formatter.date(g.startDate, true)}) is after endDate (${Formatter.date(g.endDate, true)})`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await migrateCycleGroups(cycleGroups, organization);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
console.error(JSON.stringify(cycleGroups.map((cg) => {
|
|
202
|
+
return {
|
|
203
|
+
startDate: cg.startDate.getTime(),
|
|
204
|
+
endDate: cg.endDate.getTime(),
|
|
205
|
+
groups: cg.cycles.flatMap(c => c.groups.map(g => g.id)),
|
|
206
|
+
};
|
|
207
|
+
})));
|
|
208
|
+
console.error(e);
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await cleanupCycleGroups(cycleGroups);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function cleanupCycleGroups(cycleGroups: CycleGroup[]) {
|
|
217
|
+
for (const group of cycleGroups.flatMap(cg => cg.cycles.flatMap(c => c.groups))) {
|
|
218
|
+
await cleanupGroup(group);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function cleanupGroup(group: Group) {
|
|
223
|
+
group.settings.cycleSettings = new Map();
|
|
224
|
+
group.cycle = cycleIfMigrated;
|
|
225
|
+
if (group.status === GroupStatus.Archived) {
|
|
226
|
+
group.status = GroupStatus.Closed;
|
|
227
|
+
}
|
|
228
|
+
await group.save();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function sortCycles(cycles: CycleData[]) {
|
|
232
|
+
cycles.sort((a, b) => {
|
|
233
|
+
if (a.startDate && b.startDate) {
|
|
234
|
+
return a.startDate.getTime() - b.startDate.getTime();
|
|
235
|
+
}
|
|
236
|
+
if (a.startDate === null && b.startDate === null) {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (a.startDate) {
|
|
241
|
+
return -1;
|
|
242
|
+
}
|
|
243
|
+
return 1;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type CycleGroup = {
|
|
248
|
+
startDate: Date;
|
|
249
|
+
endDate: Date;
|
|
250
|
+
cycles: CycleData[];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function groupCycles(cycles: CycleData[]): CycleGroup[] {
|
|
254
|
+
const shortCycles: CycleData[] = [];
|
|
255
|
+
const longCycles: CycleData[] = [];
|
|
256
|
+
|
|
257
|
+
for (const cycle of cycles) {
|
|
258
|
+
const { startDate, endDate } = cycle;
|
|
259
|
+
if (startDate && endDate && differenceInDays(startDate, endDate) < 240) {
|
|
260
|
+
shortCycles.push(cycle);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
longCycles.push(cycle);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
sortCycles(longCycles);
|
|
268
|
+
sortCycles(shortCycles);
|
|
269
|
+
|
|
270
|
+
let startDate = longCycles.find(a => a.startDate !== null)?.startDate;
|
|
271
|
+
if (!startDate) {
|
|
272
|
+
startDate = shortCycles.find(a => a.startDate !== null)?.startDate;
|
|
273
|
+
if (!startDate) {
|
|
274
|
+
throw new Error('No startDate found');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let currentGroup: {
|
|
279
|
+
startDate: Date;
|
|
280
|
+
cycles: CycleData[];
|
|
281
|
+
|
|
282
|
+
} = { startDate,
|
|
283
|
+
cycles: [] };
|
|
284
|
+
|
|
285
|
+
const groupsWithStart: {
|
|
286
|
+
startDate: Date;
|
|
287
|
+
cycles: CycleData[];
|
|
288
|
+
}[] = [currentGroup];
|
|
289
|
+
const skippedCycles: CycleData[] = [];
|
|
290
|
+
|
|
291
|
+
for (const cycle of longCycles) {
|
|
292
|
+
if (!cycle.startDate) {
|
|
293
|
+
skippedCycles.push(cycle);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const dayDifference = differenceInDays(startDate, cycle.startDate);
|
|
298
|
+
|
|
299
|
+
if (dayDifference < 150) {
|
|
300
|
+
currentGroup.cycles.push(cycle);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
startDate = cycle.startDate;
|
|
304
|
+
currentGroup = { startDate, cycles: [cycle] };
|
|
305
|
+
groupsWithStart.push(currentGroup);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let lastStartDate: Date | null = null;
|
|
310
|
+
|
|
311
|
+
const groupsWithStartAndEnd: CycleGroup[] = [];
|
|
312
|
+
|
|
313
|
+
for (const group of groupsWithStart.slice().reverse()) {
|
|
314
|
+
let endDate: Date;
|
|
315
|
+
|
|
316
|
+
if (lastStartDate) {
|
|
317
|
+
endDate = getDateBefore(lastStartDate);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
endDate = getDefaultEndDate(group.startDate, group.cycles);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
groupsWithStartAndEnd.push({ startDate: group.startDate, endDate, cycles: group.cycles });
|
|
324
|
+
lastStartDate = group.startDate;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (groupsWithStartAndEnd.length === 0) {
|
|
328
|
+
const startDates = shortCycles.map(a => a.startDate).filter(a => a !== null).map(a => a!.getTime());
|
|
329
|
+
const endDates = shortCycles.map(a => a.endDate).filter(a => a !== null).map(a => a!.getTime());
|
|
330
|
+
|
|
331
|
+
if (startDates.length === 0 || endDates.length === 0) {
|
|
332
|
+
throw new Error('No cycle with start and end date found.');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
groupsWithStartAndEnd.push({
|
|
336
|
+
startDate: new Date(Math.min(...startDates)),
|
|
337
|
+
cycles: shortCycles,
|
|
338
|
+
endDate: new Date(Math.max(...endDates)),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let groupAddedBefore: CycleGroup | null = null;
|
|
343
|
+
let groupAddedAfter: CycleGroup | null = null;
|
|
344
|
+
|
|
345
|
+
const getNewestGroup = () => {
|
|
346
|
+
return groupsWithStartAndEnd[0];
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const getOldestGroup = () => {
|
|
350
|
+
return groupsWithStartAndEnd[groupsWithStartAndEnd.length - 1];
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
for (const cycleToAdd of [...shortCycles, ...skippedCycles]) {
|
|
354
|
+
const bestGroupMatch = selectBestGroup(cycleToAdd, groupsWithStartAndEnd);
|
|
355
|
+
|
|
356
|
+
// if null => outside of existing cycles => add new one before or after
|
|
357
|
+
if (bestGroupMatch === null) {
|
|
358
|
+
const oldestGroup = getOldestGroup();
|
|
359
|
+
let isBefore = false;
|
|
360
|
+
|
|
361
|
+
if (cycleToAdd.endDate && cycleToAdd.endDate < oldestGroup.startDate) {
|
|
362
|
+
isBefore = true;
|
|
363
|
+
}
|
|
364
|
+
else if (cycleToAdd.startDate && cycleToAdd.startDate < oldestGroup.startDate) {
|
|
365
|
+
if (!groupAddedBefore && differenceInDays(cycleToAdd.startDate, oldestGroup.startDate) < 90) {
|
|
366
|
+
oldestGroup.startDate = cycleToAdd.startDate;
|
|
367
|
+
oldestGroup.cycles.push(cycleToAdd);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
isBefore = true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isBefore) {
|
|
374
|
+
if (groupAddedBefore) {
|
|
375
|
+
if (cycleToAdd.startDate && cycleToAdd.startDate < groupAddedBefore.startDate) {
|
|
376
|
+
groupAddedBefore.startDate = cycleToAdd.startDate;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
groupAddedBefore.cycles.push(cycleToAdd);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const newEndDate = getDateBefore(oldestGroup.startDate);
|
|
383
|
+
let newStartDate = cycleToAdd.startDate;
|
|
384
|
+
if (!newStartDate) {
|
|
385
|
+
newStartDate = new Date(oldestGroup.startDate);
|
|
386
|
+
newStartDate.setFullYear(newStartDate.getFullYear() - 1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const newGroup: CycleGroup = { startDate: newStartDate, endDate: newEndDate, cycles: [cycleToAdd] };
|
|
390
|
+
groupAddedBefore = newGroup;
|
|
391
|
+
groupsWithStartAndEnd.push(newGroup);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
if (groupAddedAfter) {
|
|
396
|
+
if (cycleToAdd.endDate && cycleToAdd.endDate > groupAddedAfter.endDate) {
|
|
397
|
+
groupAddedAfter.endDate = cycleToAdd.endDate;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
groupAddedAfter.cycles.push(cycleToAdd);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
const newestGroup = getNewestGroup();
|
|
404
|
+
const newStartDate = getDateAfter(newestGroup.endDate);
|
|
405
|
+
let newEndDate = cycleToAdd.endDate;
|
|
406
|
+
if (!newEndDate) {
|
|
407
|
+
newEndDate = new Date(newestGroup.endDate);
|
|
408
|
+
newEndDate.setFullYear(newEndDate.getFullYear() + 1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const newGroup: CycleGroup = { startDate: newStartDate, endDate: newEndDate, cycles: [cycleToAdd] };
|
|
412
|
+
groupAddedAfter = newGroup;
|
|
413
|
+
groupsWithStartAndEnd.unshift(newGroup);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
bestGroupMatch.cycles.push(cycleToAdd);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const newestGroup = getNewestGroup();
|
|
423
|
+
for (const cycle of newestGroup.cycles) {
|
|
424
|
+
if (cycle.endDate && cycle.endDate > newestGroup.endDate) {
|
|
425
|
+
newestGroup.endDate = cycle.endDate;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const oldestGroup = getOldestGroup();
|
|
429
|
+
for (const cycle of oldestGroup.cycles) {
|
|
430
|
+
if (cycle.startDate && cycle.startDate < oldestGroup.startDate) {
|
|
431
|
+
oldestGroup.startDate = cycle.startDate;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return groupsWithStartAndEnd;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function selectBestGroup(cycle: CycleData, groupsWithStartAndEnd: {
|
|
439
|
+
startDate: Date;
|
|
440
|
+
endDate: Date;
|
|
441
|
+
cycles: CycleData[];
|
|
442
|
+
}[]): {
|
|
443
|
+
startDate: Date;
|
|
444
|
+
endDate: Date;
|
|
445
|
+
cycles: CycleData[];
|
|
446
|
+
} | null {
|
|
447
|
+
const { startDate, endDate } = cycle;
|
|
448
|
+
|
|
449
|
+
if (startDate !== null && endDate !== null) {
|
|
450
|
+
let result: {
|
|
451
|
+
startDate: Date;
|
|
452
|
+
endDate: Date;
|
|
453
|
+
cycles: CycleData[];
|
|
454
|
+
} | null = null;
|
|
455
|
+
let biggestOverlap = 0;
|
|
456
|
+
|
|
457
|
+
for (const group of groupsWithStartAndEnd) {
|
|
458
|
+
let start: Date;
|
|
459
|
+
let end: Date;
|
|
460
|
+
|
|
461
|
+
if (startDate > group.endDate || endDate < group.startDate) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (startDate >= group.startDate) {
|
|
466
|
+
start = startDate;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
start = group.startDate;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (endDate <= group.endDate) {
|
|
473
|
+
end = endDate;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
end = group.endDate;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const overlap = differenceInDays(start, end);
|
|
480
|
+
|
|
481
|
+
if (overlap > biggestOverlap) {
|
|
482
|
+
biggestOverlap = overlap;
|
|
483
|
+
result = group;
|
|
484
|
+
}
|
|
485
|
+
else if (result === null) {
|
|
486
|
+
result = group;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (endDate !== null) {
|
|
494
|
+
const group = groupsWithStartAndEnd.find((g) => {
|
|
495
|
+
if (endDate >= g.startDate && endDate <= g.endDate) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return group ?? null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (startDate !== null) {
|
|
505
|
+
const group = groupsWithStartAndEnd.find((g) => {
|
|
506
|
+
if (startDate >= g.startDate && startDate <= g.endDate) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return group ?? null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return groupsWithStartAndEnd[0];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function getDefaultEndDate(startDate: Date, cycleData: CycleData[]) {
|
|
519
|
+
const endTimes = cycleData.filter(g => g.endDate !== null).map(g => g.endDate!.getTime());
|
|
520
|
+
|
|
521
|
+
if (endTimes.length === 0) {
|
|
522
|
+
return getEndDateFromStartDate(startDate);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return new Date(Math.max(...endTimes));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function getEndDateFromStartDate(startDate: Date): Date {
|
|
529
|
+
const date = new Date(startDate);
|
|
530
|
+
date.setFullYear(date.getFullYear() + 1);
|
|
531
|
+
return getDateBefore(date);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function getDateBefore(date: Date): Date {
|
|
535
|
+
return new Date(date.getTime() - 1);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function getDateAfter(date: Date): Date {
|
|
539
|
+
return new Date(date.getTime() + 1);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function differenceInDays(date1: Date, date2: Date) {
|
|
543
|
+
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
|
544
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
545
|
+
return diffDays;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function migrateCycleGroups(cycleGroups: CycleGroup[], organization: Organization) {
|
|
549
|
+
let previousPeriod: RegistrationPeriod | null = null;
|
|
550
|
+
|
|
551
|
+
const originalCategories = organization.meta.categories;
|
|
552
|
+
const originalRootCategoryId = organization.meta.rootCategoryId;
|
|
553
|
+
|
|
554
|
+
if (originalRootCategoryId === '') {
|
|
555
|
+
throw new Error('Original root category is empty');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (let i = 0; i < cycleGroups.length; i++) {
|
|
559
|
+
const cycleGroup = cycleGroups[i];
|
|
560
|
+
|
|
561
|
+
// create registration period
|
|
562
|
+
const locked = cycleGroup.endDate.getFullYear() < new Date().getFullYear();
|
|
563
|
+
const period = await new RegistrationPeriodFactory({
|
|
564
|
+
startDate: cycleGroup.startDate,
|
|
565
|
+
endDate: cycleGroup.endDate,
|
|
566
|
+
locked,
|
|
567
|
+
previousPeriodId: previousPeriod ? previousPeriod.id : undefined,
|
|
568
|
+
organization,
|
|
569
|
+
}).create();
|
|
570
|
+
|
|
571
|
+
if (i === 0) {
|
|
572
|
+
organization.periodId = period.id;
|
|
573
|
+
await organization.save();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
previousPeriod = period;
|
|
577
|
+
|
|
578
|
+
const organizationRegistrationPeriod = await new OrganizationRegistrationPeriodFactory({
|
|
579
|
+
organization,
|
|
580
|
+
period,
|
|
581
|
+
}).create();
|
|
582
|
+
|
|
583
|
+
const allGroups: { group: Group; originalGroup: Group; cylcleData: CycleData }[] = [];
|
|
584
|
+
const newGroups: Group[] = [];
|
|
585
|
+
|
|
586
|
+
// create group for each group in cycleGroup
|
|
587
|
+
for (const cycle of cycleGroup.cycles) {
|
|
588
|
+
for (const group of cycle.groups) {
|
|
589
|
+
if (cycle.cycle === group.cycle) {
|
|
590
|
+
group.periodId = period.id;
|
|
591
|
+
group.settings.startDate = cycleGroup.startDate;
|
|
592
|
+
group.settings.endDate = cycleGroup.endDate;
|
|
593
|
+
|
|
594
|
+
await group.save();
|
|
595
|
+
|
|
596
|
+
allGroups.push({ group, originalGroup: group, cylcleData: cycle });
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
const newGroup = cloneGroup(cycle, group, period);
|
|
600
|
+
await newGroup.save();
|
|
601
|
+
newGroups.push(newGroup);
|
|
602
|
+
allGroups.push({ group: newGroup, originalGroup: group, cylcleData: cycle });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// update registrations
|
|
608
|
+
for (const { group, originalGroup, cylcleData } of allGroups) {
|
|
609
|
+
let waitingList: Group | null = null;
|
|
610
|
+
|
|
611
|
+
const getOrCreateWaitingList = async () => {
|
|
612
|
+
if (group.waitingListId !== null) {
|
|
613
|
+
if (waitingList !== null) {
|
|
614
|
+
return waitingList;
|
|
615
|
+
}
|
|
616
|
+
const fetchedWaitingList = await Group.getByID(group.waitingListId);
|
|
617
|
+
|
|
618
|
+
if (!fetchedWaitingList) {
|
|
619
|
+
throw new Error('Waiting list not found');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
waitingList = fetchedWaitingList;
|
|
623
|
+
return fetchedWaitingList;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const newWaitingList = new Group();
|
|
627
|
+
newWaitingList.cycle = cycleIfMigrated;
|
|
628
|
+
newWaitingList.type = GroupType.WaitingList;
|
|
629
|
+
newWaitingList.organizationId = organization.id;
|
|
630
|
+
newWaitingList.periodId = period.id;
|
|
631
|
+
newWaitingList.settings = GroupSettings.create({
|
|
632
|
+
name: TranslatedString.create($t(`c1f1d9d0-3fa1-4633-8e14-8c4fc98b4f0f`) + ' ' + originalGroup.settings.name.toString()),
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
await newWaitingList.save();
|
|
636
|
+
|
|
637
|
+
waitingList = newWaitingList;
|
|
638
|
+
return newWaitingList;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const registrations = await Registration.select()
|
|
642
|
+
.where('groupId', originalGroup.id)
|
|
643
|
+
.andWhere('cycle', cylcleData.cycle)
|
|
644
|
+
.fetch();
|
|
645
|
+
|
|
646
|
+
for (const registration of registrations) {
|
|
647
|
+
if (registration.waitingList) {
|
|
648
|
+
const waitingList = await getOrCreateWaitingList();
|
|
649
|
+
if (group.waitingListId !== waitingList.id) {
|
|
650
|
+
group.waitingListId = waitingList.id;
|
|
651
|
+
await group.save();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
registration.groupId = waitingList.id;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
registration.groupId = group.id;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
registration.periodId = period.id;
|
|
661
|
+
registration.cycle = cylcleData.cycle;
|
|
662
|
+
await registration.save();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const archivedGroupIds = allGroups.filter(g => g.group.status === GroupStatus.Archived).map(g => g.group.id);
|
|
667
|
+
const hasArchivedGroups = archivedGroupIds.length > 0;
|
|
668
|
+
|
|
669
|
+
const archiveCategory = GroupCategory.create({
|
|
670
|
+
settings: GroupCategorySettings.create({
|
|
671
|
+
name: 'Archief',
|
|
672
|
+
description: 'Gearchiveerde groepen',
|
|
673
|
+
public: false,
|
|
674
|
+
}),
|
|
675
|
+
groupIds: [...archivedGroupIds],
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const newCategoriesData = originalCategories.map((c) => {
|
|
679
|
+
const category = GroupCategory.create({
|
|
680
|
+
settings: GroupCategorySettings.create({
|
|
681
|
+
...c.settings,
|
|
682
|
+
}),
|
|
683
|
+
groupIds: [...new Set(c.groupIds.flatMap((oldId) => {
|
|
684
|
+
const result = allGroups.find(g => g.originalGroup.id === oldId);
|
|
685
|
+
if (result) {
|
|
686
|
+
if (result.group.status === GroupStatus.Archived) {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
return [result.group.id];
|
|
690
|
+
}
|
|
691
|
+
return [];
|
|
692
|
+
}))],
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
category,
|
|
696
|
+
originalCategory: c,
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
newCategoriesData.forEach((c) => {
|
|
701
|
+
const newCategoryIds = [...new Set(c.originalCategory.categoryIds.flatMap((id) => {
|
|
702
|
+
const result = newCategoriesData.find(c => c.originalCategory.id === id);
|
|
703
|
+
if (result) {
|
|
704
|
+
return [result.category.id];
|
|
705
|
+
}
|
|
706
|
+
return [];
|
|
707
|
+
}))];
|
|
708
|
+
c.category.categoryIds = newCategoryIds;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const rootCategoryData = newCategoriesData.find(c => c.originalCategory.id === originalRootCategoryId);
|
|
712
|
+
if (!rootCategoryData) {
|
|
713
|
+
throw new Error('No root category found');
|
|
714
|
+
}
|
|
715
|
+
organizationRegistrationPeriod.settings.rootCategoryId = rootCategoryData.category.id;
|
|
716
|
+
|
|
717
|
+
organizationRegistrationPeriod.settings.categories = newCategoriesData.map(c => c.category).concat(hasArchivedGroups ? [archiveCategory] : []);
|
|
718
|
+
|
|
719
|
+
if (organizationRegistrationPeriod.settings.categories.length === 0) {
|
|
720
|
+
throw new Error('No categories found');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
await organizationRegistrationPeriod.save();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function cloneGroup(cycle: CycleData, group: Group, period: RegistrationPeriod) {
|
|
728
|
+
const newGroup = new Group();
|
|
729
|
+
newGroup.organizationId = group.organizationId;
|
|
730
|
+
newGroup.periodId = period.id;
|
|
731
|
+
newGroup.status = group.status;
|
|
732
|
+
newGroup.createdAt = group.createdAt;
|
|
733
|
+
newGroup.deletedAt = group.deletedAt;
|
|
734
|
+
|
|
735
|
+
// todo: should group ids in permissions get updated?
|
|
736
|
+
newGroup.privateSettings = GroupPrivateSettings.create({
|
|
737
|
+
...group.privateSettings,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
newGroup.cycle = cycleIfMigrated;
|
|
741
|
+
|
|
742
|
+
const newSettings = GroupSettings
|
|
743
|
+
.create({
|
|
744
|
+
...group.settings,
|
|
745
|
+
cycleSettings: new Map(),
|
|
746
|
+
startDate: cycle.startDate ?? undefined,
|
|
747
|
+
endDate: cycle.endDate ?? undefined,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
newGroup.settings = newSettings;
|
|
751
|
+
newGroup.type = group.type;
|
|
752
|
+
|
|
753
|
+
return newGroup;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export default new Migration(async () => {
|
|
757
|
+
if (STAMHOOFD.environment === 'test') {
|
|
758
|
+
console.log('skipped in tests');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
763
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
await startGroupCyclesToPeriodsMigration();
|
|
768
|
+
});
|