@stamhoofd/backend 2.90.3 → 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/audit-logs/ModelLogger.ts +0 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +20 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +30 -7
- 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/global/members/PatchOrganizationMembersEndpoint.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -7
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +112 -105
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -10
- package/src/helpers/FlagMomentCleanup.ts +13 -1
- package/src/helpers/GroupedThrottledQueue.ts +5 -3
- package/src/helpers/PeriodHelper.ts +10 -137
- package/src/helpers/SetupStepUpdater.ts +54 -7
- package/src/helpers/UitpasTokenRepository.ts +3 -3
- package/src/seeds/1750090030-records-configuration.ts +5 -1
- 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/BalanceItemService.ts +12 -7
- package/src/services/DocumentService.ts +0 -1
- package/src/services/RegistrationService.ts +30 -1
- package/src/services/uitpas/UitpasService.ts +72 -3
- package/src/services/uitpas/cancelTicketSales.ts +1 -1
- package/src/services/uitpas/checkPermissionsFor.ts +9 -9
- package/src/services/uitpas/checkUitpasNumbers.ts +3 -2
- package/src/services/uitpas/getSocialTariffForEvent.ts +4 -4
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +5 -5
- package/src/services/uitpas/registerTicketSales.ts +4 -4
- package/src/services/uitpas/searchUitpasEvents.ts +3 -3
- package/src/services/uitpas/searchUitpasOrganizers.ts +3 -3
- package/src/sql-filters/emails.ts +65 -0
- package/src/sql-filters/members.ts +1 -1
- package/src/sql-filters/organizations.ts +52 -0
- package/src/sql-sorters/emails.ts +47 -0
- package/tests/e2e/register.test.ts +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Migration, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
|
+
import { Member, mergeTwoMembers } from '@stamhoofd/models';
|
|
3
|
+
import { SQL, SQLSelect } from '@stamhoofd/sql';
|
|
4
|
+
import { PatchOrganizationMembersEndpoint } from '../endpoints/global/members/PatchOrganizationMembersEndpoint';
|
|
5
|
+
|
|
6
|
+
type MergeType = {
|
|
7
|
+
a: {
|
|
8
|
+
id: string;
|
|
9
|
+
firstName: string;
|
|
10
|
+
lastName: string;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
};
|
|
13
|
+
b: {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default new Migration(async () => {
|
|
20
|
+
if (STAMHOOFD.environment === 'test') {
|
|
21
|
+
console.log('skipped in tests');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const q = new SQLSelect(
|
|
26
|
+
(row: SQLResultNamespacedRow): MergeType => {
|
|
27
|
+
return {
|
|
28
|
+
a: {
|
|
29
|
+
id: row['a'].id as string,
|
|
30
|
+
firstName: row['a'].firstName as string,
|
|
31
|
+
lastName: row['a'].lastName as string,
|
|
32
|
+
createdAt: row['a'].createdAt as Date,
|
|
33
|
+
},
|
|
34
|
+
b: {
|
|
35
|
+
id: row['b'].id as string,
|
|
36
|
+
createdAt: row['b'].createdAt as Date,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
SQL.wildcard('a'),
|
|
41
|
+
SQL.wildcard('b'),
|
|
42
|
+
)
|
|
43
|
+
.from(Member.table, 'a')
|
|
44
|
+
.join(
|
|
45
|
+
SQL.join(Member.table, 'b')
|
|
46
|
+
.where(
|
|
47
|
+
SQL.column('b', 'id'),
|
|
48
|
+
'!=',
|
|
49
|
+
SQL.column('a', 'id'),
|
|
50
|
+
)
|
|
51
|
+
.andWhere(
|
|
52
|
+
SQL.column('b', 'firstName'),
|
|
53
|
+
SQL.column('a', 'firstName'),
|
|
54
|
+
)
|
|
55
|
+
.andWhere(
|
|
56
|
+
SQL.column('b', 'lastName'),
|
|
57
|
+
SQL.column('a', 'lastName'),
|
|
58
|
+
)
|
|
59
|
+
.andWhere(
|
|
60
|
+
SQL.column('b', 'birthDay'),
|
|
61
|
+
SQL.column('a', 'birthDay'),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
.where(
|
|
65
|
+
SQL.where(
|
|
66
|
+
SQL.column('a', 'createdAt'),
|
|
67
|
+
'<',
|
|
68
|
+
SQL.column('b', 'createdAt'),
|
|
69
|
+
).or(
|
|
70
|
+
SQL.where(
|
|
71
|
+
SQL.column('b', 'createdAt'),
|
|
72
|
+
SQL.column('a', 'createdAt'),
|
|
73
|
+
).and(
|
|
74
|
+
SQL.column('a', 'id'),
|
|
75
|
+
'<',
|
|
76
|
+
SQL.column('b', 'id'),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
.orderBy(
|
|
81
|
+
SQL.column('a', 'createdAt'),
|
|
82
|
+
'ASC',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (STAMHOOFD.userMode === 'organization') {
|
|
86
|
+
q.where(
|
|
87
|
+
SQL.column('b', 'organizationId'),
|
|
88
|
+
SQL.column('organizationId'),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const duplicates = await q.fetch();
|
|
93
|
+
|
|
94
|
+
const deletedSet = new Set<string>();
|
|
95
|
+
const mergedIntoSet = new Set<string>();
|
|
96
|
+
|
|
97
|
+
for (const duplicate of duplicates) {
|
|
98
|
+
if (mergedIntoSet.has(duplicate.b.id)) {
|
|
99
|
+
console.log('Found chained duplicate in wrong order', duplicate.a.id, 'and', duplicate.b.id);
|
|
100
|
+
continue; // Already merged this one
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (deletedSet.has(duplicate.b.id)) {
|
|
104
|
+
continue; // Already deleted this one
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (deletedSet.has(duplicate.a.id)) {
|
|
108
|
+
console.log('Skipping duplicate', duplicate.a.id, 'because it was already deleted');
|
|
109
|
+
continue; // Already deleted this one
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
'Found duplicate member',
|
|
114
|
+
duplicate.a.id,
|
|
115
|
+
duplicate.a.createdAt,
|
|
116
|
+
'and',
|
|
117
|
+
duplicate.b.id,
|
|
118
|
+
duplicate.b.createdAt,
|
|
119
|
+
'with name',
|
|
120
|
+
duplicate.a.firstName, duplicate.a.lastName);
|
|
121
|
+
deletedSet.add(duplicate.b.id);
|
|
122
|
+
mergedIntoSet.add(duplicate.a.id);
|
|
123
|
+
|
|
124
|
+
// Run the merge
|
|
125
|
+
const [memberA] = await Member.getBlobByIds(duplicate.a.id);
|
|
126
|
+
const [memberB] = await Member.getBlobByIds(duplicate.b.id);
|
|
127
|
+
|
|
128
|
+
if (memberA.details.name !== memberB.details.name) {
|
|
129
|
+
console.warn('Member names do not match', memberA.details.name, 'and', memberB.details.name);
|
|
130
|
+
continue; // Names do not match, cannot merge
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (memberA.details.birthDayFormatted === null || memberA.details.birthDayFormatted !== memberB.details.birthDayFormatted) {
|
|
134
|
+
console.warn('Member birthday do not match', memberA.details.birthDayFormatted, 'and', memberB.details.birthDayFormatted);
|
|
135
|
+
continue; // Names do not match, cannot merge
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!PatchOrganizationMembersEndpoint.shouldCheckIfMemberIsDuplicate(memberA)) {
|
|
139
|
+
console.log('Skipping merge because not eligible for duplicate check');
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await mergeTwoMembers(memberA, memberB);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Email, Organization, Platform } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment === 'test') {
|
|
6
|
+
console.log('skipped in tests');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
console.log('Start setting senderId of emails.');
|
|
11
|
+
|
|
12
|
+
const batchSize = 100;
|
|
13
|
+
let count = 0;
|
|
14
|
+
const platform = await Platform.getShared();
|
|
15
|
+
|
|
16
|
+
for await (const email of Email.select()
|
|
17
|
+
.where('senderId', null).limit(batchSize).all()) {
|
|
18
|
+
if (!email.fromAddress) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const organization = email.organizationId ? await Organization.getByID(email.organizationId) : null;
|
|
22
|
+
if (!organization) {
|
|
23
|
+
const sender = platform.privateConfig.emails.find(s => s.email === email.fromAddress);
|
|
24
|
+
if (sender) {
|
|
25
|
+
email.senderId = sender.id;
|
|
26
|
+
await email.save();
|
|
27
|
+
count += 1;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.warn(`No sender found for email ${email.fromAddress} in platform config`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const sender = organization.privateMeta.emails.find(s => s.email === email.fromAddress);
|
|
35
|
+
if (sender) {
|
|
36
|
+
email.senderId = sender.id;
|
|
37
|
+
await email.save();
|
|
38
|
+
count += 1;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.warn(`No sender found for email ${email.fromAddress} in organization ${organization.id}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('Finished saving ' + count + ' emails.');
|
|
47
|
+
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { Model } from '@simonbackx/simple-database';
|
|
1
2
|
import { BalanceItem, CachedBalance, Document, MemberUser, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
|
|
2
|
-
import { AuditLogSource, BalanceItemStatus, BalanceItemType, OrderStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
|
-
import {
|
|
3
|
+
import { AuditLogSource, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
|
+
import { GroupedThrottledQueue } from '../helpers/GroupedThrottledQueue';
|
|
5
|
+
import { ThrottledQueue } from '../helpers/ThrottledQueue';
|
|
4
6
|
import { AuditLogService } from './AuditLogService';
|
|
5
7
|
import { PaymentReallocationService } from './PaymentReallocationService';
|
|
6
8
|
import { RegistrationService } from './RegistrationService';
|
|
7
|
-
import { Model } from '@simonbackx/simple-database';
|
|
8
|
-
import { GroupedThrottledQueue } from '../helpers/GroupedThrottledQueue';
|
|
9
|
-
import { ThrottledQueue } from '../helpers/ThrottledQueue';
|
|
10
9
|
|
|
11
10
|
const memberUpdateQueue = new GroupedThrottledQueue(async (organizationId: string, memberIds: string[]) => {
|
|
12
11
|
await CachedBalance.updateForMembers(organizationId, memberIds);
|
|
@@ -210,6 +209,12 @@ export const BalanceItemService = {
|
|
|
210
209
|
if (shouldMarkUpdated) {
|
|
211
210
|
await this.markUpdated(balanceItem, payment, organization);
|
|
212
211
|
}
|
|
212
|
+
|
|
213
|
+
if (balanceItem.registrationId) {
|
|
214
|
+
if (balanceItem.type === BalanceItemType.Registration && !!payment && payment.status === PaymentStatus.Succeeded) {
|
|
215
|
+
await RegistrationService.markRepeatedPaid(balanceItem.registrationId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
213
218
|
return;
|
|
214
219
|
}
|
|
215
220
|
|
|
@@ -220,7 +225,7 @@ export const BalanceItemService = {
|
|
|
220
225
|
// If registration
|
|
221
226
|
if (balanceItem.registrationId) {
|
|
222
227
|
if (balanceItem.type === BalanceItemType.Registration) {
|
|
223
|
-
await RegistrationService.markValid(balanceItem.registrationId);
|
|
228
|
+
await RegistrationService.markValid(balanceItem.registrationId, { paid: !!payment && payment.status === PaymentStatus.Succeeded });
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -228,7 +233,7 @@ export const BalanceItemService = {
|
|
|
228
233
|
await balanceItem.save();
|
|
229
234
|
},
|
|
230
235
|
|
|
231
|
-
async markUpdated(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
236
|
+
async markUpdated(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
|
|
232
237
|
// For orders: mark order as changed (so they are refetched in front ends)
|
|
233
238
|
if (balanceItem.orderId) {
|
|
234
239
|
await AuditLogService.setContext({ source: AuditLogSource.Payment }, async () => {
|
|
@@ -9,16 +9,45 @@ import { Formatter } from '@stamhoofd/utility';
|
|
|
9
9
|
import { encodeObject } from '@simonbackx/simple-encoding';
|
|
10
10
|
|
|
11
11
|
export const RegistrationService = {
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* If the registration was marked valid, and later paid, we'll still shorten the trail until period
|
|
14
|
+
*/
|
|
15
|
+
async markRepeatedPaid(registrationId: string) {
|
|
16
|
+
const registration = await Registration.getByID(registrationId);
|
|
17
|
+
if (!registration) {
|
|
18
|
+
throw new Error('Registration not found');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (registration.registeredAt !== null && registration.deactivatedAt === null) {
|
|
22
|
+
// Already valid
|
|
23
|
+
if (registration.trialUntil && registration.trialUntil > new Date()) {
|
|
24
|
+
registration.trialUntil = new Date();
|
|
25
|
+
await registration.save();
|
|
26
|
+
await PlatformMembershipService.updateMembershipsForId(registration.memberId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// do nothing: possible that we canceled the registration and don't want to reactivate it when we mark something paid
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async markValid(registrationId: string, options?: { paid?: boolean }) {
|
|
13
33
|
const registration = await Registration.getByID(registrationId);
|
|
14
34
|
if (!registration) {
|
|
15
35
|
throw new Error('Registration not found');
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
if (registration.registeredAt !== null && registration.deactivatedAt === null) {
|
|
39
|
+
// Already valid
|
|
40
|
+
if (options?.paid && registration.trialUntil && registration.trialUntil > new Date()) {
|
|
41
|
+
registration.trialUntil = new Date();
|
|
42
|
+
await registration.save();
|
|
43
|
+
await PlatformMembershipService.updateMembershipsForId(registration.memberId);
|
|
44
|
+
}
|
|
19
45
|
return false;
|
|
20
46
|
}
|
|
21
47
|
|
|
48
|
+
if (options?.paid && registration.trialUntil && registration.trialUntil > new Date()) {
|
|
49
|
+
registration.trialUntil = new Date();
|
|
50
|
+
}
|
|
22
51
|
registration.reservedUntil = null;
|
|
23
52
|
registration.registeredAt = registration.registeredAt ?? new Date();
|
|
24
53
|
registration.deactivatedAt = null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
2
|
import { Order, WebshopUitpasNumber } from '@stamhoofd/models';
|
|
3
|
-
import { OrderStatus, Product, ProductPrice, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
3
|
+
import { Cart, OrderStatus, Product, ProductPrice, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
import { UitpasTokenRepository } from '../../helpers/UitpasTokenRepository';
|
|
6
6
|
import { searchUitpasOrganizers } from './searchUitpasOrganizers';
|
|
@@ -13,6 +13,7 @@ import { RegisterTicketSaleRequest, RegisterTicketSaleResponse, registerTicketSa
|
|
|
13
13
|
import { cancelTicketSales } from './cancelTicketSales';
|
|
14
14
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
15
15
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
16
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
16
17
|
|
|
17
18
|
type UitpasTicketSale = {
|
|
18
19
|
basePrice: number;
|
|
@@ -39,7 +40,7 @@ type InsertUitpasNumber = {
|
|
|
39
40
|
uitpasEventUrl: string | null; // null for non-official flow
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
function shouldReserveUitpasNumbers(status: OrderStatus): boolean {
|
|
43
|
+
export function shouldReserveUitpasNumbers(status: OrderStatus): boolean {
|
|
43
44
|
return status !== OrderStatus.Canceled && status !== OrderStatus.Deleted;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -77,7 +78,7 @@ function getUitpasTicketSales(order: Order): UitpasTicketSale[] {
|
|
|
77
78
|
throw new SimpleError({
|
|
78
79
|
code: 'missing_uitpas_base_product_price',
|
|
79
80
|
message: `Missing UiTPAS base product price`,
|
|
80
|
-
human: $t(`
|
|
81
|
+
human: $t(`f7eea411-fb92-458f-bf3e-e36ed870591b`),
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
const label = makeBaseProductPriceLabel(item.product, baseProductPrice);
|
|
@@ -356,4 +357,72 @@ export class UitpasService {
|
|
|
356
357
|
// Clear the uitpas client credentials for the organization
|
|
357
358
|
await UitpasTokenRepository.clearClientCredentialsFor(organizationId);
|
|
358
359
|
}
|
|
360
|
+
|
|
361
|
+
static async areThereRegisteredTicketSales(webshopId: string): Promise<boolean> {
|
|
362
|
+
return await WebshopUitpasNumber.areThereRegisteredTicketSales(webshopId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
static async validateCart(organizationId: string, webshopId: string, cart: Cart, exisitingOrderId?: string): Promise<Cart> {
|
|
366
|
+
let access_token_org: string | null = null;
|
|
367
|
+
let access_token_platform: string | null = null;
|
|
368
|
+
for (const item of cart.items) {
|
|
369
|
+
if (item.uitpasNumbers.length === 0) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// verify the UiTPAS numbers are not already used for this product
|
|
374
|
+
const hasBeenUsed = await WebshopUitpasNumber.areUitpasNumbersUsed(webshopId, item.product.id, item.uitpasNumbers.map(p => p.uitpasNumber), item.product.uitpasEvent?.url, exisitingOrderId);
|
|
375
|
+
if (hasBeenUsed) {
|
|
376
|
+
throw new SimpleError({
|
|
377
|
+
code: 'uitpas_number_already_used',
|
|
378
|
+
message: 'One or more uitpas numbers are already used',
|
|
379
|
+
human: $t('f3daff19-a227-4e45-b19a-c770bd7a6687'),
|
|
380
|
+
field: 'uitpasNumbers',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (item.product.uitpasEvent) {
|
|
385
|
+
// official flow
|
|
386
|
+
const basePrice = item.product.prices.filter(price => price.id === item.productPrice.uitpasBaseProductPriceId)[0]?.price;
|
|
387
|
+
if (!basePrice) {
|
|
388
|
+
throw new SimpleError({
|
|
389
|
+
code: 'missing_uitpas_base_product_price',
|
|
390
|
+
message: `Missing UiTPAS base product price`,
|
|
391
|
+
human: $t(`3d08a166-11a7-4429-8ff7-84458bbe3e9a`),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
access_token_org = access_token_org ?? await UitpasTokenRepository.getAccessTokenFor(organizationId);
|
|
396
|
+
const verified = await getSocialTariffForUitpasNumbers(access_token_org, item.uitpasNumbers.map(p => p.uitpasNumber), basePrice, item.product.uitpasEvent.url);
|
|
397
|
+
if (verified.length < item.uitpasNumbers.length) {
|
|
398
|
+
throw new SimpleError({
|
|
399
|
+
code: 'uitpas_social_tariff_price_mismatch',
|
|
400
|
+
message: 'UiTPAS wrong number of prices returned',
|
|
401
|
+
human: $t('83c472b8-4bc5-4282-bbc9-1c6a2d382171'),
|
|
402
|
+
field: 'uitpasNumbers',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
for (let i = 0; i < verified.length; i++) {
|
|
406
|
+
if (item.uitpasNumbers[i].uitpasTariffId !== verified[i].uitpasTariffId) {
|
|
407
|
+
// silently update
|
|
408
|
+
item.uitpasNumbers[i].uitpasTariffId = verified[i].uitpasTariffId;
|
|
409
|
+
}
|
|
410
|
+
if (item.uitpasNumbers[i].price !== verified[i].price) {
|
|
411
|
+
throw new SimpleError({
|
|
412
|
+
code: 'uitpas_social_tariff_price_mismatch',
|
|
413
|
+
message: 'UiTPAS social tariff have a different price',
|
|
414
|
+
human: $t('9a0ad099-99e3-4341-beac-f14feb3fb9d1', { correctPrice: Formatter.price(verified[i].price), orderPrice: Formatter.price(item.uitpasNumbers[i].price) }),
|
|
415
|
+
field: 'uitpasNumbers.' + i.toString(),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// non-official flow
|
|
422
|
+
access_token_platform = access_token_platform ?? await UitpasTokenRepository.getAccessTokenFor();
|
|
423
|
+
await checkUitpasNumbers(access_token_platform, item.uitpasNumbers.map(p => p.uitpasNumber));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return cart;
|
|
427
|
+
}
|
|
359
428
|
};
|
|
@@ -24,7 +24,7 @@ async function cancelTicketSale(access_token: string, ticketSaleId: string) {
|
|
|
24
24
|
throw new SimpleError({
|
|
25
25
|
code: 'unsuccessful_response_registering_ticket_sales',
|
|
26
26
|
message: `Unsuccessful response when registering UiTPAS ticket sales`,
|
|
27
|
-
human: $t(`
|
|
27
|
+
human: $t(`ed4e876c-6a40-49a7-ab65-2a4d5f31c13f`),
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
return ticketSaleId;
|
|
@@ -16,7 +16,7 @@ function assertIsPermissionsResponse(json: unknown): asserts json is Permissions
|
|
|
16
16
|
throw new SimpleError({
|
|
17
17
|
code: 'invalid_permissions_response',
|
|
18
18
|
message: 'Invalid response format for permissions',
|
|
19
|
-
human: $t('
|
|
19
|
+
human: $t('7d3a6b57-f81a-4d58-bc2b-babb2261c40b'),
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -35,7 +35,7 @@ function assertIsPermissionsResponse(json: unknown): asserts json is Permissions
|
|
|
35
35
|
throw new SimpleError({
|
|
36
36
|
code: 'invalid_permissions_response',
|
|
37
37
|
message: 'Invalid response format for permissions',
|
|
38
|
-
human: $t('
|
|
38
|
+
human: $t('7d3a6b57-f81a-4d58-bc2b-babb2261c40b'),
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -54,7 +54,7 @@ export async function checkPermissionsFor(access_token: string, organizationId:
|
|
|
54
54
|
throw new SimpleError({
|
|
55
55
|
code: 'uitpas_unreachable_checking_permissions',
|
|
56
56
|
message: `Network issue when checking UiTPAS permissions`,
|
|
57
|
-
human: $t(`
|
|
57
|
+
human: $t(`542b793c-3edf-4505-b33d-199ea409bbda`),
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
if (!response.ok) {
|
|
@@ -62,7 +62,7 @@ export async function checkPermissionsFor(access_token: string, organizationId:
|
|
|
62
62
|
throw new SimpleError({
|
|
63
63
|
code: 'unsuccessful_response_checking_permissions',
|
|
64
64
|
message: `Unsuccesful response when checking UiTPAS permissions`,
|
|
65
|
-
human: $t(`
|
|
65
|
+
human: $t(`ed4e876c-6a40-49a7-ab65-2a4d5f31c13f`),
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
const json = await response.json().catch(() => {
|
|
@@ -70,7 +70,7 @@ export async function checkPermissionsFor(access_token: string, organizationId:
|
|
|
70
70
|
throw new SimpleError({
|
|
71
71
|
code: 'invalid_json_checking_permissions',
|
|
72
72
|
message: `Invalid json when checking UiTPAS permissions`,
|
|
73
|
-
human: $t(`
|
|
73
|
+
human: $t(`93004d03-955a-4a9a-937d-2f30841dc5cc`),
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
assertIsPermissionsResponse(json);
|
|
@@ -91,18 +91,18 @@ export async function checkPermissionsFor(access_token: string, organizationId:
|
|
|
91
91
|
}];
|
|
92
92
|
const item = json.find(item => item.organizer.id === uitpasOrganizerId);
|
|
93
93
|
if (!item) {
|
|
94
|
-
const organizers = Formatter.joinLast(json.map(i => i.organizer.name), ', ', ' ' + $t('
|
|
94
|
+
const organizers = Formatter.joinLast(json.map(i => i.organizer.name), ', ', ' ' + $t('6d35156d-e452-4b0f-80f4-b1e9024d08ee') + ' ');
|
|
95
95
|
return {
|
|
96
96
|
status: UitpasClientCredentialsStatus.NoPermissions,
|
|
97
|
-
human: $t('
|
|
97
|
+
human: $t('96c8a719-dba5-47ce-bb61-ee0754a5f776') + organizers,
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
const missingPermissions = neededPermissions.filter(needed => !item.permissions.includes(needed.permission));
|
|
101
101
|
if (missingPermissions.length > 0) {
|
|
102
|
-
const missingPermissionsHuman = Formatter.joinLast(missingPermissions.map(p => p.human), ', ', ' ' + $t('
|
|
102
|
+
const missingPermissionsHuman = Formatter.joinLast(missingPermissions.map(p => p.human), ', ', ' ' + $t('6d35156d-e452-4b0f-80f4-b1e9024d08ee') + ' ');
|
|
103
103
|
return {
|
|
104
104
|
status: UitpasClientCredentialsStatus.MissingPermissions,
|
|
105
|
-
human: $t('
|
|
105
|
+
human: $t('040fa935-5cbc-4a85-b578-354bf9d7fc04') + missingPermissionsHuman,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
return {
|
|
@@ -36,7 +36,7 @@ function assertIsUitpasNumberSuccessfulResponse(
|
|
|
36
36
|
throw new SimpleError({
|
|
37
37
|
code: 'invalid_response_retrieving_pass_by_uitpas_number',
|
|
38
38
|
message: `Invalid response when retrieving pass by UiTPAS number`,
|
|
39
|
-
human: $t(`
|
|
39
|
+
human: $t(`4c6482ff-e6d9-4ea1-b11d-e12d697b4b7b`),
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -64,6 +64,7 @@ async function checkUitpasNumber(access_token: string, uitpasNumber: string) {
|
|
|
64
64
|
),
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
+
|
|
67
68
|
const baseUrl = 'https://api-test.uitpas.be'; // TO DO: Use the URL from environment variables
|
|
68
69
|
|
|
69
70
|
const url = `${baseUrl}/passes/${uitpasNumber}`;
|
|
@@ -110,7 +111,7 @@ async function checkUitpasNumber(access_token: string, uitpasNumber: string) {
|
|
|
110
111
|
throw new SimpleError({
|
|
111
112
|
code: 'unsuccessful_and_unexpected_response_retrieving_pass_by_uitpas_number',
|
|
112
113
|
message: `Unsuccesful response without message when retrieving pass by UiTPAS number`,
|
|
113
|
-
human: $t(`
|
|
114
|
+
human: $t(`4c6482ff-e6d9-4ea1-b11d-e12d697b4b7b`),
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -21,7 +21,7 @@ function assertsIsStaticSocialTariffResponse(json: unknown): asserts json is Sta
|
|
|
21
21
|
throw new SimpleError({
|
|
22
22
|
code: 'invalid_response_getting_static_uitpas_social_tariff',
|
|
23
23
|
message: `Invalid response when getting static UiTPAS social tariff`,
|
|
24
|
-
human: $t(`
|
|
24
|
+
human: $t(`a56854af-464a-45a4-8c39-10a9264b6ce4`),
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -35,7 +35,7 @@ export async function getSocialTariffForEvent(access_token: string, basePrice: n
|
|
|
35
35
|
throw new SimpleError({
|
|
36
36
|
code: 'invalid_uitpas_event_url',
|
|
37
37
|
message: `Invalid UiTPAS event URL: ${uitpasEventUrl}`,
|
|
38
|
-
human: $t(`
|
|
38
|
+
human: $t(`85fb6e02-9b69-43cc-acf7-96a576461560`),
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
params.append('eventId', eventId);
|
|
@@ -61,7 +61,7 @@ export async function getSocialTariffForEvent(access_token: string, basePrice: n
|
|
|
61
61
|
throw new SimpleError({
|
|
62
62
|
code: 'unsuccessful_response_getting_static_uitpas_social_tariff',
|
|
63
63
|
message: `Unsuccessful response when getting static UiTPAS social tariff`,
|
|
64
|
-
human: $t(`
|
|
64
|
+
human: $t(`ed4e876c-6a40-49a7-ab65-2a4d5f31c13f`),
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
const json = await response.json().catch(() => {
|
|
@@ -80,7 +80,7 @@ export async function getSocialTariffForEvent(access_token: string, basePrice: n
|
|
|
80
80
|
throw new SimpleError({
|
|
81
81
|
code: 'no_social_tariff_available',
|
|
82
82
|
message: `No social tariff available for event ${eventId}`,
|
|
83
|
-
human: $t(`
|
|
83
|
+
human: $t(`ccd8e8b4-01a7-4e7c-8ae0-92d2a4c659eb`),
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
if (json.available.length > 1) {
|
|
@@ -34,7 +34,7 @@ function assertsIsSocialTariffResponse(json: unknown): asserts json is SocialTar
|
|
|
34
34
|
throw new SimpleError({
|
|
35
35
|
code: 'invalid_response_getting_uitpas_social_tariff',
|
|
36
36
|
message: `Invalid response when getting UiTPAS social tariff`,
|
|
37
|
-
human: $t(`
|
|
37
|
+
human: $t(`466844b9-c042-4bc7-b77d-3f87376086b5`),
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -60,7 +60,7 @@ async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber
|
|
|
60
60
|
throw new SimpleError({
|
|
61
61
|
code: 'invalid_uitpas_event_url',
|
|
62
62
|
message: `Invalid UiTPAS event URL: ${uitpasEventUrl}`,
|
|
63
|
-
human: $t(`
|
|
63
|
+
human: $t(`85fb6e02-9b69-43cc-acf7-96a576461560`),
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
params.append('eventId', eventId);
|
|
@@ -110,7 +110,7 @@ async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber
|
|
|
110
110
|
throw new SimpleError({
|
|
111
111
|
code: 'unsuccessful_and_unexpected_response_retrieving_social_tariff_by_uitpas_number',
|
|
112
112
|
message: `Unsuccesful response without message when retrieving social tariff by UiTPAS number`,
|
|
113
|
-
human: $t(`
|
|
113
|
+
human: $t(`1572d069-abda-4b31-a373-f0c3760c79b1`),
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
const json = await response.json().catch(() => {
|
|
@@ -136,7 +136,7 @@ async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber
|
|
|
136
136
|
throw new SimpleError({
|
|
137
137
|
code: 'no_social_tariff_available',
|
|
138
138
|
message: `No social tariff available for event ${uitpasEventUrl}`,
|
|
139
|
-
human: $t(`
|
|
139
|
+
human: $t(`ccd8e8b4-01a7-4e7c-8ae0-92d2a4c659eb`),
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
if (json.available.length > 1) {
|
|
@@ -150,7 +150,7 @@ async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber
|
|
|
150
150
|
throw new SimpleError({
|
|
151
151
|
code: 'no_remaining_social_tariff',
|
|
152
152
|
message: `No remaining social tariff for event ${uitpasEventUrl} and UiTPAS number ${uitpasNumber}`,
|
|
153
|
-
human: $t(`
|
|
153
|
+
human: $t(`f9d6bc51-e7c9-4d3f-a13f-27871a018d83`),
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
console.log('Social tariff for UiTPAS number', uitpasNumber, 'with event id', uitpasEventUrl, 'is', json.available[0].price, 'euros');
|
|
@@ -46,7 +46,7 @@ function assertisSuccessResponse(json: unknown): asserts json is SuccessResponse
|
|
|
46
46
|
throw new SimpleError({
|
|
47
47
|
code: 'invalid_register_ticket_sale_response',
|
|
48
48
|
message: `Invalid register ticket sale response`,
|
|
49
|
-
human: $t(`
|
|
49
|
+
human: $t(`f5cac12d-ced6-43e6-bbd9-81b381807154`),
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -69,7 +69,7 @@ export async function registerTicketSales(access_token: string, registerTicketSa
|
|
|
69
69
|
throw new SimpleError({
|
|
70
70
|
code: 'invalid_uitpas_event_url',
|
|
71
71
|
message: `Invalid UiTPAS event URL: ${ticketSale.uitpasEventUrl}`,
|
|
72
|
-
human: $t(`
|
|
72
|
+
human: $t(`85fb6e02-9b69-43cc-acf7-96a576461560`),
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -108,7 +108,7 @@ export async function registerTicketSales(access_token: string, registerTicketSa
|
|
|
108
108
|
throw new SimpleError({
|
|
109
109
|
code: 'unsuccessful_response_registering_ticket_sales',
|
|
110
110
|
message: `Unsuccessful response when registering UiTPAS ticket sales`,
|
|
111
|
-
human: $t(`
|
|
111
|
+
human: $t(`ed4e876c-6a40-49a7-ab65-2a4d5f31c13f`),
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
const json = await response.json().catch(() => {});
|
|
@@ -117,7 +117,7 @@ export async function registerTicketSales(access_token: string, registerTicketSa
|
|
|
117
117
|
throw new SimpleError({
|
|
118
118
|
code: 'invalid_response_registering_ticket_sales',
|
|
119
119
|
message: `Invalid response when registering ticket sales`,
|
|
120
|
-
human: $t(`
|
|
120
|
+
human: $t(`f5cac12d-ced6-43e6-bbd9-81b381807154`),
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
const now = new Date();
|
|
@@ -49,7 +49,7 @@ function assertIsEventsResponse(json: unknown): asserts json is EventsResponse {
|
|
|
49
49
|
throw new SimpleError({
|
|
50
50
|
code: 'invalid_events_response',
|
|
51
51
|
message: `Invalid events response`,
|
|
52
|
-
human: $t(`
|
|
52
|
+
human: $t(`0b610eb8-551f-4ef6-acdc-fb3267dfc2a8`),
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -61,7 +61,7 @@ export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: st
|
|
|
61
61
|
throw new SimpleError({
|
|
62
62
|
code: 'no_client_id_for_uitpas_events',
|
|
63
63
|
message: `No client ID configured for Uitpas events`,
|
|
64
|
-
human: $t(`
|
|
64
|
+
human: $t(`0009cce2-9d13-4f03-9258-345d8f8e396e`),
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
const baseUrl = 'https://search-test.uitdatabank.be/events';
|
|
@@ -97,7 +97,7 @@ export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: st
|
|
|
97
97
|
throw new SimpleError({
|
|
98
98
|
code: 'unsuccessful_response_searching_uitpas_events',
|
|
99
99
|
message: `Unsuccessful response when searching for UiTPAS events`,
|
|
100
|
-
human: $t(`
|
|
100
|
+
human: $t(`ed4e876c-6a40-49a7-ab65-2a4d5f31c13f`),
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
const json = await response.json().catch(() => {
|