@stamhoofd/backend 2.129.3 → 2.131.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 +17 -17
- package/src/crons/index.ts +1 -0
- package/src/crons/stripe-payout-reports.ts +69 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.test.ts +324 -2
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +74 -3
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +6 -0
- package/src/endpoints/global/members/throwIfDrasticMemberDetailsChange.test.ts +96 -0
- package/src/endpoints/global/members/throwIfDrasticMemberDetailsChange.ts +58 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +68 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +7 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +44 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +31 -2
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +61 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +14 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripePayoutsExportStatusEndpoint.ts +32 -0
- package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.test.ts +103 -0
- package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.ts +125 -0
- package/src/helpers/AuthenticatedStructures.ts +5 -7
- package/src/helpers/MembershipCharger.ts +26 -1
- package/src/helpers/RegistrationPeriodRelationBackfiller.test.ts +111 -0
- package/src/helpers/RegistrationPeriodRelationBackfiller.ts +125 -0
- package/src/helpers/StripePayoutExportData.ts +195 -0
- package/src/helpers/StripePayoutExportExcel.ts +280 -0
- package/src/helpers/StripePayoutReporter.test.ts +419 -0
- package/src/helpers/StripePayoutReporter.ts +585 -0
- package/src/migrations/1783342463-remove-platform-uitpas-flag.ts +25 -0
- package/src/seeds/1783400000-backfill-registration-period-relation.ts +13 -0
- package/src/seeds/1783502528-fix-registrations-with-platform-period.ts +426 -0
- package/src/services/uitpas/UitpasService.ts +1 -1
- package/src/services/uitpas/cancelTicketSales.ts +1 -1
- package/src/services/uitpas/checkPermissionsFor.ts +1 -1
- package/src/services/uitpas/getSocialTariffForEvent.ts +1 -1
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +1 -1
- package/src/services/uitpas/registerTicketSales.ts +1 -1
- package/src/services/uitpas/searchUitpasEvents.ts +22 -18
- package/src/services/uitpas/searchUitpasOrganizers.ts +2 -1
- package/src/sql-filters/organizations.ts +52 -0
- package/tests/e2e/documents.test.ts +3 -1
- package/tests/vitest.global.setup.ts +1 -1
- package/tests/vitest.setup.ts +4 -2
- package/vitest.config.js +10 -0
- /package/src/seeds/{1780665427-schedule-stock-updates.ts → 1783515690-schedule-stock-updates.ts} +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Group, Organization, OrganizationRegistrationPeriod, Registration, RegistrationPeriod, V1GroupMigrationData } from '@stamhoofd/models';
|
|
3
|
+
import { GroupCategory, GroupCategorySettings, GroupPrivateSettings, GroupSettings, GroupStatus, TranslatedString } from '@stamhoofd/structures';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
13
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dryRun = false;
|
|
18
|
+
await start(dryRun);
|
|
19
|
+
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
throw new Error('Migration did not finish because of dryRun');
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// organizations with registration where periodId is the id of the platform period
|
|
26
|
+
const relevantOrganizations = new Set([
|
|
27
|
+
'453bd0b1-f238-4a41-a270-99bb13c71f46', 'c69512bc-ea0c-427a-ab90-08c3dcf1c856', '2cec1ef3-80bc-43ad-929e-4a9ddb6d6d8d', 'ede34921-efd4-446b-93c5-aa9c38b05f8a', '235b03ae-6513-4fe6-a438-217887826a26', '8df666ab-c645-47f7-a171-c305ab556ecb', '971e1bac-2668-4d7a-870c-8007c47f878f', 'e04bcda4-b8d3-4897-b1fb-e565368167fa', 'd017c264-8ad2-42e7-b46a-4ba6bb3d4ca6', 'f0edb665-1ce0-442a-88e7-5a62ea78c5fc', '45fcc4f2-c406-45fc-aaaf-9bdc06d9e68d', 'ed6e74f1-51a0-4e04-8195-e722b015eeb5', '45743bb7-539e-41a3-885d-c8cc57e73ac6', '9a635b47-2189-4fb5-983c-15a1cdf05ffd', 'a2b6a2de-5021-4533-88db-f64c8c07d89d', '97928f8a-b8d1-44cc-918f-09d0ae896c3b', '57815733-5750-492f-beb1-1efd6229803a', '0a78224a-2ec4-42d7-a5e5-e29b2732c6d7', '28f1a27e-1edd-4169-b98f-3ae0f8bb806f', 'eb6ccffa-41c5-45df-b4e4-1eb5749bc0fe', '27e06924-49a6-468c-a16e-b5735ace1894', '684c20b1-de07-40a4-8a92-5a7d30f32132', '3806a968-c55c-46e5-a674-95dd7b7f6f3f', 'd4b73261-a2d8-4f85-854e-38373012b22d',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// groups with registration where periodId is the id of the platform period
|
|
31
|
+
const relevantGroups = new Set([
|
|
32
|
+
'e6b18bcc-181a-4d7c-97c4-89ac2a846b80', '402ad45d-3cf1-49cb-88f0-3000df02fbe0', 'a768538a-942d-4adb-819a-f2915aabb205', '651c8b41-9760-43f1-ab7a-177c860dde28', '0ca82d1c-75fb-4096-91be-c9331e9e86e4', '5bf1c5e9-1557-4d33-a4b8-5040ec115ad3', '476c3373-81c1-47d2-a2ad-7f0e545a6207', 'e86b908b-3a9a-4974-b839-5c3e83ec1590', 'ceecef6a-e537-4954-96e5-c6c9904bec74', '60008a98-0cb2-45a2-8346-80ab7a6155dd', '0fd87a5d-2234-4b94-9d54-a663ba8c6a11', '83ab5758-d9a9-4675-9667-7735c6097446', '0e50b08a-93c7-4917-985e-5725687bcc26', '3dea4b34-4183-4e82-aa33-0ba8b61737f0', '1bf7ab25-bd12-4315-afc3-49be1878ad02', '6c04eb42-e9f6-4931-a862-e671be5f7495', 'a43054fb-9efb-46ac-b49b-e86fca343c10', 'c2757b10-3012-483b-abf0-c639ed55bb7c', 'adb253da-2696-45bd-9743-889eb52e953c', '387d06fc-771a-4b4b-b69a-f72827a49a51', '29b955ec-5987-4e25-8d20-1f6cfbfdd198', 'c85cff0f-3798-4d32-8067-8026ada0bb9c', 'e0b4a178-a682-4eb2-b720-bf82c80bfb34', 'c75af993-c9ae-41b5-83f3-5c30bf61c74e', 'd910d04c-90e2-4079-a997-316141f4739d', '3b81c7d9-5a4a-4323-a152-0219755f164f', '7b1767d5-fef5-4520-8748-5c2e3e1e97e5', 'f322530a-03ab-4b4a-b5c4-1a98608b12ee', '3c9b619d-c210-4fc9-950b-3a808e942ad1', '6548de8b-3b02-469f-87f5-a9e661f29ba0', '03e9c564-da0f-4a08-b843-13d830b3e061', 'f9e398df-4d26-4efb-af79-310aecde6678', '7f9310af-a48b-46c5-8a7f-d125d66ee9cf', '09c264e9-f327-4ccf-bf12-dd412cd23c1b', '765517b2-65df-446a-b9ad-7a7281f91af6', '9cea7eb7-6aac-4627-b6bb-7a117e99652c', '525f0600-aff5-47c7-9df4-6a18b0cfec2f', 'c9374388-e187-4dae-9f48-bee17fff4975', '60ff9fc5-b32f-4ee5-9532-54fcecbf00ba', 'bdbe1e9c-c25a-48f2-b506-d554d58cb02a', '47988375-8aef-45e9-8a5c-9a21a51f5c6b', '99dfd728-991f-408f-9b77-ad9f736600ab', 'a8d4845a-ecdb-4f2f-b66d-a6955e30c3db', '55d32f3b-6396-488e-98c2-1f1e9d37a7d6', '27842912-673a-42a6-9c2a-2285da36c5b4', '1667c735-bb37-4402-beda-aed88066abe3', '60943cd6-1498-456f-b540-413d7125c88f', '39a21999-4923-48ec-9118-5b98232543e5', '4952282e-dc63-4c79-b63b-ddbe3f9006fa', '887827d1-db4d-40ac-8bf4-6fc64910f13a', 'c9e4a223-cd65-4545-a322-400d84ece455', '5d0916ea-8f13-4f15-bf98-0358e527890d', '77d4eb10-e3b8-4e8e-bb06-272d085ff516', '790ae8d1-e98d-4bb2-b2cc-a77a21e8c712', '1599bb4e-9723-4a90-a36a-4605644983be', '6bb1dcad-ee5e-44e6-ab09-6ae381871566', 'd201de16-7bc7-4f98-a22b-5c335b2c872f', '090b0c1d-1481-4535-9034-219dbe4eb35c', 'b2831403-d272-46f4-9f2a-6084165c482e', 'ade7eda4-72c6-48e4-ac66-4b7c9a68f5cb', '09c3ac0f-ee98-4c11-a7fb-4601daf15ea1', '6d89b200-cf8d-4d4b-9cd7-ed65d3cd8c57', '820b1a0e-2c80-4402-94ad-c0e873d127f9', '094db6ad-40d9-4b0d-9670-74b6de99c0ca', '21b42f15-de4e-4c16-8ea0-ea83fbc3ba44', '2d2a3ab8-7bd9-4877-9122-23dc2435b73f', '7974762f-b0f3-4010-8b08-5ce1e45a14a8', '374c994e-85b9-4d22-92f4-3a094ae5fc5f', 'b81547bf-1a5f-4443-8a41-1f0e8f9e5ff4', 'a45fddb7-5dae-4330-907f-584f788bde14', '4828698d-6a8a-4937-bfe8-ca4b55d9d5a7', '2b3a9502-1943-4c00-baf2-24b8dcd191f2', '7b402e0d-219d-430e-9ebf-d2471bbb6629', 'd2626510-32f1-4677-8bbe-f3b31b529221', 'f4038ea4-cf03-4677-babc-d525f73d7378', '7e225069-3e49-486a-8187-f26947567bc6', '8d1f6366-8f28-47b9-b616-9afcc4575fc4', '17bd4f4a-203d-430d-a4b0-03aa8f9b9015', '968619a9-1483-4beb-931d-4f819cef674a', '68f958e1-491d-4f26-ad61-1598500202d8', '6848ce16-e637-4f4b-8a25-75f958654ee2', '9419df04-4181-4780-abc5-dcfc0d705c12', '9fa735ba-00de-4eac-b659-73966415f60f', '6ec3ea7e-0cbf-4b4a-9177-98c8bffd6cfe', 'c36b43c9-25c9-4820-a2e8-de256b165a33', '56589baa-e4a1-4ba0-921b-ae9f621cbf04', '8ab04173-37c9-48df-a8f1-f1ebbe7fd230', 'bb06cec4-a028-4e91-b177-53aa596baffc', '139c1164-672c-4d33-8bc9-c29893793adf', 'ecf914c4-84c8-4ac8-80df-e15283bbd270', 'bf16f791-4c05-418c-a6b2-129f43bf2567', '912c1022-065d-4bbd-a750-ba5356016ae3', '494882e1-8933-4e39-9523-180d250668dd',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
async function start(dryRun: boolean) {
|
|
36
|
+
await SeedTools.loop({
|
|
37
|
+
query: Organization.select().where('id', Array.from(relevantOrganizations)),
|
|
38
|
+
batchSize: 1,
|
|
39
|
+
useTransactionPerBatch: true,
|
|
40
|
+
action: async (organization: Organization) => {
|
|
41
|
+
if (!relevantOrganizations.has(organization.id)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const groups: Group[] = await Group.select()
|
|
46
|
+
.where('organizationId', organization.id)
|
|
47
|
+
.where('id', Array.from(relevantGroups))
|
|
48
|
+
.fetch();
|
|
49
|
+
|
|
50
|
+
for (const group of groups) {
|
|
51
|
+
if (!relevantGroups.has(group.id)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await fixRegistrations(organization, group, dryRun);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getOriginalGroupCycle(group: Group): Promise<number> {
|
|
63
|
+
const migrationData = await V1GroupMigrationData.select().where('oldGroupId', group.id).andWhere('newGroupId', group.id).first(true);
|
|
64
|
+
return migrationData.oldCycle;
|
|
65
|
+
}
|
|
66
|
+
async function getNewGroupForCycle(oldGroupId: string, cycle: number) {
|
|
67
|
+
const migrationData = await V1GroupMigrationData.select().where('oldGroupId', oldGroupId).andWhere('oldCycle', cycle).first(false);
|
|
68
|
+
if (!migrationData) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const newGroupId = migrationData.newGroupId;
|
|
73
|
+
const group = await Group.getByID(newGroupId);
|
|
74
|
+
if (!group) {
|
|
75
|
+
throw new Error('group not found');
|
|
76
|
+
}
|
|
77
|
+
return group;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fixRegistrations(organization: Organization, group: Group, dryRun: boolean) {
|
|
81
|
+
const originalGroupCycle = await getOriginalGroupCycle(group);
|
|
82
|
+
|
|
83
|
+
const registrations = await Registration.select()
|
|
84
|
+
.where('groupId', group.id)
|
|
85
|
+
// where cycle is different from group
|
|
86
|
+
.whereNot('cycle', originalGroupCycle)
|
|
87
|
+
.fetch();
|
|
88
|
+
|
|
89
|
+
const cycleMap = new Map<number, Registration[]>();
|
|
90
|
+
|
|
91
|
+
// group registrations by cycle
|
|
92
|
+
for (const registration of registrations) {
|
|
93
|
+
let registrations = cycleMap.get(registration.cycle);
|
|
94
|
+
if (!registrations) {
|
|
95
|
+
registrations = [];
|
|
96
|
+
cycleMap.set(registration.cycle, registrations);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
registrations.push(registration);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const archivePeriod = await RegistrationPeriod.select().where('organizationId', group.organizationId).where('customName', 'Gearchiveerde periodes').first(true);
|
|
103
|
+
|
|
104
|
+
const createdGroups: Group[] = [];
|
|
105
|
+
|
|
106
|
+
for (const [cycle, registrations] of cycleMap) {
|
|
107
|
+
// find new group
|
|
108
|
+
const newGroup = await getNewGroupForCycle(group.id, cycle);
|
|
109
|
+
|
|
110
|
+
if (newGroup) {
|
|
111
|
+
await migrateRegistrations({ registrations, newGroup, cycle }, dryRun);
|
|
112
|
+
} else {
|
|
113
|
+
console.log(`missing group for ${group.settings.name.toString()} cycle ${cycle}`);
|
|
114
|
+
|
|
115
|
+
const numberOfPeriodsAgo = originalGroupCycle - cycle;
|
|
116
|
+
const newGroup = createPreviousGroup({ originalGroup: group, period: archivePeriod, numberOfPeriodsAgo });
|
|
117
|
+
|
|
118
|
+
if (!dryRun) {
|
|
119
|
+
const migrationData = new V1GroupMigrationData();
|
|
120
|
+
migrationData.oldGroupId = group.id;
|
|
121
|
+
migrationData.oldCycle = cycle;
|
|
122
|
+
|
|
123
|
+
await newGroup.save();
|
|
124
|
+
migrationData.newGroupId = newGroup.id;
|
|
125
|
+
await migrationData.save();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
createdGroups.push(newGroup);
|
|
129
|
+
|
|
130
|
+
await migrateRegistrations({ registrations, newGroup, cycle }, dryRun);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (createdGroups.length > 0) {
|
|
135
|
+
const archiveOrganizationPeriod = await OrganizationRegistrationPeriod.select().where('organizationId', organization.id).where('periodId', archivePeriod.id).first(true);
|
|
136
|
+
const parentCategory = await findCategory(group, archiveOrganizationPeriod);
|
|
137
|
+
|
|
138
|
+
if (parentCategory) {
|
|
139
|
+
for (const newGroup of createdGroups) {
|
|
140
|
+
if (newGroup.id === undefined && dryRun) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!parentCategory.groupIds.includes(newGroup.id)) {
|
|
145
|
+
parentCategory.groupIds.push(newGroup.id);
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error('group already in category:' + newGroup.id);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await cleanupGroup(newGroup, dryRun);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const { path, organizationPeriod: currentOrganizationPeriod } = await findPath(group);
|
|
154
|
+
const allArchivedCategories = archiveOrganizationPeriod.settings.categories;
|
|
155
|
+
|
|
156
|
+
const archivedRoot = archiveOrganizationPeriod.settings.rootCategory;
|
|
157
|
+
if (!archivedRoot) {
|
|
158
|
+
throw new Error('archived root category not found');
|
|
159
|
+
}
|
|
160
|
+
let thisLayer = getChildCategories(archivedRoot, allArchivedCategories);
|
|
161
|
+
const equalArchivedPath: GroupCategory[] = [archiveOrganizationPeriod.settings.rootCategory!];
|
|
162
|
+
|
|
163
|
+
for (const currentPeriodCategory of path) {
|
|
164
|
+
if (currentPeriodCategory === currentOrganizationPeriod.settings.rootCategory) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// only exact match for last category is needed
|
|
169
|
+
const exactMatch = currentPeriodCategory !== path[path.length - 1];
|
|
170
|
+
let equalArchivedCategories = thisLayer.filter(archivedCategory => isEqualCategory(currentPeriodCategory, archivedCategory, exactMatch));
|
|
171
|
+
|
|
172
|
+
if (equalArchivedCategories.length > 1) {
|
|
173
|
+
const exactMatches = equalArchivedCategories.filter(archivedCategory => isEqualCategory(currentPeriodCategory, archivedCategory, true));
|
|
174
|
+
|
|
175
|
+
if (exactMatches.length === 0) {
|
|
176
|
+
throw new Error(`Found multiple equal categories but no exact match (${currentPeriodCategory.settings.name}): ` + equalArchivedCategories.map(c => c.settings.name).join(', '));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (exactMatches.length > 1) {
|
|
180
|
+
equalArchivedCategories = [await getCategoryWhereGroupHasSameOriginalGroup(exactMatches, group)];
|
|
181
|
+
} else {
|
|
182
|
+
equalArchivedCategories = exactMatches;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (equalArchivedCategories.length === 0) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const equalArchivedCategory = equalArchivedCategories[0];
|
|
191
|
+
thisLayer = getChildCategories(equalArchivedCategory, allArchivedCategories);
|
|
192
|
+
equalArchivedPath.push(equalArchivedCategory);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// logging
|
|
196
|
+
console.log('organization: ', organization.name);
|
|
197
|
+
console.log('group: ', group.settings.name.toString());
|
|
198
|
+
logPath(path, 'current');
|
|
199
|
+
logPath(equalArchivedPath, 'archived');
|
|
200
|
+
|
|
201
|
+
if (path.length === equalArchivedPath.length) {
|
|
202
|
+
// complete match -> add group to last category
|
|
203
|
+
const lastCategory = equalArchivedPath[equalArchivedPath.length - 1];
|
|
204
|
+
|
|
205
|
+
if (lastCategory.groupIds.length) {
|
|
206
|
+
// add created groups to last category
|
|
207
|
+
for (const createdGroup of createdGroups) {
|
|
208
|
+
if (!lastCategory.groupIds.includes(createdGroup.id)) {
|
|
209
|
+
lastCategory.groupIds.push(createdGroup.id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// create new category
|
|
214
|
+
const newCategory = createCategoryForOriginalGroup(group, createdGroups.map(g => g.id));
|
|
215
|
+
lastCategory.categoryIds.push(newCategory.id);
|
|
216
|
+
archiveOrganizationPeriod.settings.categories.push(newCategory);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// should not happen
|
|
220
|
+
throw new Error('no complete match');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!dryRun) {
|
|
225
|
+
await archiveOrganizationPeriod.save();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!dryRun) {
|
|
230
|
+
await group.updateOccupancy();
|
|
231
|
+
await group.save();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function getCategoryWhereGroupHasSameOriginalGroup(exactMatches: GroupCategory[], originalGroup: Group) {
|
|
236
|
+
for (const category of exactMatches) {
|
|
237
|
+
const migrationData = await V1GroupMigrationData.select().where('newGroupId', category.groupIds).andWhere('oldGroupId', originalGroup.id).first(false);
|
|
238
|
+
if (migrationData) {
|
|
239
|
+
return category;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
throw new Error('no category found');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createCategoryForOriginalGroup(originalGroup: Group, groupIds: string[]) {
|
|
247
|
+
return GroupCategory.create({
|
|
248
|
+
settings: GroupCategorySettings.create({
|
|
249
|
+
name: originalGroup.settings.name.toString(),
|
|
250
|
+
public: false,
|
|
251
|
+
maximumRegistrations: null,
|
|
252
|
+
}),
|
|
253
|
+
groupIds,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getChildCategories(category: GroupCategory, allCategories: GroupCategory[]) {
|
|
258
|
+
const results: GroupCategory[] = [];
|
|
259
|
+
|
|
260
|
+
for (const childId of category.categoryIds) {
|
|
261
|
+
const child = allCategories.find(c => c.id === childId);
|
|
262
|
+
if (!child) {
|
|
263
|
+
throw new Error('child category not found');
|
|
264
|
+
}
|
|
265
|
+
results.push(child);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function logPath(path: GroupCategory[], prefix: string): void {
|
|
272
|
+
console.log(`${prefix} path: ${path.map(c => c.settings.name).join(' > ')}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isEqualCategory(currentPeriodCategory: GroupCategory, archivedPeriodCategory: GroupCategory, exactMatch: boolean) {
|
|
276
|
+
if (exactMatch) {
|
|
277
|
+
return archivedPeriodCategory.settings.name === currentPeriodCategory.settings.name;
|
|
278
|
+
}
|
|
279
|
+
return archivedPeriodCategory.settings.name.startsWith(currentPeriodCategory.settings.name);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Find the path from the old group to the root category.
|
|
284
|
+
*/
|
|
285
|
+
async function findPath(oldGroup: Group): Promise<{ path: GroupCategory[]; organizationPeriod: OrganizationRegistrationPeriod }> {
|
|
286
|
+
/**
|
|
287
|
+
* Returns all the parent categories of the group starting with the root category (= start) or null if not found.
|
|
288
|
+
*/
|
|
289
|
+
function getPathToGroup(groupId: string, start: GroupCategory, allCategories: GroupCategory[]): GroupCategory[] | null {
|
|
290
|
+
if (start.groupIds.includes(groupId)) {
|
|
291
|
+
return [start];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const childCategoryId of start.categoryIds) {
|
|
295
|
+
const childCategory = allCategories.find(c => c.id === childCategoryId);
|
|
296
|
+
if (childCategory) {
|
|
297
|
+
const childPath = getPathToGroup(groupId, childCategory, allCategories);
|
|
298
|
+
if (childPath !== null) {
|
|
299
|
+
return [start, ...childPath];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const organizationPeriod = await OrganizationRegistrationPeriod.select()
|
|
308
|
+
.where('organizationId', oldGroup.organizationId)
|
|
309
|
+
.where('periodId', oldGroup.periodId)
|
|
310
|
+
.first(true);
|
|
311
|
+
|
|
312
|
+
const root = organizationPeriod.settings.rootCategory;
|
|
313
|
+
if (!root) {
|
|
314
|
+
throw new Error('root category not found');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const path = getPathToGroup(oldGroup.id, root, organizationPeriod.settings.categories);
|
|
318
|
+
if (path === null) {
|
|
319
|
+
throw new Error('path not found');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
organizationPeriod,
|
|
324
|
+
path,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function cleanupGroup(group: Group, dryRun: boolean) {
|
|
329
|
+
group.settings.cycleSettings = new Map();
|
|
330
|
+
group.settings.preventPreviousGroupIds = [];
|
|
331
|
+
group.settings.requirePreviousGroupIds = [];
|
|
332
|
+
group.cycle = cycleIfMigrated;
|
|
333
|
+
if (group.status === GroupStatus.Archived) {
|
|
334
|
+
group.status = GroupStatus.Closed;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!dryRun) {
|
|
338
|
+
await group.updateOccupancy();
|
|
339
|
+
await group.save();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function migrateRegistrations({ registrations, newGroup, cycle }: { registrations: Registration[]; newGroup: Group; cycle: number }, dryRun: boolean) {
|
|
344
|
+
for (const registration of registrations) {
|
|
345
|
+
if (registration.waitingList) {
|
|
346
|
+
throw new Error('not supported');
|
|
347
|
+
} else {
|
|
348
|
+
registration.groupId = newGroup.id;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
registration.periodId = newGroup.periodId;
|
|
352
|
+
registration.cycle = cycle;
|
|
353
|
+
|
|
354
|
+
if (!dryRun) {
|
|
355
|
+
await registration.save();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function findCategory(group: Group, archiveOrganizationPeriod: OrganizationRegistrationPeriod) {
|
|
361
|
+
const previousGroups = await V1GroupMigrationData.select().where('oldGroupId', group.id).andWhereNot('newGroupId', group.id).fetch();
|
|
362
|
+
|
|
363
|
+
for (const previousGroup of previousGroups) {
|
|
364
|
+
const previousGroupId = previousGroup.newGroupId;
|
|
365
|
+
if (previousGroup.oldGroupId !== group.id || previousGroup.newGroupId === group.id) {
|
|
366
|
+
throw new Error('group id mismatch');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const allCategories = archiveOrganizationPeriod.settings.categories;
|
|
370
|
+
const rootCategory = archiveOrganizationPeriod.settings.rootCategory;
|
|
371
|
+
if (!rootCategory) {
|
|
372
|
+
throw new Error('root category not found for archiveOrganizationPeriod: ' + archiveOrganizationPeriod.id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const parentCategory = allCategories.find(c => c.groupIds.includes(previousGroupId));
|
|
376
|
+
if (parentCategory) {
|
|
377
|
+
return parentCategory;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const cycleIfMigrated = -99;
|
|
385
|
+
|
|
386
|
+
function createPreviousGroup({ originalGroup, period, numberOfPeriodsAgo }: { originalGroup: Group; period: RegistrationPeriod; numberOfPeriodsAgo: number }) {
|
|
387
|
+
const newGroup = new Group();
|
|
388
|
+
newGroup.organizationId = originalGroup.organizationId;
|
|
389
|
+
newGroup.periodId = period.id;
|
|
390
|
+
newGroup.status = originalGroup.status;
|
|
391
|
+
newGroup.createdAt = originalGroup.createdAt;
|
|
392
|
+
newGroup.deletedAt = originalGroup.deletedAt;
|
|
393
|
+
|
|
394
|
+
const originalPrivateSettings: GroupPrivateSettings = originalGroup.privateSettings;
|
|
395
|
+
|
|
396
|
+
newGroup.privateSettings = GroupPrivateSettings.create({
|
|
397
|
+
...originalPrivateSettings,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
newGroup.cycle = cycleIfMigrated;
|
|
401
|
+
|
|
402
|
+
const originalSettings: GroupSettings = originalGroup.settings;
|
|
403
|
+
|
|
404
|
+
const periodStartDate: Date = period.startDate;
|
|
405
|
+
const periodEndDate: Date = period.endDate;
|
|
406
|
+
|
|
407
|
+
const startDate = new Date(periodStartDate);
|
|
408
|
+
const endDate = new Date(periodEndDate);
|
|
409
|
+
|
|
410
|
+
const isPlural = numberOfPeriodsAgo > 1;
|
|
411
|
+
const extraName = isPlural ? `${numberOfPeriodsAgo} periodes geleden` : `${numberOfPeriodsAgo} periode geleden`;
|
|
412
|
+
|
|
413
|
+
newGroup.settings = GroupSettings
|
|
414
|
+
.create({
|
|
415
|
+
...originalSettings,
|
|
416
|
+
cycleSettings: new Map(),
|
|
417
|
+
name: new TranslatedString(`${originalSettings.name.toString()} (${extraName})`),
|
|
418
|
+
startDate,
|
|
419
|
+
endDate,
|
|
420
|
+
hasCustomDates: true,
|
|
421
|
+
period: period.getBaseStructure(),
|
|
422
|
+
});
|
|
423
|
+
newGroup.type = originalGroup.type;
|
|
424
|
+
|
|
425
|
+
return newGroup;
|
|
426
|
+
}
|
|
@@ -297,7 +297,7 @@ export class UitpasService {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
static async registerAttendance() {
|
|
300
|
-
// https://
|
|
300
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/create-a-checkin
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
static async searchUitpasEvents(organizationId: string, uitpasOrganizerId: string, textQuery?: string) {
|
|
@@ -2,7 +2,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
2
2
|
|
|
3
3
|
async function cancelTicketSale(access_token: string, ticketSaleId: string) {
|
|
4
4
|
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/delete-a-ticket-sale
|
|
5
|
-
const url =
|
|
5
|
+
const url = `${STAMHOOFD.UITPAS_API_URL}/ticket-sales/${ticketSaleId}`;
|
|
6
6
|
const myHeaders = new Headers();
|
|
7
7
|
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
8
8
|
myHeaders.append('Accept', 'application/json');
|
|
@@ -42,7 +42,7 @@ function assertIsPermissionsResponse(json: unknown): asserts json is Permissions
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export async function checkPermissionsFor(access_token: string, organizationId: string | null, uitpasOrganizerId: string) {
|
|
45
|
-
const url =
|
|
45
|
+
const url = `${STAMHOOFD.UITPAS_API_URL}/permissions`;
|
|
46
46
|
const myHeaders = new Headers();
|
|
47
47
|
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
48
48
|
myHeaders.append('Accept', 'application/json');
|
|
@@ -27,7 +27,7 @@ function assertsIsStaticSocialTariffResponse(json: unknown): asserts json is Sta
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function getSocialTariffForEvent(access_token: string, basePrice: number, uitpasEventUrl: string) {
|
|
30
|
-
const baseUrl =
|
|
30
|
+
const baseUrl = `${STAMHOOFD.UITPAS_API_URL}/tariffs/static`;
|
|
31
31
|
const params = new URLSearchParams();
|
|
32
32
|
params.append('regularPrice', (basePrice / 10_000).toFixed(2));
|
|
33
33
|
const eventId = uitpasEventUrl.split('/').pop(); // Extract the event ID from the URL
|
|
@@ -52,7 +52,7 @@ function isSocialTariffErrorResponse(
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber: string, basePrice: number, uitpasEventUrl: string) {
|
|
55
|
-
const baseUrl =
|
|
55
|
+
const baseUrl = `${STAMHOOFD.UITPAS_API_URL}/tariffs`;
|
|
56
56
|
const params = new URLSearchParams();
|
|
57
57
|
params.append('regularPrice', (basePrice / 1_0000).toFixed(2));
|
|
58
58
|
const eventId = uitpasEventUrl.split('/').pop();
|
|
@@ -62,7 +62,7 @@ export async function registerTicketSales(access_token: string, registerTicketSa
|
|
|
62
62
|
if (registerTicketSaleRequests.length === 0) {
|
|
63
63
|
return new Map();
|
|
64
64
|
};
|
|
65
|
-
const url =
|
|
65
|
+
const url = `${STAMHOOFD.UITPAS_API_URL}/ticket-sales`;
|
|
66
66
|
const body = registerTicketSaleRequests.map((ticketSale) => {
|
|
67
67
|
const eventId = ticketSale.uitpasEventUrl.split('/').pop();
|
|
68
68
|
if (!eventId) {
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { UitpasEventResponse, UitpasEventsResponse } from '@stamhoofd/structures';
|
|
2
2
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
|
|
4
|
+
/* Pick a translated string, prefer 'nl' */
|
|
5
|
+
function pickTranslation(value: unknown): string {
|
|
6
|
+
if (typeof value !== 'object' || value === null) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
const entries = Object.entries(value as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === 'string');
|
|
10
|
+
if (entries.length === 0) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
const nl = entries.find(([key]) => key === 'nl');
|
|
14
|
+
return nl ? nl[1] : entries[0][1];
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
type EventsResponse = {
|
|
5
18
|
totalItems: number;
|
|
6
19
|
itemsPerPage: number;
|
|
7
20
|
member: Array<{
|
|
8
21
|
'@id': string;
|
|
9
|
-
'name':
|
|
10
|
-
nl: string;
|
|
11
|
-
};
|
|
22
|
+
'name': Record<string, string>;
|
|
12
23
|
'location': {
|
|
13
|
-
name:
|
|
24
|
+
name: Record<string, string>;
|
|
14
25
|
};
|
|
15
26
|
'startDate'?: string;
|
|
16
27
|
'endDate'?: string;
|
|
@@ -33,10 +44,7 @@ function assertIsEventsResponse(json: unknown): asserts json is EventsResponse {
|
|
|
33
44
|
|| !('@id' in member)
|
|
34
45
|
|| typeof member['@id'] !== 'string'
|
|
35
46
|
|| !('name' in member)
|
|
36
|
-
||
|
|
37
|
-
|| member.name === null
|
|
38
|
-
|| !('nl' in member.name)
|
|
39
|
-
|| typeof member.name.nl !== 'string'
|
|
47
|
+
|| pickTranslation(member.name) === ''
|
|
40
48
|
|| !('location' in member)
|
|
41
49
|
|| typeof member.location !== 'object'
|
|
42
50
|
|| member.location === null
|
|
@@ -111,6 +119,10 @@ export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: st
|
|
|
111
119
|
});
|
|
112
120
|
});
|
|
113
121
|
|
|
122
|
+
return parseUitpasEventsResponse(json);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseUitpasEventsResponse(json: unknown): UitpasEventsResponse {
|
|
114
126
|
assertIsEventsResponse(json);
|
|
115
127
|
const eventsResponse = new UitpasEventsResponse();
|
|
116
128
|
eventsResponse.totalItems = json.totalItems;
|
|
@@ -118,16 +130,8 @@ export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: st
|
|
|
118
130
|
eventsResponse.member = json.member.map((member) => {
|
|
119
131
|
const event = new UitpasEventResponse();
|
|
120
132
|
event.url = member['@id'];
|
|
121
|
-
event.name = member.name
|
|
122
|
-
|
|
123
|
-
const entrs = Object.entries(locationName);
|
|
124
|
-
const hasNl = entrs.find(([key]) => key === 'nl');
|
|
125
|
-
if (hasNl) {
|
|
126
|
-
event.location = locationName.nl;
|
|
127
|
-
} else {
|
|
128
|
-
const lang = entrs[0];
|
|
129
|
-
event.location = lang ? lang[1] : '';
|
|
130
|
-
}
|
|
133
|
+
event.name = pickTranslation(member.name);
|
|
134
|
+
event.location = pickTranslation(member.location.name);
|
|
131
135
|
if (member.startDate) {
|
|
132
136
|
event.startDate = new Date(member.startDate);
|
|
133
137
|
}
|
|
@@ -40,7 +40,7 @@ export async function searchUitpasOrganizers(access_token: string, name: string)
|
|
|
40
40
|
human: $t(`%1Bb`),
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
const baseUrl =
|
|
43
|
+
const baseUrl = `${STAMHOOFD.UITPAS_API_URL}/organizers`;
|
|
44
44
|
const params = new URLSearchParams();
|
|
45
45
|
params.append('name', name);
|
|
46
46
|
params.append('limit', '200');
|
|
@@ -63,6 +63,7 @@ export async function searchUitpasOrganizers(access_token: string, name: string)
|
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
if (!response.ok) {
|
|
66
|
+
console.log(response.status, response.statusText, await response.text());
|
|
66
67
|
throw new SimpleError({
|
|
67
68
|
code: 'unsuccessful_response_searching_uitpas_organizers',
|
|
68
69
|
message: `Unsuccessful response when searching for UiTPAS organizers`,
|
|
@@ -69,6 +69,58 @@ export const organizationFilterCompilers: SQLFilterDefinitions = {
|
|
|
69
69
|
type: SQLValueType.JSONArray,
|
|
70
70
|
nullable: false,
|
|
71
71
|
}),
|
|
72
|
+
recordCategoryName: createColumnFilter({
|
|
73
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'meta'), '$.value.recordsConfiguration.recordCategories[*].name'),
|
|
74
|
+
type: SQLValueType.JSONArray,
|
|
75
|
+
nullable: true,
|
|
76
|
+
}),
|
|
77
|
+
// Name of a child (sub)category in any record category, at any nesting depth
|
|
78
|
+
recordChildCategoryName: createColumnFilter({
|
|
79
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'meta'), '$.value.recordsConfiguration.recordCategories**.childCategories[*].name'),
|
|
80
|
+
type: SQLValueType.JSONArray,
|
|
81
|
+
nullable: true,
|
|
82
|
+
}),
|
|
83
|
+
recordName: createColumnFilter({
|
|
84
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'meta'), '$.value.recordsConfiguration.recordCategories**.records[*].name'),
|
|
85
|
+
type: SQLValueType.JSONArray,
|
|
86
|
+
nullable: true,
|
|
87
|
+
}),
|
|
88
|
+
documentTemplates: createExistsFilter(
|
|
89
|
+
SQL.select()
|
|
90
|
+
.from(SQL.table('document_templates'))
|
|
91
|
+
.where(
|
|
92
|
+
SQL.column('document_templates', 'organizationId'),
|
|
93
|
+
SQL.column('organizations', 'id'),
|
|
94
|
+
),
|
|
95
|
+
{
|
|
96
|
+
...baseSQLFilterCompilers,
|
|
97
|
+
type: createColumnFilter({
|
|
98
|
+
expression: SQL.jsonExtract(SQL.column('document_templates', 'privateSettings'), '$.value.templateDefinition.type'),
|
|
99
|
+
type: SQLValueType.JSONString,
|
|
100
|
+
nullable: true,
|
|
101
|
+
}),
|
|
102
|
+
year: createColumnFilter({
|
|
103
|
+
expression: SQL.column('document_templates', 'year'),
|
|
104
|
+
type: SQLValueType.Number,
|
|
105
|
+
nullable: false,
|
|
106
|
+
}),
|
|
107
|
+
status: createColumnFilter({
|
|
108
|
+
expression: SQL.column('document_templates', 'status'),
|
|
109
|
+
type: SQLValueType.String,
|
|
110
|
+
nullable: false,
|
|
111
|
+
}),
|
|
112
|
+
isLocked: createColumnFilter({
|
|
113
|
+
expression: SQL.column('document_templates', 'isLocked'),
|
|
114
|
+
type: SQLValueType.Boolean,
|
|
115
|
+
nullable: false,
|
|
116
|
+
}),
|
|
117
|
+
updatesEnabled: createColumnFilter({
|
|
118
|
+
expression: SQL.column('document_templates', 'updatesEnabled'),
|
|
119
|
+
type: SQLValueType.Boolean,
|
|
120
|
+
nullable: false,
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
),
|
|
72
124
|
setupSteps: createExistsFilter(
|
|
73
125
|
SQL.select()
|
|
74
126
|
.from(SQL.table('organization_registration_periods'))
|
|
@@ -301,13 +301,15 @@ describe('E2E.Documents', () => {
|
|
|
301
301
|
await assertNoDocument(registration);
|
|
302
302
|
|
|
303
303
|
// Change age as user (different endpoint, but should also have the same side effect)
|
|
304
|
+
// A user may only make a small birth date correction, so we only change the year (the
|
|
305
|
+
// member was born on 2000-01-01 above) to make them young again.
|
|
304
306
|
await patchUserMember({
|
|
305
307
|
organization,
|
|
306
308
|
user,
|
|
307
309
|
patch: MemberWithRegistrationsBlob.patch({
|
|
308
310
|
id: member.id,
|
|
309
311
|
details: MemberDetails.patch({
|
|
310
|
-
birthDay: new Date(),
|
|
312
|
+
birthDay: new Date(new Date().getFullYear(), 0, 1),
|
|
311
313
|
}),
|
|
312
314
|
}),
|
|
313
315
|
});
|
|
@@ -19,7 +19,7 @@ export async function setup() {
|
|
|
19
19
|
|
|
20
20
|
const { Database } = await import('@simonbackx/simple-database');
|
|
21
21
|
|
|
22
|
-
const { run } = await import('../
|
|
22
|
+
const { run } = await import('../src/migrate.js');
|
|
23
23
|
await run();
|
|
24
24
|
|
|
25
25
|
await Database.end();
|