@stamhoofd/backend 2.8.0 → 2.13.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/.env.template.json +3 -1
- package/package.json +11 -3
- package/src/crons.ts +3 -3
- package/src/decoders/StringArrayDecoder.ts +24 -0
- package/src/decoders/StringNullableDecoder.ts +18 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +20 -18
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
- package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
- package/src/helpers/AdminPermissionChecker.ts +95 -60
- package/src/helpers/AuthenticatedStructures.ts +16 -6
- package/src/helpers/Context.ts +21 -0
- package/src/helpers/EmailResumer.ts +22 -2
- package/src/helpers/MemberUserSyncer.ts +8 -2
- package/src/helpers/ViesHelper.ts +151 -0
- package/src/seeds/1722344160-update-membership.ts +19 -22
- package/src/seeds/1722344161-sync-member-users.ts +60 -0
- package/.env.json +0 -65
|
@@ -1,91 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
3
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
+
import { Payment } from '@stamhoofd/models';
|
|
6
|
+
import { SQLJsonUnquote, SQL, SQLConcat, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, SQLCast } from "@stamhoofd/sql";
|
|
7
|
+
import { CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, PaymentGeneral, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
|
|
8
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
+
|
|
10
|
+
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
11
|
+
import { Context } from '../../../../helpers/Context';
|
|
8
12
|
|
|
9
13
|
type Params = Record<string, never>;
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const parts = strValue.split(",");
|
|
25
|
-
return parts
|
|
26
|
-
.map((v, index) => {
|
|
27
|
-
return data.clone({
|
|
28
|
-
data: v,
|
|
29
|
-
context: data.context,
|
|
30
|
-
field: data.addToCurrentField(index)
|
|
31
|
-
}).decode(this.decoder)
|
|
32
|
-
});
|
|
33
|
-
}
|
|
14
|
+
type Query = LimitedFilteredRequest;
|
|
15
|
+
type Body = undefined;
|
|
16
|
+
type ResponseBody = PaginatedResponse<PaymentGeneral[], LimitedFilteredRequest>
|
|
17
|
+
|
|
18
|
+
const balanceItemPaymentsCompilers: SQLFilterDefinitions = {
|
|
19
|
+
...baseSQLFilterCompilers,
|
|
20
|
+
"id": createSQLColumnFilterCompiler(SQL.column('balance_item_payments', 'id')),
|
|
21
|
+
"price": createSQLColumnFilterCompiler(SQL.column('balance_item_payments', 'price')),
|
|
22
|
+
|
|
23
|
+
"balanceItem": createSQLFilterNamespace({
|
|
24
|
+
...baseSQLFilterCompilers,
|
|
25
|
+
id: createSQLColumnFilterCompiler(SQL.column('balance_items', 'id')),
|
|
26
|
+
description: createSQLColumnFilterCompiler(SQL.column('balance_items', 'description')),
|
|
27
|
+
})
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
const filterCompilers: SQLFilterDefinitions = {
|
|
31
|
+
...baseSQLFilterCompilers,
|
|
32
|
+
id: createSQLColumnFilterCompiler('id'),
|
|
33
|
+
method: createSQLColumnFilterCompiler('method'),
|
|
34
|
+
status: createSQLColumnFilterCompiler('status'),
|
|
35
|
+
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
36
|
+
createdAt: createSQLColumnFilterCompiler('createdAt'),
|
|
37
|
+
paidAt: createSQLColumnFilterCompiler('paidAt', {nullable: true}),
|
|
38
|
+
price: createSQLColumnFilterCompiler('price'),
|
|
39
|
+
provider: createSQLColumnFilterCompiler('provider', {nullable: true}),
|
|
40
|
+
customer: createSQLFilterNamespace({
|
|
41
|
+
...baseSQLFilterCompilers,
|
|
42
|
+
email: createSQLExpressionFilterCompiler(
|
|
43
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.email'),
|
|
44
|
+
{isJSONValue: true}
|
|
45
|
+
),
|
|
46
|
+
firstName: createSQLExpressionFilterCompiler(
|
|
47
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.firstName'),
|
|
48
|
+
{isJSONValue: true}
|
|
49
|
+
),
|
|
50
|
+
lastName: createSQLExpressionFilterCompiler(
|
|
51
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.lastName'),
|
|
52
|
+
{isJSONValue: true}
|
|
53
|
+
),
|
|
54
|
+
name: createSQLExpressionFilterCompiler(
|
|
55
|
+
new SQLCast(
|
|
56
|
+
new SQLConcat(
|
|
57
|
+
new SQLJsonUnquote(SQL.jsonValue(SQL.column('customer'), '$.value.firstName')),
|
|
58
|
+
new SQLScalar(' '),
|
|
59
|
+
new SQLJsonUnquote(SQL.jsonValue(SQL.column('customer'), '$.value.lastName')),
|
|
60
|
+
),
|
|
61
|
+
'CHAR'
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
company: createSQLFilterNamespace({
|
|
65
|
+
name: createSQLExpressionFilterCompiler(
|
|
66
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.company.name'),
|
|
67
|
+
{isJSONValue: true}
|
|
68
|
+
),
|
|
69
|
+
VATNumber: createSQLExpressionFilterCompiler(
|
|
70
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.company.VATNumber'),
|
|
71
|
+
{isJSONValue: true}
|
|
72
|
+
),
|
|
73
|
+
companyNumber: createSQLExpressionFilterCompiler(
|
|
74
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.company.companyNumber'),
|
|
75
|
+
{isJSONValue: true}
|
|
76
|
+
),
|
|
77
|
+
administrationEmail: createSQLExpressionFilterCompiler(
|
|
78
|
+
SQL.jsonValue(SQL.column('customer'), '$.value.company.administrationEmail'),
|
|
79
|
+
{isJSONValue: true}
|
|
80
|
+
),
|
|
81
|
+
})
|
|
82
|
+
}),
|
|
83
|
+
balanceItemPayments: createSQLRelationFilterCompiler(
|
|
84
|
+
SQL.select()
|
|
85
|
+
.from(
|
|
86
|
+
SQL.table('balance_item_payments')
|
|
87
|
+
).join(
|
|
88
|
+
SQL.join(
|
|
89
|
+
SQL.table('balance_items')
|
|
90
|
+
).where(
|
|
91
|
+
SQL.column('balance_items', 'id'),
|
|
92
|
+
SQL.column('balance_item_payments', 'balanceItemId')
|
|
93
|
+
)
|
|
94
|
+
).where(
|
|
95
|
+
SQL.column('paymentId'),
|
|
96
|
+
SQL.column('payments', 'id')
|
|
97
|
+
),
|
|
98
|
+
balanceItemPaymentsCompilers
|
|
99
|
+
),
|
|
100
|
+
}
|
|
42
101
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
102
|
+
const sorters: SQLSortDefinitions<Payment> = {
|
|
103
|
+
'id': {
|
|
104
|
+
getValue(a) {
|
|
105
|
+
return a.id
|
|
106
|
+
},
|
|
107
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
108
|
+
return new SQLOrderBy({
|
|
109
|
+
column: SQL.column('id'),
|
|
110
|
+
direction
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
'createdAt': {
|
|
115
|
+
getValue(a) {
|
|
116
|
+
return Formatter.dateTimeIso(a.createdAt, 'UTC')
|
|
117
|
+
},
|
|
118
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
119
|
+
return new SQLOrderBy({
|
|
120
|
+
column: SQL.column('createdAt'),
|
|
121
|
+
direction
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
'paidAt': {
|
|
126
|
+
getValue(a) {
|
|
127
|
+
return a.paidAt !== null ? Formatter.dateTimeIso(a.paidAt, 'UTC') : null
|
|
128
|
+
},
|
|
129
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
130
|
+
return new SQLOrderBy({
|
|
131
|
+
column: SQL.column('paidAt'),
|
|
132
|
+
direction
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
'price': {
|
|
137
|
+
getValue(a) {
|
|
138
|
+
return a.price
|
|
139
|
+
},
|
|
140
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
141
|
+
return new SQLOrderBy({
|
|
142
|
+
column: SQL.column('price'),
|
|
143
|
+
direction
|
|
144
|
+
})
|
|
46
145
|
}
|
|
47
|
-
|
|
48
|
-
return data.decode(this.decoder);
|
|
49
146
|
}
|
|
50
147
|
}
|
|
51
148
|
|
|
52
|
-
|
|
53
|
-
class Query extends AutoEncoder {
|
|
54
|
-
/**
|
|
55
|
-
* Usage in combination with paidSince is special!
|
|
56
|
-
*/
|
|
57
|
-
@field({ decoder: StringDecoder, optional: true })
|
|
58
|
-
afterId?: string
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Return all payments that were paid after (and including) this date.
|
|
62
|
-
* Only returns orders **equal** to this date if afterId is not provided or if the id of those payments is also higher.
|
|
63
|
-
*/
|
|
64
|
-
@field({ decoder: DateDecoder, optional: true })
|
|
65
|
-
paidSince?: Date
|
|
66
|
-
|
|
67
|
-
@field({ decoder: DateDecoder, optional: true })
|
|
68
|
-
paidBefore?: Date
|
|
69
|
-
|
|
70
|
-
@field({ decoder: IntegerDecoder, optional: true })
|
|
71
|
-
limit?: number
|
|
72
|
-
|
|
73
|
-
@field({ decoder: new StringArrayDecoder(new EnumDecoder(PaymentMethod)), optional: true })
|
|
74
|
-
methods?: PaymentMethod[]
|
|
75
|
-
|
|
76
|
-
@field({ decoder: new StringArrayDecoder(new StringNullableDecoder(new EnumDecoder(PaymentProvider))), optional: true })
|
|
77
|
-
providers?: (PaymentProvider|null)[]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
149
|
export class GetPaymentsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
81
|
-
|
|
150
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
|
|
82
151
|
|
|
83
152
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
84
153
|
if (request.method != "GET") {
|
|
85
154
|
return [false];
|
|
86
155
|
}
|
|
87
156
|
|
|
88
|
-
const params = Endpoint.parseParameters(request.url, "/
|
|
157
|
+
const params = Endpoint.parseParameters(request.url, "/payments", {});
|
|
89
158
|
|
|
90
159
|
if (params) {
|
|
91
160
|
return [true, params as Params];
|
|
@@ -93,115 +162,168 @@ export class GetPaymentsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
93
162
|
return [false];
|
|
94
163
|
}
|
|
95
164
|
|
|
96
|
-
async
|
|
97
|
-
const organization =
|
|
98
|
-
|
|
165
|
+
static async buildQuery(q: CountFilteredRequest|LimitedFilteredRequest) {
|
|
166
|
+
const organization = Context.organization
|
|
167
|
+
let scopeFilter: StamhoofdFilter|undefined = undefined;
|
|
168
|
+
|
|
169
|
+
if (!organization) {
|
|
170
|
+
throw Context.auth.error()
|
|
171
|
+
}
|
|
99
172
|
|
|
100
173
|
if (!await Context.auth.canManagePayments(organization.id)) {
|
|
101
174
|
throw Context.auth.error()
|
|
102
|
-
}
|
|
175
|
+
}
|
|
103
176
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
177
|
+
scopeFilter = {
|
|
178
|
+
organizationId: organization.id
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const query = SQL
|
|
182
|
+
.select()
|
|
183
|
+
.from(
|
|
184
|
+
SQL.table('payments')
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (scopeFilter) {
|
|
188
|
+
query.where(compileToSQLFilter(scopeFilter, filterCompilers))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (q.filter) {
|
|
192
|
+
query.where(compileToSQLFilter(q.filter, filterCompilers))
|
|
193
|
+
}
|
|
108
194
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
195
|
+
if (q.search) {
|
|
196
|
+
// todo
|
|
197
|
+
|
|
198
|
+
let searchFilter: StamhoofdFilter|null = null
|
|
199
|
+
searchFilter = {
|
|
200
|
+
$or: [
|
|
201
|
+
{
|
|
202
|
+
customer: {
|
|
203
|
+
name: {
|
|
204
|
+
$contains: q.search
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
customer: {
|
|
210
|
+
company: {
|
|
211
|
+
name: {
|
|
212
|
+
$contains: q.search
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
balanceItemPayments: {
|
|
219
|
+
$elemMatch: {
|
|
220
|
+
balanceItem: {
|
|
221
|
+
description: {
|
|
222
|
+
$contains: q.search
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (q.search.includes('@')) {
|
|
232
|
+
searchFilter = {
|
|
233
|
+
$or: [
|
|
234
|
+
{
|
|
235
|
+
customer: {
|
|
236
|
+
email: {
|
|
237
|
+
$contains: q.search
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
customer: {
|
|
243
|
+
company: {
|
|
244
|
+
administrationEmail: {
|
|
245
|
+
$contains: q.search
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
]
|
|
133
251
|
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}]
|
|
140
|
-
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (searchFilter) {
|
|
255
|
+
query.where(compileToSQLFilter(searchFilter, filterCompilers))
|
|
256
|
+
}
|
|
141
257
|
}
|
|
142
258
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
sign: query.afterId ? '>' : '>=',
|
|
147
|
-
value: paidSince
|
|
148
|
-
}, {
|
|
149
|
-
sign: '<=',
|
|
150
|
-
value: query.paidBefore
|
|
151
|
-
}] : {
|
|
152
|
-
sign: query.afterId ? '>' : '>=',
|
|
153
|
-
value: paidSince
|
|
154
|
-
},
|
|
155
|
-
method: {
|
|
156
|
-
sign: 'IN',
|
|
157
|
-
value: query.methods ?? [PaymentMethod.Transfer]
|
|
158
|
-
},
|
|
159
|
-
provider: {
|
|
160
|
-
sign: 'IN',
|
|
161
|
-
value: query.providers ?? [null]
|
|
259
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
260
|
+
if (q.pageFilter) {
|
|
261
|
+
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
162
262
|
}
|
|
163
|
-
}, {
|
|
164
|
-
limit: query.limit ? (query.limit - payments.length) : undefined,
|
|
165
|
-
sort: [{
|
|
166
|
-
column: "paidAt",
|
|
167
|
-
direction: "ASC"
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
column: "id",
|
|
171
|
-
direction: "ASC"
|
|
172
|
-
}]
|
|
173
|
-
}));
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!query.paidSince && !query.methods && !query.providers) {
|
|
177
|
-
// Default behaviour is to return all not-paid transfer payments that are not yet paid
|
|
178
|
-
|
|
179
|
-
payments.push(...
|
|
180
|
-
await Payment.where({
|
|
181
|
-
organizationId: organization.id,
|
|
182
|
-
paidAt: null,
|
|
183
|
-
method: PaymentMethod.Transfer,
|
|
184
|
-
status: {
|
|
185
|
-
sign: '!=',
|
|
186
|
-
value: PaymentStatus.Failed
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
);
|
|
190
263
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
264
|
+
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
265
|
+
query.limit(q.limit)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return query
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
272
|
+
const query = await this.buildQuery(requestQuery)
|
|
273
|
+
const data = await query.fetch()
|
|
274
|
+
|
|
275
|
+
const payments = Payment.fromRows(data, 'payments')
|
|
276
|
+
|
|
277
|
+
let next: LimitedFilteredRequest|undefined;
|
|
278
|
+
|
|
279
|
+
if (payments.length >= requestQuery.limit) {
|
|
280
|
+
const lastObject = payments[payments.length - 1];
|
|
281
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
282
|
+
|
|
283
|
+
next = new LimitedFilteredRequest({
|
|
284
|
+
filter: requestQuery.filter,
|
|
285
|
+
pageFilter: nextFilter,
|
|
286
|
+
sort: requestQuery.sort,
|
|
287
|
+
limit: requestQuery.limit,
|
|
288
|
+
search: requestQuery.search
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
292
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
293
|
+
next = undefined;
|
|
294
|
+
}
|
|
203
295
|
}
|
|
204
296
|
|
|
205
|
-
return
|
|
297
|
+
return new PaginatedResponse<PaymentGeneral[], LimitedFilteredRequest>({
|
|
298
|
+
results: await AuthenticatedStructures.paymentsGeneral(payments, false),
|
|
299
|
+
next
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
304
|
+
await Context.setOrganizationScope();
|
|
305
|
+
await Context.authenticate()
|
|
306
|
+
|
|
307
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
308
|
+
|
|
309
|
+
if (request.query.limit > maxLimit) {
|
|
310
|
+
throw new SimpleError({
|
|
311
|
+
code: 'invalid_field',
|
|
312
|
+
field: 'limit',
|
|
313
|
+
message: 'Limit can not be more than ' + maxLimit
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (request.query.limit < 1) {
|
|
318
|
+
throw new SimpleError({
|
|
319
|
+
code: 'invalid_field',
|
|
320
|
+
field: 'limit',
|
|
321
|
+
message: 'Limit can not be less than 1'
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return new Response(
|
|
326
|
+
await GetPaymentsEndpoint.buildData(request.query)
|
|
327
|
+
);
|
|
206
328
|
}
|
|
207
329
|
}
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
4
|
import { BalanceItem, Member, Order, Registration, User } from '@stamhoofd/models';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
-
import { BalanceItemStatus,
|
|
6
|
+
import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from "@stamhoofd/structures";
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
@@ -11,11 +11,11 @@ import { Context } from '../../../../helpers/Context';
|
|
|
11
11
|
|
|
12
12
|
type Params = Record<string, never>;
|
|
13
13
|
type Query = undefined;
|
|
14
|
-
type Body = PatchableArrayAutoEncoder<
|
|
15
|
-
type ResponseBody =
|
|
14
|
+
type Body = PatchableArrayAutoEncoder<BalanceItemWithPayments>
|
|
15
|
+
type ResponseBody = BalanceItemWithPayments[]
|
|
16
16
|
|
|
17
17
|
export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
-
bodyDecoder = new PatchableArrayDecoder(
|
|
18
|
+
bodyDecoder = new PatchableArrayDecoder(BalanceItemWithPayments as Decoder<BalanceItemWithPayments>, BalanceItemWithPayments.patchType() as Decoder<AutoEncoderPatchType<BalanceItemWithPayments>>, StringDecoder)
|
|
19
19
|
|
|
20
20
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
21
21
|
if (request.method != "PATCH") {
|
|
@@ -53,7 +53,10 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
53
53
|
// Create a new balance item
|
|
54
54
|
const model = new BalanceItem();
|
|
55
55
|
model.description = put.description;
|
|
56
|
-
model.
|
|
56
|
+
model.amount = put.amount;
|
|
57
|
+
model.type = BalanceItemType.Other
|
|
58
|
+
model.unitPrice = put.unitPrice;
|
|
59
|
+
model.amount = put.amount;
|
|
57
60
|
model.organizationId = organization.id;
|
|
58
61
|
model.createdAt = put.createdAt;
|
|
59
62
|
model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
|
|
@@ -75,19 +78,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
75
78
|
})
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
if (put.registration) {
|
|
79
|
-
const registration = await Registration.getByID(put.registration.id)
|
|
80
|
-
if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
|
|
81
|
-
throw new SimpleError({
|
|
82
|
-
code: 'invalid_field',
|
|
83
|
-
message: 'Registration not found',
|
|
84
|
-
field: 'registration'
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
model.registrationId = registration.id
|
|
88
|
-
registrationIds.push(registration.id)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
await model.save();
|
|
92
82
|
returnedModels.push(model);
|
|
93
83
|
}
|
|
@@ -101,6 +91,15 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
101
91
|
message: 'BalanceItem not found'
|
|
102
92
|
})
|
|
103
93
|
}
|
|
94
|
+
|
|
95
|
+
if (patch.unitPrice !== undefined) {
|
|
96
|
+
throw new SimpleError({
|
|
97
|
+
code: 'invalid_field',
|
|
98
|
+
message: 'You cannot change the unit price of a balance item',
|
|
99
|
+
human: 'Het is niet mogelijk om de eenheidsprijs van een openstaande schuld te wijzigen. Je kan de openstaande schuld verwijderen en opnieuw aanmaken indien noodzakelijk.'
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
104
103
|
// Check permissions
|
|
105
104
|
if (model.memberId) {
|
|
106
105
|
// Update old
|
|
@@ -123,30 +122,16 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
123
122
|
model.createdAt = patch.createdAt
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
if (patch.registration) {
|
|
127
|
-
const registration = await Registration.getByID(patch.registration.id)
|
|
128
|
-
if (!registration || registration.memberId !== model.memberId || registration.organizationId !== organization.id) {
|
|
129
|
-
throw new SimpleError({
|
|
130
|
-
code: 'invalid_field',
|
|
131
|
-
message: 'Registration not found',
|
|
132
|
-
field: 'registration'
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
model.registrationId = registration.id
|
|
136
|
-
|
|
137
|
-
// Update new
|
|
138
|
-
registrationIds.push(model.registrationId)
|
|
139
|
-
} else if (patch.registration === null) {
|
|
140
|
-
model.registrationId = null
|
|
141
|
-
}
|
|
142
125
|
model.description = patch.description ?? model.description;
|
|
143
|
-
model.
|
|
126
|
+
model.unitPrice = patch.unitPrice ?? model.unitPrice;
|
|
127
|
+
model.amount = patch.amount ?? model.amount;
|
|
144
128
|
|
|
145
129
|
if (model.orderId) {
|
|
146
130
|
// Not allowed to change this
|
|
147
131
|
const order = await Order.getByID(model.orderId)
|
|
148
132
|
if (order) {
|
|
149
|
-
model.
|
|
133
|
+
model.unitPrice = order.totalToPay
|
|
134
|
+
model.amount = 1
|
|
150
135
|
}
|
|
151
136
|
}
|
|
152
137
|
|
|
@@ -167,7 +152,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
167
152
|
await Registration.updateOutstandingBalance(Formatter.uniqueArray(registrationIds), organization.id)
|
|
168
153
|
|
|
169
154
|
return new Response(
|
|
170
|
-
await BalanceItem.
|
|
155
|
+
await BalanceItem.getStructureWithPayments(returnedModels)
|
|
171
156
|
);
|
|
172
157
|
}
|
|
173
158
|
|