first commit

This commit is contained in:
Bastian Wagner
2026-07-31 21:02:47 +02:00
commit 6bea4f766a
512 changed files with 64459 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class CreateTeamDTO {
@ApiProperty({ example: '1. Herren' })
@IsNotEmpty()
name: string;
}

View File

@@ -0,0 +1,46 @@
import { IsBoolean } from 'class-validator';
export class UpdatePublicAccessDto {
@IsBoolean()
enabled: boolean;
}
export interface PublicAccessStatusDto {
enabled: boolean;
token: string | null;
}
export interface PublicPlayerDto {
id: number;
firstName: string;
lastName: string;
balance: number;
active: boolean;
}
export interface PublicPenaltyDto {
id: number;
description: string;
amount: number;
}
export interface PublicTransactionDto {
id: number;
note: string;
date: string;
amount: number;
type: { id: number; name: string } | null;
}
export interface PublicTeamDto {
name: string;
balance: number;
outstanding: number;
players: PublicPlayerDto[];
penalties: PublicPenaltyDto[];
}
export interface PublicPlayerHistoryDto {
player: PublicPlayerDto;
transactions: PublicTransactionDto[];
}

View File

@@ -0,0 +1,60 @@
import {
AfterLoad,
BeforeInsert,
Column,
Entity,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { EntityHelper } from 'src/utils/entity-helper';
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
import { Player } from 'src/players/entities/player.entity';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
@Entity()
export class Team extends EntityHelper {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
alias: string;
@Column({ default: false })
publicAccessEnabled: boolean;
@Column({ length: 64, nullable: true, unique: true, select: false })
publicAccessToken?: string | null;
@OneToMany(() => TeamSetting, (setting) => setting.team, {
eager: true,
})
settings: TeamSetting[];
@OneToMany(() => TeamWalletTransaction, (transaction) => transaction.team, {
eager: false,
})
transactions: TeamWalletTransaction[];
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
balance: number;
@OneToMany(() => Player, (player) => player.team)
players: Player[];
outstanding?: number;
@BeforeInsert()
setAlias() {
if (!this.alias) {
this.alias = Date.now() + '';
}
}
@AfterLoad()
updateValue() {
this.balance = Number(this.balance);
}
}

View File

@@ -0,0 +1 @@
export type TeamSettingType = 'transaction_create_min_role' | '';

View File

@@ -0,0 +1,178 @@
import { NotFoundException } from '@nestjs/common';
import { PublicTeamAccessService } from './public-team-access.service';
describe('PublicTeamAccessService', () => {
const teamRepository = {
createQueryBuilder: jest.fn(),
findOne: jest.fn(),
save: jest.fn(async (team) => team),
};
const playerRepository = { find: jest.fn(), findOne: jest.fn() };
const transactionRepository = { find: jest.fn() };
const penaltyRepository = { find: jest.fn() };
const access = { assertMember: jest.fn(), assertManager: jest.fn() };
let service: PublicTeamAccessService;
const managedTeam = {
id: 7,
name: 'Team A',
balance: 125,
publicAccessEnabled: false,
publicAccessToken: null,
};
beforeEach(() => {
jest.resetAllMocks();
teamRepository.save.mockImplementation(async (team) => team);
service = new PublicTeamAccessService(
teamRepository as any,
playerRepository as any,
transactionRepository as any,
penaltyRepository as any,
access as any,
);
});
function mockManagedTeam(team = { ...managedTeam }) {
const getOne = jest.fn().mockResolvedValue(team);
teamRepository.createQueryBuilder.mockReturnValue({
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getOne,
});
return team;
}
it('creates a 64 character token on first activation', async () => {
const team = mockManagedTeam();
const status = await service.setEnabled(4, 7, true);
expect(access.assertManager).toHaveBeenCalledWith(4, 7);
expect(status.enabled).toBe(true);
expect(status.token).toMatch(/^[a-f0-9]{64}$/);
expect(teamRepository.save).toHaveBeenCalledWith(team);
});
it('keeps the existing token when re-enabled', async () => {
mockManagedTeam({ ...managedTeam, publicAccessToken: 'a'.repeat(64) });
const status = await service.setEnabled(4, 7, true);
expect(status.token).toBe('a'.repeat(64));
});
it('rotates the token without changing enabled state', async () => {
mockManagedTeam({
...managedTeam,
publicAccessEnabled: true,
publicAccessToken: 'a'.repeat(64),
});
const status = await service.rotate(4, 7);
expect(status.enabled).toBe(true);
expect(status.token).toMatch(/^[a-f0-9]{64}$/);
expect(status.token).not.toBe('a'.repeat(64));
});
it('returns only whitelisted public team fields and active players', async () => {
teamRepository.findOne.mockResolvedValue({
id: 7,
name: 'Team A',
balance: 125,
});
playerRepository.find.mockResolvedValue([
{
id: 3,
firstName: 'Ada',
lastName: 'Lovelace',
balance: -20,
active: true,
user: { email: 'must-not-leak@example.com' },
},
]);
penaltyRepository.find.mockResolvedValue([
{ id: 8, description: 'Zu spät', amount: 5, user: { email: 'hidden' } },
]);
await expect(service.getPublicTeam('b'.repeat(64))).resolves.toEqual({
name: 'Team A',
balance: 125,
outstanding: 20,
players: [
{
id: 3,
firstName: 'Ada',
lastName: 'Lovelace',
balance: -20,
active: true,
},
],
penalties: [{ id: 8, description: 'Zu spät', amount: 5 }],
});
});
it('rejects a player that does not belong to the shared team', async () => {
teamRepository.findOne.mockResolvedValue({
id: 7,
name: 'Team A',
balance: 125,
});
playerRepository.findOne.mockResolvedValue(null);
await expect(
service.getPublicPlayerHistory('b'.repeat(64), 99),
).rejects.toBeInstanceOf(NotFoundException);
expect(transactionRepository.find).not.toHaveBeenCalled();
});
it('returns player identity and the latest 20 transactions', async () => {
teamRepository.findOne.mockResolvedValue({
id: 7,
name: 'Team A',
balance: 125,
});
playerRepository.findOne.mockResolvedValue({
id: 3,
firstName: 'Ada',
lastName: 'Lovelace',
balance: -20,
active: true,
});
transactionRepository.find.mockResolvedValue([
{
id: 11,
note: 'Training',
date: '2026-07-30',
amount: 5,
type: { id: 11, name: 'fine' },
player: { id: 3 },
},
]);
await expect(
service.getPublicPlayerHistory('b'.repeat(64), 3),
).resolves.toEqual({
player: {
id: 3,
firstName: 'Ada',
lastName: 'Lovelace',
balance: -20,
active: true,
},
transactions: [
{
id: 11,
note: 'Training',
date: '2026-07-30',
amount: 5,
type: { id: 11, name: 'fine' },
},
],
});
expect(transactionRepository.find).toHaveBeenCalledWith(
expect.objectContaining({ order: { date: 'DESC' }, take: 20 }),
);
});
});

View File

@@ -0,0 +1,189 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { randomBytes } from 'crypto';
import { PenaltyEntity } from '../penalty/entities/penalty.entity';
import { Player } from '../players/entities/player.entity';
import { Transaction } from '../transactions/entitites/transaction.entity';
import { Repository } from 'typeorm';
import {
PublicAccessStatusDto,
PublicPlayerDto,
PublicPlayerHistoryDto,
PublicTeamDto,
PublicTransactionDto,
} from './dto/public-access.dto';
import { Team } from './entities/team.entity';
import { TeamAccessService } from './team-access.service';
@Injectable()
export class PublicTeamAccessService {
constructor(
@InjectRepository(Team)
private readonly teamRepository: Repository<Team>,
@InjectRepository(Player)
private readonly playerRepository: Repository<Player>,
@InjectRepository(Transaction)
private readonly transactionRepository: Repository<Transaction>,
@InjectRepository(PenaltyEntity)
private readonly penaltyRepository: Repository<PenaltyEntity>,
private readonly access: TeamAccessService,
) {}
async getStatus(
userId: number,
teamId: number,
): Promise<PublicAccessStatusDto> {
await this.access.assertMember(userId, teamId);
return this.toStatus(await this.loadManagedTeam(teamId));
}
async setEnabled(
userId: number,
teamId: number,
enabled: boolean,
): Promise<PublicAccessStatusDto> {
await this.access.assertManager(userId, teamId);
const team = await this.loadManagedTeam(teamId);
if (enabled && !team.publicAccessToken) {
team.publicAccessToken = this.createToken();
}
team.publicAccessEnabled = enabled;
await this.teamRepository.save(team);
return this.toStatus(team);
}
async rotate(userId: number, teamId: number): Promise<PublicAccessStatusDto> {
await this.access.assertManager(userId, teamId);
const team = await this.loadManagedTeam(teamId);
team.publicAccessToken = this.createToken();
await this.teamRepository.save(team);
return this.toStatus(team);
}
async getPublicTeam(token: string): Promise<PublicTeamDto> {
const team = await this.loadPublicTeam(token);
const [players, penalties] = await Promise.all([
this.playerRepository.find({
where: { team: { id: team.id }, active: true },
order: { lastName: 'ASC', firstName: 'ASC' },
}),
this.penaltyRepository.find({
where: { team: { id: team.id } },
order: { description: 'ASC' },
}),
]);
const publicPlayers = players.map((player) => this.toPublicPlayer(player));
return {
name: team.name,
balance: Number(team.balance),
outstanding: publicPlayers.reduce(
(total, player) => total - Number(player.balance),
0,
),
players: publicPlayers,
penalties: penalties.map((penalty) => ({
id: penalty.id,
description: penalty.description,
amount: Number(penalty.amount),
})),
};
}
async getPublicPlayerHistory(
token: string,
playerId: number,
): Promise<PublicPlayerHistoryDto> {
const team = await this.loadPublicTeam(token);
const player = await this.playerRepository.findOne({
where: { id: playerId, team: { id: team.id }, active: true },
});
if (!player) throw new NotFoundException('Freigabe nicht gefunden.');
return {
player: this.toPublicPlayer(player),
transactions: await this.loadTransactions(player.id),
};
}
async getMemberPlayerTransactions(
userId: number,
teamId: number,
playerId: number,
): Promise<PublicTransactionDto[]> {
await this.access.assertMember(userId, teamId);
const player = await this.playerRepository.findOne({
where: { id: playerId, team: { id: teamId } },
});
if (!player) throw new NotFoundException('Spieler nicht gefunden.');
return this.loadTransactions(player.id);
}
private async loadManagedTeam(teamId: number): Promise<Team> {
const team = await this.teamRepository
.createQueryBuilder('team')
.addSelect('team.publicAccessToken')
.where('team.id = :teamId', { teamId })
.getOne();
if (!team) throw new NotFoundException('Team nicht gefunden.');
return team;
}
private async loadPublicTeam(token: string): Promise<Team> {
if (!/^[a-f0-9]{64}$/.test(token)) {
throw new NotFoundException('Freigabe nicht gefunden.');
}
const team = await this.teamRepository.findOne({
where: { publicAccessToken: token, publicAccessEnabled: true },
select: {
id: true,
name: true,
balance: true,
publicAccessEnabled: true,
publicAccessToken: true,
},
});
if (!team) throw new NotFoundException('Freigabe nicht gefunden.');
return team;
}
private async loadTransactions(
playerId: number,
): Promise<PublicTransactionDto[]> {
const transactions = await this.transactionRepository.find({
where: { player: { id: playerId } },
order: { date: 'DESC' },
take: 20,
});
return transactions.map((transaction) => ({
id: transaction.id,
note: transaction.note,
date: transaction.date,
amount: Number(transaction.amount),
type: transaction.type
? { id: transaction.type.id, name: transaction.type.name }
: null,
}));
}
private toPublicPlayer(player: Player): PublicPlayerDto {
return {
id: player.id,
firstName: player.firstName,
lastName: player.lastName,
balance: Number(player.balance),
active: player.active,
};
}
private toStatus(team: Team): PublicAccessStatusDto {
return {
enabled: team.publicAccessEnabled,
token: team.publicAccessToken ?? null,
};
}
private createToken(): string {
return randomBytes(32).toString('hex');
}
}

View File

@@ -0,0 +1,20 @@
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { PublicTeamAccessService } from './public-team-access.service';
@Controller({ path: 'public/teams', version: '1' })
export class PublicTeamsController {
constructor(private readonly publicAccess: PublicTeamAccessService) {}
@Get(':token')
getTeam(@Param('token') token: string) {
return this.publicAccess.getPublicTeam(token);
}
@Get(':token/players/:playerId/transactions')
getPlayerHistory(
@Param('token') token: string,
@Param('playerId', ParseIntPipe) playerId: number,
) {
return this.publicAccess.getPublicPlayerHistory(token, playerId);
}
}

View File

@@ -0,0 +1,84 @@
import { ForbiddenException } from '@nestjs/common';
import { RoleEnum } from '../roles/roles.enum';
import { TeamAccessService } from './team-access.service';
describe('TeamAccessService', () => {
const userRepository = { findOne: jest.fn() };
const playerRepository = { find: jest.fn() };
let service: TeamAccessService;
beforeEach(() => {
jest.resetAllMocks();
service = new TeamAccessService(
userRepository as any,
playerRepository as any,
);
});
it('allows a global admin without a team membership', async () => {
userRepository.findOne.mockResolvedValue({
id: 1,
role: { id: RoleEnum.admin },
});
await expect(service.assertManager(1, 9)).resolves.toBeUndefined();
expect(playerRepository.find).not.toHaveBeenCalled();
});
it('allows an active team member to read sharing status', async () => {
userRepository.findOne.mockResolvedValue({
id: 2,
role: { id: RoleEnum.user },
});
playerRepository.find.mockResolvedValue([
{ active: true, teamRole: { id: 1 } },
]);
await expect(service.assertMember(2, 9)).resolves.toBeUndefined();
});
it.each([3, 4, 5])(
'allows active team role %s to manage sharing',
async (roleId) => {
userRepository.findOne.mockResolvedValue({
id: 2,
role: { id: RoleEnum.user },
});
playerRepository.find.mockResolvedValue([
{ active: true, teamRole: { id: roleId } },
]);
await expect(service.assertManager(2, 9)).resolves.toBeUndefined();
},
);
it('uses the highest role when one user has multiple players in a team', async () => {
userRepository.findOne.mockResolvedValue({
id: 2,
role: { id: RoleEnum.user },
});
playerRepository.find.mockResolvedValue([
{ active: true, teamRole: { id: 1 } },
{ active: true, teamRole: { id: 3 } },
]);
await expect(service.assertManager(2, 9)).resolves.toBeUndefined();
});
it.each([
['a player', [{ active: true, teamRole: { id: 1 } }]],
['a second treasurer', [{ active: true, teamRole: { id: 2 } }]],
['an inactive captain', [{ active: false, teamRole: { id: 3 } }]],
['a non-member', []],
])('rejects %s from managing sharing', async (_label, players) => {
userRepository.findOne.mockResolvedValue({
id: 2,
role: { id: RoleEnum.user },
});
playerRepository.find.mockResolvedValue(players);
await expect(service.assertManager(2, 9)).rejects.toBeInstanceOf(
ForbiddenException,
);
});
});

View File

@@ -0,0 +1,47 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Player } from '../players/entities/player.entity';
import { RoleEnum } from '../roles/roles.enum';
import { User } from '../users/entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class TeamAccessService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Player)
private readonly playerRepository: Repository<Player>,
) {}
async assertMember(userId: number, teamId: number): Promise<void> {
await this.assertMinimumRole(userId, teamId, 1);
}
async assertManager(userId: number, teamId: number): Promise<void> {
await this.assertMinimumRole(userId, teamId, 3);
}
private async assertMinimumRole(
userId: number,
teamId: number,
minimumRole: number,
): Promise<void> {
const user = await this.userRepository.findOne({ where: { id: userId } });
if (user?.role?.id === RoleEnum.admin) return;
const players = await this.playerRepository.find({
where: { user: { id: userId }, team: { id: teamId } },
});
const highestActiveRole = players
.filter((player) => player.active)
.reduce(
(highest, player) => Math.max(highest, player.teamRole?.id ?? 0),
0,
);
if (highestActiveRole < minimumRole) {
throw new ForbiddenException('Keine Berechtigung für dieses Team.');
}
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TeamsController } from './teams.controller';
describe('TeamsController', () => {
let controller: TeamsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TeamsController],
}).compile();
controller = module.get<TeamsController>(TeamsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,140 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
Patch,
ParseIntPipe,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Roles } from 'src/roles/roles.decorator';
import { RoleEnum } from 'src/roles/roles.enum';
import { RolesGuard } from 'src/roles/roles.guard';
import { CreateTeamDTO } from './dto/create-team.dto';
import { TeamsService } from './teams.service';
import { PublicTeamAccessService } from './public-team-access.service';
import { UpdatePublicAccessDto } from './dto/public-access.dto';
@ApiTags('Teams')
@Controller({
path: 'teams',
version: '1',
})
export class TeamsController {
constructor(
private service: TeamsService,
private publicAccess: PublicTeamAccessService,
) {}
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Get(':id/public-access')
getPublicAccess(@Req() req, @Param('id', ParseIntPipe) id: number) {
return this.publicAccess.getStatus(Number(req.user.id), id);
}
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Patch(':id/public-access')
setPublicAccess(
@Req() req,
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdatePublicAccessDto,
) {
return this.publicAccess.setEnabled(Number(req.user.id), id, body.enabled);
}
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Post(':id/public-access/rotate')
rotatePublicAccess(@Req() req, @Param('id', ParseIntPipe) id: number) {
return this.publicAccess.rotate(Number(req.user.id), id);
}
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Get(':id/players/:playerId/transactions')
getPlayerTransactions(
@Req() req,
@Param('id', ParseIntPipe) id: number,
@Param('playerId', ParseIntPipe) playerId: number,
) {
return this.publicAccess.getMemberPlayerTransactions(
Number(req.user.id),
id,
playerId,
);
}
@ApiBearerAuth()
@Roles([RoleEnum.user, RoleEnum.admin])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Get(':id/overview')
@HttpCode(HttpStatus.OK)
findOne(@Param('id') id: string) {
return this.service.getOverview(id);
}
@ApiOperation({
summary: 'Transactionen für ein Team',
description:
'Gibt alle Transaktionen, Spieler und Teamwallet, für ein bestimmtes Team zurück und sortiert absteigend nach Datum',
})
@ApiBearerAuth()
@Roles([RoleEnum.user, RoleEnum.admin])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Get(':id/transactions')
@HttpCode(HttpStatus.OK)
getAllTransactions(@Req() req, @Param('id') id: string) {
const userId = req.user?.id;
return this.service.getTeamTransactions(id, userId);
}
@ApiOperation({
summary: 'Neuen Spieler anlegen',
description:
'Erstellt einen neuen Spieler für das Team mit der ID aus der URL',
})
@ApiBearerAuth()
@Roles([RoleEnum.user, RoleEnum.admin])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Post(':id/players')
@HttpCode(HttpStatus.CREATED)
createPlayer(@Param('id') id: string, @Body() playerDto: any) {
return this.service.createNewPlayer(id, playerDto);
}
@Roles([RoleEnum.user, RoleEnum.admin])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@ApiOperation({
summary: 'Spieler updaten',
description: 'verändert den Spieler mit der team id',
})
@Put(':id/players')
@HttpCode(HttpStatus.CREATED)
updatePlayer(@Param('id') id: string, @Body() playerDto: any) {
return this.service.updatePlayer(playerDto);
}
@ApiOperation({
summary: 'Neues Team anlegen',
description:
'Erstellt ein neues Team mit gegebenem Namen und gibt es zurück.',
})
@ApiBearerAuth()
@Roles([RoleEnum.admin])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Req() req, @Body() teamDto: CreateTeamDTO) {
const userId = req.user?.id;
return this.service.createNewTeam(teamDto, userId);
}
}

