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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 13:42:32 +02:00
1 parent 7097fcd97c
commit d99a697739
116 files changed
+11239 -801

No files matched your search

@@ -0,0 +1,39 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { CreateInspectionDto } from './create-inspection.dto';
import { UpdateInspectionDto } from './update-inspection.dto';
async function check(
dtoClass: typeof CreateInspectionDto | typeof UpdateInspectionDto,
feedStatus: unknown,
) {
const dto = plainToInstance(dtoClass, {
colonyId: 'c',
date: '2026-05-01',
feedStatus,
});
return { dto, errors: await validate(dto) };
}
describe('Inspection DTOs feedStatus', () => {
it.each([
['niedrig', 'WEAK'],
[' Kritisch ', 'CRITICAL'],
['AUSREICHEND', 'GOOD'],
['EXCELLENT', 'EXCELLENT'],
['average', 'AVERAGE'],
['', null],
[' ', null],
])('accepts %p as %p', async (input, expected) => {
for (const dtoClass of [CreateInspectionDto, UpdateInspectionDto]) {
const { dto, errors } = await check(dtoClass, input);
expect(errors).toEqual([]);
expect(dto.feedStatus).toBe(expected);
}
});
it.each(['halb voll', 'constructor', 5, {}])('rejects %p', async (input) => {
const { errors } = await check(CreateInspectionDto, input);
expect(errors.map((e) => e.property)).toContain('feedStatus');
});
});
@@ -2,6 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsDateString,
IsEnum,
IsInt,
IsNumber,
IsOptional,
@@ -9,6 +10,9 @@ import {
Max,
Min,
} from 'class-validator';
import { Transform } from 'class-transformer';
import { ColonyCondition } from '../../../generated/prisma';
import { feedConditionFromInput } from '../../common/feed-vocabulary';
export class CreateInspectionDto {
@ApiProperty()
@@ -99,10 +103,24 @@ export class CreateInspectionDto {
@Max(5)
combBehavior?: number;
@ApiPropertyOptional()
@ApiPropertyOptional({
enum: ColonyCondition,
nullable: true,
description:
'Feed condition. Also accepts the legacy German words ausreichend (GOOD), niedrig (WEAK) and kritisch (CRITICAL), ignoring case and surrounding whitespace; a blank string counts as null. Other text is rejected.',
})
@IsOptional()
@IsString()
feedStatus?: string;
@Transform(({ value }: { value: unknown }) => {
if (typeof value !== 'string') {
return value;
}
if (value.trim() === '') {
return null;
}
return feedConditionFromInput(value).feedStatus ?? value;
})
@IsEnum(ColonyCondition)
feedStatus?: ColonyCondition;
@ApiPropertyOptional()
@IsOptional()