import { MaintenanceService } from './maintenance.service'; import { MaintenanceStatus } from './entities/maintenance-schedule.entity'; /** * KM-based auto-scheduling: completing a maintenance with an odometer reading * creates the next SCHEDULED item at completedKm + intervalKm, matched on the * schedule's (type, serviceItem) interval. Re-completing must not duplicate. */ function makeService(opts: { before: Record | null; after: Record | null; interval: Record | null; }) { const saved: Array> = []; const service = Object.create(MaintenanceService.prototype) as Record; service.scheduleRepository = { findOneBy: jest .fn() .mockResolvedValueOnce(opts.before) .mockResolvedValueOnce(opts.after), update: jest.fn(), create: jest.fn((v: Record) => v), save: jest.fn(async (v: Record) => { saved.push(v); return v; }), }; service.intervalRepository = { getByVehicleAndType: jest.fn().mockResolvedValue(opts.interval), }; service.dataSource = { getRepository: jest.fn().mockReturnValue({ update: jest.fn() }), }; service.logger = { error: jest.fn() }; return { service: service as unknown as MaintenanceService, saved }; } const base = { id: 's-1', vehicleId: 'v-1', maintenanceType: 'PREVENTIVE', serviceItem: 'oil change', description: 'Oil and filter', }; describe('MaintenanceService auto-next scheduling', () => { it('completing at 50,000 km with a 10,000 km interval schedules the next at 60,000', async () => { const { service, saved } = makeService({ before: { ...base, status: MaintenanceStatus.SCHEDULED }, after: { ...base, status: MaintenanceStatus.COMPLETED, odometerReading: 50000 }, interval: { serviceItem: 'oil change', intervalKm: '10000.00', intervalDays: null, description: 'Oil and filter' }, }); await service.updateMaintenanceSchedule('s-1', { status: MaintenanceStatus.COMPLETED, odometerReading: 50000, }); expect(saved).toHaveLength(1); expect(saved[0]).toMatchObject({ vehicleId: 'v-1', serviceItem: 'oil change', nextDueKm: 60000, status: MaintenanceStatus.SCHEDULED, }); }); it('re-completing an already COMPLETED schedule does not duplicate the next one', async () => { const { service, saved } = makeService({ before: { ...base, status: MaintenanceStatus.COMPLETED, odometerReading: 50000 }, after: { ...base, status: MaintenanceStatus.COMPLETED, odometerReading: 50000 }, interval: { serviceItem: 'oil change', intervalKm: '10000.00', intervalDays: null }, }); await service.updateMaintenanceSchedule('s-1', { status: MaintenanceStatus.COMPLETED, odometerReading: 50000, }); expect(saved).toHaveLength(0); }); it('a km + days interval produces ONE next schedule carrying both thresholds', async () => { const { service, saved } = makeService({ before: { ...base, status: MaintenanceStatus.IN_PROGRESS }, after: { ...base, status: MaintenanceStatus.COMPLETED, odometerReading: 20000 }, interval: { serviceItem: 'oil change', intervalKm: '10000.00', intervalDays: 180 }, }); await service.updateMaintenanceSchedule('s-1', { status: MaintenanceStatus.COMPLETED, odometerReading: 20000, }); expect(saved).toHaveLength(1); expect(saved[0].nextDueKm).toBe(30000); expect(saved[0].nextDueDate).toBeInstanceOf(Date); }); });