@stamhoofd/backend 2.130.0 → 2.132.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/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 +43 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +31 -2
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +1 -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 +2 -2
- package/src/helpers/CheckSettlements.test.ts +190 -0
- package/src/helpers/CheckSettlements.ts +73 -45
- 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/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/searchUitpasOrganizers.ts +2 -1
- package/tests/e2e/documents.test.ts +3 -1
- package/tests/helpers/MollieMocker.ts +73 -0
- 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) {
|
|
@@ -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`,
|
|
@@ -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
|
});
|
|
@@ -53,6 +53,22 @@ export type MollieMockRefund = {
|
|
|
53
53
|
metadata: Record<string, unknown> | null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
export type MollieMockSettlement = {
|
|
57
|
+
id: string;
|
|
58
|
+
reference: string;
|
|
59
|
+
status: 'open' | 'pending' | 'paidout' | 'failed';
|
|
60
|
+
amount: { currency: string; value: string };
|
|
61
|
+
createdAt: string;
|
|
62
|
+
/** null for the still-open settlement, a date once it has been paid out */
|
|
63
|
+
settledAt: string | null;
|
|
64
|
+
/** Mollie payment ids (tr_...) settled in this settlement */
|
|
65
|
+
paymentIds: string[];
|
|
66
|
+
/** Mollie refund ids (re_...) settled in this settlement */
|
|
67
|
+
refundIds: string[];
|
|
68
|
+
/** Mollie chargeback ids (chb_...) settled in this settlement */
|
|
69
|
+
chargebackIds: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
56
72
|
const MOLLIE_CHECKOUT_URL = 'https://molliecheckout/';
|
|
57
73
|
|
|
58
74
|
/**
|
|
@@ -76,6 +92,7 @@ export class MollieMocker {
|
|
|
76
92
|
mandates: MollieMockMandate[] = [];
|
|
77
93
|
chargebacks: MollieMockChargeback[] = [];
|
|
78
94
|
refunds: MollieMockRefund[] = [];
|
|
95
|
+
settlements: MollieMockSettlement[] = [];
|
|
79
96
|
|
|
80
97
|
#forceFailure = false;
|
|
81
98
|
|
|
@@ -85,6 +102,7 @@ export class MollieMocker {
|
|
|
85
102
|
this.mandates = [];
|
|
86
103
|
this.chargebacks = [];
|
|
87
104
|
this.refunds = [];
|
|
105
|
+
this.settlements = [];
|
|
88
106
|
this.#forceFailure = false;
|
|
89
107
|
}
|
|
90
108
|
|
|
@@ -206,6 +224,26 @@ export class MollieMocker {
|
|
|
206
224
|
return this.#listResource('chargebacks', this.chargebacks.map(c => this.#chargebackResource(c)));
|
|
207
225
|
}
|
|
208
226
|
|
|
227
|
+
// settlements + nested payments/refunds (drives the settlements cron)
|
|
228
|
+
if (parts[0] === 'settlements' && method === 'GET') {
|
|
229
|
+
if (parts.length === 1) {
|
|
230
|
+
return this.#listResource('settlements', this.settlements.map(s => this.#settlementResource(s)));
|
|
231
|
+
}
|
|
232
|
+
const settlement = this.settlements.find(s => s.id === parts[1]);
|
|
233
|
+
if (settlement && parts.length === 3 && parts[2] === 'payments') {
|
|
234
|
+
const items = this.payments.filter(p => settlement.paymentIds.includes(p.id));
|
|
235
|
+
return this.#listResource('payments', items.map(p => this.#paymentResource(p)));
|
|
236
|
+
}
|
|
237
|
+
if (settlement && parts.length === 3 && parts[2] === 'refunds') {
|
|
238
|
+
const items = this.refunds.filter(r => settlement.refundIds.includes(r.id));
|
|
239
|
+
return this.#listResource('refunds', items.map(r => this.#refundResource(r)));
|
|
240
|
+
}
|
|
241
|
+
if (settlement && parts.length === 3 && parts[2] === 'chargebacks') {
|
|
242
|
+
const items = this.chargebacks.filter(c => settlement.chargebackIds.includes(c.id));
|
|
243
|
+
return this.#listResource('chargebacks', items.map(c => this.#chargebackResource(c)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
209
247
|
console.error('MollieMocker: unhandled request', method, uri);
|
|
210
248
|
return [500, { detail: 'MollieMocker: unhandled request ' + method + ' ' + uri }];
|
|
211
249
|
}
|
|
@@ -512,6 +550,41 @@ export class MollieMocker {
|
|
|
512
550
|
};
|
|
513
551
|
}
|
|
514
552
|
|
|
553
|
+
// ---- Settlements ------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Register a settled (paid out) settlement that groups the given payments and refunds.
|
|
557
|
+
* Used to drive the settlements cron (CheckSettlements).
|
|
558
|
+
*/
|
|
559
|
+
createSettlement(options: { payments?: MollieMockPayment[]; refunds?: MollieMockRefund[]; chargebacks?: MollieMockChargeback[]; value?: string; settledAt?: Date } = {}): MollieMockSettlement {
|
|
560
|
+
const settlement: MollieMockSettlement = {
|
|
561
|
+
id: this.createId('stl'),
|
|
562
|
+
reference: '1234567.' + (this.settlements.length + 1).toString().padStart(4, '0') + '.01',
|
|
563
|
+
status: 'paidout',
|
|
564
|
+
amount: { currency: 'EUR', value: options.value ?? '0.00' },
|
|
565
|
+
createdAt: new Date().toISOString(),
|
|
566
|
+
settledAt: (options.settledAt ?? new Date()).toISOString(),
|
|
567
|
+
paymentIds: (options.payments ?? []).map(p => p.id),
|
|
568
|
+
refundIds: (options.refunds ?? []).map(r => r.id),
|
|
569
|
+
chargebackIds: (options.chargebacks ?? []).map(c => c.id),
|
|
570
|
+
};
|
|
571
|
+
this.settlements.push(settlement);
|
|
572
|
+
return settlement;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#settlementResource(settlement: MollieMockSettlement) {
|
|
576
|
+
return {
|
|
577
|
+
resource: 'settlement',
|
|
578
|
+
id: settlement.id,
|
|
579
|
+
reference: settlement.reference,
|
|
580
|
+
status: settlement.status,
|
|
581
|
+
amount: settlement.amount,
|
|
582
|
+
createdAt: settlement.createdAt,
|
|
583
|
+
settledAt: settlement.settledAt ?? null,
|
|
584
|
+
_links: { self: { href: 'https://api.mollie.com/v2/settlements/' + settlement.id, type: 'application/hal+json' } },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
515
588
|
// ---- List helper ------------------------------------------------------
|
|
516
589
|
|
|
517
590
|
#listResource(binderName: string, items: unknown[]): [number, unknown] {
|
|
@@ -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();
|
package/tests/vitest.setup.ts
CHANGED
|
@@ -60,11 +60,13 @@ beforeAll(async () => {
|
|
|
60
60
|
// cascade below, so it has to be cleared first
|
|
61
61
|
await Database.delete('DELETE FROM `invoiced_balance_items`');
|
|
62
62
|
await Database.delete('DELETE FROM `invoices`');
|
|
63
|
+
|
|
64
|
+
// payments restrict deleting stripe accounts (which cascade from organizations), and a payment
|
|
65
|
+
// can reference a stripe account of a different organization, so payments have to be cleared first
|
|
66
|
+
await Database.delete('DELETE FROM `payments`');
|
|
63
67
|
await Database.update('UPDATE registration_periods set organizationId = null, customName = ? where organizationId is not null', ['delete']);
|
|
64
68
|
await Database.delete('DELETE FROM `organizations`');
|
|
65
69
|
await Database.delete('DELETE FROM `registration_periods` where customName = ?', ['delete']);
|
|
66
|
-
|
|
67
|
-
await Database.delete('DELETE FROM `payments`');
|
|
68
70
|
await Database.delete('OPTIMIZE TABLE organizations;'); // fix breaking of indexes due to deletes (mysql bug?)
|
|
69
71
|
|
|
70
72
|
// Use random file keys in tests
|