- Weather history from Open-Meteo behind an adapter: worker loop phenology-weather with backoff, Retry-After, User-Agent, chunked writes and forecast-to-archive correction - Phenology: growing degree sum per DWD, phase from reports, GTS and calendar, calibration from previous years, daily job step, SeasonService reads the stored state - Honey chain: prefill for extractions and batches (proposals only), plausibility and withdrawal warnings, Extraction lock, chain events and batch.created webhook from HoneyBatchRecorded - Telegram base: schema, NotificationDelivery outbox, hardened bot client, startup check without crash, no-network-outside-loops guard - ARCHITECTURE.md: sections 11.11-11.13, assumptions 42-46, env table Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { berlinDayOf, daysBetween } from '../common/dates';
|
|
import { readRule } from '../automation/rule-config';
|
|
import { seasonStateFromRow } from '../phenology/phenology-row';
|
|
import type { DbClient } from '../prisma/db-client';
|
|
import { calendarSeasonState } from './season-calendar';
|
|
import type { SeasonState } from './season-types';
|
|
|
|
/** A stored row not updated for longer than this (days) means the job missed its runs. */
|
|
const MAX_ROW_AGE_DAYS = 2;
|
|
|
|
/**
|
|
* The season of apiaries: the stored state of the current year
|
|
* (ApiaryPhenology, written by the phenology step and the reports) where
|
|
* there is one and the job kept it fresh (updated at most MAX_ROW_AGE_DAYS
|
|
* days ago), else the calendar state. A stale row falls back to the calendar
|
|
* instead of recomputing GTS on the fly: that would read a year of weather per
|
|
* apiary on every read path, the calendar costs nothing. Callers do not see
|
|
* the difference.
|
|
*/
|
|
@Injectable()
|
|
export class SeasonService {
|
|
/** The season state of each given apiary on `today` (YYYY-MM-DD, Berlin). */
|
|
async statesFor(
|
|
db: DbClient,
|
|
apiaryIds: readonly string[],
|
|
today: string,
|
|
): Promise<Map<string, SeasonState>> {
|
|
const states = new Map<string, SeasonState>();
|
|
if (apiaryIds.length === 0) {
|
|
return states;
|
|
}
|
|
const ids = [...new Set(apiaryIds)];
|
|
const { config } = await readRule(db, 'PHENOLOGY');
|
|
const rows = await db.apiaryPhenology.findMany({
|
|
where: { apiaryId: { in: ids }, year: Number(today.slice(0, 4)) },
|
|
include: { apiary: { select: { latitude: true, longitude: true } } },
|
|
});
|
|
const stored = new Map(rows.map((row) => [row.apiaryId, row]));
|
|
for (const apiaryId of ids) {
|
|
const found = stored.get(apiaryId);
|
|
const row =
|
|
found !== undefined &&
|
|
daysBetween(berlinDayOf(found.updatedAt), today) <= MAX_ROW_AGE_DAYS
|
|
? found
|
|
: undefined;
|
|
states.set(
|
|
apiaryId,
|
|
row === undefined
|
|
? calendarSeasonState(apiaryId, today, config)
|
|
: seasonStateFromRow(
|
|
row,
|
|
config,
|
|
row.apiary.latitude !== null && row.apiary.longitude !== null,
|
|
today,
|
|
),
|
|
);
|
|
}
|
|
return states;
|
|
}
|
|
}
|