import type { ColonyType, VarroaMethod } from '../../generated/prisma'; import { bandsFor, classify, rateMeasurement } from './varroa-bands'; import { DEFAULT_VARROA_THRESHOLD_CONFIG } from './rule-config/varroa-threshold.config'; const DEFAULTS = DEFAULT_VARROA_THRESHOLD_CONFIG; const d = (day: string): Date => new Date(`${day}T00:00:00.000Z`); describe('varroa bands', () => { it.each< [ string, ColonyType, VarroaMethod, number, 'green' | 'yellow' | 'red' | null, ] >([ ['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 10.0, 'yellow'], ['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 10.1, 'red'], ['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 4.9, 'green'], ['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 5, 'yellow'], ['2026-11-20', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 0.6, 'red'], ['2026-11-20', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 0.5, 'green'], ['2026-06-10', 'YOUNG_COLONY', 'NATURAL_MITE_FALL', 5.1, 'red'], ['2026-06-10', 'YOUNG_COLONY', 'NATURAL_MITE_FALL', 2.5, 'yellow'], ['2026-11-20', 'NUCLEUS_COLONY', 'NATURAL_MITE_FALL', 0.3, 'red'], ['2026-11-20', 'NUCLEUS_COLONY', 'NATURAL_MITE_FALL', 0.25, 'green'], ['2026-07-10', 'PRODUCTION_COLONY', 'POWDERED_SUGAR', 5.1, 'red'], ['2026-07-10', 'PRODUCTION_COLONY', 'POWDERED_SUGAR', 1, 'yellow'], ['2026-07-10', 'YOUNG_COLONY', 'POWDERED_SUGAR', 4, 'yellow'], ['2026-09-10', 'PRODUCTION_COLONY', 'WASHING_METHOD', 1.5, 'green'], ['2026-09-10', 'PRODUCTION_COLONY', 'WASHING_METHOD', 2, 'yellow'], ['2026-04-10', 'PRODUCTION_COLONY', 'POWDERED_SUGAR', 9, null], ['2026-11-10', 'PRODUCTION_COLONY', 'WASHING_METHOD', 9, null], ['2026-07-10', 'PRODUCTION_COLONY', 'DRONE_BROOD_CONTROL', 50, null], ])('%s %s %s %p -> %p', (day, type, method, value, light) => expect( classify(value, bandsFor(day, type, DEFAULTS, []).bands[method]), ).toBe(light), ); it('classifies a missing value or band as no light', () => { const band = bandsFor('2026-08-10', null, DEFAULTS, []).bands .NATURAL_MITE_FALL; expect(classify(null, band)).toBeNull(); expect(classify(3, undefined)).toBeNull(); expect(classify(3, null)).toBeNull(); }); it('describes the band with its reason, unit and colony kind', () => { const autumn = bandsFor('2026-08-10', 'YOUNG_COLONY', DEFAULTS, []); expect(autumn.day).toBe('2026-08-10'); expect(autumn.bands.NATURAL_MITE_FALL).toEqual({ method: 'NATURAL_MITE_FALL', unit: 'MITES_PER_DAY', yellowFrom: 2.5, redAbove: 5, reason: 'NATURAL_FALL_SEASON', youngColony: true, }); expect(autumn.bands.POWDERED_SUGAR).toMatchObject({ unit: 'PERCENT', yellowFrom: 2, redAbove: 5, reason: 'INFESTATION_AUTUMN', }); const winter = bandsFor('2026-01-10', 'PRODUCTION_COLONY', DEFAULTS, []); expect(winter.bands.NATURAL_MITE_FALL).toMatchObject({ yellowFrom: null, redAbove: 0.5, reason: 'NATURAL_FALL_WINTER', youngColony: false, }); expect(winter.bands.POWDERED_SUGAR).toBeUndefined(); }); it('treats a colony without a type as a full colony', () => expect( bandsFor('2026-08-10', null, DEFAULTS, []).bands.NATURAL_MITE_FALL, ).toMatchObject({ redAbove: 10, youngColony: false })); it('names the recheck day while a treatment effect is in force', () => { const treatments = [{ startDay: '2026-07-20', endDay: '2026-08-02' }]; const until = (day: string) => bandsFor(day, null, DEFAULTS, treatments).postTreatmentUntil; expect(until('2026-07-25')).toBe('2026-08-16'); expect(until('2026-08-15')).toBe('2026-08-16'); expect(until('2026-08-16')).toBeNull(); expect(until('2026-07-19')).toBeNull(); }); it('uses the latest recheck day of overlapping treatments', () => expect( bandsFor('2026-08-01', null, DEFAULTS, [ { startDay: '2026-07-01', endDay: '2026-07-20' }, { startDay: '2026-07-25', endDay: '2026-08-10' }, ]).postTreatmentUntil, ).toBe('2026-08-24')); describe('month edges', () => { const lightOn = ( day: string, method: VarroaMethod, value: number, ): string | null => classify( value, bandsFor(day, 'PRODUCTION_COLONY', DEFAULTS, []).bands[method], ); it.each<[string, VarroaMethod, number, string | null]>([ ['2026-02-28', 'NATURAL_MITE_FALL', 3, 'red'], ['2026-03-01', 'NATURAL_MITE_FALL', 3, 'green'], ['2026-10-31', 'NATURAL_MITE_FALL', 3, 'green'], ['2026-11-01', 'NATURAL_MITE_FALL', 3, 'red'], ['2026-04-30', 'POWDERED_SUGAR', 3, null], ['2026-05-01', 'POWDERED_SUGAR', 3, 'yellow'], ['2026-07-31', 'POWDERED_SUGAR', 1.5, 'yellow'], ['2026-08-01', 'POWDERED_SUGAR', 1.5, 'green'], ['2026-10-31', 'POWDERED_SUGAR', 3, 'yellow'], ['2026-11-01', 'POWDERED_SUGAR', 3, null], ['2026-04-30', 'WASHING_METHOD', 3, null], ['2026-05-01', 'WASHING_METHOD', 3, 'yellow'], ['2026-07-31', 'WASHING_METHOD', 1.5, 'yellow'], ['2026-08-01', 'WASHING_METHOD', 1.5, 'green'], ['2026-10-31', 'WASHING_METHOD', 3, 'yellow'], ['2026-11-01', 'WASHING_METHOD', 3, null], ])('%s %s %p -> %p', (day, method, value, light) => expect(lightOn(day, method, value)).toBe(light), ); }); it('keeps the young factor off the percent bands', () => { const { bands } = bandsFor('2026-07-10', 'YOUNG_COLONY', DEFAULTS, []); expect(bands.POWDERED_SUGAR).toMatchObject({ youngColony: false, yellowFrom: 1, redAbove: 5, }); expect(bands.NATURAL_MITE_FALL?.youngColony).toBe(true); }); describe('treatment effect edges', () => { const treatments = [{ startDay: '2026-07-20', endDay: '2026-08-02' }]; const ratingOn = (day: string, method: VarroaMethod) => rateMeasurement( { date: d(day), method, calculatedValue: 12 }, null, DEFAULTS, treatments, ); it('starts on the start day and ends the day before the recheck day', () => { expect(ratingOn('2026-07-19', 'NATURAL_MITE_FALL').light).toBe('red'); expect(ratingOn('2026-07-20', 'NATURAL_MITE_FALL').light).toBe( 'POST_TREATMENT', ); expect(ratingOn('2026-08-15', 'NATURAL_MITE_FALL').light).toBe( 'POST_TREATMENT', ); expect(ratingOn('2026-08-16', 'NATURAL_MITE_FALL').light).toBe('red'); }); it('covers a single-day treatment', () => { const single = [{ startDay: '2026-08-01', endDay: '2026-08-01' }]; const until = (day: string) => bandsFor(day, null, DEFAULTS, single).postTreatmentUntil; expect(until('2026-08-01')).toBe('2026-08-15'); expect(until('2026-08-14')).toBe('2026-08-15'); expect(until('2026-08-15')).toBeNull(); }); it('applies to the percent methods and never to the drone brood check', () => { expect(ratingOn('2026-07-25', 'POWDERED_SUGAR').light).toBe( 'POST_TREATMENT', ); expect(ratingOn('2026-08-05', 'WASHING_METHOD').light).toBe( 'POST_TREATMENT', ); expect(ratingOn('2026-07-25', 'DRONE_BROOD_CONTROL').light).toBeNull(); }); }); it('rates by the Berlin calendar day of the measurement', () => { // 2026-10-31T23:30Z is 2026-11-01 00:30 in Berlin: winter band. const rating = rateMeasurement( { date: new Date('2026-11-01T00:30:00+01:00'), method: 'NATURAL_MITE_FALL', calculatedValue: 3, }, null, DEFAULTS, [], ); expect(rating.reasonCode).toBe('NATURAL_FALL_WINTER'); expect(rating.light).toBe('red'); }); describe('rateMeasurement', () => { it('a measurement 5 days after treatment end has no light and names the recheck day', () => expect( rateMeasurement( { date: d('2026-08-07'), method: 'NATURAL_MITE_FALL', calculatedValue: 12, }, 'PRODUCTION_COLONY', DEFAULTS, [{ startDay: '2026-07-20', endDay: '2026-08-02' }], ), ).toEqual({ light: 'POST_TREATMENT', reasonCode: 'POST_TREATMENT', band: null, postTreatmentUntil: '2026-08-16', })); it('rates outside a treatment effect with the band and reason', () => { const rating = rateMeasurement( { date: d('2026-08-20'), method: 'NATURAL_MITE_FALL', calculatedValue: 12, }, 'PRODUCTION_COLONY', DEFAULTS, [{ startDay: '2026-07-20', endDay: '2026-08-02' }], ); expect(rating).toMatchObject({ light: 'red', reasonCode: 'NATURAL_FALL_SEASON', postTreatmentUntil: null, }); expect(rating.band).toMatchObject({ redAbove: 10 }); }); it('has no light for a method without a band or a missing value', () => { expect( rateMeasurement( { date: d('2026-04-10'), method: 'POWDERED_SUGAR', calculatedValue: 9, }, null, DEFAULTS, [], ), ).toEqual({ light: null, reasonCode: null, band: null, postTreatmentUntil: null, }); expect( rateMeasurement( { date: d('2026-08-10'), method: 'NATURAL_MITE_FALL', calculatedValue: null, }, null, DEFAULTS, [], ).light, ).toBeNull(); }); }); });