@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.
Files changed (48) hide show
  1. package/LICENSE.md +32 -0
  2. package/package.json +14 -11
  3. package/src/boot.ts +1 -0
  4. package/src/email-recipient-loaders/documents.ts +66 -0
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +701 -4
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -10
  7. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +661 -4
  8. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -6
  9. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +291 -8
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +22 -0
  11. package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +43 -0
  12. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +219 -0
  13. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
  14. package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +72 -0
  15. package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -2
  16. package/src/excel-loaders/members.ts +27 -27
  17. package/src/helpers/AdminPermissionChecker.ts +30 -10
  18. package/src/helpers/AuthenticatedStructures.ts +24 -5
  19. package/src/helpers/StripeHelper.ts +11 -1
  20. package/src/helpers/StripePayoutChecker.ts +7 -0
  21. package/src/helpers/UitpasTokenRepository.ts +7 -5
  22. package/src/helpers/passthroughFetch.ts +24 -0
  23. package/src/helpers/updateMemberDetailsUitpasNumber.ts +149 -0
  24. package/src/seeds/data/default-email-templates.sql +2 -1
  25. package/src/seeds/wip/1769088653-uitpas-status.ts +129 -0
  26. package/src/services/InvoiceService.ts +2 -2
  27. package/src/services/uitpas/PassholderEndpoints.ts +190 -0
  28. package/src/services/uitpas/UitpasService.ts +37 -12
  29. package/src/services/uitpas/checkUitpasNumbers.ts +16 -140
  30. package/src/services/uitpas/handleUitpasResponse.ts +89 -0
  31. package/src/sql-filters/invoiced-balance-items.ts +20 -0
  32. package/src/sql-filters/invoices.ts +122 -0
  33. package/src/sql-filters/payments.ts +11 -1
  34. package/src/sql-sorters/invoices.ts +83 -0
  35. package/src/sql-sorters/payments.ts +33 -0
  36. package/tests/e2e/bundle-discounts.test.ts +8 -8
  37. package/tests/e2e/tests-disable-net-connect.test.ts +5 -0
  38. package/tests/helpers/StripeMocker.ts +5 -5
  39. package/tests/helpers/UitpasApiMocker.ts +175 -0
  40. package/tests/helpers/index.ts +1 -0
  41. package/tests/helpers/resetNock.ts +7 -0
  42. package/tests/init/index.ts +1 -0
  43. package/tests/init/initPayconiq.ts +2 -2
  44. package/tests/init/initStripe.ts +1 -1
  45. package/tests/init/initUitpasApi.ts +14 -0
  46. package/tests/jest.global.setup.ts +6 -4
  47. package/tests/jest.setup.ts +12 -6
  48. 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
- type UitpasNumberSuccessfulResponse = {
5
- socialTariff: {
6
- status: 'ACTIVE' | 'EXPIRED' | 'NONE';
7
- };
8
- messages?: Array<{
9
- text: string;
10
- }>;
11
- };
12
-
13
- type UitpasNumberErrorResponse = {
14
- title: string; // e.g., "Invalid uitpas number"
15
- endUserMessage?: {
16
- nl: string;
17
- };
18
- };
19
-
20
- function assertIsUitpasNumberSuccessfulResponse(
21
- json: unknown,
22
- ): asserts json is UitpasNumberSuccessfulResponse {
23
- if (
24
- typeof json !== 'object'
25
- || json === null
26
- || !('socialTariff' in json)
27
- || typeof json.socialTariff !== 'object'
28
- || json.socialTariff === null
29
- || !('status' in json.socialTariff)
30
- || typeof json.socialTariff.status !== 'string'
31
- || (json.socialTariff.status !== 'ACTIVE' && json.socialTariff.status !== 'EXPIRED' && json.socialTariff.status !== 'NONE')
32
- || ('messages' in json && (!Array.isArray(json.messages) || !json.messages.every(
33
- (message: unknown) => typeof message === 'object' && message !== null && 'text' in message && typeof message.text === 'string')))
34
- ) {
35
- console.error('Invalid response when retrieving pass by UiTPAS number:', json);
36
- throw new SimpleError({
37
- code: 'invalid_response_retrieving_pass_by_uitpas_number',
38
- message: `Invalid response when retrieving pass by UiTPAS number`,
39
- human: $t(`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
- await checkUitpasNumber(access_token, uitpasNumber); // Throws if invalid
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 { assertBalances } from '../assertions/assertBalances';
7
- import { testServer } from '../helpers/TestServer';
8
- import { initBundleDiscount } from '../init/initBundleDiscount';
9
- import { initStripe } from '../init/initStripe';
10
- import { initAdmin } from '../init/initAdmin';
11
- import { BalanceItemService } from '../../src/services/BalanceItemService';
12
- import { initPermissionRole } from '../init';
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
 
@@ -0,0 +1,5 @@
1
+ test('Should disallow net connect if request is not mocked', async () => {
2
+ const promise = fetch('https://www.google.com');
3
+
4
+ await expect(promise).rejects.toThrow(/Disallowed net connect/);
5
+ });
@@ -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
- nock.cleanAll();
219
- nock.disableNetConnect();
219
+ resetNock();
220
220
  }
221
221
  }
222
222