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>
97 lines
3.8 KiB
TypeScript
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();
|
|
}
|
|
}
|