@stamhoofd/backend 2.90.1 → 2.90.3
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/endpoints/{admin/organizations → organization/dashboard/organization}/SearchUitpasOrganizersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +51 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +1 -1
- package/src/helpers/UitpasTokenRepository.ts +3 -3
- package/src/services/uitpas/UitpasService.ts +206 -69
- package/src/services/uitpas/cancelTicketSales.ts +41 -0
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +10 -4
- package/src/services/uitpas/registerTicketSales.ts +139 -0
- package/src/services/uitpas/searchUitpasEvents.ts +141 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.90.
|
|
3
|
+
"version": "2.90.3",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
46
46
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
47
47
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
48
|
-
"@stamhoofd/backend-i18n": "2.90.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.90.
|
|
50
|
-
"@stamhoofd/email": "2.90.
|
|
51
|
-
"@stamhoofd/models": "2.90.
|
|
52
|
-
"@stamhoofd/queues": "2.90.
|
|
53
|
-
"@stamhoofd/sql": "2.90.
|
|
54
|
-
"@stamhoofd/structures": "2.90.
|
|
55
|
-
"@stamhoofd/utility": "2.90.
|
|
48
|
+
"@stamhoofd/backend-i18n": "2.90.3",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.90.3",
|
|
50
|
+
"@stamhoofd/email": "2.90.3",
|
|
51
|
+
"@stamhoofd/models": "2.90.3",
|
|
52
|
+
"@stamhoofd/queues": "2.90.3",
|
|
53
|
+
"@stamhoofd/sql": "2.90.3",
|
|
54
|
+
"@stamhoofd/structures": "2.90.3",
|
|
55
|
+
"@stamhoofd/utility": "2.90.3",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"axios": "^1.8.2",
|
|
58
58
|
"cookie": "^0.7.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "81248f4f4d578fb67e29c48c55ddb1a1beb12313"
|
|
74
74
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { UitpasService } from '
|
|
2
|
+
import { UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
3
3
|
import { UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
4
4
|
import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
|
-
import { Context } from '
|
|
5
|
+
import { Context } from '../../../../helpers/Context';
|
|
6
6
|
|
|
7
7
|
type Params = Record<string, never>;
|
|
8
8
|
class Query extends AutoEncoder {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
3
|
+
import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
4
|
+
import { Context } from '../../../../helpers/Context';
|
|
5
|
+
import { UitpasEventsResponse } from '@stamhoofd/structures';
|
|
6
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
class Query extends AutoEncoder {
|
|
10
|
+
@field({ decoder: StringDecoder })
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
type Body = undefined;
|
|
14
|
+
type ResponseBody = UitpasEventsResponse;
|
|
15
|
+
|
|
16
|
+
export class SearchUitpasEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
17
|
+
queryDecoder = Query as Decoder<Query>;
|
|
18
|
+
|
|
19
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
20
|
+
if (request.method !== 'GET') {
|
|
21
|
+
return [false];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const params = Endpoint.parseParameters(request.url, '/organization/search-uitpas-events', {});
|
|
25
|
+
|
|
26
|
+
if (params) {
|
|
27
|
+
return [true, params as Params];
|
|
28
|
+
}
|
|
29
|
+
return [false];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
33
|
+
const organization = await Context.setOrganizationScope();
|
|
34
|
+
await Context.authenticate();
|
|
35
|
+
|
|
36
|
+
if (!await Context.auth.hasFullAccess(organization.id)) {
|
|
37
|
+
throw Context.auth.error();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!organization.meta.uitpasOrganizerId) {
|
|
41
|
+
throw new SimpleError({
|
|
42
|
+
code: 'no_uitpas_organizer_id',
|
|
43
|
+
message: `No UiTPAS organizer ID set for organization`,
|
|
44
|
+
human: $t(`Deze organisatie heeft nog geen UiTPAS-organisatie ingesteld. Stel dit in via de algemene instellingen.`),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const uitpasOrganizersResponse = await UitpasService.searchUitpasEvents(organization.id, organization.meta.uitpasOrganizerId, request.query.text);
|
|
48
|
+
|
|
49
|
+
return new Response(uitpasOrganizersResponse);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -173,7 +173,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
173
173
|
if (item.product.uitpasEvent) {
|
|
174
174
|
const basePrice = item.product.prices.find(p => p.id === item.productPrice.uitpasBaseProductPriceId)?.price ?? 0;
|
|
175
175
|
const reducedPrices = await UitpasService.getSocialTariffForUitpasNumbers(organization.id, uitpasNumbersOnly, basePrice, item.product.uitpasEvent.url);
|
|
176
|
-
const expectedReducedPrices = item.uitpasNumbers
|
|
176
|
+
const expectedReducedPrices = item.uitpasNumbers;
|
|
177
177
|
if (reducedPrices.length < expectedReducedPrices.length) {
|
|
178
178
|
// should not happen
|
|
179
179
|
throw new SimpleError({
|
|
@@ -184,14 +184,15 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
for (let i = 0; i < expectedReducedPrices.length; i++) {
|
|
187
|
-
if (reducedPrices[i] !== expectedReducedPrices[i]) {
|
|
187
|
+
if (reducedPrices[i].price !== expectedReducedPrices[i].price) {
|
|
188
188
|
throw new SimpleError({
|
|
189
189
|
code: 'uitpas_social_tariff_price_mismatch',
|
|
190
190
|
message: 'UiTPAS social tariff have a different price',
|
|
191
|
-
human: $t('Het kansentarief voor deze UiTPAS is {correctPrice} in plaats van {orderPrice}.', { correctPrice: Formatter.price(reducedPrices[i]), orderPrice: Formatter.price(expectedReducedPrices[i]) }),
|
|
191
|
+
human: $t('Het kansentarief voor deze UiTPAS is {correctPrice} in plaats van {orderPrice}.', { correctPrice: Formatter.price(reducedPrices[i].price), orderPrice: Formatter.price(expectedReducedPrices[i].price) }),
|
|
192
192
|
field: 'uitpasNumbers.' + i.toString(),
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
|
+
item.uitpasNumbers[i].uitpasTariffId = reducedPrices[i].uitpasTariffId;
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
else {
|
|
@@ -52,7 +52,7 @@ export class RetrieveUitpasSocialTariffPricesEndpoint extends Endpoint<Params, Q
|
|
|
52
52
|
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
53
53
|
const reducedPrices = await UitpasService.getSocialTariffForUitpasNumbers(organization.id, request.body.uitpasNumbers, request.body.basePrice, request.body.uitpasEventUrl); // Throws if invalid
|
|
54
54
|
const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
|
|
55
|
-
prices: reducedPrices,
|
|
55
|
+
prices: reducedPrices.map(price => price.price), // ignore tariff id's here
|
|
56
56
|
});
|
|
57
57
|
return new Response(uitpasPriceCheckResponse);
|
|
58
58
|
}
|
|
@@ -203,10 +203,10 @@ export class UitpasTokenRepository {
|
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
static async getClientIdFor(
|
|
207
|
-
const repo = UitpasTokenRepository.getRepoFromMemory(
|
|
206
|
+
static async getClientIdFor(organizationId: string | null): Promise<string> {
|
|
207
|
+
const repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
|
|
208
208
|
if (!repo) {
|
|
209
|
-
const model = await UitpasClientCredential.select().where('organizationId',
|
|
209
|
+
const model = await UitpasClientCredential.select().where('organizationId', organizationId).first(false);
|
|
210
210
|
if (!model) {
|
|
211
211
|
return ''; // no client ID and secret configured
|
|
212
212
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
2
|
import { Order, WebshopUitpasNumber } from '@stamhoofd/models';
|
|
3
|
-
import { OrderStatus, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
3
|
+
import { 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';
|
|
@@ -8,88 +8,225 @@ import { checkPermissionsFor } from './checkPermissionsFor';
|
|
|
8
8
|
import { checkUitpasNumbers } from './checkUitpasNumbers';
|
|
9
9
|
import { getSocialTariffForEvent } from './getSocialTariffForEvent';
|
|
10
10
|
import { getSocialTariffForUitpasNumbers } from './getSocialTariffForUitpasNumbers';
|
|
11
|
+
import { searchUitpasEvents } from './searchUitpasEvents';
|
|
12
|
+
import { RegisterTicketSaleRequest, RegisterTicketSaleResponse, registerTicketSales } from './registerTicketSales';
|
|
13
|
+
import { cancelTicketSales } from './cancelTicketSales';
|
|
14
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
15
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
16
|
+
|
|
17
|
+
type UitpasTicketSale = {
|
|
18
|
+
basePrice: number;
|
|
19
|
+
uitpasNumber: string;
|
|
20
|
+
basePriceLabel: string;
|
|
21
|
+
reducedPrice: number;
|
|
22
|
+
uitpasEventUrl: string | null;
|
|
23
|
+
uitpasTariffId: string | null;
|
|
24
|
+
productId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type InsertUitpasNumber = {
|
|
28
|
+
ticketSaleId: string | null;
|
|
29
|
+
reducedPriceUitpas: number | null;
|
|
30
|
+
registeredAt: Date | null;
|
|
31
|
+
webshopId: string;
|
|
32
|
+
orderId: string;
|
|
33
|
+
productId: string;
|
|
34
|
+
uitpasNumber: string;
|
|
35
|
+
basePrice: number;
|
|
36
|
+
reducedPrice: number;
|
|
37
|
+
basePriceLabel: string;
|
|
38
|
+
uitpasTariffId: string | null; // null for non-official flow
|
|
39
|
+
uitpasEventUrl: string | null; // null for non-official flow
|
|
40
|
+
};
|
|
11
41
|
|
|
12
42
|
function shouldReserveUitpasNumbers(status: OrderStatus): boolean {
|
|
13
43
|
return status !== OrderStatus.Canceled && status !== OrderStatus.Deleted;
|
|
14
44
|
}
|
|
15
45
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
function areThereUitpasChanges(oldTicketSales: UitpasTicketSale[], newTicketSales: UitpasTicketSale[]): boolean {
|
|
47
|
+
if (oldTicketSales.length !== newTicketSales.length) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
for (const oldTicketSale of oldTicketSales) {
|
|
51
|
+
const newTicketSale = newTicketSales.find(
|
|
52
|
+
ts =>
|
|
53
|
+
ts.uitpasNumber === oldTicketSale.uitpasNumber
|
|
54
|
+
&& ts.basePrice === oldTicketSale.basePrice
|
|
55
|
+
&& ts.basePriceLabel === oldTicketSale.basePriceLabel
|
|
56
|
+
&& ts.reducedPrice === oldTicketSale.reducedPrice
|
|
57
|
+
&& ts.uitpasTariffId === oldTicketSale.uitpasTariffId
|
|
58
|
+
&& ts.uitpasEventUrl === oldTicketSale.uitpasEventUrl
|
|
59
|
+
&& ts.productId === oldTicketSale.productId,
|
|
60
|
+
);
|
|
61
|
+
if (!newTicketSale) {
|
|
62
|
+
return true;
|
|
26
63
|
}
|
|
27
64
|
}
|
|
28
|
-
return
|
|
65
|
+
return false;
|
|
29
66
|
}
|
|
30
67
|
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return true;
|
|
68
|
+
function getUitpasTicketSales(order: Order): UitpasTicketSale[] {
|
|
69
|
+
const ticketSales: UitpasTicketSale[] = [];
|
|
70
|
+
if (!shouldReserveUitpasNumbers(order.status)) {
|
|
71
|
+
return ticketSales;
|
|
36
72
|
}
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
for (const item of order.data.cart.items) {
|
|
74
|
+
if (item.uitpasNumbers.length > 0) {
|
|
75
|
+
const baseProductPrice = item.product.prices.filter(price => price.id === item.productPrice.uitpasBaseProductPriceId)[0];
|
|
76
|
+
if (!baseProductPrice) {
|
|
77
|
+
throw new SimpleError({
|
|
78
|
+
code: 'missing_uitpas_base_product_price',
|
|
79
|
+
message: `Missing UiTPAS base product price`,
|
|
80
|
+
human: $t(`Er is een fout opgetreden bij het registreren van de UiTPAS ticket verkoop. Probeer het later opnieuw.`),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const label = makeBaseProductPriceLabel(item.product, baseProductPrice);
|
|
84
|
+
for (const uitpasNumber of item.uitpasNumbers) {
|
|
85
|
+
ticketSales.push({
|
|
86
|
+
productId: item.product.id,
|
|
87
|
+
uitpasNumber: uitpasNumber.uitpasNumber,
|
|
88
|
+
basePrice: baseProductPrice.price,
|
|
89
|
+
reducedPrice: uitpasNumber.price,
|
|
90
|
+
basePriceLabel: label,
|
|
91
|
+
uitpasEventUrl: item.product.uitpasEvent?.url || null,
|
|
92
|
+
uitpasTariffId: uitpasNumber.uitpasTariffId || null,
|
|
93
|
+
});
|
|
48
94
|
}
|
|
49
95
|
}
|
|
50
96
|
}
|
|
51
|
-
return
|
|
97
|
+
return ticketSales;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function makeBaseProductPriceLabel(product: Product, productPrice: ProductPrice): string {
|
|
101
|
+
if (product.name && productPrice.name) {
|
|
102
|
+
return product.name + ' - ' + productPrice.name;
|
|
103
|
+
}
|
|
104
|
+
if (productPrice.name) {
|
|
105
|
+
return productPrice.name;
|
|
106
|
+
}
|
|
107
|
+
return product.name;
|
|
52
108
|
}
|
|
53
109
|
|
|
54
110
|
export class UitpasService {
|
|
55
111
|
static listening = false;
|
|
56
112
|
|
|
57
|
-
static async
|
|
58
|
-
await
|
|
59
|
-
await this.createUitpasNumbers(order);
|
|
113
|
+
static async getWebshopUitpasNumberFromDb(order: Order): Promise<WebshopUitpasNumber[]> {
|
|
114
|
+
return await WebshopUitpasNumber.select().where('webshopId', order.webshopId).andWhere('orderId', order.id).fetch();
|
|
60
115
|
}
|
|
61
116
|
|
|
62
|
-
static async createUitpasNumbers(
|
|
63
|
-
|
|
117
|
+
static async createUitpasNumbers(toBeInserted: InsertUitpasNumber[]) {
|
|
118
|
+
if (toBeInserted.length === 0) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
64
121
|
// add to DB
|
|
65
122
|
const insert = WebshopUitpasNumber.insert();
|
|
66
123
|
insert.columns(
|
|
67
124
|
'id',
|
|
125
|
+
'ticketSaleId',
|
|
126
|
+
'reducedPriceUitpas',
|
|
127
|
+
'registeredAt',
|
|
68
128
|
'webshopId',
|
|
69
129
|
'orderId',
|
|
70
130
|
'productId',
|
|
71
131
|
'uitpasNumber',
|
|
132
|
+
'basePrice',
|
|
133
|
+
'reducedPrice',
|
|
134
|
+
'basePriceLabel',
|
|
135
|
+
'uitpasTariffId',
|
|
136
|
+
'uitpasEventUrl',
|
|
72
137
|
);
|
|
73
|
-
const rows =
|
|
74
|
-
return
|
|
138
|
+
const rows = toBeInserted.map((insert) => {
|
|
139
|
+
return [
|
|
75
140
|
uuidv4(),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
141
|
+
insert.ticketSaleId,
|
|
142
|
+
insert.reducedPriceUitpas,
|
|
143
|
+
insert.registeredAt,
|
|
144
|
+
insert.webshopId,
|
|
145
|
+
insert.orderId,
|
|
146
|
+
insert.productId,
|
|
147
|
+
insert.uitpasNumber,
|
|
148
|
+
insert.basePrice,
|
|
149
|
+
insert.reducedPrice,
|
|
150
|
+
insert.basePriceLabel,
|
|
151
|
+
insert.uitpasTariffId,
|
|
152
|
+
insert.uitpasEventUrl,
|
|
153
|
+
];
|
|
81
154
|
});
|
|
82
|
-
|
|
83
|
-
// No uitpas numbers to insert, skipping
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
155
|
+
|
|
86
156
|
insert.values(...rows);
|
|
87
157
|
await insert.insert();
|
|
88
158
|
}
|
|
89
159
|
|
|
90
|
-
static async
|
|
91
|
-
|
|
92
|
-
|
|
160
|
+
static async updateTicketSales(order: Order, isNewOrder: boolean, ticketSalesHint?: UitpasTicketSale[]): Promise<void> {
|
|
161
|
+
const ticketSales = ticketSalesHint ?? getUitpasTicketSales(order);
|
|
162
|
+
|
|
163
|
+
// queue on order, so no race conditions if the same order is updated multiple times in short time period
|
|
164
|
+
return await QueueHandler.schedule('uitpas-order-' + order.id, async () => {
|
|
165
|
+
const registered = isNewOrder ? [] : await this.getWebshopUitpasNumberFromDb(order);
|
|
166
|
+
|
|
167
|
+
const unchangedRegistered: WebshopUitpasNumber[] = [];
|
|
168
|
+
const toBeRegistered: UitpasTicketSale[] = [];
|
|
169
|
+
|
|
170
|
+
for (const ticketSale of ticketSales) {
|
|
171
|
+
const i = registered.findIndex(request => request.uitpasNumber === ticketSale.uitpasNumber && request.basePrice === ticketSale.basePrice && request.basePriceLabel === ticketSale.basePriceLabel);
|
|
172
|
+
if (i !== -1) {
|
|
173
|
+
unchangedRegistered.push(registered[i]);
|
|
174
|
+
registered.splice(i, 1);
|
|
175
|
+
continue; // already registered, so skip
|
|
176
|
+
}
|
|
177
|
+
toBeRegistered.push(ticketSale);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const toBeCanceled = registered;
|
|
181
|
+
|
|
182
|
+
// Only register/cancel tickets if official flow is/was used
|
|
183
|
+
const toBeCanceledUitpasIds = toBeCanceled.filter(c => c.uitpasEventUrl && c.uitpasTariffId && c.ticketSaleId).map(c => c.ticketSaleId!);
|
|
184
|
+
const toBeRegisteredUitpasRequests: RegisterTicketSaleRequest[] = toBeRegistered.filter(c => c.uitpasEventUrl && c.uitpasTariffId) as RegisterTicketSaleRequest[];
|
|
185
|
+
const noUitpasTariffId = toBeRegistered.filter(c => c.uitpasEventUrl && !c.uitpasTariffId);
|
|
186
|
+
if (noUitpasTariffId.length > 0) {
|
|
187
|
+
console.warn('Some UiTPAS do not have an uitpasTariffId, although an UiTPAS event is linked (official flow)', noUitpasTariffId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let canceledUitpasId: string[] = [];
|
|
191
|
+
let newlyRegistered: Map<RegisterTicketSaleRequest, RegisterTicketSaleResponse> = new Map();
|
|
192
|
+
if (toBeRegisteredUitpasRequests.length !== 0 || toBeCanceledUitpasIds.length !== 0) {
|
|
193
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(order.organizationId);
|
|
194
|
+
canceledUitpasId = await cancelTicketSales(access_token, toBeCanceledUitpasIds);
|
|
195
|
+
if (canceledUitpasId.length !== toBeCanceledUitpasIds.length) {
|
|
196
|
+
console.error('Failed to cancel some UiTPAS ticket sales, successfully canceled:', canceledUitpasId, 'but tried to cancel:', toBeCanceledUitpasIds);
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
newlyRegistered = await registerTicketSales(access_token, toBeRegisteredUitpasRequests);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.error('Failed to register UiTPAS ticket sales', e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const effectiveDeletes = toBeCanceled.filter(c => c.ticketSaleId === null || canceledUitpasId.find(id => id === c.ticketSaleId));
|
|
207
|
+
if (effectiveDeletes.length !== 0) {
|
|
208
|
+
await WebshopUitpasNumber.delete().where('id', effectiveDeletes.map(c => c.id));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const inserts = toBeRegistered.map((c) => {
|
|
212
|
+
const response = newlyRegistered.get(c as RegisterTicketSaleRequest);
|
|
213
|
+
return {
|
|
214
|
+
ticketSaleId: response?.ticketSaleId ?? null,
|
|
215
|
+
reducedPriceUitpas: response?.reducedPriceUitpas ?? null,
|
|
216
|
+
registeredAt: response?.registeredAt ?? null,
|
|
217
|
+
webshopId: order.webshopId,
|
|
218
|
+
orderId: order.id,
|
|
219
|
+
productId: c.productId,
|
|
220
|
+
uitpasNumber: c.uitpasNumber,
|
|
221
|
+
basePrice: c.basePrice,
|
|
222
|
+
reducedPrice: c.reducedPrice,
|
|
223
|
+
basePriceLabel: c.basePriceLabel,
|
|
224
|
+
uitpasTariffId: c.uitpasTariffId, // null for non-official flow
|
|
225
|
+
uitpasEventUrl: c.uitpasEventUrl, // null for non-official flow
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
await this.createUitpasNumbers(inserts);
|
|
229
|
+
});
|
|
93
230
|
}
|
|
94
231
|
|
|
95
232
|
static listen() {
|
|
@@ -100,9 +237,11 @@ export class UitpasService {
|
|
|
100
237
|
Model.modelEventBus.addListener(this, async (event) => {
|
|
101
238
|
try {
|
|
102
239
|
if (event.model instanceof Order) {
|
|
103
|
-
// event.type ==='
|
|
240
|
+
// if (event.type === 'deleted') {
|
|
241
|
+
// delete from db is not not needed as foreign key will delete the order
|
|
242
|
+
// we do not cancel the ticket sales
|
|
104
243
|
if (event.type === 'created' && shouldReserveUitpasNumbers(event.model.status)) {
|
|
105
|
-
await this.
|
|
244
|
+
await this.updateTicketSales(event.model, true);
|
|
106
245
|
return;
|
|
107
246
|
}
|
|
108
247
|
if (event.type === 'updated') {
|
|
@@ -111,18 +250,16 @@ export class UitpasService {
|
|
|
111
250
|
const statusAfter = event.changedFields.status as OrderStatus;
|
|
112
251
|
const shouldReserveAfter = shouldReserveUitpasNumbers(statusAfter);
|
|
113
252
|
if (shouldReserveUitpasNumbers(statusBefore) !== shouldReserveAfter) {
|
|
114
|
-
|
|
115
|
-
await this.createUitpasNumbers(event.model);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
await this.deleteUitpasNumbers(event.model);
|
|
253
|
+
await this.updateTicketSales(event.model, shouldReserveAfter);
|
|
119
254
|
return;
|
|
120
255
|
}
|
|
121
256
|
}
|
|
122
257
|
if (event.changedFields.data) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
258
|
+
const oldTicketSales = getUitpasTicketSales(event.getOldModel() as Order);
|
|
259
|
+
const newTicketSales = getUitpasTicketSales(event.model);
|
|
260
|
+
if (areThereUitpasChanges(oldTicketSales, newTicketSales)) {
|
|
261
|
+
await this.updateTicketSales(event.model, false, newTicketSales);
|
|
262
|
+
return;
|
|
126
263
|
}
|
|
127
264
|
}
|
|
128
265
|
}
|
|
@@ -134,24 +271,22 @@ export class UitpasService {
|
|
|
134
271
|
});
|
|
135
272
|
}
|
|
136
273
|
|
|
137
|
-
static async getSocialTariffForUitpasNumbers(
|
|
274
|
+
static async getSocialTariffForUitpasNumbers(organizationId: string, uitpasNumbers: string[], basePrice: number, uitpasEventUrl: string) {
|
|
138
275
|
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-tariffs
|
|
139
|
-
const access_token = await UitpasTokenRepository.getAccessTokenFor(
|
|
276
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(organizationId);
|
|
140
277
|
return await getSocialTariffForUitpasNumbers(access_token, uitpasNumbers, basePrice, uitpasEventUrl);
|
|
141
278
|
}
|
|
142
279
|
|
|
143
|
-
static async getSocialTariffForEvent(
|
|
280
|
+
static async getSocialTariffForEvent(organizationId: string, basePrice: number, uitpasEventUrl: string) {
|
|
144
281
|
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/get-a-tariff-static
|
|
145
|
-
const access_token = await UitpasTokenRepository.getAccessTokenFor(
|
|
282
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(organizationId);
|
|
146
283
|
return await getSocialTariffForEvent(access_token, basePrice, uitpasEventUrl);
|
|
147
284
|
}
|
|
148
285
|
|
|
149
|
-
static async
|
|
150
|
-
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/create-a-ticket-sale
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
static async cancelTicketSale() {
|
|
286
|
+
static async cancelTicketSales(organisationId: string, ticketSaleIds: string[]) {
|
|
154
287
|
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/delete-a-ticket-sale
|
|
288
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(organisationId);
|
|
289
|
+
return await cancelTicketSales(access_token, ticketSaleIds);
|
|
155
290
|
}
|
|
156
291
|
|
|
157
292
|
static async getTicketSales() {
|
|
@@ -162,9 +297,11 @@ export class UitpasService {
|
|
|
162
297
|
// https://api-test.uitpas.be/checkins
|
|
163
298
|
}
|
|
164
299
|
|
|
165
|
-
static searchUitpasEvents(organizationId: string, uitpasOrganizerId: string, textQuery?: string) {
|
|
300
|
+
static async searchUitpasEvents(organizationId: string, uitpasOrganizerId: string, textQuery?: string) {
|
|
166
301
|
// input = client id of organization (never platform0 & uitpasOrganizerId
|
|
167
302
|
// https://docs.publiq.be/docs/uitpas/events/searching#searching-for-uitpas-events-of-one-specific-organizer
|
|
303
|
+
const clientId = await UitpasTokenRepository.getClientIdFor(organizationId);
|
|
304
|
+
return searchUitpasEvents(clientId, uitpasOrganizerId, textQuery);
|
|
168
305
|
}
|
|
169
306
|
|
|
170
307
|
static async searchUitpasOrganizers(name: string): Promise<UitpasOrganizersResponse> {
|
|
@@ -184,7 +321,7 @@ export class UitpasService {
|
|
|
184
321
|
|
|
185
322
|
/**
|
|
186
323
|
* 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
|
|
324
|
+
* @param organizationId
|
|
188
325
|
* @returns clientId or empty string if not configured
|
|
189
326
|
*/
|
|
190
327
|
static async getClientIdFor(organizationId: string | null): Promise<string> {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
|
|
3
|
+
async function cancelTicketSale(access_token: string, ticketSaleId: string) {
|
|
4
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/delete-a-ticket-sale
|
|
5
|
+
const url = 'https://api-test.uitpas.be/ticket-sales/' + ticketSaleId;
|
|
6
|
+
const myHeaders = new Headers();
|
|
7
|
+
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
8
|
+
myHeaders.append('Accept', 'application/json');
|
|
9
|
+
const requestOptions = {
|
|
10
|
+
method: 'DELETE',
|
|
11
|
+
headers: myHeaders,
|
|
12
|
+
};
|
|
13
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
14
|
+
// Handle network errors
|
|
15
|
+
throw new SimpleError({
|
|
16
|
+
code: 'uitpas_unreachable_registering_ticket_sales',
|
|
17
|
+
message: `Network issue when registering UiTPAS ticket sales`,
|
|
18
|
+
human: $t(
|
|
19
|
+
`We konden UiTPAS niet bereiken om de ticket verkoop te registreren bij UiTPAS. Probeer het later opnieuw.`,
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new SimpleError({
|
|
25
|
+
code: 'unsuccessful_response_registering_ticket_sales',
|
|
26
|
+
message: `Unsuccessful response when registering UiTPAS ticket sales`,
|
|
27
|
+
human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return ticketSaleId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Will never throw an error, but will return array of successfully canceled ticket sale ids
|
|
35
|
+
*/
|
|
36
|
+
export async function cancelTicketSales(access_token: string, ticketSaleIds: string[]) {
|
|
37
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/delete-a-ticket-sale
|
|
38
|
+
const promises = ticketSaleIds.map(ticketSaleId => cancelTicketSale(access_token, ticketSaleId));
|
|
39
|
+
const results = await Promise.allSettled(promises);
|
|
40
|
+
return results.filter(result => result.status === 'fulfilled').map(result => result.value);
|
|
41
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isSimpleError, isSimpleErrors, SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
2
|
+
import { UitpasNumberAndPrice } from '@stamhoofd/structures';
|
|
2
3
|
|
|
3
4
|
type SocialTariffReponse = {
|
|
4
5
|
available: Array<{
|
|
6
|
+
id: string;
|
|
5
7
|
price: number;
|
|
6
8
|
remaining: number;
|
|
7
9
|
// other properties ignored
|
|
@@ -25,7 +27,7 @@ function assertsIsSocialTariffResponse(json: unknown): asserts json is SocialTar
|
|
|
25
27
|
|| !('available' in json)
|
|
26
28
|
|| !Array.isArray(json.available)
|
|
27
29
|
|| !json.available.every(
|
|
28
|
-
(item: unknown) => typeof item === 'object' && item !== null && 'price' in item && typeof item.price === 'number' && 'remaining' in item && typeof item.remaining === 'number',
|
|
30
|
+
(item: unknown) => typeof item === 'object' && item !== null && 'id' in item && typeof item.id === 'string' && 'price' in item && typeof item.price === 'number' && 'remaining' in item && typeof item.remaining === 'number',
|
|
29
31
|
)
|
|
30
32
|
) {
|
|
31
33
|
console.error('Invalid response when getting UiTPAS social tariff:', json);
|
|
@@ -152,16 +154,20 @@ async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber
|
|
|
152
154
|
});
|
|
153
155
|
}
|
|
154
156
|
console.log('Social tariff for UiTPAS number', uitpasNumber, 'with event id', uitpasEventUrl, 'is', json.available[0].price, 'euros');
|
|
155
|
-
return
|
|
157
|
+
return UitpasNumberAndPrice.create({
|
|
158
|
+
uitpasNumber,
|
|
159
|
+
price: Math.round((json.available[0].price) * 100),
|
|
160
|
+
uitpasTariffId: json.available[0].id,
|
|
161
|
+
});
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
export async function getSocialTariffForUitpasNumbers(access_token: string, uitpasNumbers: string[], basePrice: number, uitpasEventUrl: string) {
|
|
159
165
|
const simpleErrors = new SimpleErrors();
|
|
160
|
-
const reducedPrices = new Array<
|
|
166
|
+
const reducedPrices = new Array<UitpasNumberAndPrice>(uitpasNumbers.length);
|
|
161
167
|
for (let i = 0; i < uitpasNumbers.length; i++) {
|
|
162
168
|
const uitpasNumber = uitpasNumbers[i];
|
|
163
169
|
try {
|
|
164
|
-
reducedPrices[i] = await getSocialTariffForUitpasNumber(access_token, uitpasNumber, basePrice, uitpasEventUrl);
|
|
170
|
+
reducedPrices[i] = await getSocialTariffForUitpasNumber(access_token, uitpasNumber, basePrice, uitpasEventUrl);
|
|
165
171
|
}
|
|
166
172
|
catch (e) {
|
|
167
173
|
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
|
|
3
|
+
export type RegisterTicketSaleRequest = {
|
|
4
|
+
basePrice: number;
|
|
5
|
+
uitpasEventUrl: string;
|
|
6
|
+
basePriceLabel: string;
|
|
7
|
+
uitpasNumber: string;
|
|
8
|
+
uitpasTariffId: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type RegisterTicketSaleResponse = {
|
|
12
|
+
ticketSaleId: string;
|
|
13
|
+
reducedPriceUitpas: number;
|
|
14
|
+
registeredAt: Date;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SuccessResponse = {
|
|
18
|
+
id: string;
|
|
19
|
+
tariff: {
|
|
20
|
+
price: number;
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
uitpasNumber: string;
|
|
24
|
+
eventId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function assertisSuccessResponse(json: unknown): asserts json is SuccessResponse {
|
|
28
|
+
if (
|
|
29
|
+
typeof json !== 'object'
|
|
30
|
+
|| json === null
|
|
31
|
+
|| !('tariff' in json)
|
|
32
|
+
|| typeof json.tariff !== 'object'
|
|
33
|
+
|| json.tariff === null
|
|
34
|
+
|| !('price' in json.tariff)
|
|
35
|
+
|| typeof json.tariff.price !== 'number'
|
|
36
|
+
|| !('id' in json.tariff)
|
|
37
|
+
|| typeof json.tariff.id !== 'string'
|
|
38
|
+
|| !('id' in json)
|
|
39
|
+
|| typeof json.id !== 'string'
|
|
40
|
+
|| !('uitpasNumber' in json)
|
|
41
|
+
|| typeof json.uitpasNumber !== 'string'
|
|
42
|
+
|| !('eventId' in json)
|
|
43
|
+
|| typeof json.eventId !== 'string'
|
|
44
|
+
) {
|
|
45
|
+
console.error('Invalid register ticket sale response', json);
|
|
46
|
+
throw new SimpleError({
|
|
47
|
+
code: 'invalid_register_ticket_sale_response',
|
|
48
|
+
message: `Invalid register ticket sale response`,
|
|
49
|
+
human: $t(`Er is een fout opgetreden bij het registreren van de UiTPAS-ticketverkoop.`),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @returns Map of request (from parameters) -> response
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
export async function registerTicketSales(access_token: string, registerTicketSaleRequests: RegisterTicketSaleRequest[]): Promise<Map<RegisterTicketSaleRequest, RegisterTicketSaleResponse>> {
|
|
59
|
+
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/create-a-ticket-sale
|
|
60
|
+
|
|
61
|
+
console.error('Registering ticket sales', registerTicketSaleRequests);
|
|
62
|
+
if (registerTicketSaleRequests.length === 0) {
|
|
63
|
+
return new Map();
|
|
64
|
+
};
|
|
65
|
+
const url = 'https://api-test.uitpas.be/ticket-sales';
|
|
66
|
+
const body = registerTicketSaleRequests.map((ticketSale) => {
|
|
67
|
+
const eventId = ticketSale.uitpasEventUrl.split('/').pop();
|
|
68
|
+
if (!eventId) {
|
|
69
|
+
throw new SimpleError({
|
|
70
|
+
code: 'invalid_uitpas_event_url',
|
|
71
|
+
message: `Invalid UiTPAS event URL: ${ticketSale.uitpasEventUrl}`,
|
|
72
|
+
human: $t(`De opgegeven UiTPAS-evenement URL is ongeldig.`),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
uitpasNumber: ticketSale.uitpasNumber,
|
|
78
|
+
eventId,
|
|
79
|
+
regularPrice: (ticketSale.basePrice / 100).toFixed(2), // Convert from cents to euros
|
|
80
|
+
regularPriceLabel: ticketSale.basePriceLabel,
|
|
81
|
+
tariff: {
|
|
82
|
+
id: ticketSale.uitpasTariffId,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
const myHeaders = new Headers();
|
|
87
|
+
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
88
|
+
myHeaders.append('Accept', 'application/json');
|
|
89
|
+
myHeaders.append('Content-Type', 'application/json');
|
|
90
|
+
const requestOptions = {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: myHeaders,
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
};
|
|
95
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
96
|
+
// Handle network errors
|
|
97
|
+
throw new SimpleError({
|
|
98
|
+
code: 'uitpas_unreachable_registering_ticket_sales',
|
|
99
|
+
message: `Network issue when registering UiTPAS ticket sales`,
|
|
100
|
+
human: $t(
|
|
101
|
+
`We konden UiTPAS niet bereiken om de ticket verkoop te registreren bij UiTPAS. Probeer het later opnieuw.`,
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const json: unknown = await response.json().catch(() => { /* ignore */ });
|
|
107
|
+
console.error('Unsuccessful response when registering ticket sales', json);
|
|
108
|
+
throw new SimpleError({
|
|
109
|
+
code: 'unsuccessful_response_registering_ticket_sales',
|
|
110
|
+
message: `Unsuccessful response when registering UiTPAS ticket sales`,
|
|
111
|
+
human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const json = await response.json().catch(() => {});
|
|
115
|
+
if (!json || !Array.isArray(json)) {
|
|
116
|
+
console.error('Invalid response when registering ticket sales', json);
|
|
117
|
+
throw new SimpleError({
|
|
118
|
+
code: 'invalid_response_registering_ticket_sales',
|
|
119
|
+
message: `Invalid response when registering ticket sales`,
|
|
120
|
+
human: $t(`Er is een fout opgetreden bij het registreren van de UiTPAS-ticketverkoop.`),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const results: Map<RegisterTicketSaleRequest, RegisterTicketSaleResponse> = new Map();
|
|
125
|
+
for (const ticketSale of json) {
|
|
126
|
+
assertisSuccessResponse(ticketSale);
|
|
127
|
+
const request = registerTicketSaleRequests.find(r => r.uitpasNumber === ticketSale.uitpasNumber && r.uitpasEventUrl.endsWith(`/${ticketSale.eventId}`) && r.uitpasTariffId === ticketSale.tariff.id);
|
|
128
|
+
if (!request) {
|
|
129
|
+
console.error('Could not find request for ticket sale', ticketSale);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
results.set(request, {
|
|
133
|
+
ticketSaleId: ticketSale.id,
|
|
134
|
+
reducedPriceUitpas: Math.round(ticketSale.tariff.price * 100),
|
|
135
|
+
registeredAt: now,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { UitpasEventResponse, UitpasEventsResponse } from '@stamhoofd/structures';
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
|
|
4
|
+
type EventsResponse = {
|
|
5
|
+
totalItems: number;
|
|
6
|
+
itemsPerPage: number;
|
|
7
|
+
member: Array<{
|
|
8
|
+
'@id': string;
|
|
9
|
+
'name': {
|
|
10
|
+
nl: string;
|
|
11
|
+
};
|
|
12
|
+
'location': {
|
|
13
|
+
name: object;
|
|
14
|
+
};
|
|
15
|
+
'startDate'?: string;
|
|
16
|
+
'endDate'?: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function assertIsEventsResponse(json: unknown): asserts json is EventsResponse {
|
|
21
|
+
if (
|
|
22
|
+
typeof json !== 'object'
|
|
23
|
+
|| json === null
|
|
24
|
+
|| !('totalItems' in json)
|
|
25
|
+
|| typeof json.totalItems !== 'number'
|
|
26
|
+
|| !('itemsPerPage' in json)
|
|
27
|
+
|| typeof json.itemsPerPage !== 'number'
|
|
28
|
+
|| !('member' in json)
|
|
29
|
+
|| !Array.isArray(json.member)
|
|
30
|
+
|| json.member.some(member => (
|
|
31
|
+
typeof member !== 'object'
|
|
32
|
+
|| member === null
|
|
33
|
+
|| !('@id' in member)
|
|
34
|
+
|| typeof member['@id'] !== 'string'
|
|
35
|
+
|| !('name' in member)
|
|
36
|
+
|| typeof member.name !== 'object'
|
|
37
|
+
|| member.name === null
|
|
38
|
+
|| !('nl' in member.name)
|
|
39
|
+
|| typeof member.name.nl !== 'string'
|
|
40
|
+
|| !('location' in member)
|
|
41
|
+
|| typeof member.location !== 'object'
|
|
42
|
+
|| member.location === null
|
|
43
|
+
|| !('name' in member.location)
|
|
44
|
+
|| typeof member.location.name !== 'object'
|
|
45
|
+
|| member.location.name === null
|
|
46
|
+
))
|
|
47
|
+
) {
|
|
48
|
+
console.error('Invalid events response', json);
|
|
49
|
+
throw new SimpleError({
|
|
50
|
+
code: 'invalid_events_response',
|
|
51
|
+
message: `Invalid events response`,
|
|
52
|
+
human: $t(`Er is een fout opgetreden bij het ophalen van de UiTPAS-evenementen.`),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: string, textQuery?: string): Promise<UitpasEventsResponse> {
|
|
58
|
+
// uses no credentials (only client id of the organization)
|
|
59
|
+
// https://docs.publiq.be/docs/uitpas/events/searching
|
|
60
|
+
if (!clientId) {
|
|
61
|
+
throw new SimpleError({
|
|
62
|
+
code: 'no_client_id_for_uitpas_events',
|
|
63
|
+
message: `No client ID configured for Uitpas events`,
|
|
64
|
+
human: $t(`Er is geen UiTPAS client ID geconfigureerd voor deze organisatie.`),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const baseUrl = 'https://search-test.uitdatabank.be/events';
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
params.append('clientId', clientId);
|
|
70
|
+
params.append('organizerId', uitpasOrganizerId);
|
|
71
|
+
params.append('embed', 'true');
|
|
72
|
+
params.append('uitpas', 'true');
|
|
73
|
+
params.append('start', '0');
|
|
74
|
+
params.append('limit', '200');
|
|
75
|
+
if (textQuery) {
|
|
76
|
+
params.append('text', textQuery);
|
|
77
|
+
}
|
|
78
|
+
const url = `${baseUrl}?${params.toString()}`;
|
|
79
|
+
const myHeaders = new Headers();
|
|
80
|
+
myHeaders.append('Accept', 'application/json');
|
|
81
|
+
const requestOptions = {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers: myHeaders,
|
|
84
|
+
};
|
|
85
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
86
|
+
// Handle network errors
|
|
87
|
+
throw new SimpleError({
|
|
88
|
+
code: 'uitpas_unreachable_searching_events',
|
|
89
|
+
message: `Network issue when searching for UiTPAS events`,
|
|
90
|
+
human: $t(
|
|
91
|
+
`We konden UiTPAS niet bereiken om UiTPAS-evenementen op te zoeken. Probeer het later opnieuw.`,
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
console.error('Unsuccessful response when searching for UiTPAS events', response);
|
|
97
|
+
throw new SimpleError({
|
|
98
|
+
code: 'unsuccessful_response_searching_uitpas_events',
|
|
99
|
+
message: `Unsuccessful response when searching for UiTPAS events`,
|
|
100
|
+
human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const json = await response.json().catch(() => {
|
|
104
|
+
// Handle JSON parsing errors
|
|
105
|
+
throw new SimpleError({
|
|
106
|
+
code: 'invalid_json_searching_uitpas_events',
|
|
107
|
+
message: `Invalid json when searching for UiTPAS events`,
|
|
108
|
+
human: $t(
|
|
109
|
+
`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`,
|
|
110
|
+
),
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assertIsEventsResponse(json);
|
|
115
|
+
const eventsResponse = new UitpasEventsResponse();
|
|
116
|
+
eventsResponse.totalItems = json.totalItems;
|
|
117
|
+
eventsResponse.itemsPerPage = json.itemsPerPage;
|
|
118
|
+
eventsResponse.member = json.member.map((member) => {
|
|
119
|
+
const event = new UitpasEventResponse();
|
|
120
|
+
event.url = member['@id'];
|
|
121
|
+
event.name = member.name.nl;
|
|
122
|
+
const locationName = member.location.name as Record<string, string>;
|
|
123
|
+
const entrs = Object.entries(locationName);
|
|
124
|
+
const hasNl = entrs.find(([key]) => key === 'nl');
|
|
125
|
+
if (hasNl) {
|
|
126
|
+
event.location = locationName.nl;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const lang = entrs[0];
|
|
130
|
+
event.location = lang ? lang[1] : '';
|
|
131
|
+
}
|
|
132
|
+
if (member.startDate) {
|
|
133
|
+
event.startDate = new Date(member.startDate);
|
|
134
|
+
}
|
|
135
|
+
if (member.endDate) {
|
|
136
|
+
event.endDate = new Date(member.endDate);
|
|
137
|
+
}
|
|
138
|
+
return event;
|
|
139
|
+
});
|
|
140
|
+
return eventsResponse;
|
|
141
|
+
}
|