Files
teamwallet/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.spec.ts
Bastian Wagner 5dae4362b2 feat(cashbox): book a catalog penalty directly as a transaction
Adds a catalog picker to the member-booking form in the cashbox (prefills
amount/note/type, stays editable) and a "Buchen" button on each penalty
catalog entry that jumps to the cashbox with that entry preselected via a
penaltyId query param. No backend changes — reuses the existing POST
/transactions flow, the catalog only supplies starting values.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 18:50:58 +02:00

218 lines
6.5 KiB
TypeScript

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 }),
);
});
});