Files
teamwallet/myteamwallet_backend/src/cashbox-export/cashbox-export.scheduler.ts
Bastian Wagner 6fceee5a07 feat: wire receivables into manual and recurring cashbox PDF export
Both the manual download endpoint and the recurring email subscription
now pass buildReceivableRows() output into buildPdf, so every PDF
report includes the Forderungen section regardless of how it was
generated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 12:26:23 +02:00

97 lines
3.8 KiB
TypeScript

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, buildReceivableRows, buildRows } from './cashbox-export.utils';
import { CashboxExportSubscription } from './entities/cashbox-export-subscription.entity';
const INTERVAL_MONTHS: Record<RecurringTransactionIntervalEnum, number> = {
[RecurringTransactionIntervalEnum.monthly]: 1,
[RecurringTransactionIntervalEnum.quarterly]: 3,
[RecurringTransactionIntervalEnum.yearly]: 12,
};
@Injectable()
export class CashboxExportScheduler {
constructor(
@InjectRepository(CashboxExportSubscription)
private readonly subscriptionRepository: Repository<CashboxExportSubscription>,
@InjectRepository(Team)
private readonly teamRepository: Repository<Team>,
private readonly mailService: MailService,
private readonly logger: LoggingService,
) {}
@Cron(CronExpression.EVERY_DAY_AT_4AM)
async runDueSubscriptions(): Promise<void> {
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<void> {
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 receivableRows = buildReceivableRows(team, from, to);
const pdf = await buildPdf(team, rows, receivableRows, 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();
}
}