import { describe, expect, it } from 'vitest' import { inventoryView, turnoverRoute, validateTurnover, yesterdayBeijing } from './turnoverNavigation' import type { TurnoverQuery } from './turnoverTypes' const valid: TurnoverQuery = { sourceAccount: 'primary', warehouseCode: 'W', groupBy: 'SKU', endDate: '2026-10-12', baseWindowDays: 30, focus: 'ALL', sortBy: 'PHYSICAL_DESC', current: 1, size: 20, includeOverview: true } describe('inventory view and turnover query contract', () => { it('normalizes repeated owner keys and validates the bounded selection', () => { expect(turnoverRoute({ customerCodes: ['B', 'A', 'A'] }).customerCodes).toEqual(['A', 'B']) expect(turnoverRoute({ customerCodes: 'A' }).customerCodes).toEqual(['A']) expect(turnoverRoute({}).customerCodes).toEqual([]) for (const value of ['', [null], Array(101).fill('A')]) expect(() => turnoverRoute({ customerCodes: value })).toThrow() expect(() => turnoverRoute({ customerCode: 'A' })).toThrow() expect(() => inventoryView({ view: 'DETAIL', customerCodes: ['A'] })).toThrow() }) it('parses only explicit booleans for the zero-inventory filter', () => { expect(turnoverRoute({ hideZeroInventory: 'false' })).toMatchObject({ hideZeroInventory: false }) expect(turnoverRoute({ hideZeroInventory: 'true' })).toMatchObject({ hideZeroInventory: true }) for (const value of ['0', '1', 'yes', ['false']]) expect(() => turnoverRoute({ hideZeroInventory: value })).toThrow() expect(() => inventoryView({ view: 'DETAIL', hideZeroInventory: 'true' })).toThrow() }) it('defaults to turnover but preserves strict dashboard and legacy detail links', () => { expect(inventoryView({ warehouseCode: 'W', sku: 'X' })).toBe('TURNOVER') for (const q of [{ from: 'overview' }, { drilldownFrom: 'DASHBOARD' }, { salesWindowDays: '10' }, { attention: 'FROZEN_STOCK' }, { view: 'DETAIL' }]) expect(inventoryView(q)).toBe('DETAIL') }) it('rejects conflicting views and unknown or duplicate parameters', () => { for (const q of [{ view: 'TURNOVER', from: 'overview' }, { view: 'TURNOVER', attention: 'ALL' }, { view: 'DETAIL', groupBy: 'SKU' }, { view: ['TURNOVER'] }, { view: 'OTHER' }]) expect(() => inventoryView(q)).toThrow() expect(() => turnoverRoute({ salesRunId: 'invented' })).toThrow() expect(() => turnoverRoute({ groupBy: ['OWNER', 'SKU'] })).toThrow() }) it('validates independent focus, threshold, group, date and pagination boundaries', () => { expect(() => validateTurnover(valid, '2026-10-12')).not.toThrow() for (const patch of [{ endDate: '2026-02-30' }, { endDate: '2026-10-13' }, { groupBy: 'OWNER', sku: 'X' }, { groupBy: 'OWNER', sortBy: 'COVERAGE_DESC' }, { groupBy: 'OWNER', sortBy: 'UNIT_VOLUME_DESC' }, { focus: 'COVERAGE_GT' }, { coverageDaysThreshold: 1.5 }, { size: 101 }, { current: 0 }, { baseWindowDays: 367 }]) expect(() => validateTurnover({ ...valid, ...patch } as TurnoverQuery, '2026-10-12')).toThrow() expect(() => validateTurnover({ ...valid, focus: 'COVERAGE_LE', coverageDaysThreshold: 45 }, '2026-10-12')).not.toThrow() }) it('uses Beijing yesterday across UTC day boundaries', () => { expect(yesterdayBeijing(new Date('2026-10-12T16:01:00Z'))).toBe('2026-10-12') }) })