import { describe, it, expect } from 'vitest'; import type { FeedSackColumnConfig } from '../../types/counting.ts'; import { buildReportSnapshot } from '../buildReportSnapshot.ts'; const START = '2026-05-21T17:00:00.000Z'; // 22 Mei 2026 00:00 WIB const COLUMNS: FeedSackColumnConfig[] = [ { id: 'in-1', label: 'Karung Masuk K1', cameraName: 'kandang_1_karung_masuk', color: '#22c55e', type: 'inbound', kandangId: 1, }, { id: 'use-1', label: 'Dituang K1', cameraName: 'kandang_1_karung_pakan', color: '#eab308', type: 'usage', kandangId: 1, }, { id: 'use-2', label: 'Dituang K2', cameraName: 'kandang_2_karung_pakan', color: '#a855f7', type: 'usage', kandangId: 2, }, { id: 'out-1', label: 'Keluar', cameraName: 'kandang_1_karung_keluar', color: '#ef4444', type: 'outbound', kandangId: 1, }, ]; const baseInput = { cycleId: 'CYCLE-TEST', cycleStartDate: START, kandangId: 1, kandangName: 'Kandang 1', area: 'Sukawarna', upToDay: 2, initialPopulation: 100, generatedAt: '2026-05-24T10:00:00.000Z', mortalityRecords: [], chickenCountByDay: {} as Record, feedSackHistory: [] as Array<{ cameraName: string; count: number; timestamp: Date | string }>, manualFeedEntries: [] as Array<{ cameraName: string; count: number; date: string }>, feedColumnConfig: COLUMNS, cageUuid: 'cage-1', weightByDay: [] as Array<{ day: number; avgManual?: number | null; avgIot?: number | null; uniformity?: number | null; }>, }; describe('buildReportSnapshot', () => { it('decreases counting after-count by mortality+harvest only for days 0–2', () => { const snapshot = buildReportSnapshot({ ...baseInput, mortalityRecords: [ { day: 1, mortalityCount: 3, afkir: 2, panen: 1, beratPanen: 4.5 }, { day: 2, mortalityCount: 1, afkir: 0, panen: 0 }, ], }); expect(snapshot.counting).toHaveLength(3); expect(snapshot.counting[0]).toMatchObject({ day: 0, countBefore: 100, mortality: 0, harvest: 0, countAfter: 100, }); expect(snapshot.counting[1]).toMatchObject({ day: 1, countBefore: 100, mortality: 5, harvest: 1, harvestWeight: 4.5, countAfter: 94, }); expect(snapshot.counting[2]).toMatchObject({ day: 2, countBefore: 94, mortality: 1, harvest: 0, countAfter: 93, }); }); it('combines left and right usage cameras for IoT and manual', () => { const snapshot = buildReportSnapshot({ ...baseInput, kandangName: 'Kandang Atas', upToDay: 0, feedColumnConfig: [ { id: 'in-1', label: 'Karung Masuk', cameraName: 'kandang_1_karung_masuk', color: '#22c55e', type: 'inbound', }, { id: 'use-atas', label: 'Karung Dituang Kandang Atas', cameraName: 'karung_tuang_feeder_kiri_atas', color: '#f97316', type: 'usage', }, { id: 'use-bawah', label: 'Karung Dituang Kandang Bawah', cameraName: 'karung_tuang_feeder_kanan_bawah', color: '#a855f7', type: 'usage', }, ], feedSackHistory: [ { cameraName: 'karung_tuang_feeder_kiri_atas', count: 15, timestamp: new Date(2026, 4, 22, 8, 0, 0), }, { cameraName: 'karung_tuang_feeder_kanan_bawah', count: 15, timestamp: new Date(2026, 4, 22, 8, 0, 0), }, ], manualFeedEntries: [ { cameraName: 'karung_tuang_feeder_kiri_atas', count: 15, date: '2026-05-22' }, { cameraName: 'karung_tuang_feeder_kanan_bawah', count: 15, date: '2026-05-22' }, ], }); expect(snapshot.feedUse).toHaveLength(1); expect(snapshot.feedUse[0]?.iotUse).toBe(30); expect(snapshot.feedUse[0]?.manualUse).toBe(30); expect(snapshot.feedUse[0]?.accuracy).toBe(100); }); it('keeps feed-in empty when inbound mapping fails, but still combines usage cameras', () => { const snapshot = buildReportSnapshot({ ...baseInput, kandangId: 99, upToDay: 0, feedSackHistory: [ { cameraName: 'kandang_1_karung_pakan', count: 10, timestamp: new Date(2026, 4, 22, 8, 0, 0), }, { cameraName: 'kandang_2_karung_pakan', count: 50, timestamp: new Date(2026, 4, 22, 8, 0, 0), }, ], }); expect(snapshot.feedUse[0]?.iotUse).toBe(60); expect(snapshot.feedUse[0]?.manualUse).toBe(0); expect(snapshot.feedIn[0]?.iotIn).toBeNull(); expect(snapshot.feedIn[0]?.manualIn).toBeNull(); }); it('returns empty weight rows without cageUuid but still builds a snapshot', () => { const snapshot = buildReportSnapshot({ ...baseInput, cageUuid: null, weightByDay: [{ day: 1, avgManual: 1000, avgIot: 900, uniformity: 80 }], }); expect(snapshot.cycleId).toBe('CYCLE-TEST'); expect(snapshot.weight.every((row) => row.avgManual == null && row.avgIot == null)).toBe( true ); }); it('drops rows after the selected day', () => { const snapshot = buildReportSnapshot({ ...baseInput, upToDay: 8, mortalityRecords: [ { day: 8, mortalityCount: 1 }, { day: 9, mortalityCount: 99 }, ], weightByDay: [ { day: 9, avgManual: 2000, avgIot: 1900 }, // 1-based day 9 → report day 8 { day: 10, avgManual: 3000, avgIot: 2900 }, // report day 9 — dropped ], }); expect(snapshot.counting.every((row) => row.day <= 8)).toBe(true); expect(snapshot.counting.some((row) => row.day === 9)).toBe(false); expect(snapshot.weight.some((row) => row.day === 8 && row.avgManual === 2000)).toBe(true); expect(snapshot.weight.some((row) => row.avgManual === 3000)).toBe(false); }); it('maps 1-based feed day 1 on cycle start to Hari ke-0', () => { const snapshot = buildReportSnapshot({ ...baseInput, upToDay: 0, feedSackHistory: [ { cameraName: 'kandang_1_karung_masuk', count: 7, timestamp: new Date(2026, 4, 22, 6, 0, 0), }, ], manualFeedEntries: [{ cameraName: 'kandang_1_karung_masuk', count: 7, date: '2026-05-22' }], }); expect(snapshot.feedIn).toHaveLength(1); expect(snapshot.feedIn[0] && 'day' in snapshot.feedIn[0]).toBe(false); expect(snapshot.feedIn[0]?.date).toBe('2026-05-22'); expect(snapshot.feedIn[0]?.iotIn).toBe(7); expect(snapshot.feedIn[0]?.manualIn).toBe(7); }); });