import { describe, expect, it } from 'vitest'; import type { ChickenCounting, Cycle, KPI, ManualInput } from '../../types/api.ts'; import { getCountingAccuracyWarning } from '../countingAccuracyWarning.ts'; const cycle = { id: 1, kandang: 1, kandang_name: 'K1', total_days: 35, current_day: 5, visible_through_date: '2026-08-15', start_date: '2026-08-10', end_date: '2026-09-13', chick_in_weight: 45, doc_in_count: 1000, feed_initial_balance: 0, status: 'gm', created_at: '', updated_at: '', } satisfies Cycle; function kpi(partial: Partial & Pick): KPI { return { id: 1, cycle: 1, kandang: 1, flock_id: 1, age: 0, mortality: 0, mortality_total: 0, feed: 0, feed_total: 0, harvest: 0, harvest_total: 0, harvest_weight: 0, harvest_weight_total: 0, chicken_life_percentage: 100, iot_weight: 0, average_harvest_day: 0, average_harvest_weight: 0, fcr: 0, eef: 0, cc: 0, cw: 0, fs: 0, manual: 0, created_at: '', updated_at: '', ...partial, }; } function counting(partial: Partial & Pick): ChickenCounting { return { id: 1, cycle: 1, kandang: 1, flock_id: 1, mortality_count: 0, created_at: '', updated_at: '', ...partial, }; } describe('getCountingAccuracyWarning', () => { it('returns null when there is no IoT counting', () => { expect( getCountingAccuracyWarning({ cycle, countings: [], kpis: [kpi({ date: '2026-08-10', chicken_life: 1000 })], manualInputs: [], }) ).toBeNull(); }); it('returns under when latest IoT is below stock and accuracy <= 95', () => { const result = getCountingAccuracyWarning({ cycle, countings: [counting({ date: '2026-08-10', total_count: 930 })], kpis: [kpi({ date: '2026-08-10', chicken_life: 1000 })], manualInputs: [], }); expect(result).toMatchObject({ day: 0, date: '2026-08-10', iotCount: 930, stockAkhir: 1000, direction: 'under', }); expect(result!.accuracy).toBe(93); expect(result!.ratioPercent).toBe(93); }); it('returns over when latest IoT is above stock and accuracy <= 95', () => { // 1060/1000 → ratio 106%, accuracy min/max = 1000/1060 ≈ 94.3% const result = getCountingAccuracyWarning({ cycle, countings: [counting({ date: '2026-08-10', total_count: 1060 })], kpis: [kpi({ date: '2026-08-10', chicken_life: 1000 })], manualInputs: [], }); expect(result).toMatchObject({ day: 0, iotCount: 1060, stockAkhir: 1000, direction: 'over', }); expect(result!.accuracy).toBe(94.3); expect(result!.ratioPercent).toBe(106); }); it('warns when accuracy is exactly 95', () => { const result = getCountingAccuracyWarning({ cycle, countings: [counting({ date: '2026-08-10', total_count: 950 })], kpis: [kpi({ date: '2026-08-10', chicken_life: 1000 })], manualInputs: [], }); expect(result).toMatchObject({ accuracy: 95, direction: 'under' }); }); it('returns null when accuracy is above 95', () => { expect( getCountingAccuracyWarning({ cycle, countings: [counting({ date: '2026-08-10', total_count: 960 })], kpis: [kpi({ date: '2026-08-10', chicken_life: 1000 })], manualInputs: [], }) ).toBeNull(); }); it('uses the latest IoT day even if an older day is worse', () => { const result = getCountingAccuracyWarning({ cycle, countings: [ counting({ id: 1, date: '2026-08-10', total_count: 800 }), // worse under counting({ id: 2, date: '2026-08-12', total_count: 970 }), // latest, accuracy 97% → no warn ], kpis: [ kpi({ date: '2026-08-10', chicken_life: 1000 }), kpi({ date: '2026-08-11', chicken_life: 1000 }), kpi({ date: '2026-08-12', chicken_life: 1000 }), ], manualInputs: [], }); expect(result).toBeNull(); }); it('warns from the latest IoT day when that day is low', () => { const result = getCountingAccuracyWarning({ cycle, countings: [ counting({ id: 1, date: '2026-08-10', total_count: 990 }), // fine counting({ id: 2, date: '2026-08-12', total_count: 900 }), // latest under ], kpis: [ kpi({ date: '2026-08-10', chicken_life: 1000 }), kpi({ date: '2026-08-11', chicken_life: 1000 }), kpi({ date: '2026-08-12', chicken_life: 1000 }), ], manualInputs: [], }); expect(result).toMatchObject({ day: 2, date: '2026-08-12', iotCount: 900, direction: 'under', }); }); it('derives stock akhir from chain when chicken_life is missing', () => { const manuals: ManualInput[] = [ { id: 1, cycle: 1, kandang: 1, flock_id: 1, date: '2026-08-10', age_manual: 0, mortality_manual: 50, mortality_manual_total: 50, feed_in_manual: 0, feed_use_manual: 0, feed_in_manual_total: 0, feed_use_manual_total: 0, feed_out_manual: 0, feed_out_manual_total: 0, harvest_manual: 0, harvest_manual_total: 0, harvest_weight_manual: 0, harvest_weight_manual_total: 0, manual_weight: 0, average_harvest_day_manual: 0, average_harvest_weight_manual: 0, fcr_manual: 0, eef_manual: 0, created_at: '', updated_at: '', }, ]; const result = getCountingAccuracyWarning({ cycle, countings: [counting({ date: '2026-08-10', total_count: 900 })], kpis: [], manualInputs: manuals, }); // stock = 1000 - 50 = 950; IoT 900 → under, accuracy 900/950 ≈ 94.7 expect(result).toMatchObject({ stockAkhir: 950, iotCount: 900, direction: 'under', }); }); });