@stamhoofd/backend 2.62.0 → 2.63.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 +11 -11
- package/src/audit-logs/PaymentLogger.ts +1 -1
- package/src/email-recipient-loaders/receivable-balances.ts +5 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +37 -22
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +80 -28
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +59 -17
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +30 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +9 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +19 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +10 -5
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +54 -6
- package/src/seeds/1733994455-balance-item-status-open.ts +30 -0
- package/src/seeds/1733996431-update-cached-outstanding-balance-from-items.ts +40 -0
- package/src/services/BalanceItemPaymentService.ts +8 -4
- package/src/sql-filters/receivable-balances.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.63.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
"@bwip-js/node": "^4.5.1",
|
|
35
35
|
"@mollie/api-client": "3.7.0",
|
|
36
36
|
"@simonbackx/simple-database": "1.27.0",
|
|
37
|
-
"@simonbackx/simple-encoding": "2.
|
|
37
|
+
"@simonbackx/simple-encoding": "2.19.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.15.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.63.0",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.63.0",
|
|
42
|
+
"@stamhoofd/email": "2.63.0",
|
|
43
|
+
"@stamhoofd/models": "2.63.0",
|
|
44
|
+
"@stamhoofd/queues": "2.63.0",
|
|
45
|
+
"@stamhoofd/sql": "2.63.0",
|
|
46
|
+
"@stamhoofd/structures": "2.63.0",
|
|
47
|
+
"@stamhoofd/utility": "2.63.0",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "0dd71a1869c5931dcf844dea1ecd2167819dc5c6"
|
|
68
68
|
}
|
|
@@ -26,7 +26,7 @@ export const PaymentLogger = new ModelLogger(Payment, {
|
|
|
26
26
|
},
|
|
27
27
|
|
|
28
28
|
createReplacements(model, options) {
|
|
29
|
-
let name = `${PaymentMethodHelper.getPaymentName(model.method)}`;
|
|
29
|
+
let name = `${PaymentMethodHelper.getPaymentName(model.method, model.type)}`;
|
|
30
30
|
|
|
31
31
|
if (model.customer?.dynamicName) {
|
|
32
32
|
name += ` van ${model.customer.dynamicName}`;
|
|
@@ -18,6 +18,11 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
|
|
|
18
18
|
email,
|
|
19
19
|
replacements: [
|
|
20
20
|
Replacement.create({
|
|
21
|
+
token: 'objectName',
|
|
22
|
+
value: balance.object.name,
|
|
23
|
+
}),
|
|
24
|
+
Replacement.create({
|
|
25
|
+
// Deprecated: for backwards compatibility
|
|
21
26
|
token: 'organizationName',
|
|
22
27
|
value: balance.object.name,
|
|
23
28
|
}),
|
|
@@ -5,7 +5,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
7
|
import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
8
|
-
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType,
|
|
8
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItem as BalanceItemStruct, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version, PaymentType } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
10
10
|
|
|
11
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -104,7 +104,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
104
104
|
|
|
105
105
|
// Validate balance items (can only happen serverside)
|
|
106
106
|
const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id);
|
|
107
|
-
let memberBalanceItemsStructs:
|
|
107
|
+
let memberBalanceItemsStructs: BalanceItemStruct[] = [];
|
|
108
108
|
let balanceItemsModels: BalanceItem[] = [];
|
|
109
109
|
if (balanceItemIds.length > 0) {
|
|
110
110
|
balanceItemsModels = await BalanceItem.where({ id: { sign: 'IN', value: balanceItemIds }, organizationId: organization.id });
|
|
@@ -114,7 +114,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
114
114
|
message: 'Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw.',
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
memberBalanceItemsStructs =
|
|
117
|
+
memberBalanceItemsStructs = balanceItemsModels.map(i => i.getStructure());
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const memberIds = Formatter.uniqueArray(
|
|
@@ -429,7 +429,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
429
429
|
balanceItem2.memberId = registration.memberId;
|
|
430
430
|
|
|
431
431
|
// If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
|
|
432
|
-
balanceItem2.status = BalanceItemStatus.Hidden;
|
|
432
|
+
balanceItem2.status = BalanceItemStatus.Hidden;
|
|
433
433
|
await balanceItem2.save();
|
|
434
434
|
|
|
435
435
|
// do not add to createdBalanceItems array because we don't want to add this to the payment if we create a payment
|
|
@@ -440,7 +440,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
440
440
|
balanceItem.userId = user.id;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
443
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
444
444
|
balanceItem.pricePaid = 0;
|
|
445
445
|
|
|
446
446
|
// Connect the 'pay back' balance item to this balance item. As soon as this balance item is paid, we'll mark the other one as pending so the outstanding balance for the member increases
|
|
@@ -554,7 +554,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
554
554
|
if (oldestMember) {
|
|
555
555
|
balanceItem.memberId = oldestMember.id;
|
|
556
556
|
}
|
|
557
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
557
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
558
558
|
await balanceItem.save();
|
|
559
559
|
createdBalanceItems.push(balanceItem);
|
|
560
560
|
}
|
|
@@ -579,7 +579,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
579
579
|
}
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
582
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
583
583
|
await balanceItem.save();
|
|
584
584
|
|
|
585
585
|
createdBalanceItems.push(balanceItem);
|
|
@@ -674,6 +674,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
674
674
|
// Calculate total price to pay
|
|
675
675
|
let totalPrice = 0;
|
|
676
676
|
const payMembers: MemberWithRegistrations[] = [];
|
|
677
|
+
let hasNegative = false;
|
|
677
678
|
|
|
678
679
|
for (const [balanceItem, price] of balanceItems) {
|
|
679
680
|
if (organization.id !== balanceItem.organizationId) {
|
|
@@ -694,6 +695,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
694
695
|
});
|
|
695
696
|
}
|
|
696
697
|
|
|
698
|
+
if (price < 0) {
|
|
699
|
+
hasNegative = true;
|
|
700
|
+
}
|
|
701
|
+
|
|
697
702
|
totalPrice += price;
|
|
698
703
|
|
|
699
704
|
if (price > 0 && balanceItem.memberId) {
|
|
@@ -709,27 +714,33 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
709
714
|
}
|
|
710
715
|
|
|
711
716
|
if (totalPrice < 0) {
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// })
|
|
717
|
+
// todo: try to make it non-negative by reducing some balance items
|
|
718
|
+
throw new SimpleError({
|
|
719
|
+
code: 'empty_data',
|
|
720
|
+
message: 'Oeps! De totaalprijs is negatief.',
|
|
721
|
+
});
|
|
718
722
|
}
|
|
723
|
+
const payment = new Payment();
|
|
724
|
+
payment.method = checkout.paymentMethod ?? PaymentMethod.Unknown;
|
|
719
725
|
|
|
720
726
|
if (totalPrice === 0) {
|
|
721
|
-
|
|
722
|
-
|
|
727
|
+
if (balanceItems.size === 0) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
// Create an egalizing payment
|
|
731
|
+
payment.method = PaymentMethod.Unknown;
|
|
723
732
|
|
|
724
|
-
|
|
733
|
+
if (hasNegative) {
|
|
734
|
+
payment.type = PaymentType.Reallocation;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else if (payment.method === PaymentMethod.Unknown) {
|
|
725
738
|
throw new SimpleError({
|
|
726
739
|
code: 'invalid_data',
|
|
727
740
|
message: 'Oeps, je hebt geen betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw.',
|
|
728
741
|
});
|
|
729
742
|
}
|
|
730
743
|
|
|
731
|
-
const payment = new Payment();
|
|
732
|
-
|
|
733
744
|
// Who will receive this money?
|
|
734
745
|
payment.organizationId = organization.id;
|
|
735
746
|
|
|
@@ -793,11 +804,16 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
793
804
|
}
|
|
794
805
|
}
|
|
795
806
|
|
|
796
|
-
payment.method = checkout.paymentMethod;
|
|
797
807
|
payment.status = PaymentStatus.Created;
|
|
808
|
+
payment.paidAt = null;
|
|
798
809
|
payment.price = totalPrice;
|
|
799
810
|
|
|
800
|
-
if (
|
|
811
|
+
if (totalPrice === 0) {
|
|
812
|
+
payment.status = PaymentStatus.Succeeded;
|
|
813
|
+
payment.paidAt = new Date();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
801
817
|
// remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
|
|
802
818
|
payment.transferSettings = organization.mappedTransferSettings;
|
|
803
819
|
|
|
@@ -821,7 +837,6 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
821
837
|
},
|
|
822
838
|
);
|
|
823
839
|
}
|
|
824
|
-
payment.paidAt = null;
|
|
825
840
|
|
|
826
841
|
// Determine the payment provider
|
|
827
842
|
// Throws if invalid
|
|
@@ -861,7 +876,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
861
876
|
console.error(e);
|
|
862
877
|
}
|
|
863
878
|
}
|
|
864
|
-
else if (payment.method !== PaymentMethod.PointOfSale) {
|
|
879
|
+
else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
|
|
865
880
|
if (!checkout.redirectUrl || !checkout.cancelUrl) {
|
|
866
881
|
throw new Error('Should have been caught earlier');
|
|
867
882
|
}
|
|
@@ -43,10 +43,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
43
43
|
const returnedModels: BalanceItem[] = [];
|
|
44
44
|
const updateOutstandingBalance: BalanceItem[] = [];
|
|
45
45
|
|
|
46
|
-
// Keep track of updates
|
|
47
|
-
const memberIds: string[] = [];
|
|
48
|
-
const registrationIds: string[] = [];
|
|
49
|
-
|
|
50
46
|
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
51
47
|
for (const { put } of request.body.getPuts()) {
|
|
52
48
|
// Create a new balance item
|
|
@@ -58,7 +54,8 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
58
54
|
model.amount = put.amount;
|
|
59
55
|
model.organizationId = organization.id;
|
|
60
56
|
model.createdAt = put.createdAt;
|
|
61
|
-
model.
|
|
57
|
+
model.dueAt = put.dueAt;
|
|
58
|
+
model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Due;
|
|
62
59
|
|
|
63
60
|
if (put.userId) {
|
|
64
61
|
model.userId = (await this.validateUserId(model, put.userId)).id;
|
|
@@ -66,10 +63,43 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
66
63
|
|
|
67
64
|
if (put.memberId) {
|
|
68
65
|
model.memberId = (await this.validateMemberId(put.memberId)).id;
|
|
69
|
-
memberIds.push(model.memberId);
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
if (
|
|
68
|
+
if (put.payingOrganizationId) {
|
|
69
|
+
// Not allowed if not full admin
|
|
70
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
71
|
+
throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
|
|
72
|
+
}
|
|
73
|
+
if (put.payingOrganizationId === model.organizationId) {
|
|
74
|
+
throw new SimpleError({
|
|
75
|
+
code: 'invalid_field',
|
|
76
|
+
message: 'payingOrganizationId cannot be the same as organizationId',
|
|
77
|
+
human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
|
|
78
|
+
field: 'payingOrganizationId',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
model.payingOrganizationId = put.payingOrganizationId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (model.dueAt && model.price < 0) {
|
|
86
|
+
throw new SimpleError({
|
|
87
|
+
code: 'invalid_price',
|
|
88
|
+
message: 'Cannot create negative balance in the future',
|
|
89
|
+
human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (model.createdAt > new Date()) {
|
|
94
|
+
throw new SimpleError({
|
|
95
|
+
code: 'invalid_field',
|
|
96
|
+
message: 'createdAt cannot be in the future',
|
|
97
|
+
human: 'De datum kan niet in de toekomst liggen',
|
|
98
|
+
field: 'createdAt',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!model.userId && !model.memberId && !model.payingOrganizationId) {
|
|
73
103
|
throw new SimpleError({
|
|
74
104
|
code: 'invalid_field',
|
|
75
105
|
message: 'No user or member provided',
|
|
@@ -93,39 +123,53 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
93
123
|
});
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
if (patch.
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
if (patch.payingOrganizationId !== undefined) {
|
|
127
|
+
// Not allowed if not full admin
|
|
128
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
129
|
+
throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
|
|
130
|
+
}
|
|
131
|
+
if (patch.payingOrganizationId === model.organizationId) {
|
|
132
|
+
throw new SimpleError({
|
|
133
|
+
code: 'invalid_field',
|
|
134
|
+
message: 'payingOrganizationId cannot be the same as organizationId',
|
|
135
|
+
human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
|
|
136
|
+
field: 'payingOrganizationId',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
103
139
|
|
|
104
|
-
|
|
105
|
-
if (model.memberId) {
|
|
106
|
-
// Update old
|
|
107
|
-
memberIds.push(model.memberId);
|
|
140
|
+
model.payingOrganizationId = patch.payingOrganizationId;
|
|
108
141
|
}
|
|
109
142
|
|
|
110
143
|
if (patch.memberId) {
|
|
111
144
|
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
145
|
}
|
|
121
146
|
|
|
122
147
|
if (patch.createdAt) {
|
|
123
148
|
model.createdAt = patch.createdAt;
|
|
149
|
+
|
|
150
|
+
if (model.createdAt > new Date()) {
|
|
151
|
+
throw new SimpleError({
|
|
152
|
+
code: 'invalid_field',
|
|
153
|
+
message: 'createdAt cannot be in the future',
|
|
154
|
+
human: 'De datum kan niet in de toekomst liggen',
|
|
155
|
+
field: 'createdAt',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
124
158
|
}
|
|
125
159
|
|
|
126
160
|
model.description = patch.description ?? model.description;
|
|
127
161
|
model.unitPrice = patch.unitPrice ?? model.unitPrice;
|
|
128
162
|
model.amount = patch.amount ?? model.amount;
|
|
163
|
+
model.dueAt = patch.dueAt === undefined ? model.dueAt : patch.dueAt;
|
|
164
|
+
|
|
165
|
+
if ((patch.dueAt !== undefined || patch.unitPrice !== undefined) && model.dueAt && model.price < 0) {
|
|
166
|
+
throw new SimpleError({
|
|
167
|
+
code: 'invalid_price',
|
|
168
|
+
message: 'Cannot create negative balance in the future',
|
|
169
|
+
human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
|
|
170
|
+
field: 'dueAt',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
129
173
|
|
|
130
174
|
if (model.orderId) {
|
|
131
175
|
// Not allowed to change this
|
|
@@ -142,13 +186,21 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
142
186
|
}
|
|
143
187
|
}
|
|
144
188
|
else if (patch.status) {
|
|
145
|
-
model.status =
|
|
189
|
+
model.status = patch.status;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!model.userId && !model.memberId && !model.payingOrganizationId) {
|
|
193
|
+
throw new SimpleError({
|
|
194
|
+
code: 'invalid_field',
|
|
195
|
+
message: 'No user or member provided',
|
|
196
|
+
field: 'userId',
|
|
197
|
+
});
|
|
146
198
|
}
|
|
147
199
|
|
|
148
200
|
await model.save();
|
|
149
201
|
returnedModels.push(model);
|
|
150
202
|
|
|
151
|
-
if (patch.unitPrice || patch.amount || patch.status) {
|
|
203
|
+
if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined) {
|
|
152
204
|
updateOutstandingBalance.push(model);
|
|
153
205
|
}
|
|
154
206
|
}
|
|
@@ -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();
|
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,26 @@ 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
|
+
}
|
|
74
103
|
}
|
|
75
104
|
|
|
76
105
|
const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
|
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';
|
|
@@ -51,9 +51,17 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
51
51
|
$or: {
|
|
52
52
|
amount: { $neq: 0 },
|
|
53
53
|
amountPending: { $neq: 0 },
|
|
54
|
+
nextDueAt: { $neq: null },
|
|
54
55
|
},
|
|
55
56
|
};
|
|
56
57
|
|
|
58
|
+
if (!Context.auth.hasSomePlatformAccess()) {
|
|
59
|
+
// Cannot see debt between organizations
|
|
60
|
+
scopeFilter.objectType = {
|
|
61
|
+
$neq: ReceivableBalanceType.organization,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
const query = CachedBalance
|
|
58
66
|
.select();
|
|
59
67
|
|
|
@@ -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
|
}
|
|
@@ -1310,7 +1310,7 @@ export class AdminPermissionChecker {
|
|
|
1310
1310
|
}
|
|
1311
1311
|
|
|
1312
1312
|
hasSomePlatformAccess(): boolean {
|
|
1313
|
-
return !!this.platformPermissions;
|
|
1313
|
+
return !!this.platformPermissions && !this.platformPermissions.isEmpty;
|
|
1314
1314
|
}
|
|
1315
1315
|
|
|
1316
1316
|
canManagePlatformAdmins() {
|
|
@@ -40,8 +40,6 @@ export class AuthenticatedStructures {
|
|
|
40
40
|
|
|
41
41
|
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions;
|
|
42
42
|
|
|
43
|
-
console.log('includeSettlements', includeSettlements);
|
|
44
|
-
|
|
45
43
|
const { payingOrganizations } = await Payment.loadPayingOrganizations(payments);
|
|
46
44
|
|
|
47
45
|
return Payment.getGeneralStructureFromRelations({
|
|
@@ -576,6 +574,11 @@ export class AuthenticatedStructures {
|
|
|
576
574
|
]);
|
|
577
575
|
const members = memberIds.length > 0 ? await Member.getByIDs(...memberIds) : [];
|
|
578
576
|
|
|
577
|
+
const userIds = Formatter.uniqueArray([
|
|
578
|
+
...balances.filter(b => b.objectType === ReceivableBalanceType.user).map(b => b.objectId),
|
|
579
|
+
]);
|
|
580
|
+
const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
|
|
581
|
+
|
|
579
582
|
const result: ReceivableBalanceStruct[] = [];
|
|
580
583
|
for (const balance of balances) {
|
|
581
584
|
let object = ReceivableBalanceObject.create({
|
|
@@ -584,7 +587,7 @@ export class AuthenticatedStructures {
|
|
|
584
587
|
});
|
|
585
588
|
|
|
586
589
|
if (balance.objectType === ReceivableBalanceType.organization) {
|
|
587
|
-
const organization = organizationStructs.find(o => o.id
|
|
590
|
+
const organization = organizationStructs.find(o => o.id === balance.objectId) ?? null;
|
|
588
591
|
if (organization) {
|
|
589
592
|
const theseResponsibilities = responsibilities.filter(r => r.organizationId === organization.id);
|
|
590
593
|
const thisMembers = members.flatMap((m) => {
|
|
@@ -605,6 +608,7 @@ export class AuthenticatedStructures {
|
|
|
605
608
|
lastName: member.lastName ?? '',
|
|
606
609
|
emails: member.details.getMemberEmails(),
|
|
607
610
|
meta: {
|
|
611
|
+
type: 'organization',
|
|
608
612
|
responsibilityIds: responsibilities.map(r => r.responsibilityId),
|
|
609
613
|
url: organization.dashboardUrl + '/boekhouding/openstaand/' + (Context.organization?.uri ?? ''),
|
|
610
614
|
},
|
|
@@ -615,14 +619,58 @@ export class AuthenticatedStructures {
|
|
|
615
619
|
else if (balance.objectType === ReceivableBalanceType.member) {
|
|
616
620
|
const member = members.find(m => m.id === balance.objectId) ?? null;
|
|
617
621
|
if (member) {
|
|
622
|
+
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
618
623
|
object = ReceivableBalanceObject.create({
|
|
619
624
|
id: balance.objectId,
|
|
620
625
|
name: member.details.name,
|
|
626
|
+
contacts: [
|
|
627
|
+
...(member.details.getMemberEmails().length
|
|
628
|
+
? [
|
|
629
|
+
ReceivableBalanceObjectContact.create({
|
|
630
|
+
firstName: member.details.firstName ?? '',
|
|
631
|
+
lastName: member.details.lastName ?? '',
|
|
632
|
+
emails: member.details.getMemberEmails(),
|
|
633
|
+
meta: {
|
|
634
|
+
type: 'member',
|
|
635
|
+
responsibilityIds: [],
|
|
636
|
+
url,
|
|
637
|
+
},
|
|
638
|
+
}),
|
|
639
|
+
]
|
|
640
|
+
: []),
|
|
641
|
+
|
|
642
|
+
...(member.details.parentsHaveAccess
|
|
643
|
+
? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
|
|
644
|
+
firstName: p.firstName ?? '',
|
|
645
|
+
lastName: p.lastName ?? '',
|
|
646
|
+
emails: [p.email!],
|
|
647
|
+
meta: {
|
|
648
|
+
type: 'parent',
|
|
649
|
+
responsibilityIds: [],
|
|
650
|
+
url,
|
|
651
|
+
},
|
|
652
|
+
}))
|
|
653
|
+
: []),
|
|
654
|
+
],
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else if (balance.objectType === ReceivableBalanceType.user) {
|
|
659
|
+
const user = users.find(m => m.id === balance.objectId) ?? null;
|
|
660
|
+
if (user) {
|
|
661
|
+
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
662
|
+
object = ReceivableBalanceObject.create({
|
|
663
|
+
id: balance.objectId,
|
|
664
|
+
name: user.name || user.email,
|
|
621
665
|
contacts: [
|
|
622
666
|
ReceivableBalanceObjectContact.create({
|
|
623
|
-
firstName:
|
|
624
|
-
lastName:
|
|
625
|
-
emails:
|
|
667
|
+
firstName: user.firstName ?? '',
|
|
668
|
+
lastName: user.lastName ?? '',
|
|
669
|
+
emails: [user.email],
|
|
670
|
+
meta: {
|
|
671
|
+
responsibilityIds: [],
|
|
672
|
+
url,
|
|
673
|
+
},
|
|
626
674
|
}),
|
|
627
675
|
],
|
|
628
676
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Database, Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { BalanceItemStatus } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment === 'test') {
|
|
6
|
+
console.log('skipped in tests');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const query = `
|
|
11
|
+
UPDATE
|
|
12
|
+
balance_items
|
|
13
|
+
SET status = ?
|
|
14
|
+
WHERE status IN (?)`;
|
|
15
|
+
await Database.update(query, [
|
|
16
|
+
BalanceItemStatus.Due,
|
|
17
|
+
['Paid', 'Pending'],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const q2 = `
|
|
21
|
+
UPDATE
|
|
22
|
+
balance_items
|
|
23
|
+
SET status = ?,
|
|
24
|
+
amount = coalesce(nullif(ROUND(coalesce(pricePaid / nullif(unitPrice, 0), 0)), 0), 1)
|
|
25
|
+
WHERE amount = 0 AND status = ?`;
|
|
26
|
+
await Database.update(q2, [
|
|
27
|
+
BalanceItemStatus.Canceled,
|
|
28
|
+
BalanceItemStatus.Due,
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
+
import { BalanceItem } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment == 'test') {
|
|
7
|
+
console.log('skipped in tests');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.stdout.write('\n');
|
|
12
|
+
let c = 0;
|
|
13
|
+
let id: string = '';
|
|
14
|
+
|
|
15
|
+
await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
|
|
16
|
+
while (true) {
|
|
17
|
+
const items = await BalanceItem.where({
|
|
18
|
+
id: {
|
|
19
|
+
value: id,
|
|
20
|
+
sign: '>',
|
|
21
|
+
},
|
|
22
|
+
}, { limit: 1000, sort: ['id'] });
|
|
23
|
+
|
|
24
|
+
await BalanceItem.updateOutstanding(items);
|
|
25
|
+
|
|
26
|
+
c += items.length;
|
|
27
|
+
process.stdout.write('.');
|
|
28
|
+
|
|
29
|
+
if (items.length < 1000) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
id = items[items.length - 1].id;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log('Updated outstanding balance for ' + c + ' items');
|
|
37
|
+
|
|
38
|
+
// Do something here
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
});
|
|
@@ -7,16 +7,20 @@ type Loaded<T> = (T) extends ManyToOneRelation<infer Key, infer Model> ? Record<
|
|
|
7
7
|
|
|
8
8
|
export const BalanceItemPaymentService = {
|
|
9
9
|
async markPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
10
|
+
const wasPaid = balanceItemPayment.balanceItem.priceOpen === 0;
|
|
11
|
+
|
|
10
12
|
// Update cached amountPaid of the balance item (balanceItemPayment will get overwritten later, but we need it to calculate the status)
|
|
11
13
|
balanceItemPayment.balanceItem.pricePaid += balanceItemPayment.price;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Hidden && balanceItemPayment.balanceItem.pricePaid !== 0) {
|
|
16
|
+
balanceItemPayment.balanceItem.status = BalanceItemStatus.Due;
|
|
17
|
+
}
|
|
18
|
+
|
|
16
19
|
await balanceItemPayment.balanceItem.save();
|
|
20
|
+
const isPaid = balanceItemPayment.balanceItem.priceOpen === 0;
|
|
17
21
|
|
|
18
22
|
// Do logic of balance item
|
|
19
|
-
if (
|
|
23
|
+
if (isPaid && !wasPaid && balanceItemPayment.price >= 0 && balanceItemPayment.balanceItem.status === BalanceItemStatus.Due) {
|
|
20
24
|
// Only call markPaid once (if it wasn't (partially) paid before)
|
|
21
25
|
await BalanceItemService.markPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
22
26
|
}
|
|
@@ -10,4 +10,5 @@ export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
|
|
|
10
10
|
objectType: createSQLColumnFilterCompiler('objectType'),
|
|
11
11
|
amount: createSQLColumnFilterCompiler('amount'),
|
|
12
12
|
amountPending: createSQLColumnFilterCompiler('amountPending'),
|
|
13
|
+
nextDueAt: createSQLColumnFilterCompiler('nextDueAt'),
|
|
13
14
|
};
|