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 { Team } from 'src/teams/entities/team.entity'; import { MailService } from 'src/mail/mail.service'; import { LessThanOrEqual, Repository } from 'typeorm'; import { RecurringTransactionIntervalEnum } from 'src/recurring-transactions/recurring-transaction-interval.enum'; import { buildPdf, buildRows } from './cashbox-export.utils'; import { CashboxExportSubscription } from './entities/cashbox-export-subscription.entity'; const INTERVAL_MONTHS: Record = { [RecurringTransactionIntervalEnum.monthly]: 1, [RecurringTransactionIntervalEnum.quarterly]: 3, [RecurringTransactionIntervalEnum.yearly]: 12, }; @Injectable() export class CashboxExportScheduler { constructor( @InjectRepository(CashboxExportSubscription) private readonly subscriptionRepository: Repository, @InjectRepository(Team) private readonly teamRepository: Repository, private readonly mailService: MailService, private readonly logger: LoggingService, ) {} @Cron(CronExpression.EVERY_DAY_AT_4AM) async runDueSubscriptions(): Promise { const today = new Date().toISOString(); const due = await this.subscriptionRepository.find({ where: { active: true, nextRunDate: LessThanOrEqual(today) }, relations: ['team'], }); for (const subscription of due) { try { await this.runOne(subscription); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await this.logger.error({ event: 'cashbox_export_subscription_run_fail', details: `recurring subscription failed: subscriptionId=${subscription.id} teamId=${subscription.team.id}: ${errorMessage}`, userId: -1, }); } } } private async runOne(subscription: CashboxExportSubscription): Promise { const team = await this.teamRepository.findOne({ where: { id: subscription.team.id }, relations: ['players', 'players.transactions', 'transactions'], }); if (!team) return; const { from, to } = this.periodBounds(subscription.nextRunDate, subscription.interval); const rows = buildRows(team, from, to); const pdf = await buildPdf(team, rows, from, to); const filename = `kassenbuch_${team.alias}_${from}_${to}.pdf`; await this.mailService.cashboxExport( { to: subscription.recipients.join(', '), data: { teamName: team.name, from, to } }, pdf, filename, ); subscription.nextRunDate = this.advance(subscription.nextRunDate, subscription.interval); await this.subscriptionRepository.save(subscription); await this.logger.info({ event: 'cashbox_export_subscription_run', details: `teamId=${team.id} recipients=${subscription.recipients.length} from=${from} to=${to}`, userId: -1, }); } private periodBounds( nextRunDate: string, interval: RecurringTransactionIntervalEnum, ): { from: string; to: string } { const end = new Date(nextRunDate); end.setUTCDate(end.getUTCDate() - 1); const start = new Date(nextRunDate); start.setUTCMonth(start.getUTCMonth() - INTERVAL_MONTHS[interval]); return { from: start.toISOString().slice(0, 10), to: end.toISOString().slice(0, 10) }; } private advance(nextRunDate: string, interval: RecurringTransactionIntervalEnum): string { const date = new Date(nextRunDate); date.setUTCMonth(date.getUTCMonth() + INTERVAL_MONTHS[interval]); return date.toISOString(); } }