- Rule configs with typed parsers and registry; Berlin day helpers - Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as ColonyCondition with legacy text mapping), split queen source, varroa sample size; migration harness for data migrations - SuggestionsService: propose/refresh/resolve/obsolete/expire, accept/dismiss/reopen incl. bulk, lock order, concurrency e2e - SeasonService: phenology-based segments, inspection/varroa due dates, queen age; TimeEventsStep with JobPartialFailure - Frontend: per-rule settings forms with visible value sources Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
import { addDays } from '../common/dates';
|
|
import type { SeasonAutopilotConfig } from '../automation/rule-config/rule-config.types';
|
|
import type { SeasonState } from './season-types';
|
|
|
|
export interface VarroaDue {
|
|
dueOn: string;
|
|
kind: 'INTERVAL' | 'WINTER_WINDOW';
|
|
/** Changes with every new measurement (or winter), so one due date stays one reminder. */
|
|
anchor: string;
|
|
}
|
|
|
|
/*
|
|
* Invariant: the winter window (SEASON_AUTOPILOT.winterVarroaWindow, default
|
|
* 11-15) lies inside WINTERRUHE, i.e. it does not begin before the WINTER
|
|
* date of PHENOLOGY.calendarFallback (default 11-15). Before WINTER the
|
|
* interval rule applies instead.
|
|
*/
|
|
|
|
/** A measurement from this day on counts for the winter window of the year. */
|
|
const WINTER_MEASUREMENT_FROM = '11-01';
|
|
|
|
/**
|
|
* The day a varroa measurement is due. Outside WINTERRUHE: the last
|
|
* measurement (else the creation of the colony) plus the interval. In
|
|
* WINTERRUHE only inside the winter window, once, if there was no
|
|
* measurement since 11-01.
|
|
*/
|
|
export function varroaMeasurementDue(
|
|
input: {
|
|
createdOn: string;
|
|
lastMeasuredOn: string | null;
|
|
lastMeasurementId: string | null;
|
|
},
|
|
season: Pick<SeasonState, 'segment' | 'springSince'>,
|
|
config: SeasonAutopilotConfig,
|
|
today: string,
|
|
): VarroaDue | null {
|
|
if (season.segment !== 'WINTERRUHE') {
|
|
const base = input.lastMeasuredOn ?? input.createdOn;
|
|
const intervalDue = addDays(base, config.varroaMeasureIntervalDays);
|
|
// A due day that fell into the winter is taken up at the start of
|
|
// FRUEHJAHR, like the first inspection of the year.
|
|
const dueOn =
|
|
season.springSince !== null && intervalDue < season.springSince
|
|
? season.springSince
|
|
: intervalDue;
|
|
return {
|
|
dueOn,
|
|
kind: 'INTERVAL',
|
|
anchor: input.lastMeasurementId ?? 'none',
|
|
};
|
|
}
|
|
const year = today.slice(0, 4);
|
|
const { from, to } = config.winterVarroaWindow;
|
|
const inWindow = today >= `${year}-${from}` && today <= `${year}-${to}`;
|
|
const measuredThisWinter =
|
|
input.lastMeasuredOn !== null &&
|
|
input.lastMeasuredOn >= `${year}-${WINTER_MEASUREMENT_FROM}`;
|
|
if (!inWindow || measuredThisWinter) {
|
|
return null;
|
|
}
|
|
return {
|
|
dueOn: `${year}-${from}`,
|
|
kind: 'WINTER_WINDOW',
|
|
anchor: `winter-${year}`,
|
|
};
|
|
}
|