Files
ApiCore/ApiCore-Backend/src/season/varroa-due.ts
T
Nick SzaunigandClaude Opus 5.5 d99a697739 feat: T4 waves 1-2 - rule configs, schema, suggestions core, season, rule forms
- 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]>
2026-10-14 13:42:32 +02:00

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}`,
};
}