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> { const states = new Map(); 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; } }