@stamhoofd/backend 2.62.0 → 2.64.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/index.ts +8 -6
- package/package.json +11 -11
- package/src/audit-logs/PaymentLogger.ts +1 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/update-cached-balances.ts +39 -0
- package/src/email-recipient-loaders/receivable-balances.ts +5 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
- package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +85 -25
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +89 -30
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +62 -17
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +52 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +8 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +19 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +10 -5
- package/src/helpers/AdminPermissionChecker.ts +3 -2
- package/src/helpers/AuthenticatedStructures.ts +127 -9
- package/src/helpers/MembershipCharger.ts +4 -0
- package/src/helpers/OrganizationCharger.ts +4 -0
- package/src/seeds/1733994455-balance-item-status-open.ts +30 -0
- package/src/seeds/1734596144-fill-previous-period-id.ts +55 -0
- package/src/seeds/1734700082-update-cached-outstanding-balance-from-items.ts +40 -0
- package/src/services/BalanceItemPaymentService.ts +8 -4
- package/src/services/BalanceItemService.ts +22 -3
- package/src/services/PaymentReallocationService.test.ts +746 -0
- package/src/services/PaymentReallocationService.ts +339 -0
- package/src/services/PaymentService.ts +13 -0
- package/src/services/PlatformMembershipService.ts +167 -137
- package/src/sql-filters/receivable-balances.ts +2 -1
- package/src/sql-sorters/receivable-balances.ts +3 -3
|
@@ -6,6 +6,7 @@ import { QueueHandler } from '@stamhoofd/queues';
|
|
|
6
6
|
import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { BalanceItemService } from '../../../../services/BalanceItemService';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -36,17 +37,13 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
36
37
|
throw Context.auth.error();
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
if (request.body.changes.length
|
|
40
|
+
if (request.body.changes.length === 0) {
|
|
40
41
|
return new Response([]);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const returnedModels: BalanceItem[] = [];
|
|
44
45
|
const updateOutstandingBalance: BalanceItem[] = [];
|
|
45
46
|
|
|
46
|
-
// Keep track of updates
|
|
47
|
-
const memberIds: string[] = [];
|
|
48
|
-
const registrationIds: string[] = [];
|
|
49
|
-
|
|
50
47
|
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
51
48
|
for (const { put } of request.body.getPuts()) {
|
|
52
49
|
// Create a new balance item
|
|
@@ -58,7 +55,8 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
58
55
|
model.amount = put.amount;
|
|
59
56
|
model.organizationId = organization.id;
|
|
60
57
|
model.createdAt = put.createdAt;
|
|
61
|
-
model.
|
|
58
|
+
model.dueAt = put.dueAt;
|
|
59
|
+
model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Due;
|
|
62
60
|
|
|
63
61
|
if (put.userId) {
|
|
64
62
|
model.userId = (await this.validateUserId(model, put.userId)).id;
|
|
@@ -66,10 +64,43 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
66
64
|
|
|
67
65
|
if (put.memberId) {
|
|
68
66
|
model.memberId = (await this.validateMemberId(put.memberId)).id;
|
|
69
|
-
memberIds.push(model.memberId);
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
if (
|
|
69
|
+
if (put.payingOrganizationId) {
|
|
70
|
+
// Not allowed if not full admin
|
|
71
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
72
|
+
throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
|
|
73
|
+
}
|
|
74
|
+
if (put.payingOrganizationId === model.organizationId) {
|
|
75
|
+
throw new SimpleError({
|
|
76
|
+
code: 'invalid_field',
|
|
77
|
+
message: 'payingOrganizationId cannot be the same as organizationId',
|
|
78
|
+
human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
|
|
79
|
+
field: 'payingOrganizationId',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
model.payingOrganizationId = put.payingOrganizationId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (model.dueAt && model.price < 0) {
|
|
87
|
+
throw new SimpleError({
|
|
88
|
+
code: 'invalid_price',
|
|
89
|
+
message: 'Cannot create negative balance in the future',
|
|
90
|
+
human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (model.createdAt > new Date()) {
|
|
95
|
+
throw new SimpleError({
|
|
96
|
+
code: 'invalid_field',
|
|
97
|
+
message: 'createdAt cannot be in the future',
|
|
98
|
+
human: 'De datum kan niet in de toekomst liggen',
|
|
99
|
+
field: 'createdAt',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!model.userId && !model.memberId && !model.payingOrganizationId) {
|
|
73
104
|
throw new SimpleError({
|
|
74
105
|
code: 'invalid_field',
|
|
75
106
|
message: 'No user or member provided',
|
|
@@ -93,39 +124,53 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
93
124
|
});
|
|
94
125
|
}
|
|
95
126
|
|
|
96
|
-
if (patch.
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
if (patch.payingOrganizationId !== undefined) {
|
|
128
|
+
// Not allowed if not full admin
|
|
129
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
130
|
+
throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
|
|
131
|
+
}
|
|
132
|
+
if (patch.payingOrganizationId === model.organizationId) {
|
|
133
|
+
throw new SimpleError({
|
|
134
|
+
code: 'invalid_field',
|
|
135
|
+
message: 'payingOrganizationId cannot be the same as organizationId',
|
|
136
|
+
human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
|
|
137
|
+
field: 'payingOrganizationId',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
103
140
|
|
|
104
|
-
|
|
105
|
-
if (model.memberId) {
|
|
106
|
-
// Update old
|
|
107
|
-
memberIds.push(model.memberId);
|
|
141
|
+
model.payingOrganizationId = patch.payingOrganizationId;
|
|
108
142
|
}
|
|
109
143
|
|
|
110
144
|
if (patch.memberId) {
|
|
111
145
|
model.memberId = (await this.validateMemberId(patch.memberId)).id;
|
|
112
|
-
|
|
113
|
-
// Update new
|
|
114
|
-
memberIds.push(model.memberId);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (model.registrationId) {
|
|
118
|
-
// Update old
|
|
119
|
-
registrationIds.push(model.registrationId);
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
if (patch.createdAt) {
|
|
123
149
|
model.createdAt = patch.createdAt;
|
|
150
|
+
|
|
151
|
+
if (model.createdAt > new Date()) {
|
|
152
|
+
throw new SimpleError({
|
|
153
|
+
code: 'invalid_field',
|
|
154
|
+
message: 'createdAt cannot be in the future',
|
|
155
|
+
human: 'De datum kan niet in de toekomst liggen',
|
|
156
|
+
field: 'createdAt',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
124
159
|
}
|
|
125
160
|
|
|
126
161
|
model.description = patch.description ?? model.description;
|
|
127
162
|
model.unitPrice = patch.unitPrice ?? model.unitPrice;
|
|
128
163
|
model.amount = patch.amount ?? model.amount;
|
|
164
|
+
model.dueAt = patch.dueAt === undefined ? model.dueAt : patch.dueAt;
|
|
165
|
+
|
|
166
|
+
if ((patch.dueAt !== undefined || patch.unitPrice !== undefined) && model.dueAt && model.price < 0) {
|
|
167
|
+
throw new SimpleError({
|
|
168
|
+
code: 'invalid_price',
|
|
169
|
+
message: 'Cannot create negative balance in the future',
|
|
170
|
+
human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
|
|
171
|
+
field: 'dueAt',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
129
174
|
|
|
130
175
|
if (model.orderId) {
|
|
131
176
|
// Not allowed to change this
|
|
@@ -142,13 +187,21 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
142
187
|
}
|
|
143
188
|
}
|
|
144
189
|
else if (patch.status) {
|
|
145
|
-
model.status =
|
|
190
|
+
model.status = patch.status;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!model.userId && !model.memberId && !model.payingOrganizationId) {
|
|
194
|
+
throw new SimpleError({
|
|
195
|
+
code: 'invalid_field',
|
|
196
|
+
message: 'No user or member provided',
|
|
197
|
+
field: 'userId',
|
|
198
|
+
});
|
|
146
199
|
}
|
|
147
200
|
|
|
148
201
|
await model.save();
|
|
149
202
|
returnedModels.push(model);
|
|
150
203
|
|
|
151
|
-
if (patch.unitPrice || patch.amount || patch.status) {
|
|
204
|
+
if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined) {
|
|
152
205
|
updateOutstandingBalance.push(model);
|
|
153
206
|
}
|
|
154
207
|
}
|
|
@@ -156,8 +209,14 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
156
209
|
|
|
157
210
|
await BalanceItem.updateOutstanding(updateOutstandingBalance);
|
|
158
211
|
|
|
212
|
+
// Reallocate
|
|
213
|
+
await BalanceItemService.reallocate(updateOutstandingBalance, organization.id);
|
|
214
|
+
|
|
215
|
+
// Reload returnedModels
|
|
216
|
+
const returnedModelsReloaded = await BalanceItem.getByIDs(...returnedModels.map(m => m.id));
|
|
217
|
+
|
|
159
218
|
return new Response(
|
|
160
|
-
await BalanceItem.getStructureWithPayments(
|
|
219
|
+
await BalanceItem.getStructureWithPayments(returnedModelsReloaded),
|
|
161
220
|
);
|
|
162
221
|
}
|
|
163
222
|
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { BalanceItem, BalanceItemPayment, Payment
|
|
4
|
+
import { BalanceItem, BalanceItemPayment, Payment } from '@stamhoofd/models';
|
|
5
5
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
-
import { Payment as PaymentStruct,
|
|
6
|
+
import { PaymentGeneral, PaymentMethod, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
10
|
-
import { ExchangePaymentEndpoint } from '../../shared/ExchangePaymentEndpoint';
|
|
11
|
-
import { PaymentService } from '../../../../services/PaymentService';
|
|
12
10
|
import { BalanceItemService } from '../../../../services/BalanceItemService';
|
|
11
|
+
import { PaymentService } from '../../../../services/PaymentService';
|
|
13
12
|
|
|
14
13
|
type Params = Record<string, never>;
|
|
15
14
|
type Query = undefined;
|
|
@@ -49,11 +48,11 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
49
48
|
// Modify payments
|
|
50
49
|
for (const { put } of request.body.getPuts()) {
|
|
51
50
|
// Create a new payment
|
|
52
|
-
if (put.balanceItemPayments.length
|
|
51
|
+
if (put.balanceItemPayments.length === 0) {
|
|
53
52
|
throw new SimpleError({
|
|
54
53
|
code: 'invalid_field',
|
|
55
54
|
message: 'You need to add at least one balance item payment',
|
|
56
|
-
human: 'Een betaling moet ten minste één
|
|
55
|
+
human: 'Een betaling moet ten minste één item bestaan',
|
|
57
56
|
field: 'balanceItemPayments',
|
|
58
57
|
});
|
|
59
58
|
}
|
|
@@ -72,8 +71,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
72
71
|
payment.status = PaymentStatus.Created;
|
|
73
72
|
payment.method = put.method;
|
|
74
73
|
payment.customer = put.customer;
|
|
74
|
+
payment.type = put.type;
|
|
75
75
|
|
|
76
|
-
if (payment.method
|
|
76
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
77
77
|
if (!put.transferSettings) {
|
|
78
78
|
throw new SimpleError({
|
|
79
79
|
code: 'invalid_field',
|
|
@@ -112,7 +112,11 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
112
112
|
balanceItemPayment.organizationId = organization.id;
|
|
113
113
|
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
114
114
|
balanceItemPayment.price = item.price;
|
|
115
|
-
|
|
115
|
+
|
|
116
|
+
if (item.price !== 0) {
|
|
117
|
+
// Otherwise skip
|
|
118
|
+
balanceItemPayments.push(balanceItemPayment);
|
|
119
|
+
}
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
// Check permissions
|
|
@@ -122,17 +126,55 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
122
126
|
|
|
123
127
|
// Check total price
|
|
124
128
|
const totalPrice = balanceItemPayments.reduce((total, item) => total + item.price, 0);
|
|
129
|
+
payment.price = totalPrice;
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
switch (payment.type) {
|
|
132
|
+
case PaymentType.Payment: {
|
|
133
|
+
if (totalPrice <= 0) {
|
|
134
|
+
throw new SimpleError({
|
|
135
|
+
code: 'invalid_field',
|
|
136
|
+
message: 'The price should be greater than zero',
|
|
137
|
+
human: 'Het totaalbedrag moet groter zijn dan 0 euro',
|
|
138
|
+
field: 'price',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
case PaymentType.Chargeback:
|
|
145
|
+
case PaymentType.Refund: {
|
|
146
|
+
if (totalPrice >= 0) {
|
|
147
|
+
throw new SimpleError({
|
|
148
|
+
code: 'invalid_field',
|
|
149
|
+
message: 'The price should be smaller than zero',
|
|
150
|
+
human: 'Het totaalbedrag moet kleiner zijn dan 0 euro',
|
|
151
|
+
field: 'price',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case PaymentType.Reallocation:
|
|
158
|
+
{
|
|
159
|
+
if (totalPrice !== 0) {
|
|
160
|
+
throw new SimpleError({
|
|
161
|
+
code: 'invalid_field',
|
|
162
|
+
message: 'Total price should be zero',
|
|
163
|
+
human: 'Het totaalbedrag moet 0 euro zijn',
|
|
164
|
+
field: 'price',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (balanceItemPayments.length < 2) {
|
|
169
|
+
throw new SimpleError({
|
|
170
|
+
code: 'missing_items',
|
|
171
|
+
message: 'At least two items are required for a reallocation',
|
|
172
|
+
human: 'Er moeten minstens twee items in een verrekening zitten',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
136
178
|
|
|
137
179
|
// Save payment
|
|
138
180
|
await payment.save();
|
|
@@ -158,6 +200,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
158
200
|
}
|
|
159
201
|
|
|
160
202
|
await BalanceItem.updateOutstanding(balanceItems);
|
|
203
|
+
|
|
204
|
+
// Reallocate
|
|
205
|
+
await BalanceItemService.reallocate(balanceItems, organization.id);
|
|
161
206
|
}
|
|
162
207
|
|
|
163
208
|
changedPayments.push(payment);
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
CHANGED
|
@@ -44,10 +44,19 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
44
44
|
case ReceivableBalanceType.organization: {
|
|
45
45
|
paymentModels = await Payment.select()
|
|
46
46
|
.where('organizationId', organization.id)
|
|
47
|
-
.where('payingOrganizationId', request.params.id)
|
|
48
47
|
.andWhere(
|
|
49
48
|
SQL.whereNot('status', PaymentStatus.Failed),
|
|
50
49
|
)
|
|
50
|
+
.join(
|
|
51
|
+
SQL.join(BalanceItemPayment.table)
|
|
52
|
+
.where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
|
|
53
|
+
)
|
|
54
|
+
.join(
|
|
55
|
+
SQL.join(BalanceItem.table)
|
|
56
|
+
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
57
|
+
)
|
|
58
|
+
.where(SQL.column(BalanceItem.table, 'payingOrganizationId'), request.params.id)
|
|
59
|
+
.groupBy(SQL.column(Payment.table, 'id'))
|
|
51
60
|
.fetch();
|
|
52
61
|
break;
|
|
53
62
|
}
|
|
@@ -71,6 +80,46 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
71
80
|
.fetch();
|
|
72
81
|
break;
|
|
73
82
|
}
|
|
83
|
+
|
|
84
|
+
case ReceivableBalanceType.user: {
|
|
85
|
+
paymentModels = await Payment.select()
|
|
86
|
+
.where('organizationId', organization.id)
|
|
87
|
+
.join(
|
|
88
|
+
SQL.join(BalanceItemPayment.table)
|
|
89
|
+
.where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
|
|
90
|
+
)
|
|
91
|
+
.join(
|
|
92
|
+
SQL.join(BalanceItem.table)
|
|
93
|
+
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
94
|
+
)
|
|
95
|
+
.where(SQL.column(BalanceItem.table, 'userId'), request.params.id)
|
|
96
|
+
.andWhere(
|
|
97
|
+
SQL.whereNot('status', PaymentStatus.Failed),
|
|
98
|
+
)
|
|
99
|
+
.groupBy(SQL.column(Payment.table, 'id'))
|
|
100
|
+
.fetch();
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case ReceivableBalanceType.registration: {
|
|
105
|
+
paymentModels = await Payment.select()
|
|
106
|
+
.where('organizationId', organization.id)
|
|
107
|
+
.join(
|
|
108
|
+
SQL.join(BalanceItemPayment.table)
|
|
109
|
+
.where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
|
|
110
|
+
)
|
|
111
|
+
.join(
|
|
112
|
+
SQL.join(BalanceItem.table)
|
|
113
|
+
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
114
|
+
)
|
|
115
|
+
.where(SQL.column(BalanceItem.table, 'registrationId'), request.params.id)
|
|
116
|
+
.andWhere(
|
|
117
|
+
SQL.whereNot('status', PaymentStatus.Failed),
|
|
118
|
+
)
|
|
119
|
+
.groupBy(SQL.column(Payment.table, 'id'))
|
|
120
|
+
.fetch();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
74
123
|
}
|
|
75
124
|
|
|
76
125
|
const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
|
|
@@ -79,8 +128,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
79
128
|
const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
|
|
80
129
|
|
|
81
130
|
const created = new CachedBalance();
|
|
82
|
-
created.
|
|
131
|
+
created.amountOpen = 0;
|
|
83
132
|
created.amountPending = 0;
|
|
133
|
+
created.amountPaid = 0;
|
|
84
134
|
created.organizationId = organization.id;
|
|
85
135
|
created.objectId = request.params.id;
|
|
86
136
|
created.objectType = request.params.type;
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { CachedBalance } from '@stamhoofd/models';
|
|
5
5
|
import { compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
|
|
6
|
-
import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
6
|
+
import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
@@ -49,8 +49,14 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
49
49
|
scopeFilter = {
|
|
50
50
|
organizationId: organization.id,
|
|
51
51
|
$or: {
|
|
52
|
-
|
|
52
|
+
amountOpen: { $neq: 0 },
|
|
53
53
|
amountPending: { $neq: 0 },
|
|
54
|
+
nextDueAt: { $neq: null },
|
|
55
|
+
},
|
|
56
|
+
$not: {
|
|
57
|
+
objectType: {
|
|
58
|
+
$in: Context.auth.hasSomePlatformAccess() ? [ReceivableBalanceType.registration] : [ReceivableBalanceType.organization, ReceivableBalanceType.registration],
|
|
59
|
+
},
|
|
54
60
|
},
|
|
55
61
|
};
|
|
56
62
|
|
|
@@ -346,7 +346,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
346
346
|
model.settings.period = period.getBaseStructure();
|
|
347
347
|
|
|
348
348
|
if (model.type !== GroupType.EventRegistration) {
|
|
349
|
-
|
|
349
|
+
// Note: start date is curomizable, as long as it stays between period start and end
|
|
350
|
+
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
351
|
+
model.settings.startDate = period.startDate;
|
|
352
|
+
}
|
|
353
|
+
|
|
350
354
|
model.settings.endDate = period.endDate;
|
|
351
355
|
}
|
|
352
356
|
}
|
|
@@ -446,9 +450,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
446
450
|
model.status = struct.status;
|
|
447
451
|
model.type = struct.type;
|
|
448
452
|
model.settings.period = period.getBaseStructure();
|
|
449
|
-
model.settings.startDate = period.startDate;
|
|
450
453
|
model.settings.endDate = period.endDate;
|
|
451
454
|
|
|
455
|
+
// Note: start date is curomizable, as long as it stays between period start and end
|
|
456
|
+
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
457
|
+
model.settings.startDate = period.startDate;
|
|
458
|
+
}
|
|
459
|
+
|
|
452
460
|
if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
453
461
|
// Create a temporary permission role for this user
|
|
454
462
|
const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId);
|
|
@@ -169,7 +169,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
169
169
|
balanceItem.description = webshop.meta.name;
|
|
170
170
|
balanceItem.pricePaid = 0;
|
|
171
171
|
balanceItem.organizationId = organization.id;
|
|
172
|
-
balanceItem.status = BalanceItemStatus.
|
|
172
|
+
balanceItem.status = BalanceItemStatus.Due;
|
|
173
173
|
balanceItem.relations = new Map([
|
|
174
174
|
[
|
|
175
175
|
BalanceItemRelationType.Webshop,
|
|
@@ -271,13 +271,23 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
271
271
|
const items = await BalanceItem.where({ orderId: model.id });
|
|
272
272
|
if (items.length >= 1) {
|
|
273
273
|
model.markUpdated();
|
|
274
|
-
|
|
275
|
-
items
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
|
|
275
|
+
const paidItem = items.find(i => i.status === BalanceItemStatus.Due && i.pricePaid !== 0) ?? items[0];
|
|
276
|
+
|
|
277
|
+
paidItem.unitPrice = model.data.totalPrice;
|
|
278
|
+
paidItem.amount = 1;
|
|
279
|
+
|
|
280
|
+
if (model.isDue) {
|
|
281
|
+
paidItem.status = BalanceItemStatus.Due;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
paidItem.status = BalanceItemStatus.Canceled;
|
|
285
|
+
}
|
|
286
|
+
paidItem.description = model.generateBalanceDescription(webshop);
|
|
287
|
+
await paidItem.save();
|
|
278
288
|
|
|
279
289
|
// Zero out the other items
|
|
280
|
-
const otherItems = items.
|
|
290
|
+
const otherItems = items.filter(i => i.id !== paidItem.id);
|
|
281
291
|
await BalanceItem.deleteItems(otherItems);
|
|
282
292
|
}
|
|
283
293
|
else if (items.length === 0
|
|
@@ -285,11 +295,12 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
285
295
|
model.markUpdated();
|
|
286
296
|
const balanceItem = new BalanceItem();
|
|
287
297
|
balanceItem.orderId = model.id;
|
|
288
|
-
balanceItem.unitPrice = model.
|
|
298
|
+
balanceItem.unitPrice = model.data.totalPrice;
|
|
299
|
+
balanceItem.amount = 1;
|
|
300
|
+
balanceItem.status = BalanceItemStatus.Due;
|
|
289
301
|
balanceItem.description = model.generateBalanceDescription(webshop);
|
|
290
302
|
balanceItem.pricePaid = 0;
|
|
291
303
|
balanceItem.organizationId = organization.id;
|
|
292
|
-
balanceItem.status = BalanceItemStatus.Pending;
|
|
293
304
|
await balanceItem.save();
|
|
294
305
|
}
|
|
295
306
|
}
|
|
@@ -6,7 +6,7 @@ import { I18n } from '@stamhoofd/backend-i18n';
|
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
7
|
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
8
8
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
|
-
import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct, WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation, AuditLogSource } from '@stamhoofd/structures';
|
|
9
|
+
import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct, WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation, AuditLogSource, PaymentCustomer } from '@stamhoofd/structures';
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
|
|
12
12
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
@@ -169,6 +169,11 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
169
169
|
payment.status = PaymentStatus.Created;
|
|
170
170
|
payment.price = totalPrice;
|
|
171
171
|
payment.paidAt = null;
|
|
172
|
+
payment.customer = PaymentCustomer.create({
|
|
173
|
+
firstName: request.body.customer.firstName,
|
|
174
|
+
lastName: request.body.customer.lastName,
|
|
175
|
+
email: request.body.customer.email,
|
|
176
|
+
});
|
|
172
177
|
|
|
173
178
|
// Determine the payment provider
|
|
174
179
|
// Throws if invalid
|
|
@@ -219,18 +224,18 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
219
224
|
let paymentUrl: string | null = null;
|
|
220
225
|
const description = webshop.meta.name + ' - ' + payment.id;
|
|
221
226
|
|
|
222
|
-
if (payment.method
|
|
227
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
223
228
|
await order.markValid(payment, []);
|
|
224
229
|
|
|
225
230
|
if (order.number) {
|
|
226
231
|
balanceItem.description = order.generateBalanceDescription(webshop);
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
balanceItem.status = BalanceItemStatus.
|
|
234
|
+
balanceItem.status = BalanceItemStatus.Due;
|
|
230
235
|
await balanceItem.save();
|
|
231
236
|
await payment.save();
|
|
232
237
|
}
|
|
233
|
-
else if (payment.method
|
|
238
|
+
else if (payment.method === PaymentMethod.PointOfSale) {
|
|
234
239
|
// Not really paid, but needed to create the tickets if needed
|
|
235
240
|
await order.markPaid(payment, organization, webshop);
|
|
236
241
|
|
|
@@ -238,7 +243,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
238
243
|
balanceItem.description = order.generateBalanceDescription(webshop);
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
balanceItem.status = BalanceItemStatus.
|
|
246
|
+
balanceItem.status = BalanceItemStatus.Due;
|
|
242
247
|
await balanceItem.save();
|
|
243
248
|
await payment.save();
|
|
244
249
|
}
|
|
@@ -263,7 +263,7 @@ export class AdminPermissionChecker {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
const cachedBalance = await CachedBalance.getForObjects([member.id]);
|
|
266
|
-
if (cachedBalance.length === 0 || (cachedBalance[0].
|
|
266
|
+
if (cachedBalance.length === 0 || (cachedBalance[0].amountOpen === 0 && cachedBalance[0].amountPending === 0)) {
|
|
267
267
|
return true;
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -1063,6 +1063,7 @@ export class AdminPermissionChecker {
|
|
|
1063
1063
|
for (const registration of cloned.registrations) {
|
|
1064
1064
|
registration.price = 0;
|
|
1065
1065
|
registration.pricePaid = 0;
|
|
1066
|
+
registration.balances = [];
|
|
1066
1067
|
}
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
@@ -1310,7 +1311,7 @@ export class AdminPermissionChecker {
|
|
|
1310
1311
|
}
|
|
1311
1312
|
|
|
1312
1313
|
hasSomePlatformAccess(): boolean {
|
|
1313
|
-
return !!this.platformPermissions;
|
|
1314
|
+
return !!this.platformPermissions && !this.platformPermissions.isEmpty;
|
|
1314
1315
|
}
|
|
1315
1316
|
|
|
1316
1317
|
canManagePlatformAdmins() {
|