From 6531f2553fcb7b0e347ed230c59ce11a75105a81 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 3 Aug 2026 17:27:23 +0200 Subject: [PATCH] fix: wrap team creation in a transaction, add more-menu entry point, fix lint Co-Authored-By: Claude Sonnet 5 --- .../src/teams/teams.controller.spec.ts | 2 +- .../src/teams/teams.service.spec.ts | 56 +++++++++++++++++-- .../src/teams/teams.service.ts | 41 +++++++++----- .../src/app/features/team/more/more.html | 9 +++ .../src/app/features/team/more/more.scss | 24 ++++++-- .../src/app/features/team/more/more.spec.ts | 56 +++++++++++++++++++ .../src/app/features/team/more/more.ts | 21 +++++++ 7 files changed, 185 insertions(+), 24 deletions(-) diff --git a/myteamwallet_backend/src/teams/teams.controller.spec.ts b/myteamwallet_backend/src/teams/teams.controller.spec.ts index 9794ff9..0ff4fe1 100644 --- a/myteamwallet_backend/src/teams/teams.controller.spec.ts +++ b/myteamwallet_backend/src/teams/teams.controller.spec.ts @@ -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); }); diff --git a/myteamwallet_backend/src/teams/teams.service.spec.ts b/myteamwallet_backend/src/teams/teams.service.spec.ts index 26786fc..59d302f 100644 --- a/myteamwallet_backend/src/teams/teams.service.spec.ts +++ b/myteamwallet_backend/src/teams/teams.service.spec.ts @@ -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(); + }); }); diff --git a/myteamwallet_backend/src/teams/teams.service.ts b/myteamwallet_backend/src/teams/teams.service.ts index 6e8f88c..2a5a38f 100644 --- a/myteamwallet_backend/src/teams/teams.service.ts +++ b/myteamwallet_backend/src/teams/teams.service.ts @@ -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, + 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 { + private async addCreatorAsCaptain( + manager: EntityManager, + team: Team, + userId: string, + ): Promise { 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 { + private async generateBasicTeamSettings( + manager: EntityManager, + team: Team, + ): Promise { + 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)), ); } diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/more.html b/myteamwallet_frontend_modern/src/app/features/team/more/more.html index 5d1bf8d..ea7e4fc 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/more.html +++ b/myteamwallet_frontend_modern/src/app/features/team/more/more.html @@ -5,6 +5,15 @@