feat: T4 waves 1-2 - rule configs, schema, suggestions core, season, rule forms
- Rule configs with typed parsers and registry; Berlin day helpers - Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as ColonyCondition with legacy text mapping), split queen source, varroa sample size; migration harness for data migrations - SuggestionsService: propose/refresh/resolve/obsolete/expire, accept/dismiss/reopen incl. bulk, lock order, concurrency e2e - SeasonService: phenology-based segments, inspection/varroa due dates, queen age; TimeEventsStep with JobPartialFailure - Frontend: per-rule settings forms with visible value sources Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7097fcd97c
commit
d99a697739
116 files changed
+11239
-801
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { berlinToday } from '../common/dates';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('DashboardService', () => {
|
||||
@@ -11,6 +12,7 @@ describe('DashboardService', () => {
|
||||
honeyBatch: { count: jest.Mock };
|
||||
stockItem: { findMany: jest.Mock };
|
||||
swarmControlCheck: { findMany: jest.Mock };
|
||||
automationRule: { findUnique: jest.Mock };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -28,6 +30,8 @@ describe('DashboardService', () => {
|
||||
honeyBatch: { count: jest.fn().mockResolvedValue(1) },
|
||||
stockItem: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
swarmControlCheck: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
// No stored SEASON_AUTOPILOT rule: the built-in setting applies.
|
||||
automationRule: { findUnique: jest.fn().mockResolvedValue(null) },
|
||||
};
|
||||
service = new DashboardService(prisma as unknown as PrismaService);
|
||||
});
|
||||
@@ -137,33 +141,44 @@ describe('DashboardService', () => {
|
||||
});
|
||||
|
||||
describe('OLD_QUEEN', () => {
|
||||
it('flags a colony whose queen is at least 3 years old', async () => {
|
||||
const currentYear = Number(berlinToday().slice(0, 4));
|
||||
const queenedColony = (hatchYear: number) => [
|
||||
{ id: 'colony-1', code: 'V-001', currentQueen: { hatchYear } },
|
||||
];
|
||||
|
||||
it('OLD_QUEEN starts at queenMaxAgeYears (2)', async () => {
|
||||
prisma.colony.findMany
|
||||
.mockResolvedValueOnce([]) // attentionColonies
|
||||
.mockResolvedValueOnce([
|
||||
{ id: 'colony-1', code: 'V-001', currentQueen: { hatchYear: 2020 } },
|
||||
]);
|
||||
.mockResolvedValueOnce(queenedColony(currentYear - 2));
|
||||
|
||||
const alerts = await service.alerts();
|
||||
|
||||
expect(alerts).toHaveLength(1);
|
||||
expect(alerts[0].type).toBe('OLD_QUEEN');
|
||||
expect(alerts[0].relatedId).toBe('colony-1');
|
||||
expect(alerts[0].message).toContain('2 Jahre');
|
||||
});
|
||||
|
||||
it('does not flag a queen younger than the threshold', async () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
prisma.colony.findMany.mockResolvedValueOnce([]).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'colony-1',
|
||||
code: 'V-001',
|
||||
currentQueen: { hatchYear: currentYear },
|
||||
},
|
||||
]);
|
||||
prisma.colony.findMany
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce(queenedColony(currentYear - 1));
|
||||
|
||||
const alerts = await service.alerts();
|
||||
expect(await service.alerts()).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(alerts).toHaveLength(0);
|
||||
it('follows queenMaxAgeYears of the SEASON_AUTOPILOT rule', async () => {
|
||||
prisma.automationRule.findUnique.mockResolvedValue({
|
||||
id: 'rule-1',
|
||||
active: true,
|
||||
actionTemplate: null,
|
||||
config: { queenMaxAgeYears: 4 },
|
||||
});
|
||||
prisma.colony.findMany
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce(queenedColony(currentYear - 3));
|
||||
|
||||
expect(await service.alerts()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readRule } from '../automation/rule-config';
|
||||
import { berlinToday } from '../common/dates';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { LIVING_COLONY_WHERE } from '../colonies/colony-status';
|
||||
import { isQueenAged } from '../season/queen-age';
|
||||
|
||||
export interface DashboardSummary {
|
||||
apiariesActive: number;
|
||||
@@ -24,10 +27,6 @@ export interface DashboardAlert {
|
||||
}
|
||||
|
||||
const ATTENTION_COLONY_STATUSES = ['WEAK', 'QUEENLESS', 'CONSPICUOUS'] as const;
|
||||
// Queens rarely stay reliably productive past their third season - laying
|
||||
// rate and swarming risk both worsen well before then, so this flags a
|
||||
// colony for evaluation rather than demanding immediate requeening.
|
||||
const OLD_QUEEN_AGE_YEARS = 3;
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
@@ -158,15 +157,23 @@ export class DashboardService {
|
||||
});
|
||||
}
|
||||
}
|
||||
const currentYear = now.getFullYear();
|
||||
// The age comes from the rule, like QueenAged and the autopilot: a flag
|
||||
// for evaluation rather than a demand for immediate requeening.
|
||||
const { config: autopilot } = await readRule(
|
||||
this.prisma,
|
||||
'SEASON_AUTOPILOT',
|
||||
);
|
||||
const today = berlinToday(now);
|
||||
const currentYear = Number(today.slice(0, 4));
|
||||
for (const colony of queenedColonies) {
|
||||
// The where clause above already requires currentQueen and its
|
||||
// hatchYear to exist, but the relation is still nullable in the type.
|
||||
if (!colony.currentQueen?.hatchYear) {
|
||||
continue;
|
||||
}
|
||||
const ageYears = currentYear - colony.currentQueen.hatchYear;
|
||||
if (ageYears >= OLD_QUEEN_AGE_YEARS) {
|
||||
const { hatchYear } = colony.currentQueen;
|
||||
if (isQueenAged(hatchYear, today, autopilot.queenMaxAgeYears)) {
|
||||
const ageYears = currentYear - hatchYear;
|
||||
alerts.push({
|
||||
type: 'OLD_QUEEN',
|
||||
message: `Königin in Volk ${colony.code} ist ${ageYears} Jahre alt - Nachzucht prüfen`,
|
||||
|
||||
Reference in new issue
Block a user