Files
teamwallet/myteamwallet_backend/src/transactions/transactions.service.ts
Bastian Wagner 6bea4f766a first commit
2026-07-31 21:02:47 +02:00

189 lines
5.7 KiB
TypeScript

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<Transaction>,
@InjectRepository(Player)
private playersRepository: Repository<Player>,
@InjectRepository(TransactionType)
private transactionTypesRepository: Repository<TransactionType>,
@InjectRepository(User)
private usersRepository: Repository<User>,
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;
}
}