feat: T4b/T4c wave 9 - phenology reports, autopilot A4-A10, honey ledger, Telegram delivery
- Phenology reports API with corrections and revokes; recompute per apiary via PhenologyRecomputeRequested, allowBackward for corrections; season changes are announced in the morning digest (changes since the previous digest, each once) - Season autopilot A4-A10 with per-apiary sync (Apiary before Colony locks) and WinterCheckRecorded - Honey batch ledger at runtime: initial amount, BOTTLING bookings, manual movements, one gram rounding, untracked bottlings never book - Telegram delivery: planning, Berlin delivery times incl. DST, quiet hours, permission re-check and stale skip at send time, rate limits, cleanup of old deliveries - ARCHITECTURE.md: assumptions 47-50 and updated sections 11.x Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
1971b2d9c4
commit
16cc557462
74 files changed
+9634
-340
No files matched your search
@@ -1,21 +1,35 @@
|
||||
import type { Prisma } from '../../generated/prisma';
|
||||
import { berlinDayOf, dayToDate } from '../common/dates';
|
||||
import { addDays, berlinDayOf, dayToDate } from '../common/dates';
|
||||
import type { SeasonState } from '../season/season-types';
|
||||
import type { AutopilotInput } from './autopilot.types';
|
||||
|
||||
/**
|
||||
* Until Task 27 passes the real start of the late summer, the feedings and
|
||||
* treatments of A4-A6 are counted from this day of the year (the built-in
|
||||
* calendar day of TRACHTENDE).
|
||||
*/
|
||||
const LATE_SUMMER_FALLBACK_START = '08-01';
|
||||
|
||||
const LATEST_FIRST = [{ date: 'desc' }, { createdAt: 'desc' }] as const;
|
||||
|
||||
function lateSummerStart(season: SeasonState): string | null {
|
||||
return season.segment === 'SPAETSOMMER' || season.segment === 'HERBST'
|
||||
? `${season.year}-${LATE_SUMMER_FALLBACK_START}`
|
||||
: null;
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +42,8 @@ export async function loadAutopilotInputIn(
|
||||
tx: Prisma.TransactionClient,
|
||||
colonyId: string,
|
||||
season: SeasonState,
|
||||
calendarTrachtende: string,
|
||||
today: string,
|
||||
): Promise<AutopilotInput | null> {
|
||||
const colony = await tx.colony.findUnique({
|
||||
where: { id: colonyId },
|
||||
@@ -86,13 +102,18 @@ export async function loadAutopilotInputIn(
|
||||
(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,
|
||||
...(inspection === null ? {} : { date: { gte: inspection.date } }),
|
||||
},
|
||||
where: { colonyId, date: { gte: feedingsSince } },
|
||||
});
|
||||
const lateSummer = lateSummerStart(season);
|
||||
const lateSummer = lateSummerStart(season, calendarTrachtende);
|
||||
const sinceLateSummer =
|
||||
lateSummer === null ? null : { gte: dayToDate(lateSummer) };
|
||||
const feedingsSinceLateSummer =
|
||||
|
||||
Reference in new issue
Block a user