@stamhoofd/backend 2.111.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 +14 -11
- package/src/boot.ts +1 -0
- package/src/email-recipient-loaders/documents.ts +66 -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/invoices/GetInvoicesCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +219 -0
- 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 +2 -2
- 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
|
@@ -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
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLCast, SQLConcat, SQLFilterDefinitions, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
-
import { balanceItemPaymentsCompilers } from './balance-item-payments';
|
|
2
|
+
import { balanceItemPaymentsCompilers } from './balance-item-payments.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Defines how to filter payments in the database from StamhoofdFilter objects
|
|
@@ -16,6 +16,11 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
16
16
|
type: SQLValueType.String,
|
|
17
17
|
nullable: false,
|
|
18
18
|
}),
|
|
19
|
+
type: createColumnFilter({
|
|
20
|
+
expression: SQL.column('type'),
|
|
21
|
+
type: SQLValueType.String,
|
|
22
|
+
nullable: false,
|
|
23
|
+
}),
|
|
19
24
|
status: createColumnFilter({
|
|
20
25
|
expression: SQL.column('status'),
|
|
21
26
|
type: SQLValueType.String,
|
|
@@ -56,6 +61,11 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
56
61
|
type: SQLValueType.String,
|
|
57
62
|
nullable: true,
|
|
58
63
|
}),
|
|
64
|
+
hasInvoice: createColumnFilter({
|
|
65
|
+
expression: SQL.isNull(SQL.column('invoiceId')),
|
|
66
|
+
type: SQLValueType.Boolean,
|
|
67
|
+
nullable: false,
|
|
68
|
+
}),
|
|
59
69
|
customer: {
|
|
60
70
|
...baseSQLFilterCompilers,
|
|
61
71
|
email: createColumnFilter({
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Invoice } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
|
|
5
|
+
export const invoiceSorters: SQLSortDefinitions<Invoice> = {
|
|
6
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
7
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
8
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
9
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
10
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
11
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
12
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
13
|
+
|
|
14
|
+
id: {
|
|
15
|
+
getValue(a) {
|
|
16
|
+
return a.id;
|
|
17
|
+
},
|
|
18
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
19
|
+
return new SQLOrderBy({
|
|
20
|
+
column: SQL.column('id'),
|
|
21
|
+
direction,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
invoicedAt: {
|
|
26
|
+
getValue(a) {
|
|
27
|
+
return a.invoicedAt !== null ? Formatter.dateTimeIso(a.invoicedAt, 'UTC') : null;
|
|
28
|
+
},
|
|
29
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
30
|
+
return new SQLOrderBy({
|
|
31
|
+
column: SQL.column('invoicedAt'),
|
|
32
|
+
direction,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
createdAt: {
|
|
37
|
+
getValue(a) {
|
|
38
|
+
return Formatter.dateTimeIso(a.createdAt, 'UTC');
|
|
39
|
+
},
|
|
40
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
41
|
+
return new SQLOrderBy({
|
|
42
|
+
column: SQL.column('createdAt'),
|
|
43
|
+
direction,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
totalWithVAT: {
|
|
49
|
+
getValue(a) {
|
|
50
|
+
return a.totalWithVAT;
|
|
51
|
+
},
|
|
52
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
53
|
+
return new SQLOrderBy({
|
|
54
|
+
column: SQL.column('totalWithVAT'),
|
|
55
|
+
direction,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
totalWithoutVAT: {
|
|
61
|
+
getValue(a) {
|
|
62
|
+
return a.totalWithoutVAT;
|
|
63
|
+
},
|
|
64
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
65
|
+
return new SQLOrderBy({
|
|
66
|
+
column: SQL.column('totalWithoutVAT'),
|
|
67
|
+
direction,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
VATTotalAmount: {
|
|
73
|
+
getValue(a) {
|
|
74
|
+
return a.VATTotalAmount;
|
|
75
|
+
},
|
|
76
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
77
|
+
return new SQLOrderBy({
|
|
78
|
+
column: SQL.column('VATTotalAmount'),
|
|
79
|
+
direction,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -55,4 +55,37 @@ export const paymentSorters: SQLSortDefinitions<Payment> = {
|
|
|
55
55
|
});
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
|
+
hasInvoice: {
|
|
59
|
+
getValue(a) {
|
|
60
|
+
return a.invoiceId ? 1 : 0;
|
|
61
|
+
},
|
|
62
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
63
|
+
return new SQLOrderBy({
|
|
64
|
+
column: SQL.isNull(SQL.column('invoiceId')),
|
|
65
|
+
direction,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
method: {
|
|
70
|
+
getValue(a) {
|
|
71
|
+
return a.method;
|
|
72
|
+
},
|
|
73
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
74
|
+
return new SQLOrderBy({
|
|
75
|
+
column: SQL.column('method'),
|
|
76
|
+
direction,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
type: {
|
|
81
|
+
getValue(a) {
|
|
82
|
+
return a.type;
|
|
83
|
+
},
|
|
84
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
85
|
+
return new SQLOrderBy({
|
|
86
|
+
column: SQL.column('type'),
|
|
87
|
+
direction,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
},
|
|
58
91
|
};
|
|
@@ -2,14 +2,14 @@ import { Request } from '@simonbackx/simple-endpoints';
|
|
|
2
2
|
import { BalanceItem, BalanceItemFactory, GroupFactory, MemberFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
3
3
|
import { AccessRight, AppliedRegistrationDiscount, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BooleanStatus, GroupPriceDiscount, GroupPriceDiscountType, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, PaymentMethod, PermissionLevel, Permissions, PermissionsResourceType, ReduceablePrice, ResourcePermissions } from '@stamhoofd/structures';
|
|
4
4
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
5
|
-
import { RegisterMembersEndpoint } from '../../src/endpoints/global/registration/RegisterMembersEndpoint';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
5
|
+
import { RegisterMembersEndpoint } from '../../src/endpoints/global/registration/RegisterMembersEndpoint.js';
|
|
6
|
+
import { BalanceItemService } from '../../src/services/BalanceItemService.js';
|
|
7
|
+
import { assertBalances } from '../assertions/assertBalances.js';
|
|
8
|
+
import { testServer } from '../helpers/TestServer.js';
|
|
9
|
+
import { initAdmin } from '../init/initAdmin.js';
|
|
10
|
+
import { initBundleDiscount } from '../init/initBundleDiscount.js';
|
|
11
|
+
import { initPermissionRole } from '../init/initPermissionRole.js';
|
|
12
|
+
import { initStripe } from '../init/initStripe.js';
|
|
13
13
|
|
|
14
14
|
const baseUrl = `/members/register`;
|
|
15
15
|
|
|
@@ -4,9 +4,10 @@ import nock from 'nock';
|
|
|
4
4
|
import qs from 'qs';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
|
|
7
|
-
import { StripeWebookEndpoint } from '../../src/endpoints/global/payments/StripeWebhookEndpoint';
|
|
8
|
-
import { StripeHelper } from '../../src/helpers/StripeHelper';
|
|
9
|
-
import { testServer } from './TestServer';
|
|
7
|
+
import { StripeWebookEndpoint } from '../../src/endpoints/global/payments/StripeWebhookEndpoint.js';
|
|
8
|
+
import { StripeHelper } from '../../src/helpers/StripeHelper.js';
|
|
9
|
+
import { testServer } from './TestServer.js';
|
|
10
|
+
import { resetNock } from './resetNock.js';
|
|
10
11
|
|
|
11
12
|
export class StripeMocker {
|
|
12
13
|
paymentIntents: { id: string }[] = [];
|
|
@@ -215,8 +216,7 @@ export class StripeMocker {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
stop() {
|
|
218
|
-
|
|
219
|
-
nock.disableNetConnect();
|
|
219
|
+
resetNock();
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|