View File

@@ -0,0 +1,35 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggingModule } from 'src/database/logging/logging.module';
import { Player } from 'src/players/entities/player.entity';
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { Transaction } from 'src/transactions/entitites/transaction.entity';
import { Team } from './entities/team.entity';
import { TeamsController } from './teams.controller';
import { TeamsService } from './teams.service';
import { User } from '../users/entities/user.entity';
import { TeamAccessService } from './team-access.service';
import { PublicTeamAccessService } from './public-team-access.service';
import { PublicTeamsController } from './public-teams.controller';
import { PenaltyEntity } from '../penalty/entities/penalty.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Team,
Player,
Transaction,
TeamRole,
TeamSetting,
TeamWalletTransaction,
User,
PenaltyEntity,
]),
LoggingModule,
],
controllers: [TeamsController, PublicTeamsController],
providers: [TeamsService, TeamAccessService, PublicTeamAccessService],
})
export class TeamsModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TeamsService } from './teams.service';
describe('TeamsService', () => {
let service: TeamsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TeamsService],
}).compile();
service = module.get<TeamsService>(TeamsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,254 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LoggingService } from 'src/database/logging/logging.service';
import { Player } from 'src/players/entities/player.entity';
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
import { CreateTeamSettingDTO } from 'src/team-settings/dto/create-team-setting.dto';
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { Transaction } from 'src/transactions/entitites/transaction.entity';
import { Repository } from 'typeorm';
import { CreateTeamDTO } from './dto/create-team.dto';
import { Team } from './entities/team.entity';
@Injectable()
export class TeamsService {
constructor(
@InjectRepository(Team)
private repository: Repository<Team>,
@InjectRepository(Player)
private playerRepository: Repository<Player>,
@InjectRepository(Transaction)
private transactionsRepository: Repository<Transaction>,
@InjectRepository(TeamRole)
private rolesRepository: Repository<TeamRole>,
@InjectRepository(TeamSetting)
private settingsRepository: Repository<TeamSetting>,
@InjectRepository(TeamWalletTransaction)
private teamWalletTransactionRepository: Repository<TeamWalletTransaction>,
private logger: LoggingService,
) {}
async getOverview(teamId: string) {
const id = Number(teamId);
const team = await this.repository.findOneOrFail({
where: { id },
relations: ['players'],
});
if (team.players) {
const out = team.players
.filter((p) => p.active)
.reduce((acc, succ) => acc + Number(succ.balance), 0);
team.outstanding = out * -1;
} else {
team.outstanding = 0;
}
team.balance = Number(team.balance);
return team;
}
async getOverviewByAlias(alias: string) {
const team = await this.repository.findOneOrFail({
where: { alias },
relations: ['players'],
});
if (team.players) {
const out = team.players
.filter((p) => p.active)
.reduce((acc, succ) => acc + Number(succ.balance), 0);
team.outstanding = out * -1;
} else {
team.outstanding = 0;
}
return team;
}
async getUserTransactionsFromTeam(teamId: string, userId: string) {
const player = await this.playerRepository.findOne({
where: { id: Number(userId) },
relations: ['transactions'],
});
if (!player) {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
errors: {
user: 'user_not_found',
},
},
HttpStatus.NOT_FOUND,
);
}
const transactions = await this.transactionsRepository.find({
where: {
player: {
id: player.id,
},
},
order: {
date: 'DESC',
},
take: 20,
});
return transactions;
}
async createNewPlayer(
id: string,
player: { firstName: string; lastName: string; teamRole: any },
) {
if (typeof player.teamRole == 'string') {
player.teamRole = await this.rolesRepository.findOneBy({
name: player.teamRole,
});
} else if (typeof player.teamRole == 'number') {
player.teamRole = await this.rolesRepository.findOneBy({
id: player.teamRole,
});
} else if (!player.teamRole) {
player.teamRole = await this.rolesRepository.findOneBy({
id: 1, // player
});
}
const team = await this.repository.findOneBy({
id: Number(id),
});
if (!team) {
throw new HttpException('No Team Found', HttpStatus.UNPROCESSABLE_ENTITY);
}
const p = {
...player,
team,
};
const created: Player = this.playerRepository.create(p);
const playerSaved: Player = await this.playerRepository.save(created);
await this.logger.info({
event: 'player_creation',
details: `Spieler ${playerSaved.id}, ${p.firstName} ${p.lastName} erstellt`,
userId: Number(id),
});
return playerSaved;
}
async createNewTeam(teamdto: CreateTeamDTO, userId: string) {
const createTeam = this.repository.create(teamdto);
const team = await this.repository.save(createTeam);
await this.generateBasicTeamSettings(team);
await this.logger.info({
event: 'team_create',
details: `created team id: ${team.id}`,
userId: Number(userId),
});
return team;
}
private generateBasicTeamSettings(team: Team): Promise<void> {
return new Promise<void>(async (resolve) => {
const s: CreateTeamSettingDTO = {
key: 'transaction_create_min_role',
value: '2',
team,
};
const setting = this.settingsRepository.create(s);
await this.settingsRepository.save(setting);
resolve();
});
}
async getTeamTransactions(teamId: string | number, userId: string | number) {
const start = new Date();
teamId = Number(teamId);
const team = await this.repository.findOneOrFail({
where: {
id: teamId,
},
relations: ['players', 'players.transactions', 'transactions'],
});
const transactions = [];
for (const t of team.transactions) {
transactions.push({
id: t.id,
date: t.date,
amount: t.amount,
type: t.type.name,
note: t.note,
isTeamWalletTransaction: true,
});
}
for (const p of team.players) {
for (const t of p.transactions) {
transactions.push({
id: t.id,
date: t.date,
amount: t.amount,
type: t.type.name,
note: t.note,
playerName: p.firstName + ' ' + p.lastName,
isTeamWalletTransaction: false,
});
}
}
const result = transactions.sort((a, b) => (a.date < b.date ? 1 : -1));
const duration = new Date().getTime() - start.getTime();
await this.logger.debug({
event: 'team_transaction_get',
details: `load ${result.length} transactions for TeamID: ${team.id}`,
userId: Number(userId),
duration,
});
return result;
}
async updatePlayer(playerDTO: any) {
const player = await this.playerRepository.findOneOrFail({
where: {
id: playerDTO.id,
},
relations: ['teamRole', 'transactions', 'team', 'transactions.type'],
});
player.active = playerDTO.active;
player.balance = playerDTO.balance;
player.firstName = playerDTO.firstName;
player.lastName = playerDTO.lastName;
if (player.teamRole.id != playerDTO.teamRole.id) {
player.teamRole = await this.rolesRepository.findOneByOrFail({
id: playerDTO.reamRole.id,
});
}
let balance = player.balance;
if (!player.active && player.balance != 0) {
player.balance = 0;
} else if (player.active && player.balance == 0) {
balance = player.transactions.reduce((acc, succ) => {
let b = succ.amount;
if (succ.type?.id > 10 && succ.amount > 0) {
b = b * -1;
}
return acc + b;
}, 0);
player.balance = balance;
}
const res = await this.playerRepository.save(player);
return res;
}
}