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
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user