Files
teamwallet/myteamwallet_backend/src/cashbox-export/cashbox-export.scheduler.ts
Bastian Wagner eed8266da2 fix: add error handling for CashboxExportScheduler subscription processing
- Wrap runOne(subscription) in try/catch to ensure one subscription failure doesn't block remaining subscriptions
- Log failed subscriptions with new 'cashbox_export_subscription_run_fail' event
- Add new LOGEVENT type for subscription run failures
- Add test to verify second subscription processes even when first fails (continues processing independently)
- All 7 tests passing: 6 original + 1 new failure handling test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 07:56:39 +02:00

96 lines
3.7 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, 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 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();
}
}