feat: T6 wave 3 – treatment books from lot, label print lock, restricted zone, authority notices
Backend Image / build-and-push (push) Successful in 5m13s
Frontend Image / build-and-push (push) Successful in 8m54s

- Treatments book their dose from a medicine lot (delta booking on edit,
  bulk all-or-nothing, idempotent); Bestandsbuch columns from the lot,
  FEHLT exactly where treatmentLogGaps reports a gap; Bestandsbuch export
  needs veterinary-medicines:read.
- Label printing: 409 LABEL_PARTICULARS_MISSING on gaps, proof print with
  watermark, PRINT audit before the PDF, check endpoints; digit rules only
  from the seeded ComplianceSettings row (fail closed).
- Restricted zone (Berlin day): create, update, move, merge and CSV import
  blocked inside an active restriction; override with reason and
  compliance:manage, audited, never stored on the colony; split, harvest
  and dissolve stay free; restriction details only with compliance:read.
- Authority notices: settings, herd snapshot, registration/herd change/TSK
  notices with one open draft per kind under the settings lock.
- Label check refinements (geometric reading order, number|unit pairs).
- ARCHITECTURE.md §12.6–12.8, Annahmen 71–77; spec §5.6 Berlin day.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-15 14:18:41 +02:00
1 parent c18860c75c
commit edc193f128
73 files changed
+9470 -171

No files matched your search

@@ -0,0 +1,77 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
ArrayMaxSize,
ArrayMinSize,
ArrayUnique,
IsArray,
IsDateString,
IsNotEmpty,
IsOptional,
IsString,
Matches,
MaxLength,
} from 'class-validator';
export const MAX_RESTRICTION_APIARIES = 50;
const MAX_ID_LENGTH = 64;
const MAX_TEXT_LENGTH = 200;
const MAX_NOTE_LENGTH = 2000;
const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const DAY_MESSAGE = 'muss ein Datum im Format JJJJ-MM-TT sein';
/** A restricted zone declared by the authority, for one or several apiaries. */
export class CreateApiaryRestrictionDto {
@ApiProperty({
type: [String],
description: 'Ein Datensatz je Stand, 1 bis 50 Stände',
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(MAX_RESTRICTION_APIARIES)
@ArrayUnique()
@IsString({ each: true })
@Matches(ID_PATTERN, { each: true })
@MaxLength(MAX_ID_LENGTH, { each: true })
apiaryIds!: string[];
@ApiProperty({ description: 'Seuche, z. B. Amerikanische Faulbrut' })
@IsString()
@IsNotEmpty()
@Matches(/\S/, { message: 'disease darf nicht nur aus Leerzeichen bestehen' })
@MaxLength(MAX_TEXT_LENGTH)
disease!: string;
@ApiPropertyOptional({ nullable: true, description: 'Anordnende Behörde' })
@IsOptional()
@IsString()
@MaxLength(MAX_TEXT_LENGTH)
authority?: string | null;
@ApiPropertyOptional({ nullable: true, description: 'Aktenzeichen' })
@IsOptional()
@IsString()
@MaxLength(MAX_TEXT_LENGTH)
reference?: string | null;
@ApiProperty({ example: '2026-10-01', description: 'Erster Tag der Sperre' })
@Matches(DAY_PATTERN, { message: `startsOn ${DAY_MESSAGE}` })
@IsDateString({ strict: true })
startsOn!: string;
@ApiPropertyOptional({
nullable: true,
example: '2026-12-01',
description: 'Letzter Tag der Sperre; leer = bis auf Weiteres',
})
@IsOptional()
@Matches(DAY_PATTERN, { message: `endsOn ${DAY_MESSAGE}` })
@IsDateString({ strict: true })
endsOn?: string | null;
@ApiPropertyOptional({ nullable: true })
@IsOptional()
@IsString()
@MaxLength(MAX_NOTE_LENGTH)
notes?: string | null;
}
@@ -0,0 +1,14 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { booleanQuery } from '../../veterinary-medicines/dto/medicine-list-query.dto';
export class RestrictionListQuery {
@ApiPropertyOptional({
description: 'true: nur heute geltende Sperren; false: nur beendete',
})
@IsOptional()
@Transform(booleanQuery)
@IsBoolean()
active?: boolean;
}
@@ -0,0 +1,12 @@
import { OmitType, PartialType } from '@nestjs/swagger';
import { CreateApiaryRestrictionDto } from './create-apiary-restriction.dto';
/**
* The apiary of a restriction is fixed: a restriction for another stand is a
* new restriction. Omitted stays, null clears a nullable field (endsOn ends
* the restriction as open again), null on disease or startsOn is a 400.
*/
export class UpdateApiaryRestrictionDto extends PartialType(
OmitType(CreateApiaryRestrictionDto, ['apiaryIds'] as const),
{ skipNullProperties: false },
) {}