@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.
Files changed (33) hide show
  1. package/index.ts +8 -6
  2. package/package.json +11 -11
  3. package/src/audit-logs/PaymentLogger.ts +1 -1
  4. package/src/crons/index.ts +1 -0
  5. package/src/crons/update-cached-balances.ts +39 -0
  6. package/src/email-recipient-loaders/receivable-balances.ts +5 -0
  7. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
  8. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
  9. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +85 -25
  11. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
  12. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +89 -30
  13. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +62 -17
  14. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +52 -2
  15. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +8 -2
  16. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
  17. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +19 -8
  18. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +10 -5
  19. package/src/helpers/AdminPermissionChecker.ts +3 -2
  20. package/src/helpers/AuthenticatedStructures.ts +127 -9
  21. package/src/helpers/MembershipCharger.ts +4 -0
  22. package/src/helpers/OrganizationCharger.ts +4 -0
  23. package/src/seeds/1733994455-balance-item-status-open.ts +30 -0
  24. package/src/seeds/1734596144-fill-previous-period-id.ts +55 -0
  25. package/src/seeds/1734700082-update-cached-outstanding-balance-from-items.ts +40 -0
  26. package/src/services/BalanceItemPaymentService.ts +8 -4
  27. package/src/services/BalanceItemService.ts +22 -3
  28. package/src/services/PaymentReallocationService.test.ts +746 -0
  29. package/src/services/PaymentReallocationService.ts +339 -0
  30. package/src/services/PaymentService.ts +13 -0
  31. package/src/services/PlatformMembershipService.ts +167 -137
  32. package/src/sql-filters/receivable-balances.ts +2 -1
  33. 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
+ });