@stamhoofd/backend 2.138.2 → 2.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/crons/fake-settlements.test.ts +165 -0
- package/src/crons/fake-settlements.ts +117 -0
- package/src/crons/index.ts +1 -0
- package/src/endpoints/auth/CreateTokenEndpoint.ts +5 -2
- package/src/endpoints/auth/ForgotPasswordEndpoint.test.ts +45 -0
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +2 -23
- package/src/endpoints/auth/MFA.test.ts +187 -3
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +543 -0
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.ts +78 -0
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +3 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +1095 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.ts +111 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -79
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +3 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +1 -1
- package/src/excel-loaders/balance-item-payments.ts +145 -0
- package/src/excel-loaders/index.ts +1 -0
- package/src/excel-loaders/payments.ts +45 -37
- package/src/helpers/StripeInvoicer.ts +1 -1
- package/src/helpers/TwoFactorHelper.ts +91 -9
- package/src/helpers/breakdownRelations.ts +48 -0
- package/src/helpers/getNextPageRequest.ts +24 -0
- package/src/helpers/streamForBreakdown.test.ts +107 -0
- package/src/helpers/streamForBreakdown.ts +104 -0
- package/src/services/PasswordForgotService.ts +31 -1
- package/src/services/PaymentService.ts +1 -1
- package/src/services/SSOService.ts +14 -1
- package/src/sql-filters/balance-item-payments-root.ts +48 -0
- package/src/sql-filters/balance-items.ts +55 -0
- package/src/sql-filters/payment-settlement.test.ts +68 -0
- package/src/sql-filters/payment-settlement.ts +43 -0
- package/src/sql-filters/payments.ts +93 -31
- package/src/sql-sorters/balance-item-payments.ts +32 -0
- package/tests/helpers/ExportSlice.ts +71 -0
|
@@ -1,41 +1,39 @@
|
|
|
1
1
|
import { Payment } from '@stamhoofd/models';
|
|
2
2
|
import type { SQLFilterDefinitions } from '@stamhoofd/sql';
|
|
3
3
|
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createJoinedRelationFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
|
|
4
|
+
import type { StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
|
+
import { TransferSettings } from '@stamhoofd/structures';
|
|
4
6
|
import { balanceItemPaymentsCompilers } from './balance-item-payments.js';
|
|
5
7
|
import { organizationFilterCompilers } from './organizations.js';
|
|
8
|
+
import { paymentSettlementFilterCompilers } from './payment-settlement.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Defines how to filter payments in the database from StamhoofdFilter objects
|
|
12
|
+
*
|
|
13
|
+
* All expressions are qualified with the payments table so they keep working when payments is joined
|
|
14
|
+
* into another query instead of being selected from (see balance-item-payments-root.ts).
|
|
9
15
|
*/
|
|
10
16
|
export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
11
17
|
...baseSQLFilterCompilers,
|
|
18
|
+
// method, provider, status and settlement
|
|
19
|
+
...paymentSettlementFilterCompilers,
|
|
12
20
|
id: createColumnFilter({
|
|
13
|
-
expression: SQL.column('id'),
|
|
14
|
-
type: SQLValueType.String,
|
|
15
|
-
nullable: false,
|
|
16
|
-
}),
|
|
17
|
-
method: createColumnFilter({
|
|
18
|
-
expression: SQL.column(Payment.table, 'method'),
|
|
21
|
+
expression: SQL.column(Payment.table, 'id'),
|
|
19
22
|
type: SQLValueType.String,
|
|
20
23
|
nullable: false,
|
|
21
24
|
}),
|
|
22
25
|
type: createColumnFilter({
|
|
23
|
-
expression: SQL.column('type'),
|
|
24
|
-
type: SQLValueType.String,
|
|
25
|
-
nullable: false,
|
|
26
|
-
}),
|
|
27
|
-
status: createColumnFilter({
|
|
28
|
-
expression: SQL.column(Payment.table, 'status'),
|
|
26
|
+
expression: SQL.column(Payment.table, 'type'),
|
|
29
27
|
type: SQLValueType.String,
|
|
30
28
|
nullable: false,
|
|
31
29
|
}),
|
|
32
30
|
organizationId: createColumnFilter({
|
|
33
|
-
expression: SQL.column('organizationId'),
|
|
31
|
+
expression: SQL.column(Payment.table, 'organizationId'),
|
|
34
32
|
type: SQLValueType.String,
|
|
35
33
|
nullable: true,
|
|
36
34
|
}),
|
|
37
35
|
payingOrganizationId: createColumnFilter({
|
|
38
|
-
expression: SQL.column('payingOrganizationId'),
|
|
36
|
+
expression: SQL.column(Payment.table, 'payingOrganizationId'),
|
|
39
37
|
type: SQLValueType.String,
|
|
40
38
|
nullable: true,
|
|
41
39
|
}),
|
|
@@ -44,22 +42,22 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
44
42
|
organizationFilterCompilers,
|
|
45
43
|
),
|
|
46
44
|
invoiceId: createColumnFilter({
|
|
47
|
-
expression: SQL.column('invoiceId'),
|
|
45
|
+
expression: SQL.column(Payment.table, 'invoiceId'),
|
|
48
46
|
type: SQLValueType.String,
|
|
49
47
|
nullable: true,
|
|
50
48
|
}),
|
|
51
49
|
createdAt: createColumnFilter({
|
|
52
|
-
expression: SQL.column('createdAt'),
|
|
50
|
+
expression: SQL.column(Payment.table, 'createdAt'),
|
|
53
51
|
type: SQLValueType.Datetime,
|
|
54
52
|
nullable: false,
|
|
55
53
|
}),
|
|
56
54
|
updatedAt: createColumnFilter({
|
|
57
|
-
expression: SQL.column('updatedAt'),
|
|
55
|
+
expression: SQL.column(Payment.table, 'updatedAt'),
|
|
58
56
|
type: SQLValueType.Datetime,
|
|
59
57
|
nullable: false,
|
|
60
58
|
}),
|
|
61
59
|
paidAt: createColumnFilter({
|
|
62
|
-
expression: SQL.column('paidAt'),
|
|
60
|
+
expression: SQL.column(Payment.table, 'paidAt'),
|
|
63
61
|
type: SQLValueType.Datetime,
|
|
64
62
|
nullable: true,
|
|
65
63
|
}),
|
|
@@ -68,44 +66,73 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
68
66
|
type: SQLValueType.Number,
|
|
69
67
|
nullable: false,
|
|
70
68
|
}),
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
|
|
70
|
+
stripeAccountId: createColumnFilter({
|
|
71
|
+
expression: SQL.column(Payment.table, 'stripeAccountId'),
|
|
73
72
|
type: SQLValueType.String,
|
|
74
73
|
nullable: true,
|
|
75
74
|
}),
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* What a payment rounded away, which belongs to the payment as a whole instead of to one of the
|
|
78
|
+
* things it paid for (see PaymentBreakdownBuilder.addRounding).
|
|
79
|
+
*/
|
|
80
|
+
roundingAmount: createColumnFilter({
|
|
81
|
+
expression: SQL.column(Payment.table, 'roundingAmount'),
|
|
82
|
+
type: SQLValueType.Number,
|
|
83
|
+
nullable: false,
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The account a transfer was made to. Used to narrow a breakdown down to the money that arrived on
|
|
88
|
+
* one account (see PaymentBreakdown).
|
|
89
|
+
*/
|
|
90
|
+
transferSettings: {
|
|
91
|
+
...baseSQLFilterCompilers,
|
|
92
|
+
iban: createColumnFilter({
|
|
93
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'transferSettings'), '$.value.iban'),
|
|
94
|
+
type: SQLValueType.JSONString,
|
|
95
|
+
nullable: true,
|
|
96
|
+
}),
|
|
97
|
+
creditor: createColumnFilter({
|
|
98
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'transferSettings'), '$.value.creditor'),
|
|
99
|
+
type: SQLValueType.JSONString,
|
|
100
|
+
nullable: true,
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
76
103
|
transferDescription: createColumnFilter({
|
|
77
104
|
expression: SQL.column(Payment.table, 'transferDescription'),
|
|
78
105
|
type: SQLValueType.String,
|
|
79
106
|
nullable: true,
|
|
80
107
|
}),
|
|
81
108
|
hasInvoice: createColumnFilter({
|
|
82
|
-
expression: SQL.isNull(SQL.column('invoiceId')),
|
|
109
|
+
expression: SQL.isNull(SQL.column(Payment.table, 'invoiceId')),
|
|
83
110
|
type: SQLValueType.Boolean,
|
|
84
111
|
nullable: false,
|
|
85
112
|
}),
|
|
86
113
|
customer: {
|
|
87
114
|
...baseSQLFilterCompilers,
|
|
88
115
|
email: createColumnFilter({
|
|
89
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.email'),
|
|
116
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.email'),
|
|
90
117
|
type: SQLValueType.JSONString,
|
|
91
118
|
nullable: true,
|
|
92
119
|
}),
|
|
93
120
|
firstName: createColumnFilter({
|
|
94
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.firstName'),
|
|
121
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.firstName'),
|
|
95
122
|
type: SQLValueType.JSONString,
|
|
96
123
|
nullable: true,
|
|
97
124
|
}),
|
|
98
125
|
lastName: createColumnFilter({
|
|
99
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.lastName'),
|
|
126
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.lastName'),
|
|
100
127
|
type: SQLValueType.JSONString,
|
|
101
128
|
nullable: true,
|
|
102
129
|
}),
|
|
103
130
|
name: createColumnFilter({
|
|
104
131
|
expression: new SQLCast(
|
|
105
132
|
new SQLConcat(
|
|
106
|
-
new SQLJsonUnquote(SQL.jsonExtract(SQL.column('customer'), '$.value.firstName')),
|
|
133
|
+
new SQLJsonUnquote(SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.firstName')),
|
|
107
134
|
new SQLScalar(' '),
|
|
108
|
-
new SQLJsonUnquote(SQL.jsonExtract(SQL.column('customer'), '$.value.lastName')),
|
|
135
|
+
new SQLJsonUnquote(SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.lastName')),
|
|
109
136
|
),
|
|
110
137
|
'CHAR',
|
|
111
138
|
),
|
|
@@ -115,22 +142,22 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
115
142
|
company: {
|
|
116
143
|
...baseSQLFilterCompilers,
|
|
117
144
|
name: createColumnFilter({
|
|
118
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.name'),
|
|
145
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.company.name'),
|
|
119
146
|
type: SQLValueType.JSONString,
|
|
120
147
|
nullable: true,
|
|
121
148
|
}),
|
|
122
149
|
VATNumber: createColumnFilter({
|
|
123
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.VATNumber'),
|
|
150
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.company.VATNumber'),
|
|
124
151
|
type: SQLValueType.JSONString,
|
|
125
152
|
nullable: true,
|
|
126
153
|
}),
|
|
127
154
|
companyNumber: createColumnFilter({
|
|
128
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.companyNumber'),
|
|
155
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.company.companyNumber'),
|
|
129
156
|
type: SQLValueType.JSONString,
|
|
130
157
|
nullable: true,
|
|
131
158
|
}),
|
|
132
159
|
administrationEmail: createColumnFilter({
|
|
133
|
-
expression: SQL.jsonExtract(SQL.column('customer'), '$.value.company.administrationEmail'),
|
|
160
|
+
expression: SQL.jsonExtract(SQL.column(Payment.table, 'customer'), '$.value.company.administrationEmail'),
|
|
134
161
|
type: SQLValueType.JSONString,
|
|
135
162
|
nullable: true,
|
|
136
163
|
}),
|
|
@@ -148,9 +175,44 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
148
175
|
SQL.column('balance_item_payments', 'balanceItemId'),
|
|
149
176
|
),
|
|
150
177
|
).where(
|
|
151
|
-
SQL.column('paymentId'),
|
|
178
|
+
SQL.column('balance_item_payments', 'paymentId'),
|
|
152
179
|
SQL.column('payments', 'id'),
|
|
153
180
|
),
|
|
154
181
|
balanceItemPaymentsCompilers,
|
|
155
182
|
),
|
|
156
183
|
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What a search term typed in a list of payments selects, as a filter on the payments table.
|
|
187
|
+
*
|
|
188
|
+
* Shared by everything that lists or exports payments, so a search always selects the same payments no
|
|
189
|
+
* matter which of those it went through.
|
|
190
|
+
*/
|
|
191
|
+
export function getPaymentSearchFilter(search: string): StamhoofdFilter {
|
|
192
|
+
const transferDescription = search.replaceAll('+', '').replaceAll('/', '');
|
|
193
|
+
|
|
194
|
+
if (transferDescription.length === '562100153542'.length && !isNaN(parseInt(transferDescription))) {
|
|
195
|
+
// A structured transfer reference is only ever meant to find that one payment
|
|
196
|
+
return {
|
|
197
|
+
transferDescription: TransferSettings.structureOGM(transferDescription),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (search.includes('@')) {
|
|
202
|
+
return {
|
|
203
|
+
$or: [
|
|
204
|
+
{ customer: { email: { $contains: search } } },
|
|
205
|
+
{ customer: { company: { administrationEmail: { $contains: search } } } },
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
$or: [
|
|
212
|
+
{ customer: { name: { $contains: search } } },
|
|
213
|
+
{ customer: { company: { name: { $contains: search } } } },
|
|
214
|
+
{ balanceItemPayments: { $elemMatch: { balanceItem: { description: { $contains: search } } } } },
|
|
215
|
+
{ transferDescription: { $contains: search } },
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BalanceItemPayment } from '@stamhoofd/models';
|
|
2
|
+
import type { SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
|
+
import { SQL, SQLOrderBy } from '@stamhoofd/sql';
|
|
4
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
|
+
|
|
6
|
+
export const balanceItemPaymentSorters: SQLSortDefinitions<BalanceItemPayment> = {
|
|
7
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY! See balanceItemSorters for why sorting on anything that is
|
|
8
|
+
// not 1:1 with a column breaks pagination.
|
|
9
|
+
|
|
10
|
+
id: {
|
|
11
|
+
getValue(a) {
|
|
12
|
+
return a.id;
|
|
13
|
+
},
|
|
14
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
15
|
+
return new SQLOrderBy({
|
|
16
|
+
column: SQL.column('balance_item_payments', 'id'),
|
|
17
|
+
direction,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
createdAt: {
|
|
22
|
+
getValue(a) {
|
|
23
|
+
return Formatter.dateTimeIso(a.createdAt, 'UTC');
|
|
24
|
+
},
|
|
25
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
26
|
+
return new SQLOrderBy({
|
|
27
|
+
column: SQL.column('balance_item_payments', 'createdAt'),
|
|
28
|
+
direction,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import type { Organization, User } from '@stamhoofd/models';
|
|
3
|
+
import { Token } from '@stamhoofd/models';
|
|
4
|
+
import type { StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
|
+
import { ExcelExportType, LimitedFilteredRequest, SortItemDirection } from '@stamhoofd/structures';
|
|
6
|
+
import { ExportToExcelEndpoint } from '../../src/endpoints/global/files/ExportToExcelEndpoint.js';
|
|
7
|
+
import { Context } from '../../src/helpers/Context.js';
|
|
8
|
+
import { testServer } from './TestServer.js';
|
|
9
|
+
|
|
10
|
+
// Registers the balance item payment export, which is what a row that holds a part of its objects is
|
|
11
|
+
// written out with
|
|
12
|
+
import '../../src/excel-loaders/balance-item-payments.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads what the balance item payment export of a filter comes down to, from inside a request so it
|
|
16
|
+
* runs in the same scope an export runs in.
|
|
17
|
+
*/
|
|
18
|
+
class ExportSliceEndpoint extends Endpoint<Record<string, never>, undefined, undefined, undefined> {
|
|
19
|
+
result = { count: 0, price: 0 };
|
|
20
|
+
|
|
21
|
+
constructor(private readonly sliceFilter: StamhoofdFilter, private readonly search: string | null) {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected doesMatch(): [true, Record<string, never>] {
|
|
26
|
+
return [true, {}];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle() {
|
|
30
|
+
await Context.setOrganizationScope();
|
|
31
|
+
await Context.authenticate();
|
|
32
|
+
|
|
33
|
+
const loader = ExportToExcelEndpoint.loaders.get(ExcelExportType.BalanceItemPayments)!;
|
|
34
|
+
const data = await loader.fetch(new LimitedFilteredRequest({
|
|
35
|
+
filter: this.sliceFilter,
|
|
36
|
+
search: this.search,
|
|
37
|
+
limit: 100,
|
|
38
|
+
sort: [{ key: 'id', order: SortItemDirection.ASC }],
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Only the first page is added up, so a fixture that outgrows it would silently assert too little
|
|
42
|
+
expect(data.next).toBeUndefined();
|
|
43
|
+
|
|
44
|
+
const rows = data.results as { balanceItemPayment: { price: number } }[];
|
|
45
|
+
|
|
46
|
+
this.result = {
|
|
47
|
+
count: rows.length,
|
|
48
|
+
price: rows.reduce((total, row) => total + row.balanceItemPayment.price, 0),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return new Response(undefined);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How many balance item payments a filter selects, and what they are worth together.
|
|
57
|
+
*/
|
|
58
|
+
export async function exportSlice({ organization, user, filter, search = null }: { organization: Organization; user: User; filter: StamhoofdFilter; search?: string | null }) {
|
|
59
|
+
const token = await Token.createToken(user);
|
|
60
|
+
|
|
61
|
+
const request = Request.get({
|
|
62
|
+
path: '/exports/balance-item-payments',
|
|
63
|
+
host: organization.getApiHost(),
|
|
64
|
+
headers: { authorization: 'Bearer ' + token.accessToken },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const endpoint = new ExportSliceEndpoint(filter, search);
|
|
68
|
+
await testServer.test(endpoint, request);
|
|
69
|
+
|
|
70
|
+
return endpoint.result;
|
|
71
|
+
}
|