@stamhoofd/backend 2.89.2 → 2.90.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 +12 -11
- package/src/boot.ts +2 -0
- package/src/crons/balance-emails.ts +1 -6
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/SearchUitpasOrganizersEndpoint.ts +42 -0
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +23 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +10 -8
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +3 -6
- package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +38 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +31 -1
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +108 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -2
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +9 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -9
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +1 -7
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +68 -1
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +27 -20
- package/src/helpers/AdminPermissionChecker.ts +129 -22
- package/src/helpers/AuthenticatedStructures.ts +13 -10
- package/src/helpers/Context.ts +1 -1
- package/src/helpers/UitpasTokenRepository.ts +125 -35
- package/src/helpers/ViesHelper.ts +2 -1
- package/src/seeds/0000000002-clear-stamhoofd-email-templates.ts +13 -0
- package/src/seeds/0000000003-default-email-templates.ts +20 -0
- package/src/seeds/data/default-email-templates.sql +55 -0
- package/src/services/RegistrationService.ts +6 -4
- package/src/services/uitpas/UitpasService.test.ts +23 -0
- package/src/services/uitpas/UitpasService.ts +222 -0
- package/src/services/uitpas/checkPermissionsFor.ts +111 -0
- package/src/services/uitpas/checkUitpasNumbers.ts +180 -0
- package/src/services/uitpas/getSocialTariffForEvent.ts +90 -0
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +181 -0
- package/src/services/uitpas/searchUitpasOrganizers.ts +93 -0
- package/src/sql-filters/audit-logs.ts +26 -6
- package/src/sql-filters/balance-item-payments.ts +23 -8
- package/src/sql-filters/base-registration-filter-compilers.ts +74 -23
- package/src/sql-filters/documents.ts +46 -13
- package/src/sql-filters/event-notifications.ts +48 -12
- package/src/sql-filters/events.ts +62 -26
- package/src/sql-filters/groups.ts +12 -12
- package/src/sql-filters/members.ts +325 -137
- package/src/sql-filters/orders.ts +96 -48
- package/src/sql-filters/organization-registration-periods.ts +16 -4
- package/src/sql-filters/organizations.ts +105 -99
- package/src/sql-filters/payments.ts +97 -47
- package/src/sql-filters/receivable-balances.ts +56 -19
- package/src/sql-filters/registration-periods.ts +16 -4
- package/src/sql-filters/registrations.ts +2 -2
- package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +14 -8
- package/src/sql-filters/tickets.ts +26 -6
- package/tests/e2e/charge-members.test.ts +1 -0
- package/src/helpers/UitpasNumberValidator.test.ts +0 -23
- package/src/helpers/UitpasNumberValidator.ts +0 -185
|
@@ -6,7 +6,7 @@ import { GroupService } from './GroupService';
|
|
|
6
6
|
import { PlatformMembershipService } from './PlatformMembershipService';
|
|
7
7
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
-
import { encodeObject
|
|
9
|
+
import { encodeObject } from '@simonbackx/simple-encoding';
|
|
10
10
|
|
|
11
11
|
export const RegistrationService = {
|
|
12
12
|
async markValid(registrationId: string) {
|
|
@@ -28,9 +28,11 @@ export const RegistrationService = {
|
|
|
28
28
|
|
|
29
29
|
await PlatformMembershipService.updateMembershipsForId(registration.memberId);
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
if (registration.sendConfirmationEmail) {
|
|
32
|
+
await registration.sendEmailTemplate({
|
|
33
|
+
type: EmailTemplateType.RegistrationConfirmation,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
const member = await Member.getByID(registration.memberId);
|
|
36
38
|
if (member) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { STExpect } from '@stamhoofd/test-utils';
|
|
2
|
+
import { UitpasService } from './UitpasService';
|
|
3
|
+
|
|
4
|
+
describe.skip('UitpasService', () => {
|
|
5
|
+
it('should validate a correct Uitpas number with kansentarief', async () => {
|
|
6
|
+
const validNumbers = ['0900000067513'];
|
|
7
|
+
await expect(UitpasService.checkUitpasNumbers(validNumbers)).resolves.toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should throw an error for an invalid Uitpas number', async () => {
|
|
11
|
+
const invalidNumbers = ['1234567890123'];
|
|
12
|
+
await expect(UitpasService.checkUitpasNumbers(invalidNumbers)).rejects.toThrow(
|
|
13
|
+
STExpect.simpleError({ code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number' }),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should throw an error for a Uitpas number with kansentarief expired', async () => {
|
|
18
|
+
const expiredNumbers = ['0900000058918'];
|
|
19
|
+
await expect(UitpasService.checkUitpasNumbers(expiredNumbers)).rejects.toThrow(
|
|
20
|
+
STExpect.simpleError({ code: 'uitpas_number_issue' }),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Model } from '@simonbackx/simple-database';
|
|
2
|
+
import { Order, WebshopUitpasNumber } from '@stamhoofd/models';
|
|
3
|
+
import { OrderStatus, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { UitpasTokenRepository } from '../../helpers/UitpasTokenRepository';
|
|
6
|
+
import { searchUitpasOrganizers } from './searchUitpasOrganizers';
|
|
7
|
+
import { checkPermissionsFor } from './checkPermissionsFor';
|
|
8
|
+
import { checkUitpasNumbers } from './checkUitpasNumbers';
|
|
9
|
+
import { getSocialTariffForEvent } from './getSocialTariffForEvent';
|
|
10
|
+
import { getSocialTariffForUitpasNumbers } from './getSocialTariffForUitpasNumbers';
|
|
11
|
+
|
|
12
|
+
function shouldReserveUitpasNumbers(status: OrderStatus): boolean {
|
|
13
|
+
return status !== OrderStatus.Canceled && status !== OrderStatus.Deleted;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mapUitpasNumbersToProducts(order: Order): Map<string, string[]> {
|
|
17
|
+
const items = order.data.cart.items;
|
|
18
|
+
const productIdToUitpasNumbers: Map<string, string[]> = new Map();
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
const a = productIdToUitpasNumbers.get(item.product.id);
|
|
21
|
+
if (a) {
|
|
22
|
+
a.push(...item.uitpasNumbers.map(p => p.uitpasNumber));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
productIdToUitpasNumbers.set(item.product.id, [...item.uitpasNumbers.map(p => p.uitpasNumber)]); // make a copy
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return productIdToUitpasNumbers;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function areUitpasNumbersChanged(oldOrder: Order, newOrder: Order): boolean {
|
|
32
|
+
const oldMap = mapUitpasNumbersToProducts(oldOrder);
|
|
33
|
+
const newMap = mapUitpasNumbersToProducts(newOrder);
|
|
34
|
+
if (oldMap.size !== newMap.size) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
for (const [productId, uitpasNumbers] of oldMap.entries()) {
|
|
38
|
+
const newUitpasNumbers = newMap.get(productId);
|
|
39
|
+
if (!newUitpasNumbers) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (newUitpasNumbers.length !== uitpasNumbers.length) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
for (const uitpasNumber of uitpasNumbers) {
|
|
46
|
+
if (!newUitpasNumbers.includes(uitpasNumber)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class UitpasService {
|
|
55
|
+
static listening = false;
|
|
56
|
+
|
|
57
|
+
static async updateUitpasNumbers(order: Order) {
|
|
58
|
+
await this.deleteUitpasNumbers(order);
|
|
59
|
+
await this.createUitpasNumbers(order);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async createUitpasNumbers(order: Order) {
|
|
63
|
+
const mappedUitpasNumbers = mapUitpasNumbersToProducts(order); // productId -> Set of uitpas numbers
|
|
64
|
+
// add to DB
|
|
65
|
+
const insert = WebshopUitpasNumber.insert();
|
|
66
|
+
insert.columns(
|
|
67
|
+
'id',
|
|
68
|
+
'webshopId',
|
|
69
|
+
'orderId',
|
|
70
|
+
'productId',
|
|
71
|
+
'uitpasNumber',
|
|
72
|
+
);
|
|
73
|
+
const rows = [...mappedUitpasNumbers].flatMap(([productId, uitpasNumbers]) => {
|
|
74
|
+
return uitpasNumbers.map(uitpasNumber => [
|
|
75
|
+
uuidv4(),
|
|
76
|
+
order.webshopId,
|
|
77
|
+
order.id,
|
|
78
|
+
productId,
|
|
79
|
+
uitpasNumber,
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
if (rows.length === 0) {
|
|
83
|
+
// No uitpas numbers to insert, skipping
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
insert.values(...rows);
|
|
87
|
+
await insert.insert();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static async deleteUitpasNumbers(order: Order) {
|
|
91
|
+
await WebshopUitpasNumber.delete().where('webshopId', order.webshopId)
|
|
92
|
+
.andWhere('orderId', order.id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static listen() {
|
|
96
|
+
if (this.listening) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.listening = true;
|
|
100
|
+
Model.modelEventBus.addListener(this, async (event) => {
|
|
101
|
+
try {
|
|
102
|
+
if (event.model instanceof Order) {
|
|
103
|
+
// event.type ==='deteled' -> not needed as foreign key will delete the order
|
|
104
|
+
if (event.type === 'created' && shouldReserveUitpasNumbers(event.model.status)) {
|
|
105
|
+
await this.createUitpasNumbers(event.model);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (event.type === 'updated') {
|
|
109
|
+
if (event.changedFields.status) {
|
|
110
|
+
const statusBefore = event.originalFields.status as OrderStatus;
|
|
111
|
+
const statusAfter = event.changedFields.status as OrderStatus;
|
|
112
|
+
const shouldReserveAfter = shouldReserveUitpasNumbers(statusAfter);
|
|
113
|
+
if (shouldReserveUitpasNumbers(statusBefore) !== shouldReserveAfter) {
|
|
114
|
+
if (shouldReserveAfter) {
|
|
115
|
+
await this.createUitpasNumbers(event.model);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await this.deleteUitpasNumbers(event.model);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (event.changedFields.data) {
|
|
123
|
+
const oldOrder = event.getOldModel() as Order;
|
|
124
|
+
if (areUitpasNumbersChanged(oldOrder, event.model)) {
|
|
125
|
+
await this.updateUitpasNumbers(event.model);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
console.error('Failed to update UiTPAS-numbers after order update', e);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static async getSocialTariffForUitpasNumbers(organisationId: string, uitpasNumbers: string[], basePrice: number, uitpasEventUrl: string) {
|
|
138
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-tariffs
|
|
139
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(organisationId);
|
|
140
|
+
return await getSocialTariffForUitpasNumbers(access_token, uitpasNumbers, basePrice, uitpasEventUrl);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static async getSocialTariffForEvent(organisationId: string, basePrice: number, uitpasEventUrl: string) {
|
|
144
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/get-a-tariff-static
|
|
145
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(organisationId);
|
|
146
|
+
return await getSocialTariffForEvent(access_token, basePrice, uitpasEventUrl);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static async registerTicketSales() {
|
|
150
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/create-a-ticket-sale
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static async cancelTicketSale() {
|
|
154
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/delete-a-ticket-sale
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static async getTicketSales() {
|
|
158
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-ticket-sales
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static async registerAttendance() {
|
|
162
|
+
// https://api-test.uitpas.be/checkins
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static searchUitpasEvents(organizationId: string, uitpasOrganizerId: string, textQuery?: string) {
|
|
166
|
+
// input = client id of organization (never platform0 & uitpasOrganizerId
|
|
167
|
+
// https://docs.publiq.be/docs/uitpas/events/searching#searching-for-uitpas-events-of-one-specific-organizer
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static async searchUitpasOrganizers(name: string): Promise<UitpasOrganizersResponse> {
|
|
171
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-organizers
|
|
172
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(); // uses platform credentials
|
|
173
|
+
return await searchUitpasOrganizers(access_token, name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static async checkPermissionsFor(organizationId: string | null, uitpasOrganizerId: string): Promise<{
|
|
177
|
+
status: UitpasClientCredentialsStatus;
|
|
178
|
+
human?: string;
|
|
179
|
+
}> {
|
|
180
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-permissions
|
|
181
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(organizationId);
|
|
182
|
+
return await checkPermissionsFor(access_token, organizationId, uitpasOrganizerId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns the client ID if it is configured for the organization, otherwise an empty string. Empty strings means no client ID and secret configured.
|
|
187
|
+
* @param organisationId
|
|
188
|
+
* @returns clientId or empty string if not configured
|
|
189
|
+
*/
|
|
190
|
+
static async getClientIdFor(organizationId: string | null): Promise<string> {
|
|
191
|
+
// Get the uitpas client credentials for the organization
|
|
192
|
+
return await UitpasTokenRepository.getClientIdFor(organizationId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Checks multiple uitpas numbers
|
|
197
|
+
* If any of the uitpas numbers is invalid, it will throw a SimpleErrors instance with all errors.
|
|
198
|
+
* The field of the error will be the index of the uitpas number in the array.
|
|
199
|
+
* @param uitpasNumbers The uitpas numbers to check
|
|
200
|
+
*/
|
|
201
|
+
static async checkUitpasNumbers(uitpasNumbers: string[]) {
|
|
202
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/get-a-pass
|
|
203
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(); // use platform credentials
|
|
204
|
+
return await checkUitpasNumbers(access_token, uitpasNumbers);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Store the uitpas client credentials if they are valid
|
|
209
|
+
* @param organizationId null for platform
|
|
210
|
+
* @param clientId
|
|
211
|
+
* @param clientSecret
|
|
212
|
+
* @returns wether the credentials were valid and thus stored successfully
|
|
213
|
+
*/
|
|
214
|
+
static async storeIfValid(organizationId: string | null, clientId: string, clientSecret: string): Promise<boolean> {
|
|
215
|
+
return await UitpasTokenRepository.storeIfValid(organizationId, clientId, clientSecret);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static async clearClientCredentialsFor(organizationId: string | null) {
|
|
219
|
+
// Clear the uitpas client credentials for the organization
|
|
220
|
+
await UitpasTokenRepository.clearClientCredentialsFor(organizationId);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { UitpasClientCredentialsStatus } from '@stamhoofd/structures';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
|
|
5
|
+
type PermissionsResponse = Array<{
|
|
6
|
+
organizer: {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
permissions: Array<string>;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
function assertIsPermissionsResponse(json: unknown): asserts json is PermissionsResponse {
|
|
14
|
+
if (!Array.isArray(json)) {
|
|
15
|
+
console.error('Invalid PermissionsResponse:', json);
|
|
16
|
+
throw new SimpleError({
|
|
17
|
+
code: 'invalid_permissions_response',
|
|
18
|
+
message: 'Invalid response format for permissions',
|
|
19
|
+
human: $t('Er is iets misgelopen bij het ophalen van je rechten. Probeer het later opnieuw.'),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const item of json) {
|
|
24
|
+
if (
|
|
25
|
+
typeof item !== 'object' || item === null
|
|
26
|
+
|| !('organizer' in item)
|
|
27
|
+
|| typeof (item as any).organizer !== 'object' || (item as any).organizer === null
|
|
28
|
+
|| typeof (item as any).organizer.id !== 'string'
|
|
29
|
+
|| typeof (item as any).organizer.name !== 'string'
|
|
30
|
+
|| !('permissions' in item)
|
|
31
|
+
|| !Array.isArray((item as any).permissions)
|
|
32
|
+
|| !(item as any).permissions.every((perm: unknown) => typeof perm === 'string')
|
|
33
|
+
) {
|
|
34
|
+
console.error('Invalid PermissionsResponse:', json);
|
|
35
|
+
throw new SimpleError({
|
|
36
|
+
code: 'invalid_permissions_response',
|
|
37
|
+
message: 'Invalid response format for permissions',
|
|
38
|
+
human: $t('Er is iets misgelopen bij het ophalen van je rechten. Probeer het later opnieuw.'),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function checkPermissionsFor(access_token: string, organizationId: string | null, uitpasOrganizerId: string) {
|
|
45
|
+
const url = 'https://api-test.uitpas.be/permissions';
|
|
46
|
+
const myHeaders = new Headers();
|
|
47
|
+
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
48
|
+
myHeaders.append('Accept', 'application/json');
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
headers: myHeaders,
|
|
52
|
+
}).catch(() => {
|
|
53
|
+
// Handle network errors
|
|
54
|
+
throw new SimpleError({
|
|
55
|
+
code: 'uitpas_unreachable_checking_permissions',
|
|
56
|
+
message: `Network issue when checking UiTPAS permissions`,
|
|
57
|
+
human: $t(`We konden UiTPAS niet bereiken om de rechten te controleren. Probeer het later opnieuw.`),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
console.error(`Unsuccessful response whe n checking UiTPAS permissions for organization with id ${organizationId}:`, response.statusText);
|
|
62
|
+
throw new SimpleError({
|
|
63
|
+
code: 'unsuccessful_response_checking_permissions',
|
|
64
|
+
message: `Unsuccesful response when checking UiTPAS permissions`,
|
|
65
|
+
human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const json = await response.json().catch(() => {
|
|
69
|
+
// Handle JSON parsing errors
|
|
70
|
+
throw new SimpleError({
|
|
71
|
+
code: 'invalid_json_checking_permissions',
|
|
72
|
+
message: `Invalid json when checking UiTPAS permissions`,
|
|
73
|
+
human: $t(`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`),
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
assertIsPermissionsResponse(json);
|
|
77
|
+
const neededPermissions = organizationId
|
|
78
|
+
? [{
|
|
79
|
+
permission: 'PASSES_READ',
|
|
80
|
+
human: 'Basis UiTPAS informatie ophalen met UiTPAS nummer',
|
|
81
|
+
}]
|
|
82
|
+
: [{
|
|
83
|
+
permission: 'TARIFFS_READ',
|
|
84
|
+
human: 'Tarieven opvragen',
|
|
85
|
+
}, {
|
|
86
|
+
permission: 'TICKETSALES_REGISTER',
|
|
87
|
+
human: 'Ticketsales registreren',
|
|
88
|
+
}, {
|
|
89
|
+
permission: 'TICKETSALES_SEARCH',
|
|
90
|
+
human: 'Ticketsales zoeken',
|
|
91
|
+
}];
|
|
92
|
+
const item = json.find(item => item.organizer.id === uitpasOrganizerId);
|
|
93
|
+
if (!item) {
|
|
94
|
+
const organizers = Formatter.joinLast(json.map(i => i.organizer.name), ', ', ' ' + $t(' en ') + ' ');
|
|
95
|
+
return {
|
|
96
|
+
status: UitpasClientCredentialsStatus.NoPermissions,
|
|
97
|
+
human: $t('Jouw UiTPAS-integratie heeft geen toegansrechten tot de geselecteerde UiTPAS-organisator, maar wel tot ') + organizers,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const missingPermissions = neededPermissions.filter(needed => !item.permissions.includes(needed.permission));
|
|
101
|
+
if (missingPermissions.length > 0) {
|
|
102
|
+
const missingPermissionsHuman = Formatter.joinLast(missingPermissions.map(p => p.human), ', ', ' ' + $t(' en ') + ' ');
|
|
103
|
+
return {
|
|
104
|
+
status: UitpasClientCredentialsStatus.MissingPermissions,
|
|
105
|
+
human: $t('Jouw UiTPAS-integratie mist de volgende toegangsrechten voor de geselecteerde UiTPAS-organisator: ') + missingPermissionsHuman,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
status: UitpasClientCredentialsStatus.Ok,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { isSimpleError, isSimpleErrors, SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
2
|
+
import { DataValidator } from '@stamhoofd/utility';
|
|
3
|
+
|
|
4
|
+
type UitpasNumberSuccessfulResponse = {
|
|
5
|
+
socialTariff: {
|
|
6
|
+
status: 'ACTIVE' | 'EXPIRED' | 'NONE';
|
|
7
|
+
};
|
|
8
|
+
messages?: Array<{
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type UitpasNumberErrorResponse = {
|
|
14
|
+
title: string; // e.g., "Invalid uitpas number"
|
|
15
|
+
endUserMessage?: {
|
|
16
|
+
nl: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function assertIsUitpasNumberSuccessfulResponse(
|
|
21
|
+
json: unknown,
|
|
22
|
+
): asserts json is UitpasNumberSuccessfulResponse {
|
|
23
|
+
if (
|
|
24
|
+
typeof json !== 'object'
|
|
25
|
+
|| json === null
|
|
26
|
+
|| !('socialTariff' in json)
|
|
27
|
+
|| typeof json.socialTariff !== 'object'
|
|
28
|
+
|| json.socialTariff === null
|
|
29
|
+
|| !('status' in json.socialTariff)
|
|
30
|
+
|| typeof json.socialTariff.status !== 'string'
|
|
31
|
+
|| (json.socialTariff.status !== 'ACTIVE' && json.socialTariff.status !== 'EXPIRED' && json.socialTariff.status !== 'NONE')
|
|
32
|
+
|| ('messages' in json && (!Array.isArray(json.messages) || !json.messages.every(
|
|
33
|
+
(message: unknown) => typeof message === 'object' && message !== null && 'text' in message && typeof message.text === 'string')))
|
|
34
|
+
) {
|
|
35
|
+
console.error('Invalid response when retrieving pass by UiTPAS number:', json);
|
|
36
|
+
throw new SimpleError({
|
|
37
|
+
code: 'invalid_response_retrieving_pass_by_uitpas_number',
|
|
38
|
+
message: `Invalid response when retrieving pass by UiTPAS number`,
|
|
39
|
+
human: $t(`Er is een fout opgetreden bij het ophalen van je UiTPAS. Kijk je het nummer even na?`),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isUitpasNumberErrorResponse(
|
|
45
|
+
json: unknown,
|
|
46
|
+
): json is UitpasNumberErrorResponse {
|
|
47
|
+
return typeof json === 'object'
|
|
48
|
+
&& json !== null
|
|
49
|
+
&& 'title' in json
|
|
50
|
+
&& typeof json.title === 'string'
|
|
51
|
+
&& (!('endUserMessage' in json)
|
|
52
|
+
|| (typeof json.endUserMessage === 'object' && json.endUserMessage !== null && 'nl' in json.endUserMessage && typeof json.endUserMessage.nl === 'string')
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function checkUitpasNumber(access_token: string, uitpasNumber: string) {
|
|
57
|
+
// static check (using regex)
|
|
58
|
+
if (!DataValidator.isUitpasNumberValid(uitpasNumber)) {
|
|
59
|
+
throw new SimpleError({
|
|
60
|
+
code: 'invalid_uitpas_number',
|
|
61
|
+
message: `Invalid UiTPAS number: ${uitpasNumber}`,
|
|
62
|
+
human: $t(
|
|
63
|
+
`Het opgegeven UiTPAS-nummer is ongeldig. Controleer het nummer en probeer het opnieuw.`,
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const baseUrl = 'https://api-test.uitpas.be'; // TO DO: Use the URL from environment variables
|
|
68
|
+
|
|
69
|
+
const url = `${baseUrl}/passes/${uitpasNumber}`;
|
|
70
|
+
const myHeaders = new Headers();
|
|
71
|
+
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
72
|
+
const requestOptions = {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: myHeaders,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
78
|
+
// Handle network errors
|
|
79
|
+
throw new SimpleError({
|
|
80
|
+
code: 'uitpas_unreachable_retrieving_pass_by_uitpas_number',
|
|
81
|
+
message: `Network issue when retrieving pass by UiTPAS number`,
|
|
82
|
+
human: $t(
|
|
83
|
+
`We konden UiTPAS niet bereiken om jouw UiTPAS-nummer te valideren. Probeer het later opnieuw.`,
|
|
84
|
+
),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const json: unknown = await response.json().catch(() => { /* ignore */ });
|
|
89
|
+
let endUserMessage = '';
|
|
90
|
+
|
|
91
|
+
if (json) {
|
|
92
|
+
console.error(`UiTPAS API returned an error for UiTPAS number ${uitpasNumber}:`, json);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.error(`UiTPAS API returned an error for UiTPAS number ${uitpasNumber}:`, response.statusText);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (isUitpasNumberErrorResponse(json)) {
|
|
99
|
+
endUserMessage = json.endUserMessage ? json.endUserMessage.nl : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (endUserMessage) {
|
|
103
|
+
throw new SimpleError({
|
|
104
|
+
code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number',
|
|
105
|
+
message: `Unsuccesful response with message when retrieving pass by UiTPAS number, message: ${endUserMessage}`,
|
|
106
|
+
human: endUserMessage,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw new SimpleError({
|
|
111
|
+
code: 'unsuccessful_and_unexpected_response_retrieving_pass_by_uitpas_number',
|
|
112
|
+
message: `Unsuccesful response without message when retrieving pass by UiTPAS number`,
|
|
113
|
+
human: $t(`Er is een fout opgetreden bij het ophalen van je UiTPAS. Kijk je het nummer even na?`),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const json = await response.json().catch(() => {
|
|
118
|
+
// Handle JSON parsing errors
|
|
119
|
+
throw new SimpleError({
|
|
120
|
+
code: 'invalid_json_retrieving_pass_by_uitpas_number',
|
|
121
|
+
message: `Invalid json when retrieving pass by UiTPAS number`,
|
|
122
|
+
human: $t(
|
|
123
|
+
`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`,
|
|
124
|
+
),
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
assertIsUitpasNumberSuccessfulResponse(json);
|
|
128
|
+
if (json.messages) {
|
|
129
|
+
const humanMessage = json.messages[0].text; // only display the first message
|
|
130
|
+
|
|
131
|
+
// alternatively, join all messages
|
|
132
|
+
// const text = json.messages.map((message: any) => message.text).join(', ');
|
|
133
|
+
|
|
134
|
+
throw new SimpleError({
|
|
135
|
+
code: 'uitpas_number_issue',
|
|
136
|
+
message: `UiTPAS API returned an error: ${humanMessage}`,
|
|
137
|
+
human: humanMessage,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (json.socialTariff.status !== 'ACTIVE') {
|
|
141
|
+
// THIS SHOULD NOT HAPPEN, as in that case json.messages should be present
|
|
142
|
+
throw new SimpleError({
|
|
143
|
+
code: 'non_active_social_tariff',
|
|
144
|
+
message: `UiTPAS social tariff is not ACTIVE but ${json.socialTariff.status}`,
|
|
145
|
+
human: $t(
|
|
146
|
+
`Het opgegeven UiTPAS-nummer heeft geen actief kansentarief. Neem contact op met de UiTPAS-organisatie voor meer informatie.`,
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// no errors -> the uitpas number is valid and social tariff is applicable
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Checks multiple uitpas numbers
|
|
155
|
+
* If any of the uitpas numbers is invalid, it will throw a SimpleErrors instance with all errors.
|
|
156
|
+
* The field of the error will be the index of the uitpas number in the array.
|
|
157
|
+
* @param uitpasNumbers The uitpas numbers to check
|
|
158
|
+
*/
|
|
159
|
+
export async function checkUitpasNumbers(access_token: string, uitpasNumbers: string[]) {
|
|
160
|
+
const simpleErrors = new SimpleErrors();
|
|
161
|
+
for (let i = 0; i < uitpasNumbers.length; i++) {
|
|
162
|
+
const uitpasNumber = uitpasNumbers[i];
|
|
163
|
+
try {
|
|
164
|
+
await checkUitpasNumber(access_token, uitpasNumber); // Throws if invalid
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
168
|
+
e.addNamespace(i.toString());
|
|
169
|
+
e.addNamespace('uitpasNumbers');
|
|
170
|
+
simpleErrors.addError(e);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (simpleErrors.errors.length > 0) {
|
|
178
|
+
throw simpleErrors;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
|
|
3
|
+
type StaticSocialTariffReponse = {
|
|
4
|
+
available: Array<{
|
|
5
|
+
price: number;
|
|
6
|
+
// other properties ignored
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function assertsIsStaticSocialTariffResponse(json: unknown): asserts json is StaticSocialTariffReponse {
|
|
11
|
+
if (
|
|
12
|
+
typeof json !== 'object'
|
|
13
|
+
|| json === null
|
|
14
|
+
|| !('available' in json)
|
|
15
|
+
|| !Array.isArray(json.available)
|
|
16
|
+
|| !json.available.every(
|
|
17
|
+
(item: unknown) => typeof item === 'object' && item !== null && 'price' in item && typeof item.price === 'number',
|
|
18
|
+
)
|
|
19
|
+
) {
|
|
20
|
+
console.error('Invalid response when getting static UiTPAS social tariff:', json);
|
|
21
|
+
throw new SimpleError({
|
|
22
|
+
code: 'invalid_response_getting_static_uitpas_social_tariff',
|
|
23
|
+
message: `Invalid response when getting static UiTPAS social tariff`,
|
|
24
|
+
human: $t(`Er is een fout opgetreden bij het ophalen van het kansentarief voor dit evenement. Probeer het later opnieuw.`),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getSocialTariffForEvent(access_token: string, basePrice: number, uitpasEventUrl: string) {
|
|
30
|
+
const baseUrl = 'https://api-test.uitpas.be/tariffs/static';
|
|
31
|
+
const params = new URLSearchParams();
|
|
32
|
+
params.append('regularPrice', (basePrice / 100).toFixed(2));
|
|
33
|
+
const eventId = uitpasEventUrl.split('/').pop(); // Extract the event ID from the URL
|
|
34
|
+
if (!eventId) {
|
|
35
|
+
throw new SimpleError({
|
|
36
|
+
code: 'invalid_uitpas_event_url',
|
|
37
|
+
message: `Invalid UiTPAS event URL: ${uitpasEventUrl}`,
|
|
38
|
+
human: $t(`De opgegeven UiTPAS-evenement URL is ongeldig.`),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
params.append('eventId', eventId);
|
|
42
|
+
const url = `${baseUrl}?${params.toString()}`;
|
|
43
|
+
const myHeaders = new Headers();
|
|
44
|
+
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
45
|
+
myHeaders.append('Accept', 'application/json');
|
|
46
|
+
const requestOptions = {
|
|
47
|
+
method: 'GET',
|
|
48
|
+
headers: myHeaders,
|
|
49
|
+
};
|
|
50
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
51
|
+
// Handle network errors
|
|
52
|
+
throw new SimpleError({
|
|
53
|
+
code: 'uitpas_unreachable_getting_static_uitpas_social_tariff',
|
|
54
|
+
message: `Network issue when getting static UiTPAS social tariff`,
|
|
55
|
+
human: $t(
|
|
56
|
+
`We konden UiTPAS niet bereiken om het kansentarief op te zoeken. Probeer het later opnieuw.`,
|
|
57
|
+
),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new SimpleError({
|
|
62
|
+
code: 'unsuccessful_response_getting_static_uitpas_social_tariff',
|
|
63
|
+
message: `Unsuccessful response when getting static UiTPAS social tariff`,
|
|
64
|
+
human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const json = await response.json().catch(() => {
|
|
68
|
+
// Handle JSON parsing errors
|
|
69
|
+
throw new SimpleError({
|
|
70
|
+
code: 'invalid_json_getting_static_uitpas_social_tariff',
|
|
71
|
+
message: `Invalid json when getting static UiTPAS social tariff`,
|
|
72
|
+
human: $t(
|
|
73
|
+
`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`,
|
|
74
|
+
),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assertsIsStaticSocialTariffResponse(json);
|
|
79
|
+
if (json.available.length === 0) {
|
|
80
|
+
throw new SimpleError({
|
|
81
|
+
code: 'no_social_tariff_available',
|
|
82
|
+
message: `No social tariff available for event ${eventId}`,
|
|
83
|
+
human: $t(`Er is geen kansentarief beschikbaar voor dit evenement.`),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (json.available.length > 1) {
|
|
87
|
+
console.warn('Multiple social tariffs available for event', eventId, '(used ', json.available[0].price, ' as base price. All options:', json.available);
|
|
88
|
+
}
|
|
89
|
+
return Math.round(json.available[0].price * 100);
|
|
90
|
+
}
|