import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { AuthStore } from '../../../core/auth/auth-store'; import { PenaltyApi } from '../../../core/team/penalty-api'; import { TeamStore } from '../../../core/team/team-store'; import { TransactionsApi } from '../../../core/team/transactions-api'; import { Cashbox } from './cashbox'; describe('Cashbox', () => { const team = { id: 5, name: 'Team A', alias: 'team-a', balance: 120, players: [ { id: 1, firstName: 'Alex', lastName: 'Muster', balance: -12, active: true, teamRole: { id: 2, name: 'scnd_treasurer' }, user: { id: 42, email: 'alex@example.com', firstName: 'Alex', lastName: 'Muster' }, }, { id: 2, firstName: 'Bea', lastName: 'Test', balance: 5, active: true, teamRole: { id: 1, name: 'player' }, }, { id: 3, firstName: 'Chris', lastName: 'Drittel', balance: 0, active: true, teamRole: { id: 1, name: 'player' }, }, ], }; const activities = [ { id: 9, date: '2026-07-31T10:00:00.000Z', amount: 12, type: 'fine', note: 'Training', playerName: 'Bea Test', isTeamWalletTransaction: false, }, ]; const penalties = [ { id: 101, description: 'Zu spät zum Training', amount: 5 }, { id: 102, description: 'Handy vergessen', amount: 2.5 }, ]; async function setup(roleId = 2, confirm = true, penaltyIdParam: string | null = null) { const createPlayerTransactions = vi.fn(() => of([])); const createTeamWalletTransaction = vi.fn(() => of(activities[0])); const reverseTransaction = vi.fn(() => of(activities[0])); const loadTeamTransactions = vi.fn(() => of(activities)); const loadPenalties = vi.fn(() => of(penalties)); const refreshTeam = vi.fn(); const dialog = { open: vi.fn(() => ({ afterClosed: () => of(confirm) })), }; await TestBed.configureTestingModule({ imports: [Cashbox], providers: [ { provide: AuthStore, useValue: { currentUser: signal({ id: 42, email: 'alex@example.com', firstName: 'Alex', lastName: 'Muster', role: { id: roleId === 99 ? 1 : 2 }, }), }, }, { provide: TeamStore, useValue: { team: signal(team), loading: signal(false), refreshTeam }, }, { provide: TransactionsApi, useValue: { loadTeamTransactions, createPlayerTransactions, createTeamWalletTransaction, reverseTransaction, }, }, { provide: PenaltyApi, useValue: { loadPenalties } }, { provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: convertToParamMap( penaltyIdParam ? { penaltyId: penaltyIdParam } : {}, ), }, }, }, { provide: MatDialog, useValue: dialog }, ], }).compileComponents(); if (roleId === 1) { team.players[0].teamRole.id = 1; } else { team.players[0].teamRole.id = 2; } const fixture = TestBed.createComponent(Cashbox); fixture.detectChanges(); return { fixture, component: fixture.componentInstance, createPlayerTransactions, reverseTransaction, dialog, refreshTeam, }; } it('shows the activity feed and booking controls to a treasurer', async () => { const { fixture } = await setup(); expect(fixture.nativeElement.textContent).toContain('Bea Test'); expect(fixture.nativeElement.textContent).toContain('-12,00'); expect(fixture.nativeElement.querySelector('[data-testid="player-booking"]')).not.toBeNull(); }); it('submits cent-preserving split transactions for selected players', async () => { const { component, createPlayerTransactions, refreshTeam } = await setup(); component['playerForm'].setValue({ playerIds: [1, 2, 3], amount: 10, type: 11, note: 'Training', date: '2026-07-31', total: true, }); component['submitPlayerBooking'](); expect(createPlayerTransactions).toHaveBeenCalledWith([ expect.objectContaining({ playerId: 1, amount: 3.34, type: 11 }), expect.objectContaining({ playerId: 2, amount: 3.33, type: 11 }), expect.objectContaining({ playerId: 3, amount: 3.33, type: 11 }), ]); expect(refreshTeam).toHaveBeenCalled(); }); it('asks for confirmation before booking a high amount', async () => { const { component, createPlayerTransactions, dialog } = await setup(2, false); component['playerForm'].setValue({ playerIds: [1], amount: 300, type: 11, note: '', date: '2026-07-31', total: false, }); component['submitPlayerBooking'](); expect(dialog.open).toHaveBeenCalled(); expect(createPlayerTransactions).not.toHaveBeenCalled(); }); it('does not show mutation controls to a regular member', async () => { const { fixture } = await setup(1); expect(fixture.nativeElement.querySelector('[data-testid="player-booking"]')).toBeNull(); expect(fixture.nativeElement.querySelector('[data-testid="reverse-booking"]')).toBeNull(); }); it('prefills amount, note and type from a manually selected catalog entry, and stays editable', async () => { const { component } = await setup(); component['onPenaltySelect'](102); expect(component['playerForm'].getRawValue()).toEqual( expect.objectContaining({ amount: 2.5, note: 'Handy vergessen', type: 11 }), ); component['playerForm'].patchValue({ amount: 7 }); expect(component['playerForm'].getRawValue().amount).toBe(7); }); it('prefills the booking form automatically from a penaltyId query param', async () => { const { component } = await setup(2, true, '101'); expect(component['playerForm'].getRawValue()).toEqual( expect.objectContaining({ amount: 5, note: 'Zu spät zum Training', type: 11 }), ); }); it('ignores an unknown penaltyId query param without error', async () => { const { component } = await setup(2, true, '999'); expect(component['playerForm'].getRawValue()).toEqual( expect.objectContaining({ amount: 0, note: '', type: 11 }), ); }); });