Files
teamwallet/myteamwallet_backend/src/recurring-transactions/recurring-transactions.scheduler.spec.ts
Bastian Wagner df634e7601 fix: update stale assertion for unconditional start/finish logging
runDueRecurringTransactions() always logs a start/finish marker for
observability, even when nothing is due, but the "does nothing" test
still asserted logger.info was never called. Pre-existing baseline
failure, unrelated to the notification-center work about to start.
2026-08-04 18:10:40 +02:00

157 lines
5.1 KiB
TypeScript

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).toHaveBeenCalledWith(
expect.objectContaining({ event: 'scheduled_recurring_transaction_check_start' }),
);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({ event: 'scheduled_recurring_transaction_check_finished' }),
);
});
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);
});
});