From 3cb8cd9a4abe925f7044705d38c3c793e913132b Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 3 Aug 2026 11:24:34 +0200 Subject: [PATCH] fix: strengthen deactivation-exclusion test and align sign rule with canonical logic - teams.service.spec.ts: pick a checkpoint older than the adjustment's own month so the exclusion test actually fails without the exclusion filter - teams.service.ts: only negate fine/levy/fee amounts when positive, matching TeamMembersService.recomputeBalance and Transaction.setBalance() exactly, instead of negating unconditionally - teams.service.ts: outstanding-history helper now returns a positive value when players owe money, matching the house convention already established by getOverview()'s team.outstanding - overview.spec.ts: assert the balance chart's legend becomes visible - teams.service.spec.ts: add coverage for inactive-player exclusion and a positive-balance (prepaid credit) case Co-Authored-By: Claude Sonnet 5 --- .../src/teams/teams.service.spec.ts | 88 ++++++++++++++++++- .../src/teams/teams.service.ts | 15 ++-- .../features/team/overview/overview.spec.ts | 1 + 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/myteamwallet_backend/src/teams/teams.service.spec.ts b/myteamwallet_backend/src/teams/teams.service.spec.ts index 9800dbb..9219989 100644 --- a/myteamwallet_backend/src/teams/teams.service.spec.ts +++ b/myteamwallet_backend/src/teams/teams.service.spec.ts @@ -103,15 +103,95 @@ describe('TeamsService#getOverviewStats theoretical balance', () => { const result = await service.getOverviewStats(9, 42); - const beforeFine = result.balanceHistory.find((p) => p.month === monthKey(4)); + // Older than the adjustment's own month (6 months ago) — the only kind of checkpoint + // where wrongly including the €999 adjustment would still show up as "already happened". + const beforeAdjustment = result.balanceHistory.find((p) => p.month === monthKey(9)); const now = result.balanceHistory.find((p) => p.month === monthKey(0)); - // Wäre die Ausgleichsbuchung (999) nicht ausgeschlossen, würde sie hier bereits - // durchschlagen (beforeFine liegt chronologisch nach ihrem Datum) — tut sie aber nicht. - expect(beforeFine?.theoreticalBalance).toBe(50); + // Without exclusion this would read 1049 (50 + the wrongly-included €999 adjustment); + // with correct exclusion only the (not-yet-existing-at-month-9) fine is irrelevant here too, + // so it stays at the team's flat 50. + expect(beforeAdjustment?.theoreticalBalance).toBe(50); expect(now?.theoreticalBalance).toBe(70); }); + it("excludes an inactive player's outstanding debt from the theoretical balance", async () => { + repository.findOneOrFail.mockResolvedValue({ + id: 9, + balance: 100, + transactions: [{ date: isoDate(13, 5), amount: 100, type: { name: 'credit' } }], + players: [ + { + id: 1, + firstName: 'Alex', + lastName: 'Muster', + active: true, + balance: 0, + transactions: [], + }, + { + id: 2, + firstName: 'Bea', + lastName: 'Beispiel', + active: false, + balance: -40, + transactions: [ + { + date: isoDate(2, 10), + amount: 40, + type: { id: 11, name: 'fine' }, + note: 'Zu spät', + }, + ], + }, + ], + }); + + const result = await service.getOverviewStats(9, 42); + + const now = result.balanceHistory.find((p) => p.month === monthKey(0)); + + // The inactive player's unpaid fine is real debt, but no longer counted at all — + // the theoretical line must match the actual cash balance exactly. + expect(now?.balance).toBe(100); + expect(now?.theoreticalBalance).toBe(100); + }); + + it('lowers the theoretical balance when a player has prepaid credit not yet reflected as spent', async () => { + repository.findOneOrFail.mockResolvedValue({ + id: 9, + balance: 100, + transactions: [{ date: isoDate(13, 5), amount: 100, type: { name: 'credit' } }], + players: [ + { + id: 1, + firstName: 'Alex', + lastName: 'Muster', + active: true, + balance: 30, + transactions: [ + { + date: isoDate(2, 10), + amount: 30, + type: { id: 1, name: 'credit' }, + note: 'Vorauszahlung', + }, + ], + }, + ], + }); + + const result = await service.getOverviewStats(9, 42); + + const now = result.balanceHistory.find((p) => p.month === monthKey(0)); + + // Prepaid credit is not yet "spent" in the reconstruction, so the theoretical + // line dips below the actual cash balance at this checkpoint. + expect(now?.balance).toBe(100); + expect(now?.theoreticalBalance).toBe(70); + expect(now!.theoreticalBalance).toBeLessThan(now!.balance); + }); + it('keeps returning an empty balance history when the team has no cash movement at all', async () => { repository.findOneOrFail.mockResolvedValue({ id: 9, diff --git a/myteamwallet_backend/src/teams/teams.service.ts b/myteamwallet_backend/src/teams/teams.service.ts index 7d1e560..22027fc 100644 --- a/myteamwallet_backend/src/teams/teams.service.ts +++ b/myteamwallet_backend/src/teams/teams.service.ts @@ -324,7 +324,7 @@ export class TeamsService { const outstandingHistory = this.reconstructOutstandingHistory(months, players); const balanceHistoryWithTheoretical = balanceHistory.map((point, index) => ({ ...point, - theoreticalBalance: this.round(point.balance - outstandingHistory[index]), + theoreticalBalance: this.round(point.balance + outstandingHistory[index]), })); return { balanceHistory: balanceHistoryWithTheoretical, monthlyFlow, topOutstanding }; @@ -384,7 +384,9 @@ export class TeamsService { }); } - return totals; + // House convention (see getOverview()'s team.outstanding = out * -1): "outstanding" + // is a positive number when players owe money, negative when they're in credit. + return totals.map((total) => total * -1); } private reconstructPlayerBalanceHistory( @@ -392,10 +394,11 @@ export class TeamsService { currentBalance: number, transactions: Transaction[], ): number[] { - const signedMovements = transactions.map((t) => ({ - date: t.date, - amount: t.type && t.type.id > 10 ? -Number(t.amount) : Number(t.amount), - })); + const signedMovements = transactions.map((t) => { + const rawAmount = Number(t.amount); + const amount = t.type && t.type.id > 10 && rawAmount > 0 ? -rawAmount : rawAmount; + return { date: t.date, amount }; + }); return this.reconstructBackward(months, currentBalance, signedMovements); } diff --git a/myteamwallet_frontend_modern/src/app/features/team/overview/overview.spec.ts b/myteamwallet_frontend_modern/src/app/features/team/overview/overview.spec.ts index 2bf39f4..b81431e 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/overview/overview.spec.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/overview/overview.spec.ts @@ -197,6 +197,7 @@ describe('Overview', () => { expect(balanceChart.data.datasets[0].data).toEqual([100, 125]); expect(balanceChart.data.datasets[1].label).toBe('Theoretisch (inkl. offene Beiträge)'); expect(balanceChart.data.datasets[1].data).toEqual([100, 150]); + expect(balanceChart.options?.plugins?.legend?.position).toBe('bottom'); expect(flowChart.type).toBe('bar'); expect(flowChart.data.datasets).toHaveLength(2); expect(outstandingChart.type).toBe('bar');