@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
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { BalanceItem, BalanceItemPayment, CachedBalance, Payment } from '@stamhoofd/models';
|
|
2
|
+
import { SQL } from '@stamhoofd/sql';
|
|
3
|
+
import { BalanceItemStatus, doBalanceItemRelationsMatch, PaymentMethod, PaymentStatus, PaymentType, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
|
+
import { Sorter } from '@stamhoofd/utility';
|
|
5
|
+
|
|
6
|
+
type BalanceItemWithRemaining = {
|
|
7
|
+
balanceItem: BalanceItem;
|
|
8
|
+
remaining: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const PaymentReallocationService = {
|
|
12
|
+
/**
|
|
13
|
+
* Move all canceled balance items payments to one single non-canceled item (note: they should be equal)
|
|
14
|
+
*
|
|
15
|
+
* This avoids the situation where you have multiple balance items for a registration, but only one registration
|
|
16
|
+
* -> this can cause confusion because people might think 'hey' the price for that registration is z, not b (while it is about a canceled registration)
|
|
17
|
+
*/
|
|
18
|
+
async mergeBalanceItems(mergeToItem: BalanceItem, otherItems: BalanceItem[]) {
|
|
19
|
+
// Move all balance item payments to the merged item
|
|
20
|
+
await SQL.update(BalanceItemPayment.table)
|
|
21
|
+
.set('balanceItemId', mergeToItem.id)
|
|
22
|
+
.where('balanceItemId', otherItems.map(bi => bi.id))
|
|
23
|
+
.update();
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async reallocate(organizationId: string, objectId: string, type: ReceivableBalanceType) {
|
|
27
|
+
if (STAMHOOFD.environment === 'production') {
|
|
28
|
+
// Disabled on production for now
|
|
29
|
+
// until this has been tested more
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let balanceItems = await CachedBalance.balanceForObjects(organizationId, [objectId], type);
|
|
34
|
+
|
|
35
|
+
const didMerge: BalanceItem[] = [];
|
|
36
|
+
|
|
37
|
+
// First try to merge balance items that are the same and have canceled variants
|
|
38
|
+
for (const balanceItem of balanceItems) {
|
|
39
|
+
if (balanceItem.status !== BalanceItemStatus.Due) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const similarDueItems = balanceItems.filter(b => b.id !== balanceItem.id && b.status === BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
|
|
44
|
+
|
|
45
|
+
if (similarDueItems.length) {
|
|
46
|
+
// Not possible to merge into one: there are 2 due items
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.status !== BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
|
|
50
|
+
|
|
51
|
+
if (similarCanceledItems.length) {
|
|
52
|
+
await this.mergeBalanceItems(balanceItem, similarCanceledItems);
|
|
53
|
+
didMerge.push(balanceItem, ...similarCanceledItems);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (didMerge.length) {
|
|
58
|
+
// Update outstanding
|
|
59
|
+
await BalanceItem.updateOutstanding(didMerge);
|
|
60
|
+
|
|
61
|
+
// Reload balance items
|
|
62
|
+
balanceItems = await CachedBalance.balanceForObjects(organizationId, [objectId], type);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The algorithm:
|
|
66
|
+
// Search balance items that were paid too much, or have a negative open amount.
|
|
67
|
+
// Find the best match in the positive list (balances that have an open amount). Try to find either:
|
|
68
|
+
// - Same type, One with the same relation ids and the same amount -> special case: try to move the balance item payments around since this is safe (no impact on finances or tax)
|
|
69
|
+
// - Same type, One with the same relation ids
|
|
70
|
+
// - Same type, tne with one mismatching relation ids (e.g. same group id, different price id; same webshop, same product, different price)
|
|
71
|
+
// - Same type, One with two mismatching relation ids (e.g. same webshop, different product)
|
|
72
|
+
// - Same type, same amount
|
|
73
|
+
// - Same type
|
|
74
|
+
// - Unrelated balance items
|
|
75
|
+
// This priority must be given for each balance item, so we start with priority 1 for all items, then priority 2 for all items...
|
|
76
|
+
// The result should be a list of postive and negative balance items that is maximized (as much paid items should have been resolved) and equals zero.
|
|
77
|
+
|
|
78
|
+
const negativeItems = balanceItems.filter(b => b.priceOpen < 0).map(balanceItem => ({ balanceItem, remaining: balanceItem.priceOpen }));
|
|
79
|
+
const positiveItems = balanceItems.filter(b => b.priceOpen > 0).map(balanceItem => ({ balanceItem, remaining: balanceItem.priceOpen }));
|
|
80
|
+
|
|
81
|
+
if (negativeItems.length === 0 || positiveItems.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Now we reach the part where we are going to create reallocation payments (we are no longer going to alter existing payments)
|
|
86
|
+
const pendingPayment = new Map<BalanceItem, number>();
|
|
87
|
+
|
|
88
|
+
const matchMethods: { alterPayments: boolean; match: (negativeItem: BalanceItemWithRemaining, positive: BalanceItemWithRemaining) => boolean }[] = [
|
|
89
|
+
{
|
|
90
|
+
// Priority 1: same relation ids, same amount
|
|
91
|
+
alterPayments: true,
|
|
92
|
+
match: (negativeItem, p) => {
|
|
93
|
+
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
// Priority 2: same relation ids, different amount
|
|
98
|
+
alterPayments: true,
|
|
99
|
+
match: (negativeItem, p) => {
|
|
100
|
+
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
// Priority 3: same type, one mismatching relation id
|
|
105
|
+
alterPayments: false,
|
|
106
|
+
match: (negativeItem, p) => {
|
|
107
|
+
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 1);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
// Priority 4: same type, two mismatching relation ids
|
|
112
|
+
alterPayments: false,
|
|
113
|
+
match: (negativeItem, p) => {
|
|
114
|
+
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 2);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
// Priority 5: same type, same amount
|
|
119
|
+
alterPayments: false,
|
|
120
|
+
match: (negativeItem, p) => {
|
|
121
|
+
return p.balanceItem.type === negativeItem.balanceItem.type && p.remaining === -negativeItem.remaining;
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
// Priority 6: same type
|
|
126
|
+
alterPayments: false,
|
|
127
|
+
match: (negativeItem, p) => {
|
|
128
|
+
return p.balanceItem.type === negativeItem.balanceItem.type;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
// Priority: any
|
|
133
|
+
alterPayments: false,
|
|
134
|
+
match: () => {
|
|
135
|
+
return true;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const matchMethod of matchMethods) {
|
|
141
|
+
// Sort negative items on hight > low
|
|
142
|
+
negativeItems.sort((a, b) => b.remaining - a.remaining);
|
|
143
|
+
|
|
144
|
+
// Sort positive items on due date, then hight > low
|
|
145
|
+
positiveItems.sort((a, b) => Sorter.stack(
|
|
146
|
+
Sorter.byDateValue(b.balanceItem.dueAt ?? new Date(0), a.balanceItem.dueAt ?? new Date(0)),
|
|
147
|
+
b.remaining - a.remaining,
|
|
148
|
+
));
|
|
149
|
+
|
|
150
|
+
for (const negativeItem of negativeItems) {
|
|
151
|
+
if (negativeItem.remaining >= 0) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const match = positiveItems.find((p) => {
|
|
156
|
+
return p.remaining > 0 && matchMethod.match(negativeItem, p);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!match) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (matchMethod.alterPayments) {
|
|
164
|
+
await this.swapPayments(negativeItem, match);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Add to pending payment
|
|
168
|
+
const moveAmount = Math.min(match.remaining, -negativeItem.remaining);
|
|
169
|
+
|
|
170
|
+
negativeItem.remaining += moveAmount;
|
|
171
|
+
match.remaining -= moveAmount;
|
|
172
|
+
|
|
173
|
+
pendingPayment.set(negativeItem.balanceItem, (pendingPayment.get(negativeItem.balanceItem) ?? 0) - moveAmount);
|
|
174
|
+
pendingPayment.set(match.balanceItem, (pendingPayment.get(match.balanceItem) ?? 0) + moveAmount);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Create payment
|
|
180
|
+
if (pendingPayment.size !== 0) {
|
|
181
|
+
// Assert total is zero
|
|
182
|
+
const total = Array.from(pendingPayment.values()).reduce((a, b) => a + b, 0);
|
|
183
|
+
if (total !== 0) {
|
|
184
|
+
throw new Error('Total is not zero');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const payment = new Payment();
|
|
188
|
+
payment.organizationId = organizationId;
|
|
189
|
+
payment.price = 0;
|
|
190
|
+
payment.type = PaymentType.Reallocation;
|
|
191
|
+
payment.method = PaymentMethod.Unknown;
|
|
192
|
+
payment.status = PaymentStatus.Succeeded;
|
|
193
|
+
await payment.save();
|
|
194
|
+
|
|
195
|
+
// Create balance item payments
|
|
196
|
+
for (const [balanceItem, price] of pendingPayment) {
|
|
197
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
198
|
+
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
199
|
+
balanceItemPayment.paymentId = payment.id;
|
|
200
|
+
balanceItemPayment.price = price;
|
|
201
|
+
balanceItemPayment.organizationId = organizationId;
|
|
202
|
+
await balanceItemPayment.save();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Update outstanding
|
|
207
|
+
await BalanceItem.updateOutstanding([
|
|
208
|
+
...negativeItems.filter(n => n.remaining !== n.balanceItem.priceOpen).map(n => n.balanceItem),
|
|
209
|
+
...positiveItems.filter(p => p.remaining !== p.balanceItem.priceOpen).map(p => p.balanceItem),
|
|
210
|
+
]);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async swapPayments(negativeItem: BalanceItemWithRemaining, match: BalanceItemWithRemaining) {
|
|
214
|
+
const { balanceItemPayments: allBalanceItemPayments, payments } = await BalanceItem.loadPayments([negativeItem.balanceItem, match.balanceItem]);
|
|
215
|
+
|
|
216
|
+
// Remove balance item payments of failed or deleted payments
|
|
217
|
+
const balanceItemPayments = allBalanceItemPayments.filter((bp) => {
|
|
218
|
+
const payment = payments.find(p => p.id === bp.paymentId);
|
|
219
|
+
if (!payment || payment.status === PaymentStatus.Failed) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (balanceItemPayments.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// First try to find exact matches
|
|
230
|
+
await this.doSwapPayments(balanceItemPayments, negativeItem, match, { split: false, exactOnly: true });
|
|
231
|
+
|
|
232
|
+
// Try with not exact matches
|
|
233
|
+
await this.doSwapPayments(balanceItemPayments, negativeItem, match, { split: false, exactOnly: false });
|
|
234
|
+
|
|
235
|
+
// Try with matches that are too big
|
|
236
|
+
await this.doSwapPayments(balanceItemPayments, negativeItem, match, { split: true, exactOnly: false });
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async doSwapPayments(balanceItemPayments: BalanceItemPayment[], negativeItem: BalanceItemWithRemaining, match: BalanceItemWithRemaining, options: { split: boolean; exactOnly: boolean }) {
|
|
240
|
+
if (negativeItem.remaining >= 0 || match.remaining <= 0) {
|
|
241
|
+
// Stop because one item is zero or switched sign
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Try with matches that are too big
|
|
246
|
+
for (const bp of balanceItemPayments) {
|
|
247
|
+
const price = bp.price;
|
|
248
|
+
|
|
249
|
+
if (bp.balanceItemId === match.balanceItem.id) {
|
|
250
|
+
if (price < 0) {
|
|
251
|
+
if (price < negativeItem.remaining || price < -match.remaining) {
|
|
252
|
+
if (!options.split) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Split the balance item payment in two: a part for negative item, and a part for match
|
|
256
|
+
const swap = -Math.min(-negativeItem.remaining, match.remaining);
|
|
257
|
+
|
|
258
|
+
// We need to split
|
|
259
|
+
bp.price -= swap;
|
|
260
|
+
await bp.save();
|
|
261
|
+
|
|
262
|
+
// Create a new duplicate
|
|
263
|
+
const newBP = new BalanceItemPayment();
|
|
264
|
+
newBP.organizationId = bp.organizationId;
|
|
265
|
+
newBP.paymentId = bp.paymentId;
|
|
266
|
+
newBP.balanceItemId = negativeItem.balanceItem.id;
|
|
267
|
+
newBP.price = swap;
|
|
268
|
+
await newBP.save();
|
|
269
|
+
|
|
270
|
+
negativeItem.remaining -= swap;
|
|
271
|
+
match.remaining += swap;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
if (options.exactOnly && !(price === negativeItem.remaining || price === -match.remaining)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Swap
|
|
279
|
+
bp.balanceItemId = negativeItem.balanceItem.id;
|
|
280
|
+
await bp.save();
|
|
281
|
+
|
|
282
|
+
negativeItem.remaining -= price;
|
|
283
|
+
match.remaining += price;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (negativeItem.remaining >= 0 || match.remaining <= 0) {
|
|
287
|
+
// Stop because one item is zero or switched sign
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
if (price > 0) {
|
|
294
|
+
if (price > -negativeItem.remaining || price > match.remaining) {
|
|
295
|
+
if (!options.split) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Split the balance item payment in two: a part for negative item, and a part for match
|
|
300
|
+
const swap = Math.min(-negativeItem.remaining, match.remaining);
|
|
301
|
+
|
|
302
|
+
// We need to split
|
|
303
|
+
bp.price -= swap;
|
|
304
|
+
await bp.save();
|
|
305
|
+
|
|
306
|
+
// Create a new duplicate
|
|
307
|
+
const newBP = new BalanceItemPayment();
|
|
308
|
+
newBP.organizationId = bp.organizationId;
|
|
309
|
+
newBP.paymentId = bp.paymentId;
|
|
310
|
+
newBP.balanceItemId = match.balanceItem.id;
|
|
311
|
+
newBP.price = swap;
|
|
312
|
+
await newBP.save();
|
|
313
|
+
|
|
314
|
+
negativeItem.remaining += swap;
|
|
315
|
+
match.remaining -= swap;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
if (options.exactOnly && !(price === -negativeItem.remaining || price === match.remaining)) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Swap
|
|
323
|
+
bp.balanceItemId = match.balanceItem.id;
|
|
324
|
+
await bp.save();
|
|
325
|
+
|
|
326
|
+
negativeItem.remaining += price;
|
|
327
|
+
match.remaining -= price;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (negativeItem.remaining >= 0 || match.remaining <= 0) {
|
|
331
|
+
// Stop because one item is zero or switched sign
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
};
|
|
@@ -6,6 +6,7 @@ import { BuckarooHelper } from '../helpers/BuckarooHelper';
|
|
|
6
6
|
import { StripeHelper } from '../helpers/StripeHelper';
|
|
7
7
|
import { BalanceItemPaymentService } from './BalanceItemPaymentService';
|
|
8
8
|
import { AuditLogService } from './AuditLogService';
|
|
9
|
+
import { BalanceItemService } from './BalanceItemService';
|
|
9
10
|
|
|
10
11
|
export const PaymentService = {
|
|
11
12
|
async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
@@ -31,6 +32,9 @@ export const PaymentService = {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
35
|
+
|
|
36
|
+
// Reallocate
|
|
37
|
+
await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
|
|
34
38
|
});
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
@@ -55,6 +59,9 @@ export const PaymentService = {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
62
|
+
|
|
63
|
+
// Reallocate
|
|
64
|
+
await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
|
|
58
65
|
});
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -70,6 +77,9 @@ export const PaymentService = {
|
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
80
|
+
|
|
81
|
+
// Reallocate
|
|
82
|
+
await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
|
|
73
83
|
});
|
|
74
84
|
}
|
|
75
85
|
|
|
@@ -85,6 +95,9 @@ export const PaymentService = {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
98
|
+
|
|
99
|
+
// Reallocate
|
|
100
|
+
await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
|
|
88
101
|
});
|
|
89
102
|
}
|
|
90
103
|
});
|