import type { Prisma } from '../../generated/prisma'; import { addDays, berlinDayOf, dayToDate } from '../common/dates'; import type { SeasonState } from '../season/season-types'; import type { AutopilotInput } from './autopilot.types'; const LATEST_FIRST = [{ date: 'desc' }, { createdAt: 'desc' }] as const; /** Without an inspection a feeding counts for A6 if it is at most this old. */ const FEEDING_LOOKBACK_DAYS = 60; /** * The day SPAETSOMMER began, for the feedings and treatments of A4/A5. In * SPAETSOMMER that is the real start of the segment. The stored state keeps * only the start of the current segment, so in HERBST it is the calendar day * of TRACHTENDE (`calendarTrachtende`, MM-DD), at the latest the start of * HERBST. * * TODO: store the real start as `lateSummerSince` on ApiaryPhenology (needs * a migration) so HERBST no longer depends on the calendar day. */ export function lateSummerStart( season: SeasonState, calendarTrachtende: string, ): string | null { if (season.segment === 'SPAETSOMMER') { return season.segmentSince; } if (season.segment !== 'HERBST') { return null; } const calendarDay = `${season.year}-${calendarTrachtende}`; return calendarDay < season.segmentSince ? calendarDay : season.segmentSince; } /** * What the autopilot rules need of one colony, in a fixed sequence of * queries (no query per rule, none per record). Resolves to null for a * colony that does not exist. The caller holds the colony row lock, so the * picture stays consistent while the rules run. */ export async function loadAutopilotInputIn( tx: Prisma.TransactionClient, colonyId: string, season: SeasonState, calendarTrachtende: string, today: string, ): Promise { const colony = await tx.colony.findUnique({ where: { id: colonyId }, select: { id: true, code: true, status: true, colonyType: true, apiaryId: true, createdAt: true, feedStatus: true, currentQueen: { select: { id: true, hatchYear: true } }, }, }); if (colony === null) { return null; } const inspection = await tx.inspection.findFirst({ where: { colonyId }, orderBy: [...LATEST_FIRST], select: { id: true, date: true, colonyStrength: true, feedStatus: true, queenCellsPresent: true, }, }); const varroa = await tx.varroaMeasurement.findFirst({ where: { colonyId }, orderBy: [...LATEST_FIRST], select: { id: true, date: true }, }); // The latest END day wins, not the latest entry: a backdated or // overlapping treatment must not hide one that ends later. const treatments = await tx.treatment.findMany({ where: { colonyId }, orderBy: [{ date: 'desc' }, { id: 'asc' }], select: { id: true, date: true, startDate: true, endDate: true }, }); const lastTreatment = treatments .map((treatment) => ({ id: treatment.id, startDay: berlinDayOf(treatment.startDate ?? treatment.date), endDay: berlinDayOf(treatment.endDate ?? treatment.date), })) .reduce<{ id: string; startDay: string; endDay: string } | null>( (latest, candidate) => latest === null || candidate.endDay > latest.endDay ? candidate : latest, null, ); const varroaSinceLastTreatmentEnd = lastTreatment !== null && (await tx.varroaMeasurement.count({ where: { colonyId, date: { gt: dayToDate(lastTreatment.endDay) } }, })) > 0; // Feedings that answer a low feed status: since the inspection; without // one only recent ones, from the colony's creation at the earliest. const createdOn = berlinDayOf(colony.createdAt); const lookbackFrom = addDays(today, -FEEDING_LOOKBACK_DAYS); const feedingsSince = inspection === null ? dayToDate(createdOn > lookbackFrom ? createdOn : lookbackFrom) : inspection.date; const feedingsSinceInspection = await tx.feeding.count({ where: { colonyId, date: { gte: feedingsSince } }, }); const lateSummer = lateSummerStart(season, calendarTrachtende); const sinceLateSummer = lateSummer === null ? null : { gte: dayToDate(lateSummer) }; const feedingsSinceLateSummer = sinceLateSummer === null ? 0 : await tx.feeding.count({ where: { colonyId, date: sinceLateSummer } }); const treatmentsSinceLateSummer = sinceLateSummer === null ? 0 : await tx.treatment.count({ where: { colonyId, date: sinceLateSummer }, }); const winterChecks = await tx.winterCheck.findMany({ where: { colonyId, winterYear: { gte: season.year - 1 } }, select: { winterYear: true, season: true }, orderBy: [{ winterYear: 'asc' }, { season: 'asc' }], }); return { colony: { id: colony.id, code: colony.code, status: colony.status, colonyType: colony.colonyType, apiaryId: colony.apiaryId, createdOn: berlinDayOf(colony.createdAt), feedStatus: colony.feedStatus, }, lastInspection: inspection === null ? null : { ...inspection, date: berlinDayOf(inspection.date) }, lastVarroa: varroa === null ? null : { id: varroa.id, date: berlinDayOf(varroa.date) }, lastTreatment, varroaSinceLastTreatmentEnd, feedingsSinceInspection, feedingsSinceLateSummer, treatmentsSinceLateSummer, queen: colony.currentQueen, winterChecks, season, }; }