@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.
Files changed (35) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +11 -3
  3. package/src/crons.ts +3 -3
  4. package/src/decoders/StringArrayDecoder.ts +24 -0
  5. package/src/decoders/StringNullableDecoder.ts +18 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +20 -18
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  10. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  11. package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  13. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
  15. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  16. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  17. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
  18. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  19. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
  24. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  25. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  26. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  27. package/src/helpers/AdminPermissionChecker.ts +95 -60
  28. package/src/helpers/AuthenticatedStructures.ts +16 -6
  29. package/src/helpers/Context.ts +21 -0
  30. package/src/helpers/EmailResumer.ts +22 -2
  31. package/src/helpers/MemberUserSyncer.ts +8 -2
  32. package/src/helpers/ViesHelper.ts +151 -0
  33. package/src/seeds/1722344160-update-membership.ts +19 -22
  34. package/src/seeds/1722344161-sync-member-users.ts +60 -0
  35. package/.env.json +0 -65
@@ -1,91 +1,160 @@
1
- import { AutoEncoder, Data, DateDecoder, Decoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
2
- import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
- import { Organization, Payment } from "@stamhoofd/models";
4
- import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
5
-
6
- import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
7
- import { Context } from "../../../../helpers/Context";
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 Body = undefined
11
- type ResponseBody = PaymentGeneral[]
12
-
13
- export class StringArrayDecoder<T> implements Decoder<T[]> {
14
- decoder: Decoder<T>;
15
-
16
- constructor(decoder: Decoder<T>) {
17
- this.decoder = decoder;
18
- }
19
-
20
- decode(data: Data): T[] {
21
- const strValue = data.string;
22
-
23
- // Split on comma
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
- export class StringNullableDecoder<T> implements Decoder<T | null> {
37
- decoder: Decoder<T>;
38
-
39
- constructor(decoder: Decoder<T>) {
40
- this.decoder = decoder;
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
- decode(data: Data): T | null {
44
- if (data.value === 'null') {
45
- return null;
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
- protected queryDecoder = Query;
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, "/organization/payments", {});
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 handle(request: DecodedRequest<Params, Query, Body>) {
97
- const organization = await Context.setOrganizationScope();
98
- await Context.authenticate()
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
- return new Response(
105
- (await this.getPayments(organization, request.query))
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
- async getPayments(organization: Organization, query: Query) {
110
- const paidSince = query.paidSince ?? new Date(Date.now() - (24 * 60 * 60 * 1000 * 7 ))
111
- paidSince.setMilliseconds(0)
112
- const payments: Payment[] = []
113
-
114
- if (query.afterId) {
115
- // First return all payments with id > afterId and paidAt == paidSince
116
- payments.push(...await Payment.where({
117
- organizationId: organization.id,
118
- paidAt: {
119
- sign: '=',
120
- value: paidSince
121
- },
122
- id: {
123
- sign: '>',
124
- value: query.afterId ?? ""
125
- },
126
- method: {
127
- sign: 'IN',
128
- value: query.methods ?? [PaymentMethod.Transfer]
129
- },
130
- provider: {
131
- sign: 'IN',
132
- value: query.providers ?? [null]
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
- limit: query.limit ?? undefined,
136
- sort: [{
137
- column: "id",
138
- direction: "ASC"
139
- }]
140
- }));
252
+ }
253
+
254
+ if (searchFilter) {
255
+ query.where(compileToSQLFilter(searchFilter, filterCompilers))
256
+ }
141
257
  }
142
258
 
143
- payments.push(...await Payment.where({
144
- organizationId: organization.id,
145
- paidAt: query.paidBefore ? [{
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
- payments.push(...
192
- await Payment.where({
193
- organizationId: organization.id,
194
- paidAt: null,
195
- updatedAt: {
196
- sign: '>',
197
- value: new Date(Date.now() - (24 * 60 * 60 * 1000 * 7 ))
198
- },
199
- method: PaymentMethod.Transfer,
200
- status: PaymentStatus.Failed
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 await AuthenticatedStructures.paymentsGeneral(payments, true)
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, MemberBalanceItem, PermissionLevel } from "@stamhoofd/structures";
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<MemberBalanceItem>
15
- type ResponseBody = MemberBalanceItem[]
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(MemberBalanceItem as Decoder<MemberBalanceItem>, MemberBalanceItem.patchType() as Decoder<AutoEncoderPatchType<MemberBalanceItem>>, StringDecoder)
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.price = put.price;
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.price = patch.price ?? model.price;
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.price = order.totalToPay
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.getMemberStructure(returnedModels)
155
+ await BalanceItem.getStructureWithPayments(returnedModels)
171
156
  );
172
157
  }
173
158