@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,746 @@
|
|
|
1
|
+
import { BalanceItem, BalanceItemPayment, MemberFactory, Organization, OrganizationFactory, Payment } from '@stamhoofd/models';
|
|
2
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, PaymentMethod, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
|
+
import { PaymentReallocationService } from './PaymentReallocationService';
|
|
4
|
+
|
|
5
|
+
let sharedOrganization: Organization | undefined;
|
|
6
|
+
|
|
7
|
+
async function getOrganization() {
|
|
8
|
+
if (!sharedOrganization) {
|
|
9
|
+
sharedOrganization = await new OrganizationFactory({}).create();
|
|
10
|
+
}
|
|
11
|
+
return sharedOrganization;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function getMember() {
|
|
15
|
+
return await new MemberFactory({
|
|
16
|
+
organization: (await getOrganization()),
|
|
17
|
+
}).create();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function createItem(options: {
|
|
21
|
+
objectId?: string;
|
|
22
|
+
unitPrice: number;
|
|
23
|
+
amount: number;
|
|
24
|
+
status?: BalanceItemStatus;
|
|
25
|
+
paid?: number[];
|
|
26
|
+
pending?: number[];
|
|
27
|
+
failed?: number[];
|
|
28
|
+
pricePaid?: number;
|
|
29
|
+
priceOpen?: number;
|
|
30
|
+
pricePending?: number;
|
|
31
|
+
dueAt?: Date | null;
|
|
32
|
+
relations?: Partial<Record<BalanceItemRelationType, string>>;
|
|
33
|
+
}): Promise<BalanceItem> {
|
|
34
|
+
const b = new BalanceItem();
|
|
35
|
+
b.unitPrice = options.unitPrice;
|
|
36
|
+
b.amount = options.amount;
|
|
37
|
+
b.status = options.status ?? BalanceItemStatus.Due;
|
|
38
|
+
b.pricePaid = options.paid?.reduce((a, b) => a + b, 0) ?? 0;
|
|
39
|
+
b.pricePending = options.pending?.reduce((a, b) => a + b, 0) ?? 0;
|
|
40
|
+
b.organizationId = (await getOrganization()).id;
|
|
41
|
+
b.memberId = options.objectId ?? null;
|
|
42
|
+
b.relations = new Map();
|
|
43
|
+
b.dueAt = options.dueAt ?? null;
|
|
44
|
+
|
|
45
|
+
if (options.relations) {
|
|
46
|
+
for (const [type, id] of Object.entries(options.relations)) {
|
|
47
|
+
b.relations.set(type as BalanceItemRelationType, BalanceItemRelation.create({
|
|
48
|
+
id,
|
|
49
|
+
name: 'Test ' + id,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await b.save();
|
|
54
|
+
|
|
55
|
+
for (const paid of options.paid ?? []) {
|
|
56
|
+
const payment = new Payment();
|
|
57
|
+
payment.method = PaymentMethod.Transfer;
|
|
58
|
+
payment.status = PaymentStatus.Succeeded;
|
|
59
|
+
payment.organizationId = b.organizationId;
|
|
60
|
+
payment.price = paid;
|
|
61
|
+
await payment.save();
|
|
62
|
+
|
|
63
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
64
|
+
balanceItemPayment.balanceItemId = b.id;
|
|
65
|
+
balanceItemPayment.paymentId = payment.id;
|
|
66
|
+
balanceItemPayment.price = paid;
|
|
67
|
+
balanceItemPayment.organizationId = b.organizationId;
|
|
68
|
+
await balanceItemPayment.save();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const pending of options.pending ?? []) {
|
|
72
|
+
const payment = new Payment();
|
|
73
|
+
payment.method = PaymentMethod.Transfer;
|
|
74
|
+
payment.status = PaymentStatus.Pending;
|
|
75
|
+
payment.organizationId = b.organizationId;
|
|
76
|
+
payment.price = pending;
|
|
77
|
+
await payment.save();
|
|
78
|
+
|
|
79
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
80
|
+
balanceItemPayment.balanceItemId = b.id;
|
|
81
|
+
balanceItemPayment.paymentId = payment.id;
|
|
82
|
+
balanceItemPayment.price = pending;
|
|
83
|
+
balanceItemPayment.organizationId = b.organizationId;
|
|
84
|
+
await balanceItemPayment.save();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const failed of options.failed ?? []) {
|
|
88
|
+
const payment = new Payment();
|
|
89
|
+
payment.method = PaymentMethod.Transfer;
|
|
90
|
+
payment.status = PaymentStatus.Failed;
|
|
91
|
+
payment.organizationId = b.organizationId;
|
|
92
|
+
payment.price = failed;
|
|
93
|
+
await payment.save();
|
|
94
|
+
|
|
95
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
96
|
+
balanceItemPayment.balanceItemId = b.id;
|
|
97
|
+
balanceItemPayment.paymentId = payment.id;
|
|
98
|
+
balanceItemPayment.price = failed;
|
|
99
|
+
balanceItemPayment.organizationId = b.organizationId;
|
|
100
|
+
await balanceItemPayment.save();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await BalanceItem.updateOutstanding([b]);
|
|
104
|
+
const balance = (await BalanceItem.getByID(b.id))!;
|
|
105
|
+
|
|
106
|
+
await expectItem(balance, {
|
|
107
|
+
pricePaid: options.pricePaid,
|
|
108
|
+
priceOpen: options.priceOpen,
|
|
109
|
+
pricePending: options.pricePending,
|
|
110
|
+
paid: options.paid,
|
|
111
|
+
pending: options.pending,
|
|
112
|
+
failed: options.failed,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return balance;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function expectItem(b: BalanceItem, options: { pricePaid?: number; priceOpen?: number; pricePending?: number; paid?: number[]; pending?: number[]; failed?: number[] }) {
|
|
119
|
+
await BalanceItem.updateOutstanding([b]);
|
|
120
|
+
b = (await BalanceItem.getByID(b.id))!;
|
|
121
|
+
|
|
122
|
+
const loaded = await BalanceItem.loadPayments([b]);
|
|
123
|
+
|
|
124
|
+
const actualPaidList = loaded.balanceItemPayments.filter(bp => bp.balanceItemId === b.id && loaded.payments.find(p => p.status === PaymentStatus.Succeeded && p.id === bp.paymentId)).map(bp => bp.price);
|
|
125
|
+
const actualPendingList = loaded.balanceItemPayments.filter(bp => bp.balanceItemId === b.id && loaded.payments.find(p => (p.status === PaymentStatus.Pending || p.status === PaymentStatus.Created) && p.id === bp.paymentId)).map(bp => bp.price);
|
|
126
|
+
const actualFailedList = loaded.balanceItemPayments.filter(bp => bp.balanceItemId === b.id && loaded.payments.find(p => p.status === PaymentStatus.Failed && p.id === bp.paymentId)).map(bp => bp.price);
|
|
127
|
+
|
|
128
|
+
if (options.paid !== undefined) {
|
|
129
|
+
expect(actualPaidList).toIncludeSameMembers(options.paid);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.pending !== undefined) {
|
|
133
|
+
expect(actualPendingList).toIncludeSameMembers(options.pending);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.failed !== undefined) {
|
|
137
|
+
expect(actualFailedList).toIncludeSameMembers(options.failed);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.pricePaid !== undefined) {
|
|
141
|
+
expect(b.pricePaid).toBe(options.pricePaid);
|
|
142
|
+
}
|
|
143
|
+
if (options.priceOpen !== undefined) {
|
|
144
|
+
expect(b.priceOpen).toBe(options.priceOpen);
|
|
145
|
+
}
|
|
146
|
+
if (options.pricePending !== undefined) {
|
|
147
|
+
expect(b.pricePending).toBe(options.pricePending);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe('PaymentReallocationService', () => {
|
|
152
|
+
describe('swapPayments', () => {
|
|
153
|
+
it('Equal balance item payments', async () => {
|
|
154
|
+
const b1 = await createItem({
|
|
155
|
+
unitPrice: 30 * 100,
|
|
156
|
+
amount: 1,
|
|
157
|
+
status: BalanceItemStatus.Canceled,
|
|
158
|
+
paid: [30 * 100],
|
|
159
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const b2 = await createItem({
|
|
163
|
+
unitPrice: 30 * 100,
|
|
164
|
+
amount: 1,
|
|
165
|
+
paid: [],
|
|
166
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Now do a swap
|
|
170
|
+
await PaymentReallocationService.swapPayments(
|
|
171
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
172
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await expectItem(b1, {
|
|
176
|
+
priceOpen: 0,
|
|
177
|
+
paid: [],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await expectItem(b2, {
|
|
181
|
+
priceOpen: 0,
|
|
182
|
+
paid: [30 * 100],
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('Equal pending balance items', async () => {
|
|
187
|
+
const b1 = await createItem({
|
|
188
|
+
unitPrice: 30 * 100,
|
|
189
|
+
amount: 1,
|
|
190
|
+
status: BalanceItemStatus.Canceled,
|
|
191
|
+
pending: [30 * 100],
|
|
192
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const b2 = await createItem({
|
|
196
|
+
unitPrice: 30 * 100,
|
|
197
|
+
amount: 1,
|
|
198
|
+
pending: [],
|
|
199
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Now do a swap
|
|
203
|
+
await PaymentReallocationService.swapPayments(
|
|
204
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
205
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await expectItem(b1, {
|
|
209
|
+
priceOpen: 0,
|
|
210
|
+
pending: [],
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await expectItem(b2, {
|
|
214
|
+
priceOpen: 0,
|
|
215
|
+
pending: [30 * 100],
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('Failed payments are not moved', async () => {
|
|
220
|
+
const b1 = await createItem({
|
|
221
|
+
unitPrice: -30 * 100,
|
|
222
|
+
amount: 1,
|
|
223
|
+
failed: [30 * 100],
|
|
224
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const b2 = await createItem({
|
|
228
|
+
unitPrice: 30 * 100,
|
|
229
|
+
amount: 1,
|
|
230
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Now do a swap
|
|
234
|
+
await PaymentReallocationService.swapPayments(
|
|
235
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
236
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await expectItem(b1, {
|
|
240
|
+
priceOpen: -30 * 100,
|
|
241
|
+
failed: [30 * 100],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await expectItem(b2, {
|
|
245
|
+
priceOpen: 30 * 100,
|
|
246
|
+
pending: [],
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('Larger negative balance', async () => {
|
|
251
|
+
const b1 = await createItem({
|
|
252
|
+
unitPrice: 30 * 100,
|
|
253
|
+
amount: 1,
|
|
254
|
+
status: BalanceItemStatus.Canceled,
|
|
255
|
+
paid: [30 * 100],
|
|
256
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const b2 = await createItem({
|
|
260
|
+
unitPrice: 15 * 100,
|
|
261
|
+
amount: 1,
|
|
262
|
+
paid: [],
|
|
263
|
+
priceOpen: 15 * 100, // This adds internal assert
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Now do a swap
|
|
267
|
+
await PaymentReallocationService.swapPayments(
|
|
268
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
269
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
await expectItem(b1, {
|
|
273
|
+
priceOpen: -15 * 100,
|
|
274
|
+
paid: [15 * 100],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await expectItem(b2, {
|
|
278
|
+
priceOpen: 0,
|
|
279
|
+
paid: [15 * 100],
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('Larger positive balance', async () => {
|
|
284
|
+
const b1 = await createItem({
|
|
285
|
+
unitPrice: 15 * 100,
|
|
286
|
+
amount: 1,
|
|
287
|
+
status: BalanceItemStatus.Canceled,
|
|
288
|
+
paid: [15 * 100],
|
|
289
|
+
priceOpen: -15 * 100, // This adds internal assert
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const b2 = await createItem({
|
|
293
|
+
unitPrice: 30 * 100,
|
|
294
|
+
amount: 1,
|
|
295
|
+
paid: [],
|
|
296
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Now do a swap
|
|
300
|
+
await PaymentReallocationService.swapPayments(
|
|
301
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
302
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
await expectItem(b1, {
|
|
306
|
+
priceOpen: 0,
|
|
307
|
+
paid: [],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await expectItem(b2, {
|
|
311
|
+
priceOpen: 15 * 100,
|
|
312
|
+
paid: [15 * 100],
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('Spits up payments', async () => {
|
|
317
|
+
const b1 = await createItem({
|
|
318
|
+
unitPrice: 15 * 100,
|
|
319
|
+
amount: 1,
|
|
320
|
+
status: BalanceItemStatus.Canceled,
|
|
321
|
+
paid: [50 * 100],
|
|
322
|
+
priceOpen: -50 * 100, // This adds internal assert
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const b2 = await createItem({
|
|
326
|
+
unitPrice: 30 * 100,
|
|
327
|
+
amount: 1,
|
|
328
|
+
paid: [],
|
|
329
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Now do a swap
|
|
333
|
+
await PaymentReallocationService.swapPayments(
|
|
334
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
335
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
await expectItem(b1, {
|
|
339
|
+
paid: [20 * 100],
|
|
340
|
+
priceOpen: -20 * 100, // This adds internal assert
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await expectItem(b2, {
|
|
344
|
+
priceOpen: 0,
|
|
345
|
+
paid: [30 * 100],
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('Moves multiple payments', async () => {
|
|
350
|
+
const b1 = await createItem({
|
|
351
|
+
unitPrice: 30 * 100,
|
|
352
|
+
amount: 1,
|
|
353
|
+
status: BalanceItemStatus.Canceled,
|
|
354
|
+
paid: [20 * 100, 5 * 100, 10 * 100],
|
|
355
|
+
priceOpen: -35 * 100, // This adds internal assert
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const b2 = await createItem({
|
|
359
|
+
unitPrice: 15 * 100,
|
|
360
|
+
amount: 1,
|
|
361
|
+
paid: [],
|
|
362
|
+
priceOpen: 15 * 100, // This adds internal assert
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Now do a swap
|
|
366
|
+
await PaymentReallocationService.swapPayments(
|
|
367
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
368
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
await expectItem(b1, {
|
|
372
|
+
priceOpen: -20 * 100,
|
|
373
|
+
paid: [20 * 100],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await expectItem(b2, {
|
|
377
|
+
priceOpen: 0,
|
|
378
|
+
paid: [5 * 100, 10 * 100],
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('Moves multiple payments in both directions', async () => {
|
|
383
|
+
const b1 = await createItem({
|
|
384
|
+
unitPrice: -30 * 100,
|
|
385
|
+
amount: 1,
|
|
386
|
+
paid: [],
|
|
387
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const b2 = await createItem({
|
|
391
|
+
unitPrice: 15 * 100,
|
|
392
|
+
amount: 1,
|
|
393
|
+
paid: [-20 * 100, -10 * 100],
|
|
394
|
+
priceOpen: 45 * 100, // This adds internal assert
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Now do a swap
|
|
398
|
+
await PaymentReallocationService.swapPayments(
|
|
399
|
+
{ balanceItem: b1, remaining: b1.priceOpen },
|
|
400
|
+
{ balanceItem: b2, remaining: b2.priceOpen },
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
await expectItem(b1, {
|
|
404
|
+
priceOpen: 0,
|
|
405
|
+
paid: [-20 * 100, -10 * 100],
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await expectItem(b2, {
|
|
409
|
+
priceOpen: 15 * 100,
|
|
410
|
+
paid: [],
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('reallocate', () => {
|
|
416
|
+
it('Balances with same relations should move existing payments first', async () => {
|
|
417
|
+
const memberId = (await getMember()).id;
|
|
418
|
+
const b1 = await createItem({
|
|
419
|
+
unitPrice: 30 * 100,
|
|
420
|
+
amount: 1,
|
|
421
|
+
status: BalanceItemStatus.Canceled,
|
|
422
|
+
paid: [30 * 100],
|
|
423
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
424
|
+
objectId: memberId,
|
|
425
|
+
relations: {
|
|
426
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
427
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
428
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const b2 = await createItem({
|
|
433
|
+
unitPrice: 30 * 100,
|
|
434
|
+
amount: 1,
|
|
435
|
+
paid: [],
|
|
436
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
437
|
+
objectId: memberId,
|
|
438
|
+
relations: {
|
|
439
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
440
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
441
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await PaymentReallocationService.reallocate(
|
|
446
|
+
(await getOrganization()).id,
|
|
447
|
+
memberId,
|
|
448
|
+
ReceivableBalanceType.member,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Check if the balance items are now equal
|
|
452
|
+
await expectItem(b1, {
|
|
453
|
+
priceOpen: 0,
|
|
454
|
+
paid: [],
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await expectItem(b2, {
|
|
458
|
+
priceOpen: 0,
|
|
459
|
+
paid: [30 * 100],
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('Balances with different relations should create a reallocation payment', async () => {
|
|
464
|
+
const memberId = (await getMember()).id;
|
|
465
|
+
const b1 = await createItem({
|
|
466
|
+
unitPrice: 30 * 100,
|
|
467
|
+
amount: 1,
|
|
468
|
+
status: BalanceItemStatus.Canceled,
|
|
469
|
+
paid: [30 * 100],
|
|
470
|
+
priceOpen: -30 * 100, // This adds internal assert
|
|
471
|
+
objectId: memberId,
|
|
472
|
+
relations: {
|
|
473
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
474
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
475
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const b2 = await createItem({
|
|
480
|
+
unitPrice: 30 * 100,
|
|
481
|
+
amount: 1,
|
|
482
|
+
paid: [],
|
|
483
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
484
|
+
objectId: memberId,
|
|
485
|
+
relations: {
|
|
486
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
487
|
+
[BalanceItemRelationType.GroupPrice]: 'price2', // This one is different
|
|
488
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await PaymentReallocationService.reallocate(
|
|
493
|
+
(await getOrganization()).id,
|
|
494
|
+
memberId,
|
|
495
|
+
ReceivableBalanceType.member,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Check if the balance items are now equal
|
|
499
|
+
await expectItem(b1, {
|
|
500
|
+
priceOpen: 0,
|
|
501
|
+
paid: [30 * 100, -30 * 100],
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await expectItem(b2, {
|
|
505
|
+
priceOpen: 0,
|
|
506
|
+
paid: [30 * 100],
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('Balances with different relations should create a reallocation payment with 3 items', async () => {
|
|
511
|
+
const memberId = (await getMember()).id;
|
|
512
|
+
const b1 = await createItem({
|
|
513
|
+
unitPrice: 45 * 100,
|
|
514
|
+
amount: 1,
|
|
515
|
+
status: BalanceItemStatus.Canceled,
|
|
516
|
+
paid: [45 * 100],
|
|
517
|
+
priceOpen: -45 * 100, // This adds internal assert
|
|
518
|
+
objectId: memberId,
|
|
519
|
+
relations: {
|
|
520
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
521
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
522
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const b2 = await createItem({
|
|
527
|
+
unitPrice: 30 * 100,
|
|
528
|
+
amount: 1,
|
|
529
|
+
paid: [],
|
|
530
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
531
|
+
objectId: memberId,
|
|
532
|
+
relations: {
|
|
533
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
534
|
+
[BalanceItemRelationType.GroupPrice]: 'price2', // This one is different
|
|
535
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const b3 = await createItem({
|
|
540
|
+
unitPrice: 15 * 100,
|
|
541
|
+
amount: 1,
|
|
542
|
+
paid: [],
|
|
543
|
+
priceOpen: 15 * 100, // This adds internal assert
|
|
544
|
+
objectId: memberId,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await PaymentReallocationService.reallocate(
|
|
548
|
+
(await getOrganization()).id,
|
|
549
|
+
memberId,
|
|
550
|
+
ReceivableBalanceType.member,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Check if the balance items are now equal
|
|
554
|
+
await expectItem(b1, {
|
|
555
|
+
priceOpen: 0,
|
|
556
|
+
paid: [45 * 100, -45 * 100],
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await expectItem(b2, {
|
|
560
|
+
priceOpen: 0,
|
|
561
|
+
paid: [30 * 100],
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
await expectItem(b3, {
|
|
565
|
+
priceOpen: 0,
|
|
566
|
+
paid: [15 * 100],
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer most similar item', async () => {
|
|
571
|
+
const memberId = (await getMember()).id;
|
|
572
|
+
const b1 = await createItem({
|
|
573
|
+
unitPrice: 40 * 100,
|
|
574
|
+
amount: 1,
|
|
575
|
+
status: BalanceItemStatus.Canceled,
|
|
576
|
+
paid: [40 * 100],
|
|
577
|
+
priceOpen: -40 * 100, // This adds internal assert
|
|
578
|
+
objectId: memberId,
|
|
579
|
+
relations: {
|
|
580
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
581
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
582
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const b2 = await createItem({
|
|
587
|
+
unitPrice: 30 * 100,
|
|
588
|
+
amount: 1,
|
|
589
|
+
paid: [],
|
|
590
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
591
|
+
objectId: memberId,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const b3 = await createItem({
|
|
595
|
+
unitPrice: 15 * 100,
|
|
596
|
+
amount: 1,
|
|
597
|
+
paid: [],
|
|
598
|
+
priceOpen: 15 * 100, // This adds internal assert
|
|
599
|
+
objectId: memberId,
|
|
600
|
+
relations: {
|
|
601
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
602
|
+
[BalanceItemRelationType.GroupPrice]: 'price2', // This one is different
|
|
603
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
await PaymentReallocationService.reallocate(
|
|
608
|
+
(await getOrganization()).id,
|
|
609
|
+
memberId,
|
|
610
|
+
ReceivableBalanceType.member,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// Check if the balance items are now equal
|
|
614
|
+
await expectItem(b1, {
|
|
615
|
+
priceOpen: 0,
|
|
616
|
+
paid: [40 * 100, -40 * 100],
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await expectItem(b2, {
|
|
620
|
+
priceOpen: 5 * 100,
|
|
621
|
+
paid: [25 * 100],
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// b3 was more similar to b1 and takes all first
|
|
625
|
+
await expectItem(b3, {
|
|
626
|
+
priceOpen: 0 * 100,
|
|
627
|
+
paid: [15 * 100],
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Note: if this one fails randomly, it might because it isn't working stable enough and doesn't fulfil the requirements
|
|
633
|
+
*/
|
|
634
|
+
it('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer largest amount', async () => {
|
|
635
|
+
const memberId = (await getMember()).id;
|
|
636
|
+
const b1 = await createItem({
|
|
637
|
+
unitPrice: 40 * 100,
|
|
638
|
+
amount: 1,
|
|
639
|
+
status: BalanceItemStatus.Canceled,
|
|
640
|
+
paid: [40 * 100],
|
|
641
|
+
priceOpen: -40 * 100, // This adds internal assert
|
|
642
|
+
objectId: memberId,
|
|
643
|
+
relations: {
|
|
644
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
645
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
646
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const b2 = await createItem({
|
|
651
|
+
unitPrice: 30 * 100,
|
|
652
|
+
amount: 1,
|
|
653
|
+
paid: [],
|
|
654
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
655
|
+
objectId: memberId,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const b3 = await createItem({
|
|
659
|
+
unitPrice: 15 * 100,
|
|
660
|
+
amount: 1,
|
|
661
|
+
paid: [],
|
|
662
|
+
priceOpen: 15 * 100, // This adds internal assert
|
|
663
|
+
objectId: memberId,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
await PaymentReallocationService.reallocate(
|
|
667
|
+
(await getOrganization()).id,
|
|
668
|
+
memberId,
|
|
669
|
+
ReceivableBalanceType.member,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Check if the balance items are now equal
|
|
673
|
+
await expectItem(b1, {
|
|
674
|
+
priceOpen: 0,
|
|
675
|
+
paid: [40 * 100, -40 * 100],
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await expectItem(b2, {
|
|
679
|
+
priceOpen: 0 * 100,
|
|
680
|
+
paid: [30 * 100],
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await expectItem(b3, {
|
|
684
|
+
priceOpen: 5 * 100,
|
|
685
|
+
paid: [10 * 100],
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer first due amount', async () => {
|
|
690
|
+
const memberId = (await getMember()).id;
|
|
691
|
+
const b1 = await createItem({
|
|
692
|
+
unitPrice: 40 * 100,
|
|
693
|
+
amount: 1,
|
|
694
|
+
status: BalanceItemStatus.Canceled,
|
|
695
|
+
paid: [40 * 100],
|
|
696
|
+
priceOpen: -40 * 100, // This adds internal assert
|
|
697
|
+
objectId: memberId,
|
|
698
|
+
relations: {
|
|
699
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
700
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
701
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const b2 = await createItem({
|
|
706
|
+
unitPrice: 30 * 100,
|
|
707
|
+
amount: 1,
|
|
708
|
+
paid: [],
|
|
709
|
+
priceOpen: 30 * 100, // This adds internal assert
|
|
710
|
+
objectId: memberId,
|
|
711
|
+
// This is due later, so it should be the last one to be paid
|
|
712
|
+
dueAt: new Date('2050-01-01'),
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const b3 = await createItem({
|
|
716
|
+
unitPrice: 15 * 100,
|
|
717
|
+
amount: 1,
|
|
718
|
+
paid: [],
|
|
719
|
+
priceOpen: 15 * 100, // This adds internal assert
|
|
720
|
+
objectId: memberId,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
await PaymentReallocationService.reallocate(
|
|
724
|
+
(await getOrganization()).id,
|
|
725
|
+
memberId,
|
|
726
|
+
ReceivableBalanceType.member,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Check if the balance items are now equal
|
|
730
|
+
await expectItem(b1, {
|
|
731
|
+
priceOpen: 0,
|
|
732
|
+
paid: [40 * 100, -40 * 100],
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
await expectItem(b2, {
|
|
736
|
+
priceOpen: 5 * 100,
|
|
737
|
+
paid: [25 * 100],
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
await expectItem(b3, {
|
|
741
|
+
priceOpen: 0 * 100,
|
|
742
|
+
paid: [15 * 100],
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
});
|