import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } 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 { RoleEnum } from 'src/roles/roles.enum'; import { User } from 'src/users/entities/user.entity'; import { Repository } from 'typeorm'; import { CreateTransactionDto } from './dto/create-transaction.dto'; import { TransactionType } from './entitites/transaction-type.entity'; import { Transaction } from './entitites/transaction.entity'; import { TransactionTypeEnum } from './transaction-type.enum'; @Injectable() export class TransactionsService { constructor( @InjectRepository(Transaction) private transactionsRepository: Repository, @InjectRepository(Player) private playersRepository: Repository, @InjectRepository(TransactionType) private transactionTypesRepository: Repository, @InjectRepository(User) private usersRepository: Repository, private logger: LoggingService, ) {} async createTransactions(data: CreateTransactionDto[], userId: string) { const res = []; for (const d of data) { const r = await this.create(d, userId); res.push(r); } return res; } async create(data: CreateTransactionDto, userId: string) { const player = await this.playersRepository.findOne({ where: { id: data.playerId }, }); const creatingUser = await this.usersRepository.findOne({ where: { id: Number(userId), }, relations: ['players'], }); const transactionType = await this.transactionTypesRepository.findOne({ where: { id: TransactionTypeEnum[TransactionTypeEnum[data.type]] }, }); if (creatingUser.role.id != RoleEnum.admin) { if ( !player || !transactionType || !creatingUser || !creatingUser.players || creatingUser.players.length == 0 ) { await this.logger.warn({ event: 'transaction_create_fail', details: `Player: ${data.playerId}, amount: ${data.amount}, typeEnum: ${data.type}`, userId: Number(userId), }); return; } const teamPlayer = creatingUser.players.find( (p) => p.team.id == player.team.id && p.teamRole.id >= Number( player.team.settings.find( (s) => s.key == 'transaction_create_min_role', )['value'], ), ); if (!teamPlayer) { return; } } const transaction = this.transactionsRepository.create({ amount: data.amount, date: data.date, player: player, note: data.note || '', type: transactionType, }); await this.logger.info({ event: 'transaction_create', details: `Player: ${transaction.player.id}, amount: ${transaction.amount}, type: ${transaction.type?.name}`, userId: Number(userId), }); return this.transactionsRepository.save(transaction); } async reverse(transactionId: number, userId: string) { const original = await this.transactionsRepository.findOne({ where: { id: transactionId }, relations: ['player', 'type'], }); if (!original) { throw new NotFoundException('Buchung nicht gefunden'); } if (original.note?.startsWith('Stornierung von Buchung #')) { throw new BadRequestException( 'Eine Stornobuchung kann nicht erneut storniert werden', ); } const alreadyReversed = await this.transactionsRepository.findOne({ where: { player: { id: original.player.id }, note: `Stornierung von Buchung #${original.id}`, }, }); if (alreadyReversed) { throw new BadRequestException('Diese Buchung wurde bereits storniert'); } const creatingUser = await this.usersRepository.findOne({ where: { id: Number(userId) }, relations: ['players'], }); if (creatingUser.role.id != RoleEnum.admin) { const teamPlayer = creatingUser.players?.find( (p) => p.team.id == original.player.team.id && p.teamRole.id >= Number( original.player.team.settings.find( (s) => s.key == 'transaction_create_min_role', )?.['value'] ?? 0, ), ); if (!teamPlayer) { throw new ForbiddenException( 'Keine Berechtigung, diese Buchung zu stornieren', ); } } const originalAmount = Math.abs(Number(original.amount)); let reversalType = original.type; let reversalAmount = -originalAmount; // Buchungstypen > 10 (z.B. Strafe/Umlage/Gebühr) ziehen im Entity-Hook // immer vom Guthaben ab, egal welches Vorzeichen der Betrag hat. Um sie // auszugleichen, wird die Gegenbuchung stattdessen als "credit" gebucht. if (original.type.id > 10) { reversalType = await this.transactionTypesRepository.findOne({ where: { id: TransactionTypeEnum.credit }, }); reversalAmount = originalAmount; } const reversal = this.transactionsRepository.create({ amount: reversalAmount, date: new Date().toISOString(), player: original.player, note: `Stornierung von Buchung #${original.id}`, type: reversalType, }); const saved = await this.transactionsRepository.save(reversal); await this.logger.info({ event: 'transaction_reverse', details: `Buchung #${original.id} storniert durch Buchung #${saved.id}, Player: ${original.player.id}, amount: ${original.amount}`, userId: Number(userId), }); return saved; } }