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>
This commit is contained in:
Bastian Wagner
2026-08-04 07:56:39 +02:00
parent 5779483b21
commit eed8266da2
3 changed files with 38 additions and 3 deletions

View File

@@ -5,7 +5,7 @@ describe('CashboxExportScheduler', () => {
const subscriptionRepository = { find: jest.fn(), save: jest.fn((v) => v) };
const teamRepository = { findOne: jest.fn() };
const mailService = { cashboxExport: jest.fn() };
const logger = { info: jest.fn() };
const logger = { info: jest.fn(), error: jest.fn() };
let scheduler: CashboxExportScheduler;
beforeEach(() => {
@@ -118,4 +118,29 @@ describe('CashboxExportScheduler', () => {
expect(mailService.cashboxExport).toHaveBeenCalledTimes(2);
});
it('continues processing when one subscription fails', async () => {
subscriptionRepository.find.mockResolvedValue([
{ id: 1, team: { id: 5 }, recipients: ['a@example.com'], interval: RecurringTransactionIntervalEnum.monthly, nextRunDate: '2026-09-01T00:00:00.000Z', active: true },
{ id: 2, team: { id: 6 }, recipients: ['b@example.com'], interval: RecurringTransactionIntervalEnum.monthly, nextRunDate: '2026-09-01T00:00:00.000Z', active: true },
]);
teamRepository.findOne.mockResolvedValue({
id: 5,
name: 'Team A',
alias: 'team-a',
transactions: [],
players: [],
});
mailService.cashboxExport.mockRejectedValueOnce(new Error('smtp down'));
await scheduler.runDueSubscriptions();
expect(mailService.cashboxExport).toHaveBeenCalledTimes(2);
expect(subscriptionRepository.save).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith({
event: 'cashbox_export_subscription_run_fail',
details: expect.stringContaining('subscriptionId=1 teamId=5'),
userId: -1,
});
});
});

View File

@@ -35,7 +35,16 @@ export class CashboxExportScheduler {
});
for (const subscription of due) {
await this.runOne(subscription);
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,
});
}
}
}

View File

@@ -31,6 +31,7 @@ export type LOGEVENT =
| 'recurring_transaction_update'
| 'recurring_transaction_delete'
| 'recurring_transaction_run'
| 'cashbox_export_subscription_run';
| 'cashbox_export_subscription_run'
| 'cashbox_export_subscription_run_fail';
export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';