feat: add CashboxExportScheduler for recurring PDF mailing
- Implement CashboxExportScheduler with @Cron(EVERY_DAY_AT_4AM) - Query due subscriptions (active=true, nextRunDate <= today) - For each subscription: fetch team, build PDF, send email, advance nextRunDate - Support monthly/quarterly/yearly intervals via INTERVAL_MONTHS map - Add cashbox_export_subscription_run to LOGEVENT type for logging - All 6 tests passing: empty state, monthly/quarterly/yearly periods, multiple subscriptions Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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, 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) {
|
||||
await this.runOne(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user