feat: add recurring transactions (Wiederkehrende Buchungen)
Lets treasurers/captains/coaches define recurring fee/levy dues that are automatically booked for all active players on a monthly, quarterly, or yearly schedule via a daily cron job, instead of having to book them manually every cycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LoggingService } from 'src/database/logging/logging.service';
|
||||
import { Player } from 'src/players/entities/player.entity';
|
||||
import { Transaction } from 'src/transactions/entitites/transaction.entity';
|
||||
import { DataSource, LessThanOrEqual, Repository } from 'typeorm';
|
||||
import { RecurringTransactionIntervalEnum } from './recurring-transaction-interval.enum';
|
||||
import { RecurringTransaction } from './entities/recurring-transaction.entity';
|
||||
|
||||
const INTERVAL_MONTHS: Record<RecurringTransactionIntervalEnum, number> = {
|
||||
[RecurringTransactionIntervalEnum.monthly]: 1,
|
||||
[RecurringTransactionIntervalEnum.quarterly]: 3,
|
||||
[RecurringTransactionIntervalEnum.yearly]: 12,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RecurringTransactionsScheduler {
|
||||
constructor(
|
||||
@InjectRepository(RecurringTransaction)
|
||||
private readonly repository: Repository<RecurringTransaction>,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly logger: LoggingService,
|
||||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_DAY_AT_3AM)
|
||||
async runDueRecurringTransactions(): Promise<void> {
|
||||
const today = new Date().toISOString();
|
||||
const due = await this.repository.find({
|
||||
where: { active: true, nextRunDate: LessThanOrEqual(today) },
|
||||
relations: ['team', 'type'],
|
||||
});
|
||||
|
||||
for (const definition of due) {
|
||||
await this.runOne(definition);
|
||||
}
|
||||
}
|
||||
|
||||
private async runOne(definition: RecurringTransaction): Promise<void> {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const players = await manager
|
||||
.getRepository(Player)
|
||||
.find({ where: { team: { id: definition.team.id }, active: true } });
|
||||
|
||||
const transactionRepository = manager.getRepository(Transaction);
|
||||
for (const player of players) {
|
||||
const transaction = transactionRepository.create({
|
||||
amount: definition.amount,
|
||||
date: new Date().toISOString(),
|
||||
note: definition.description,
|
||||
player,
|
||||
type: definition.type,
|
||||
});
|
||||
await transactionRepository.save(transaction);
|
||||
}
|
||||
|
||||
const definitionRepository = manager.getRepository(RecurringTransaction);
|
||||
definition.nextRunDate = this.advance(
|
||||
definition.nextRunDate,
|
||||
definition.interval,
|
||||
);
|
||||
await definitionRepository.save(definition);
|
||||
|
||||
await this.logger.info(
|
||||
{
|
||||
event: 'recurring_transaction_run',
|
||||
details: `recurringTransactionId=${definition.id} teamId=${definition.team.id} gebuchteSpieler=${players.length}`,
|
||||
userId: -1,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private advance(
|
||||
nextRunDate: string,
|
||||
interval: RecurringTransactionIntervalEnum,
|
||||
): string {
|
||||
const date = new Date(nextRunDate);
|
||||
date.setUTCMonth(date.getUTCMonth() + INTERVAL_MONTHS[interval]);
|
||||
return date.toISOString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user