@stamhoofd/backend 2.110.0 → 2.112.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/LICENSE.md +32 -0
- package/package.json +15 -12
- package/src/boot.ts +1 -0
- package/src/email-recipient-loaders/documents.ts +66 -0
- package/src/endpoints/auth/PatchUserEndpoint.test.ts +56 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +701 -4
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -10
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +661 -4
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -6
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +291 -8
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +22 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.ts +16 -18
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +219 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
- package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +72 -0
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -2
- package/src/excel-loaders/members.ts +27 -27
- package/src/helpers/AdminPermissionChecker.ts +30 -10
- package/src/helpers/AuthenticatedStructures.ts +24 -5
- package/src/helpers/StripeHelper.ts +11 -1
- package/src/helpers/StripePayoutChecker.ts +7 -0
- package/src/helpers/UitpasTokenRepository.ts +7 -5
- package/src/helpers/passthroughFetch.ts +24 -0
- package/src/helpers/updateMemberDetailsUitpasNumber.ts +149 -0
- package/src/seeds/data/default-email-templates.sql +2 -1
- package/src/seeds/wip/1769088653-uitpas-status.ts +129 -0
- package/src/services/InvoiceService.ts +114 -0
- package/src/services/uitpas/PassholderEndpoints.ts +190 -0
- package/src/services/uitpas/UitpasService.ts +37 -12
- package/src/services/uitpas/checkUitpasNumbers.ts +16 -140
- package/src/services/uitpas/handleUitpasResponse.ts +89 -0
- package/src/sql-filters/invoiced-balance-items.ts +20 -0
- package/src/sql-filters/invoices.ts +122 -0
- package/src/sql-filters/payments.ts +11 -1
- package/src/sql-sorters/invoices.ts +83 -0
- package/src/sql-sorters/payments.ts +33 -0
- package/tests/e2e/bundle-discounts.test.ts +8 -8
- package/tests/e2e/tests-disable-net-connect.test.ts +5 -0
- package/tests/helpers/StripeMocker.ts +5 -5
- package/tests/helpers/UitpasApiMocker.ts +175 -0
- package/tests/helpers/index.ts +1 -0
- package/tests/helpers/resetNock.ts +7 -0
- package/tests/init/index.ts +1 -0
- package/tests/init/initPayconiq.ts +2 -2
- package/tests/init/initStripe.ts +1 -1
- package/tests/init/initUitpasApi.ts +14 -0
- package/tests/jest.global.setup.ts +6 -4
- package/tests/jest.setup.ts +12 -6
- package/LICENSE +0 -665
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { handleUitpasResponse } from './handleUitpasResponse.js';
|
|
3
|
+
|
|
4
|
+
export type GetPassResponse = {
|
|
5
|
+
passholderId?: string;
|
|
6
|
+
uitpasNumber: string;
|
|
7
|
+
firstName?: string;
|
|
8
|
+
points?: number;
|
|
9
|
+
postalCode?: string;
|
|
10
|
+
socialTariff: {
|
|
11
|
+
status: 'ACTIVE' | 'EXPIRED' | 'NONE' | 'UNKNOWN';
|
|
12
|
+
endDate?: string;
|
|
13
|
+
};
|
|
14
|
+
messages?: {
|
|
15
|
+
level: 'INFO' | 'WARN' | 'ERROR';
|
|
16
|
+
text: string;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class PassholderEndpoints {
|
|
21
|
+
private readonly baseUrl = getBaseUrl();
|
|
22
|
+
|
|
23
|
+
constructor(private readonly access_token: string) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/get-a-pass
|
|
27
|
+
* @param uitpasNumber
|
|
28
|
+
*/
|
|
29
|
+
async getPassByUitpasNumber(uitpasNumber: string): Promise<GetPassResponse> {
|
|
30
|
+
let response: Response;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(`${this.baseUrl}/passes/${uitpasNumber}`, {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: this.getHeaders(),
|
|
36
|
+
signal: AbortSignal.timeout(10000),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (isTimeout(error)) {
|
|
41
|
+
throw new SimpleError({
|
|
42
|
+
statusCode: 408,
|
|
43
|
+
code: 'uitpas_timeout',
|
|
44
|
+
message: `Timeout when retrieving pass by UiTPAS number`,
|
|
45
|
+
human: $t(
|
|
46
|
+
`We konden UiTPAS niet bereiken om jouw UiTPAS-nummer te valideren. Probeer het later opnieuw.`,
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new SimpleError({
|
|
52
|
+
code: 'uitpas_unreachable_retrieving_pass_by_uitpas_number',
|
|
53
|
+
message: `Network issue when retrieving pass by UiTPAS number`,
|
|
54
|
+
human: $t(
|
|
55
|
+
`We konden UiTPAS niet bereiken om jouw UiTPAS-nummer te valideren. Probeer het later opnieuw.`,
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const json = await handleUitpasResponse(response);
|
|
62
|
+
return jsonToGetPassResponse(json);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
66
|
+
if (e.hasCode('https://api.publiq.be/probs/uitpas/pass-not-found')) {
|
|
67
|
+
throw new SimpleError({
|
|
68
|
+
code: 'https://api.publiq.be/probs/uitpas/pass-not-found',
|
|
69
|
+
message: 'Pass not found',
|
|
70
|
+
human: $t('02f758f9-a1b1-428b-9575-69c4963c2419'),
|
|
71
|
+
statusCode: 404,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getHeaders(): Headers {
|
|
80
|
+
const headers = new Headers();
|
|
81
|
+
headers.append('Authorization', 'Bearer ' + this.access_token);
|
|
82
|
+
return headers;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isTimeout(error: any): boolean {
|
|
87
|
+
return error['name'] === 'TimeoutError';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function jsonToGetPassResponse(json: unknown): GetPassResponse {
|
|
91
|
+
assertValidGetPassResponse(json);
|
|
92
|
+
return {
|
|
93
|
+
passholderId: json.passholderId,
|
|
94
|
+
uitpasNumber: json.uitpasNumber,
|
|
95
|
+
firstName: json.firstName,
|
|
96
|
+
points: json.points,
|
|
97
|
+
postalCode: json.postalCode,
|
|
98
|
+
socialTariff: {
|
|
99
|
+
status: json.socialTariff.status,
|
|
100
|
+
endDate: json.socialTariff.endDate,
|
|
101
|
+
},
|
|
102
|
+
messages: json.messages?.map((m) => {
|
|
103
|
+
return {
|
|
104
|
+
level: m.level,
|
|
105
|
+
text: m.text,
|
|
106
|
+
};
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertValidGetPassResponse(json: unknown): asserts json is GetPassResponse {
|
|
112
|
+
try {
|
|
113
|
+
if (typeof json !== 'object') {
|
|
114
|
+
throw new Error('Not an object');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (json === null) {
|
|
118
|
+
throw new Error('Null');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stringProperties = ['passholderId', 'uitpasNumber', 'firstName', 'postalCode'];
|
|
122
|
+
|
|
123
|
+
stringProperties.forEach((key) => {
|
|
124
|
+
if (typeof json[key] !== 'string') {
|
|
125
|
+
throw new Error(`Invalid ${key}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const points = json['points'];
|
|
130
|
+
if (typeof points !== 'number') {
|
|
131
|
+
throw new Error('Invalid points');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const socialTariff = json['socialTariff'];
|
|
135
|
+
if (typeof socialTariff !== 'object' || socialTariff === null) {
|
|
136
|
+
throw new Error('Invalid socialTariff');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const status = socialTariff['status'];
|
|
140
|
+
if (!(status === 'ACTIVE' || status === 'EXPIRED' || status === 'NONE')) {
|
|
141
|
+
throw new Error('Invalid socialTariff status');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const endDate = socialTariff['endDate'];
|
|
145
|
+
if (endDate !== undefined && typeof endDate !== 'string') {
|
|
146
|
+
throw new Error('Invalid socialTariff endDate');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const messages = json['messages'];
|
|
150
|
+
if (messages !== undefined) {
|
|
151
|
+
if (!Array.isArray(messages)) {
|
|
152
|
+
throw new Error('Invalid messages');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
messages.forEach((message) => {
|
|
156
|
+
if (typeof message !== 'object' || message === null) {
|
|
157
|
+
throw new Error('Invalid message');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const level = message['level'];
|
|
161
|
+
if (!(level === 'INFO' || level === 'WARN' || level === 'ERROR')) {
|
|
162
|
+
throw new Error('Invalid message level');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const text = message['text'];
|
|
166
|
+
if (typeof text !== 'string') {
|
|
167
|
+
throw new Error('Invalid message text');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error('Invalid response when retrieving pass by UiTPAS number:', json);
|
|
174
|
+
throw new SimpleError({
|
|
175
|
+
code: 'invalid_response_retrieving_pass_by_uitpas_number',
|
|
176
|
+
message: `Invalid response when retrieving pass by UiTPAS number: ${error.message}`,
|
|
177
|
+
human: $t(`4c6482ff-e6d9-4ea1-b11d-e12d697b4b7b`),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getBaseUrl(): string {
|
|
183
|
+
const baseUrl = STAMHOOFD.UITPAS_API_URL;
|
|
184
|
+
|
|
185
|
+
if (baseUrl === undefined) {
|
|
186
|
+
throw new Error('Missing environment variable UITPAS_API_URL');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return baseUrl;
|
|
190
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
|
+
import { isSimpleError, SimpleError } from '@simonbackx/simple-errors';
|
|
2
3
|
import { Order, WebshopUitpasNumber } from '@stamhoofd/models';
|
|
3
|
-
import { Cart, OrderStatus, Product, ProductPrice, 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
|
-
import { searchUitpasEvents } from './searchUitpasEvents';
|
|
12
|
-
import { RegisterTicketSaleRequest, RegisterTicketSaleResponse, registerTicketSales } from './registerTicketSales';
|
|
13
|
-
import { cancelTicketSales } from './cancelTicketSales';
|
|
14
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
15
4
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
5
|
+
import { Cart, OrderStatus, Product, ProductPrice, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
16
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
import { UitpasTokenRepository } from '../../helpers/UitpasTokenRepository.js';
|
|
9
|
+
import { cancelTicketSales } from './cancelTicketSales.js';
|
|
10
|
+
import { checkPermissionsFor } from './checkPermissionsFor.js';
|
|
11
|
+
import { checkUitpasNumbers } from './checkUitpasNumbers.js';
|
|
12
|
+
import { getSocialTariffForEvent } from './getSocialTariffForEvent.js';
|
|
13
|
+
import { getSocialTariffForUitpasNumbers } from './getSocialTariffForUitpasNumbers.js';
|
|
14
|
+
import { GetPassResponse, PassholderEndpoints } from './PassholderEndpoints.js';
|
|
15
|
+
import { RegisterTicketSaleRequest, RegisterTicketSaleResponse, registerTicketSales } from './registerTicketSales.js';
|
|
16
|
+
import { searchUitpasEvents } from './searchUitpasEvents.js';
|
|
17
|
+
import { searchUitpasOrganizers } from './searchUitpasOrganizers.js';
|
|
17
18
|
|
|
18
19
|
type UitpasTicketSale = {
|
|
19
20
|
basePrice: number;
|
|
@@ -342,6 +343,30 @@ export class UitpasService {
|
|
|
342
343
|
return await checkUitpasNumbers(access_token, uitpasNumbers);
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
/**
|
|
347
|
+
* @returns the passholder endpoints
|
|
348
|
+
*/
|
|
349
|
+
static async getPassByUitpasNumber(uitpasNumber: string): Promise<GetPassResponse> {
|
|
350
|
+
let access_token!: string;
|
|
351
|
+
try {
|
|
352
|
+
access_token = await UitpasTokenRepository.getAccessTokenFor(); // use platform credentials
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
if (isSimpleError(e) && e.hasCode('uitpas_api_not_configured_for_platform')) {
|
|
356
|
+
console.error('UiTPAS API has not been configured for this platform, so defaulting to an unknown status for number ', uitpasNumber);
|
|
357
|
+
// Ignore and return status of unknown
|
|
358
|
+
return {
|
|
359
|
+
uitpasNumber,
|
|
360
|
+
socialTariff: {
|
|
361
|
+
status: 'UNKNOWN',
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
throw e;
|
|
366
|
+
}
|
|
367
|
+
return new PassholderEndpoints(access_token).getPassByUitpasNumber(uitpasNumber);
|
|
368
|
+
}
|
|
369
|
+
|
|
345
370
|
/**
|
|
346
371
|
* Store the uitpas client credentials if they are valid
|
|
347
372
|
* @param organizationId null for platform
|
|
@@ -1,62 +1,11 @@
|
|
|
1
1
|
import { isSimpleError, isSimpleErrors, SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
2
2
|
import { DataValidator } from '@stamhoofd/utility';
|
|
3
|
+
import { PassholderEndpoints } from './PassholderEndpoints.js';
|
|
3
4
|
|
|
4
|
-
|
|
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(`4c6482ff-e6d9-4ea1-b11d-e12d697b4b7b`),
|
|
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)
|
|
5
|
+
export function throwIfInvalidUitpasNumber(uitpasNumber: string) {
|
|
58
6
|
if (!DataValidator.isUitpasNumberValid(uitpasNumber)) {
|
|
59
7
|
throw new SimpleError({
|
|
8
|
+
statusCode: 400,
|
|
60
9
|
code: 'invalid_uitpas_number',
|
|
61
10
|
message: `Invalid UiTPAS number: ${uitpasNumber}`,
|
|
62
11
|
human: $t(
|
|
@@ -64,91 +13,6 @@ async function checkUitpasNumber(access_token: string, uitpasNumber: string) {
|
|
|
64
13
|
),
|
|
65
14
|
});
|
|
66
15
|
}
|
|
67
|
-
|
|
68
|
-
const baseUrl = 'https://api-test.uitpas.be'; // TO DO: Use the URL from environment variables
|
|
69
|
-
|
|
70
|
-
const url = `${baseUrl}/passes/${uitpasNumber}`;
|
|
71
|
-
const myHeaders = new Headers();
|
|
72
|
-
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
73
|
-
const requestOptions = {
|
|
74
|
-
method: 'GET',
|
|
75
|
-
headers: myHeaders,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const response = await fetch(url, requestOptions).catch(() => {
|
|
79
|
-
// Handle network errors
|
|
80
|
-
throw new SimpleError({
|
|
81
|
-
code: 'uitpas_unreachable_retrieving_pass_by_uitpas_number',
|
|
82
|
-
message: `Network issue when retrieving pass by UiTPAS number`,
|
|
83
|
-
human: $t(
|
|
84
|
-
`We konden UiTPAS niet bereiken om jouw UiTPAS-nummer te valideren. Probeer het later opnieuw.`,
|
|
85
|
-
),
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
const json: unknown = await response.json().catch(() => { /* ignore */ });
|
|
90
|
-
let endUserMessage = '';
|
|
91
|
-
|
|
92
|
-
if (json) {
|
|
93
|
-
console.error(`UiTPAS API returned an error for UiTPAS number ${uitpasNumber}:`, json);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
console.error(`UiTPAS API returned an error for UiTPAS number ${uitpasNumber}:`, response.statusText);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (isUitpasNumberErrorResponse(json)) {
|
|
100
|
-
endUserMessage = json.endUserMessage ? json.endUserMessage.nl : '';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (endUserMessage) {
|
|
104
|
-
throw new SimpleError({
|
|
105
|
-
code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number',
|
|
106
|
-
message: `Unsuccesful response with message when retrieving pass by UiTPAS number, message: ${endUserMessage}`,
|
|
107
|
-
human: endUserMessage,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
throw new SimpleError({
|
|
112
|
-
code: 'unsuccessful_and_unexpected_response_retrieving_pass_by_uitpas_number',
|
|
113
|
-
message: `Unsuccesful response without message when retrieving pass by UiTPAS number`,
|
|
114
|
-
human: $t(`4c6482ff-e6d9-4ea1-b11d-e12d697b4b7b`),
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const json = await response.json().catch(() => {
|
|
119
|
-
// Handle JSON parsing errors
|
|
120
|
-
throw new SimpleError({
|
|
121
|
-
code: 'invalid_json_retrieving_pass_by_uitpas_number',
|
|
122
|
-
message: `Invalid json when retrieving pass by UiTPAS number`,
|
|
123
|
-
human: $t(
|
|
124
|
-
`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`,
|
|
125
|
-
),
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
assertIsUitpasNumberSuccessfulResponse(json);
|
|
129
|
-
if (json.messages) {
|
|
130
|
-
const humanMessage = json.messages[0].text; // only display the first message
|
|
131
|
-
|
|
132
|
-
// alternatively, join all messages
|
|
133
|
-
// const text = json.messages.map((message: any) => message.text).join(', ');
|
|
134
|
-
|
|
135
|
-
throw new SimpleError({
|
|
136
|
-
code: 'uitpas_number_issue',
|
|
137
|
-
message: `UiTPAS API returned an error: ${humanMessage}`,
|
|
138
|
-
human: humanMessage,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
if (json.socialTariff.status !== 'ACTIVE') {
|
|
142
|
-
// THIS SHOULD NOT HAPPEN, as in that case json.messages should be present
|
|
143
|
-
throw new SimpleError({
|
|
144
|
-
code: 'non_active_social_tariff',
|
|
145
|
-
message: `UiTPAS social tariff is not ACTIVE but ${json.socialTariff.status}`,
|
|
146
|
-
human: $t(
|
|
147
|
-
`Het opgegeven UiTPAS-nummer heeft geen actief kansentarief. Neem contact op met de UiTPAS-organisatie voor meer informatie.`,
|
|
148
|
-
),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
// no errors -> the uitpas number is valid and social tariff is applicable
|
|
152
16
|
}
|
|
153
17
|
|
|
154
18
|
/**
|
|
@@ -158,11 +22,23 @@ async function checkUitpasNumber(access_token: string, uitpasNumber: string) {
|
|
|
158
22
|
* @param uitpasNumbers The uitpas numbers to check
|
|
159
23
|
*/
|
|
160
24
|
export async function checkUitpasNumbers(access_token: string, uitpasNumbers: string[]) {
|
|
25
|
+
const passholderEndpoints = new PassholderEndpoints(access_token);
|
|
26
|
+
|
|
161
27
|
const simpleErrors = new SimpleErrors();
|
|
162
28
|
for (let i = 0; i < uitpasNumbers.length; i++) {
|
|
163
29
|
const uitpasNumber = uitpasNumbers[i];
|
|
164
30
|
try {
|
|
165
|
-
|
|
31
|
+
throwIfInvalidUitpasNumber(uitpasNumber);
|
|
32
|
+
const result = await passholderEndpoints.getPassByUitpasNumber(uitpasNumber);
|
|
33
|
+
if (result.socialTariff.status !== 'ACTIVE') {
|
|
34
|
+
new SimpleError({
|
|
35
|
+
code: 'non_active_social_tariff',
|
|
36
|
+
message: `UiTPAS social tariff is not ACTIVE but ${result.socialTariff.status}`,
|
|
37
|
+
human: $t(
|
|
38
|
+
`Het opgegeven UiTPAS-nummer heeft geen actief kansentarief. Neem contact op met de UiTPAS-organisatie voor meer informatie.`,
|
|
39
|
+
),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
166
42
|
}
|
|
167
43
|
catch (e) {
|
|
168
44
|
if (isSimpleError(e) || isSimpleErrors(e)) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
|
|
3
|
+
type UitpasErrorResponse = {
|
|
4
|
+
type: string;
|
|
5
|
+
title: string;
|
|
6
|
+
endUserMessage?: {
|
|
7
|
+
nl: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Throws a SimpleError if the response is not ok
|
|
13
|
+
* @param response
|
|
14
|
+
* @returns json
|
|
15
|
+
*/
|
|
16
|
+
export async function handleUitpasResponse(response: Response) {
|
|
17
|
+
if (response.ok) {
|
|
18
|
+
const json = await response.json();
|
|
19
|
+
return json;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const error = await uitpasErrorToSimpleError(response);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function uitpasErrorToSimpleError(response: Response) {
|
|
27
|
+
const json: unknown = await response.json().catch(() => { /* ignore */ });
|
|
28
|
+
console.error('[UITPAS ERROR]', json);
|
|
29
|
+
|
|
30
|
+
const statusCode = response.status;
|
|
31
|
+
let human: string | undefined;
|
|
32
|
+
let message: string | undefined;
|
|
33
|
+
let code: string | undefined;
|
|
34
|
+
|
|
35
|
+
if (json) {
|
|
36
|
+
const uitpasError = jsonToUitpasErrorResponse(json);
|
|
37
|
+
if (uitpasError.endUserMessage) {
|
|
38
|
+
human = uitpasError.endUserMessage.nl;
|
|
39
|
+
}
|
|
40
|
+
message = uitpasError.title;
|
|
41
|
+
code = uitpasError.type;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new SimpleError({
|
|
45
|
+
statusCode,
|
|
46
|
+
code: code ?? 'get-uitpas-error',
|
|
47
|
+
message: message ?? `Error when retrieving pass by UiTPAS number`,
|
|
48
|
+
human: human ?? $t('2fcd8eb2-603e-44bb-8b8d-faa131936888'),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function jsonToUitpasErrorResponse(json: unknown): UitpasErrorResponse {
|
|
53
|
+
assertValidUitpasError(json);
|
|
54
|
+
return {
|
|
55
|
+
type: json.type,
|
|
56
|
+
title: json.title,
|
|
57
|
+
endUserMessage: json.endUserMessage
|
|
58
|
+
? {
|
|
59
|
+
nl: json.endUserMessage.nl,
|
|
60
|
+
}
|
|
61
|
+
: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function assertValidUitpasError(json: unknown): asserts json is UitpasErrorResponse {
|
|
66
|
+
if (json === null || typeof json !== 'object') {
|
|
67
|
+
throw new Error('Not an object');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stringProperties = ['title', 'type'];
|
|
71
|
+
|
|
72
|
+
stringProperties.forEach((key) => {
|
|
73
|
+
if (typeof json[key] !== 'string') {
|
|
74
|
+
throw new Error(`Invalid ${key}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const endUserMessage = json['endUserMessage'];
|
|
79
|
+
if (endUserMessage !== undefined) {
|
|
80
|
+
if (typeof endUserMessage !== 'object' || endUserMessage === null) {
|
|
81
|
+
throw new Error('Invalid endUserMessage');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const nl = endUserMessage['nl'];
|
|
85
|
+
if (typeof nl !== 'string') {
|
|
86
|
+
throw new Error('Invalid endUserMessage nl');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
export const invoicedBalanceItemCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createColumnFilter({
|
|
6
|
+
expression: SQL.column('id'),
|
|
7
|
+
type: SQLValueType.String,
|
|
8
|
+
nullable: false,
|
|
9
|
+
}),
|
|
10
|
+
name: createColumnFilter({
|
|
11
|
+
expression: SQL.column('name'),
|
|
12
|
+
type: SQLValueType.String,
|
|
13
|
+
nullable: false,
|
|
14
|
+
}),
|
|
15
|
+
description: createColumnFilter({
|
|
16
|
+
expression: SQL.column('description'),
|
|
17
|
+
type: SQLValueType.String,
|
|
18
|
+
nullable: false,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLCast, SQLConcat, SQLFilterDefinitions, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
import { invoicedBalanceItemCompilers } from './invoiced-balance-items.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines how to filter payments in the database from StamhoofdFilter objects
|
|
6
|
+
*/
|
|
7
|
+
export const invoiceFilterCompilers: SQLFilterDefinitions = {
|
|
8
|
+
...baseSQLFilterCompilers,
|
|
9
|
+
id: createColumnFilter({
|
|
10
|
+
expression: SQL.column('id'),
|
|
11
|
+
type: SQLValueType.String,
|
|
12
|
+
nullable: false,
|
|
13
|
+
}),
|
|
14
|
+
number: createColumnFilter({
|
|
15
|
+
expression: SQL.column('number'),
|
|
16
|
+
type: SQLValueType.String,
|
|
17
|
+
nullable: true,
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
organizationId: createColumnFilter({
|
|
21
|
+
expression: SQL.column('organizationId'),
|
|
22
|
+
type: SQLValueType.String,
|
|
23
|
+
nullable: true,
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
totalWithVAT: createColumnFilter({
|
|
27
|
+
expression: SQL.column('totalWithVAT'),
|
|
28
|
+
type: SQLValueType.Number,
|
|
29
|
+
nullable: false,
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
totalWithoutVAT: createColumnFilter({
|
|
33
|
+
expression: SQL.column('totalWithoutVAT'),
|
|
34
|
+
type: SQLValueType.Number,
|
|
35
|
+
nullable: false,
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
VATTotalAmount: createColumnFilter({
|
|
39
|
+
expression: SQL.column('VATTotalAmount'),
|
|
40
|
+
type: SQLValueType.Number,
|
|
41
|
+
nullable: false,
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
createdAt: createColumnFilter({
|
|
45
|
+
expression: SQL.column('createdAt'),
|
|
46
|
+
type: SQLValueType.Datetime,
|
|
47
|
+
nullable: false,
|
|
48
|
+
}),
|
|
49
|
+
updatedAt: createColumnFilter({
|
|
50
|
+
expression: SQL.column('updatedAt'),
|
|
51
|
+
type: SQLValueType.Datetime,
|
|
52
|
+
nullable: false,
|
|
53
|
+
}),
|
|
54
|
+
invoicedAt: createColumnFilter({
|
|
55
|
+
expression: SQL.column('invoicedAt'),
|
|
56
|
+
type: SQLValueType.Datetime,
|
|
57
|
+
nullable: true,
|
|
58
|
+
}),
|
|
59
|
+
customer: {
|
|
60
|
+
...baseSQLFilterCompilers,
|
|
61
|
+
email: createColumnFilter({
|
|
62
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.email'),
|
|
63
|
+
type: SQLValueType.JSONString,
|
|
64
|
+
nullable: true,
|
|
65
|
+
}),
|
|
66
|
+
firstName: createColumnFilter({
|
|
67
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.firstName'),
|
|
68
|
+
type: SQLValueType.JSONString,
|
|
69
|
+
nullable: true,
|
|
70
|
+
}),
|
|
71
|
+
lastName: createColumnFilter({
|
|
72
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.lastName'),
|
|
73
|
+
type: SQLValueType.JSONString,
|
|
74
|
+
nullable: true,
|
|
75
|
+
}),
|
|
76
|
+
name: createColumnFilter({
|
|
77
|
+
expression: new SQLCast(
|
|
78
|
+
new SQLConcat(
|
|
79
|
+
new SQLJsonUnquote(SQL.jsonExtract(SQL.column('customer'), '$.value.firstName')),
|
|
80
|
+
new SQLScalar(' '),
|
|
81
|
+
new SQLJsonUnquote(SQL.jsonExtract(SQL.column('customer'), '$.value.lastName')),
|
|
82
|
+
),
|
|
83
|
+
'CHAR',
|
|
84
|
+
),
|
|
85
|
+
type: SQLValueType.String,
|
|
86
|
+
nullable: true,
|
|
87
|
+
}),
|
|
88
|
+
company: {
|
|
89
|
+
...baseSQLFilterCompilers,
|
|
90
|
+
name: createColumnFilter({
|
|
91
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.name'),
|
|
92
|
+
type: SQLValueType.JSONString,
|
|
93
|
+
nullable: true,
|
|
94
|
+
}),
|
|
95
|
+
VATNumber: createColumnFilter({
|
|
96
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.VATNumber'),
|
|
97
|
+
type: SQLValueType.JSONString,
|
|
98
|
+
nullable: true,
|
|
99
|
+
}),
|
|
100
|
+
companyNumber: createColumnFilter({
|
|
101
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.companyNumber'),
|
|
102
|
+
type: SQLValueType.JSONString,
|
|
103
|
+
nullable: true,
|
|
104
|
+
}),
|
|
105
|
+
administrationEmail: createColumnFilter({
|
|
106
|
+
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.administrationEmail'),
|
|
107
|
+
type: SQLValueType.JSONString,
|
|
108
|
+
nullable: true,
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
items: createExistsFilter(
|
|
113
|
+
SQL.select()
|
|
114
|
+
.from(
|
|
115
|
+
SQL.table('invoiced_balance_items'),
|
|
116
|
+
).where(
|
|
117
|
+
SQL.column('invoiceId'),
|
|
118
|
+
SQL.parentColumn('id'),
|
|
119
|
+
),
|
|
120
|
+
invoicedBalanceItemCompilers,
|
|
121
|
+
),
|
|
122
|
+
};
|