fix: wrap team creation in a transaction, add more-menu entry point, fix lint

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-03 17:27:23 +02:00
parent b03198baa6
commit 6531f2553f
7 changed files with 185 additions and 24 deletions

View File

@@ -31,7 +31,7 @@ describe('TeamsController', () => {
const req = { user: { id: 42 } };
const dto = { name: '1. Herren' };
controller.create(req as any, dto as any);
void controller.create(req as any, dto as any);
expect(service.createNewTeam).toHaveBeenCalledWith(dto, 42);
});

View File

@@ -30,6 +30,7 @@ describe('TeamsService#getOverviewStats theoretical balance', () => {
{ info: jest.fn(), debug: jest.fn(), warn: jest.fn() } as any,
access as any,
{} as any,
{} as any,
);
});
@@ -288,6 +289,7 @@ describe('TeamsService#getTeamTransactionsJournal', () => {
{ info: jest.fn(), debug: jest.fn(), warn: jest.fn() } as any,
access as any,
{} as any,
{} as any,
);
});
@@ -372,24 +374,46 @@ describe('TeamsService#createNewTeam', () => {
const settingsRepository = { create: jest.fn(), save: jest.fn() };
const usersRepository = { findOneBy: jest.fn() };
const logger = { info: jest.fn(), debug: jest.fn(), warn: jest.fn() };
let manager: any;
let dataSource: any;
let service: TeamsService;
beforeEach(() => {
jest.resetAllMocks();
manager = {
getRepository: jest.fn((entity: { name: string }) => {
switch (entity.name) {
case 'Team':
return repository;
case 'Player':
return playerRepository;
case 'TeamRole':
return rolesRepository;
case 'TeamSetting':
return settingsRepository;
case 'User':
return usersRepository;
default:
throw new Error(`unexpected entity ${entity.name}`);
}
}),
};
dataSource = { transaction: jest.fn((work: any) => work(manager)) };
service = new TeamsService(
repository as any,
playerRepository as any,
{} as any,
rolesRepository as any,
settingsRepository as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
logger as any,
{} as any,
usersRepository as any,
{} as any,
dataSource as any,
);
});
it('creates the team, its default settings, and a captain membership for the creator', async () => {
it('creates the team, its default settings, and a captain membership for the creator, all inside one transaction', async () => {
const savedTeam = { id: 7, name: '1. Herren' };
repository.create.mockReturnValue({ name: '1. Herren' });
repository.save.mockResolvedValue(savedTeam);
@@ -406,6 +430,7 @@ describe('TeamsService#createNewTeam', () => {
const result = await service.createNewTeam({ name: '1. Herren' } as any, '42');
expect(dataSource.transaction).toHaveBeenCalledTimes(1);
expect(result).toBe(savedTeam);
expect(rolesRepository.findOneBy).toHaveBeenCalledWith({ id: 3 });
expect(usersRepository.findOneBy).toHaveBeenCalledWith({ id: 42 });
@@ -415,8 +440,27 @@ describe('TeamsService#createNewTeam', () => {
lastName: 'Muster',
team: savedTeam,
teamRole: { id: 3, name: 'captain' },
user: { id: 42, firstName: 'Alex', lastName: 'Muster' },
}),
);
expect(playerRepository.save).toHaveBeenCalled();
});
it('does not create the team at all if the captain-membership write fails', async () => {
const savedTeam = { id: 7, name: '1. Herren' };
repository.create.mockReturnValue({ name: '1. Herren' });
repository.save.mockResolvedValue(savedTeam);
settingsRepository.create.mockImplementation((s: unknown) => s);
settingsRepository.save.mockResolvedValue([]);
usersRepository.findOneBy.mockResolvedValue({ id: 42, firstName: 'Alex', lastName: 'Muster' });
rolesRepository.findOneBy.mockResolvedValue({ id: 3, name: 'captain' });
playerRepository.create.mockImplementation((p: unknown) => p);
playerRepository.save.mockRejectedValue(new Error('db down'));
await expect(
service.createNewTeam({ name: '1. Herren' } as any, '42'),
).rejects.toThrow('db down');
expect(logger.info).not.toHaveBeenCalled();
});
});

View File

@@ -10,7 +10,7 @@ import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/tea
import { Transaction } from 'src/transactions/entitites/transaction.entity';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
import { DataSource, EntityManager, Repository } from 'typeorm';
import {
TransactionsQueryDto,
TransactionsSortableField,
@@ -50,6 +50,7 @@ export class TeamsService {
private access: TeamAccessService,
@InjectRepository(User)
private usersRepository: Repository<User>,
private dataSource: DataSource,
) {}
async getOverview(teamId: string, actorUserId: string) {
@@ -168,11 +169,16 @@ export class TeamsService {
}
async createNewTeam(teamdto: CreateTeamDTO, userId: string) {
const createTeam = this.repository.create(teamdto);
const team = await this.dataSource.transaction(async (manager) => {
const teamRepository = manager.getRepository(Team);
const createTeam = teamRepository.create(teamdto);
const savedTeam = await teamRepository.save(createTeam);
const team = await this.repository.save(createTeam);
await this.generateBasicTeamSettings(team);
await this.addCreatorAsCaptain(team, userId);
await this.generateBasicTeamSettings(manager, savedTeam);
await this.addCreatorAsCaptain(manager, savedTeam, userId);
return savedTeam;
});
await this.logger.info({
event: 'team_create',
@@ -182,13 +188,18 @@ export class TeamsService {
return team;
}
private async addCreatorAsCaptain(team: Team, userId: string): Promise<void> {
private async addCreatorAsCaptain(
manager: EntityManager,
team: Team,
userId: string,
): Promise<void> {
const [user, captainRole] = await Promise.all([
this.usersRepository.findOneBy({ id: Number(userId) }),
this.rolesRepository.findOneBy({ id: TeamRolesEnum.captain }),
manager.getRepository(User).findOneBy({ id: Number(userId) }),
manager.getRepository(TeamRole).findOneBy({ id: TeamRolesEnum.captain }),
]);
const player = this.playerRepository.create({
const playerRepository = manager.getRepository(Player);
const player = playerRepository.create({
firstName: user?.firstName ?? '',
lastName: user?.lastName ?? '',
team,
@@ -196,10 +207,14 @@ export class TeamsService {
user: user ?? undefined,
});
await this.playerRepository.save(player);
await playerRepository.save(player);
}
private async generateBasicTeamSettings(team: Team): Promise<void> {
private async generateBasicTeamSettings(
manager: EntityManager,
team: Team,
): Promise<void> {
const settingsRepository = manager.getRepository(TeamSetting);
const settings: CreateTeamSettingDTO[] = Object.entries(
TEAM_SETTING_DEFAULTS,
).map(([key, value]) => ({
@@ -208,8 +223,8 @@ export class TeamsService {
team,
}));
await this.settingsRepository.save(
settings.map((s) => this.settingsRepository.create(s)),
await settingsRepository.save(
settings.map((s) => settingsRepository.create(s)),
);
}