import { TransactionTypeEnum } from '../transactions/transaction-type.enum'; import { RecurringTransactionIntervalEnum } from './recurring-transaction-interval.enum'; import { RecurringTransactionsScheduler } from './recurring-transactions.scheduler'; describe('RecurringTransactionsScheduler', () => { const dueRepository = { find: jest.fn() }; const playerRepository = { find: jest.fn() }; const definitionWriteRepository = { save: jest.fn((value) => value) }; const transactionWriteRepository = { create: jest.fn((value) => value), save: jest.fn(async (value) => ({ id: 1, ...value })), }; const manager = { getRepository: jest.fn((entity) => { if (entity.name === 'Player') return playerRepository; if (entity.name === 'Transaction') return transactionWriteRepository; return definitionWriteRepository; }), }; const dataSource = { transaction: jest.fn((work) => work(manager)) }; const logger = { info: jest.fn() }; let scheduler: RecurringTransactionsScheduler; const player = (id: number, active: boolean) => ({ id, active, team: { id: 5 } }); beforeEach(() => { jest.clearAllMocks(); }); function buildScheduler() { return new RecurringTransactionsScheduler( dueRepository as any, dataSource as any, logger as any, ); } it('does nothing when no recurring transaction is due', async () => { dueRepository.find.mockResolvedValue([]); scheduler = buildScheduler(); await scheduler.runDueRecurringTransactions(); expect(dataSource.transaction).not.toHaveBeenCalled(); expect(logger.info).not.toHaveBeenCalled(); }); it('books a transaction for every active player and skips inactive ones', async () => { dueRepository.find.mockResolvedValue([ { id: 3, team: { id: 5 }, description: 'Monatsbeitrag', amount: 10, type: { id: TransactionTypeEnum.fee }, interval: RecurringTransactionIntervalEnum.monthly, nextRunDate: '2026-08-01T00:00:00.000Z', active: true, }, ]); // The DB query filters by active:true itself (asserted below) — an // inactive player would never be returned, so the mock reflects that. playerRepository.find.mockResolvedValue([player(1, true), player(3, true)]); scheduler = buildScheduler(); await scheduler.runDueRecurringTransactions(); expect(playerRepository.find).toHaveBeenCalledWith({ where: { team: { id: 5 }, active: true }, }); expect(transactionWriteRepository.save).toHaveBeenCalledTimes(2); expect(transactionWriteRepository.create).toHaveBeenCalledWith( expect.objectContaining({ amount: 10, note: 'Monatsbeitrag', player: player(1, true), type: { id: TransactionTypeEnum.fee }, }), ); expect(logger.info).toHaveBeenCalledWith( { event: 'recurring_transaction_run', details: 'recurringTransactionId=3 teamId=5 gebuchteSpieler=2', userId: -1, }, manager, ); }); it.each([ [RecurringTransactionIntervalEnum.monthly, '2026-08-01T00:00:00.000Z', '2026-09-01T00:00:00.000Z'], [RecurringTransactionIntervalEnum.quarterly, '2026-08-01T00:00:00.000Z', '2026-11-01T00:00:00.000Z'], [RecurringTransactionIntervalEnum.yearly, '2026-08-01T00:00:00.000Z', '2027-08-01T00:00:00.000Z'], ])( 'advances nextRunDate by %s from %s to %s', async (interval, nextRunDate, expected) => { dueRepository.find.mockResolvedValue([ { id: 3, team: { id: 5 }, description: 'Monatsbeitrag', amount: 10, type: { id: TransactionTypeEnum.fee }, interval, nextRunDate, active: true, }, ]); playerRepository.find.mockResolvedValue([player(1, true)]); scheduler = buildScheduler(); await scheduler.runDueRecurringTransactions(); expect(definitionWriteRepository.save).toHaveBeenCalledWith( expect.objectContaining({ nextRunDate: expected }), ); }, ); it('processes each due definition inside its own transaction', async () => { dueRepository.find.mockResolvedValue([ { id: 3, team: { id: 5 }, description: 'A', amount: 10, type: { id: TransactionTypeEnum.fee }, interval: RecurringTransactionIntervalEnum.monthly, nextRunDate: '2026-08-01T00:00:00.000Z', active: true, }, { id: 4, team: { id: 6 }, description: 'B', amount: 20, type: { id: TransactionTypeEnum.levy }, interval: RecurringTransactionIntervalEnum.yearly, nextRunDate: '2026-08-01T00:00:00.000Z', active: true, }, ]); playerRepository.find.mockResolvedValue([player(1, true)]); scheduler = buildScheduler(); await scheduler.runDueRecurringTransactions(); expect(dataSource.transaction).toHaveBeenCalledTimes(2); }); });