From d6733eff0d75ed804e8868cb2285e5ee9ae2998f Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Tue, 4 Aug 2026 19:13:42 +0200 Subject: [PATCH] feat: emit notification events on player active/role changes --- .../src/teams/team-members.service.spec.ts | 51 ++++++++++++++++++- .../src/teams/team-members.service.ts | 47 ++++++++++++++--- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/myteamwallet_backend/src/teams/team-members.service.spec.ts b/myteamwallet_backend/src/teams/team-members.service.spec.ts index 956614f..2c84646 100644 --- a/myteamwallet_backend/src/teams/team-members.service.spec.ts +++ b/myteamwallet_backend/src/teams/team-members.service.spec.ts @@ -18,6 +18,7 @@ describe('TeamMembersService', () => { let dataSource: any; let logger: any; let access: any; + let eventEmitter: any; let service: TeamMembersService; beforeEach(() => { @@ -44,7 +45,8 @@ describe('TeamMembersService', () => { dataSource = { transaction: jest.fn((work) => work(manager)) }; logger = { info: jest.fn() }; access = { assertAtLeast: jest.fn(() => Promise.resolve()) }; - service = new TeamMembersService(dataSource, logger, access as any); + eventEmitter = { emit: jest.fn() }; + service = new TeamMembersService(dataSource, logger, access as any, eventEmitter as any); }); it('checks the team-manager permission before touching the database', async () => { @@ -188,6 +190,53 @@ describe('TeamMembersService', () => { ).rejects.toBeInstanceOf(NotFoundException); }); + it('emits a player-active-changed event after a real deactivation', async () => { + player.balance = 42; + treasurers = [player, makePlayer(102, true, TeamRolesEnum.treasurer, 0)]; + + await service.setActive(5, teamId, player.id, false); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'notifications.player.active_changed', + expect.objectContaining({ + teamId, + actorUserId: 5, + playerId: player.id, + playerName: 'Pat Player', + active: false, + }), + ); + }); + + it('does not emit when the active state is unchanged (idempotent)', async () => { + player = makePlayer(101, true, TeamRolesEnum.player, 0); + lockedPlayerQuery = chain({ getOne: jest.fn(() => player) }); + playerRepository.createQueryBuilder = jest.fn((alias: string) => + alias === 'lockedPlayer' ? lockedPlayerQuery : treasurerLockQuery, + ); + + await service.setActive(5, teamId, player.id, true); + + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + + it('emits a player-role-changed event after a real role change', async () => { + treasurers = [player, makePlayer(102, true, TeamRolesEnum.treasurer, 0)]; + + await service.setTeamRole(5, teamId, player.id, TeamRolesEnum.captain); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'notifications.player.role_changed', + expect.objectContaining({ + teamId, + actorUserId: 5, + playerId: player.id, + playerName: 'Pat Player', + teamRoleId: TeamRolesEnum.captain, + }), + ); + }); + function makePlayer( id: number, active: boolean, diff --git a/myteamwallet_backend/src/teams/team-members.service.ts b/myteamwallet_backend/src/teams/team-members.service.ts index b373975..6d3ff02 100644 --- a/myteamwallet_backend/src/teams/team-members.service.ts +++ b/myteamwallet_backend/src/teams/team-members.service.ts @@ -1,6 +1,10 @@ import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { DataSource, EntityManager, Repository } from 'typeorm'; import { LoggingService } from '../database/logging/logging.service'; +import { NOTIFICATION_EVENT_NAME } from '../notifications/events/notification-event-names'; +import { PlayerActiveChangedEvent } from '../notifications/events/player-active-changed.event'; +import { PlayerRoleChangedEvent } from '../notifications/events/player-role-changed.event'; import { Player } from '../players/entities/player.entity'; import { TeamRole } from '../team-roles/entities/team-roles.entity'; import { TeamRolesEnum } from '../team-roles/team-roles.enum'; @@ -18,6 +22,7 @@ export class TeamMembersService { private readonly dataSource: DataSource, private readonly logger: LoggingService, private readonly access: TeamAccessService, + private readonly eventEmitter: EventEmitter2, ) {} async setActive( @@ -33,12 +38,12 @@ export class TeamMembersService { TeamRolesEnum.captain, ); - return this.dataSource.transaction(async (manager) => { + const result = await this.dataSource.transaction(async (manager) => { const activeTreasurers = await this.lockActiveTreasurers(manager, teamId); const playerRepository = manager.getRepository(Player); const player = await this.findLockedPlayer(playerRepository, playerId, teamId); - if (player.active === active) return player; + if (player.active === active) return { player, changed: false }; const isDeactivation = player.active && !active; if ( @@ -66,8 +71,23 @@ export class TeamMembersService { actorUserId, `teamId=${teamId} playerId=${playerId} active=${active}`, ); - return player; + return { player, changed: true }; }); + + if (result.changed) { + this.eventEmitter.emit( + NOTIFICATION_EVENT_NAME.playerActiveChanged, + new PlayerActiveChangedEvent( + teamId, + actorUserId, + playerId, + `${result.player.firstName} ${result.player.lastName}`, + active, + ), + ); + } + + return result.player; } async setTeamRole( @@ -83,12 +103,12 @@ export class TeamMembersService { TeamRolesEnum.captain, ); - return this.dataSource.transaction(async (manager) => { + const result = await this.dataSource.transaction(async (manager) => { const activeTreasurers = await this.lockActiveTreasurers(manager, teamId); const playerRepository = manager.getRepository(Player); const player = await this.findLockedPlayer(playerRepository, playerId, teamId); - if (player.teamRole?.id === teamRoleId) return player; + if (player.teamRole?.id === teamRoleId) return { player, changed: false }; const isDemotionFromTreasurer = player.active && @@ -108,8 +128,23 @@ export class TeamMembersService { actorUserId, `teamId=${teamId} playerId=${playerId} teamRoleId=${teamRoleId}`, ); - return player; + return { player, changed: true }; }); + + if (result.changed) { + this.eventEmitter.emit( + NOTIFICATION_EVENT_NAME.playerRoleChanged, + new PlayerRoleChangedEvent( + teamId, + actorUserId, + playerId, + `${result.player.firstName} ${result.player.lastName}`, + teamRoleId, + ), + ); + } + + return result.player; } // insert() statt save(): umgeht bewusst @BeforeInsert setBalance() auf Transaction,