import { BadRequestException } from '@nestjs/common'; import { TeamRolesEnum } from '../team-roles/team-roles.enum'; import { RecurringTransactionIntervalEnum } from '../recurring-transactions/recurring-transaction-interval.enum'; import { CashboxExportSubscriptionService } from './cashbox-export-subscription.service'; describe('CashboxExportSubscriptionService', () => { const repository = { findOne: jest.fn(), create: jest.fn((v) => v), save: jest.fn(async (v) => v) }; const access = { assertAtLeast: jest.fn() }; let service: CashboxExportSubscriptionService; beforeEach(() => { jest.clearAllMocks(); service = new CashboxExportSubscriptionService(repository as any, access as any); }); it('returns a paused default when no subscription exists yet', async () => { repository.findOne.mockResolvedValue(null); await expect(service.getSubscription(5, 42)).resolves.toEqual({ recipients: [], interval: RecurringTransactionIntervalEnum.monthly, active: false, nextRunDate: null, }); expect(access.assertAtLeast).toHaveBeenCalledWith( 42, 5, 'transaction_create_min_role', TeamRolesEnum.scnd_treasurer, ); }); it('returns the existing subscription', async () => { repository.findOne.mockResolvedValue({ recipients: ['vorstand@example.com'], interval: RecurringTransactionIntervalEnum.yearly, active: true, nextRunDate: '2027-01-01T00:00:00.000Z', }); await expect(service.getSubscription(5, 42)).resolves.toEqual({ recipients: ['vorstand@example.com'], interval: RecurringTransactionIntervalEnum.yearly, active: true, nextRunDate: '2027-01-01T00:00:00.000Z', }); }); it('rejects activating with an empty recipient list', async () => { repository.findOne.mockResolvedValue(null); await expect( service.upsertSubscription(5, 42, { recipients: [], interval: RecurringTransactionIntervalEnum.monthly, active: true, }), ).rejects.toBeInstanceOf(BadRequestException); expect(repository.save).not.toHaveBeenCalled(); }); it('creates a new subscription and computes the next period boundary on first activation', async () => { repository.findOne.mockResolvedValue(null); jest.useFakeTimers().setSystemTime(new Date('2026-08-15T10:00:00.000Z')); const result = await service.upsertSubscription(5, 42, { recipients: ['vorstand@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, }); expect(result.nextRunDate).toBe('2026-09-01T00:00:00.000Z'); expect(repository.save).toHaveBeenCalledWith( expect.objectContaining({ recipients: ['vorstand@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, nextRunDate: '2026-09-01T00:00:00.000Z', }), ); jest.useRealTimers(); }); it('keeps the existing nextRunDate when editing recipients without changing interval or activation state', async () => { repository.findOne.mockResolvedValue({ recipients: ['old@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, nextRunDate: '2026-09-01T00:00:00.000Z', }); const result = await service.upsertSubscription(5, 42, { recipients: ['new@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, }); expect(result.nextRunDate).toBe('2026-09-01T00:00:00.000Z'); }); it('recomputes nextRunDate when the interval changes', async () => { repository.findOne.mockResolvedValue({ recipients: ['a@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, nextRunDate: '2026-09-01T00:00:00.000Z', }); jest.useFakeTimers().setSystemTime(new Date('2026-08-15T10:00:00.000Z')); const result = await service.upsertSubscription(5, 42, { recipients: ['a@example.com'], interval: RecurringTransactionIntervalEnum.yearly, active: true, }); expect(result.nextRunDate).toBe('2027-08-01T00:00:00.000Z'); jest.useRealTimers(); }); });