import { ColonyType, ComplianceSettings } from '../../generated/prisma'; import { LIVING_COLONY_WHERE } from '../colonies/colony-status'; import { DbClient } from '../prisma/db-client'; export const HERD_SNAPSHOT_VERSION = 1; export interface HerdApiarySnapshot { apiaryId: string; name: string; address: string | null; location: string | null; latitude: number | null; longitude: number | null; livingColonies: number; byType: Partial>; } export interface HerdSnapshot { version: 1; takenAt: string; apiaries: HerdApiarySnapshot[]; totalLiving: number; } /** An active apiary with the colony type of each living colony standing there. */ export interface HerdApiaryRow { apiaryId: string; name: string; address: string | null; location: string | null; latitude: number | null; longitude: number | null; colonyTypes: ColonyType[]; } export interface HerdDeviation { triggered: boolean; colonyCountChanged: boolean; apiariesAdded: string[]; apiariesRemoved: string[]; apiariesRelocated: string[]; } type DeviationSettings = Pick< ComplianceSettings, 'noticeOnColonyCountChange' | 'noticeOnApiaryChange' >; /** Pure: counts the living colonies per apiary and per type. */ export function buildHerdSnapshot( rows: readonly HerdApiaryRow[], takenAt: Date, excludedTypes: readonly ColonyType[] = [], ): HerdSnapshot { const apiaries = rows.map((row): HerdApiarySnapshot => { const byType: Partial> = {}; let livingColonies = 0; for (const type of row.colonyTypes) { if (excludedTypes.includes(type)) { continue; } byType[type] = (byType[type] ?? 0) + 1; livingColonies += 1; } return { apiaryId: row.apiaryId, name: row.name, address: row.address, location: row.location, latitude: row.latitude, longitude: row.longitude, livingColonies, byType, }; }); return { version: HERD_SNAPSHOT_VERSION, takenAt: takenAt.toISOString(), apiaries, totalLiving: apiaries.reduce( (sum, apiary) => sum + apiary.livingColonies, 0, ), }; } /** Active apiaries with their living colonies (LIVING_COLONY_WHERE). */ export async function loadHerdApiaryRows( db: DbClient, ): Promise { const apiaries = await db.apiary.findMany({ where: { status: 'ACTIVE' }, select: { id: true, name: true, address: true, location: true, latitude: true, longitude: true, colonies: { where: LIVING_COLONY_WHERE, select: { colonyType: true } }, }, orderBy: [{ name: 'asc' }, { id: 'asc' }], }); return apiaries.map((apiary) => ({ apiaryId: apiary.id, name: apiary.name, address: apiary.address, location: apiary.location, latitude: apiary.latitude, longitude: apiary.longitude, colonyTypes: apiary.colonies.map((colony) => colony.colonyType), })); } function sameSite(a: HerdApiarySnapshot, b: HerdApiarySnapshot): boolean { return ( a.address === b.address && a.location === b.location && a.latitude === b.latitude && a.longitude === b.longitude ); } /** * How the current herd differs from the baseline. The count is compared with * the corrected `totalColonies` of the notice, not with its snapshot. A move * between apiaries keeps the total and triggers nothing. */ export function herdDeviation( baseline: { snapshot: HerdSnapshot; totalColonies: number }, current: HerdSnapshot, settings: DeviationSettings, ): HerdDeviation { const before = new Map( baseline.snapshot.apiaries.map((apiary) => [apiary.apiaryId, apiary]), ); const now = new Map( current.apiaries.map((apiary) => [apiary.apiaryId, apiary]), ); const apiariesAdded = [...now.keys()].filter((id) => !before.has(id)); const apiariesRemoved = [...before.keys()].filter((id) => !now.has(id)); const apiariesRelocated = [...now.entries()] .filter(([id, apiary]) => { const old = before.get(id); return old !== undefined && !sameSite(old, apiary); }) .map(([id]) => id); const colonyCountChanged = baseline.totalColonies !== current.totalLiving; const apiaryChanged = apiariesAdded.length + apiariesRemoved.length + apiariesRelocated.length > 0; return { triggered: (settings.noticeOnColonyCountChange && colonyCountChanged) || (settings.noticeOnApiaryChange && apiaryChanged), colonyCountChanged, apiariesAdded, apiariesRemoved, apiariesRelocated, }; }