231 lines
6.6 KiB
TypeScript
231 lines
6.6 KiB
TypeScript
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<number, number>,
|
||
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);
|
||
});
|
||
});
|