189 lines
5.7 KiB
TypeScript
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;
|
|
}
|
|
}
|