feat: T6 wave 3 – treatment books from lot, label print lock, restricted zone, authority notices
- 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:
1 parent
c18860c75c
commit
edc193f128
73 files changed
+9470
-171
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
Matches,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { AuthorityNoticeKind } from '../../../generated/prisma';
|
||||
|
||||
export const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
export const DAY_MESSAGE = 'muss ein Datum im Format JJJJ-MM-TT sein';
|
||||
export const MIN_NOTICE_YEAR = 2000;
|
||||
export const MAX_NOTICE_YEAR = 2100;
|
||||
|
||||
export class CreateAuthorityNoticeDto {
|
||||
@ApiProperty({ enum: AuthorityNoticeKind })
|
||||
@IsEnum(AuthorityNoticeKind)
|
||||
kind!: AuthorityNoticeKind;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Nur für die TSK-Zählung (dort Pflicht)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(MIN_NOTICE_YEAR)
|
||||
@Max(MAX_NOTICE_YEAR)
|
||||
year?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2027-01-01',
|
||||
description: 'Vorgabe: heute (Berlin)',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(DAY_PATTERN, { message: `referenceDate ${DAY_MESSAGE}` })
|
||||
@IsDateString({ strict: true })
|
||||
referenceDate?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-05-01',
|
||||
description:
|
||||
'Gesetzt: die Anzeige entsteht gleich als gesendet (Altbestand)',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(DAY_PATTERN, { message: `sentOn ${DAY_MESSAGE}` })
|
||||
@IsDateString({ strict: true })
|
||||
sentOn?: string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsDateString,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { DAY_MESSAGE, DAY_PATTERN } from './create-authority-notice.dto';
|
||||
import { MAX_NOTICE_TEXT_LENGTH } from './update-authority-notice.dto';
|
||||
|
||||
export class MarkSentDto {
|
||||
@ApiPropertyOptional({
|
||||
example: '2027-01-15',
|
||||
description: 'Vorgabe: heute (Berlin)',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(DAY_PATTERN, { message: `sentOn ${DAY_MESSAGE}` })
|
||||
@IsDateString({ strict: true })
|
||||
sentOn?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'z. B. E-Mail, Post' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NOTICE_TEXT_LENGTH)
|
||||
sentVia?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Aktenzeichen' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NOTICE_TEXT_LENGTH)
|
||||
reference?: string;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
import { AuthorityNoticeKind } from '../../../generated/prisma';
|
||||
import {
|
||||
MAX_NOTICE_YEAR,
|
||||
MIN_NOTICE_YEAR,
|
||||
} from './create-authority-notice.dto';
|
||||
|
||||
export class NoticeListQuery {
|
||||
@ApiPropertyOptional({ enum: AuthorityNoticeKind })
|
||||
@IsOptional()
|
||||
@IsEnum(AuthorityNoticeKind)
|
||||
kind?: AuthorityNoticeKind;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(MIN_NOTICE_YEAR)
|
||||
@Max(MAX_NOTICE_YEAR)
|
||||
year?: number;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsDateString,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { IsOptionalNotNull } from '../../common/validation/is-optional-not-null';
|
||||
import { DAY_MESSAGE, DAY_PATTERN } from './create-authority-notice.dto';
|
||||
|
||||
export const MAX_NOTICE_TEXT_LENGTH = 300;
|
||||
export const MAX_NOTICE_NOTES_LENGTH = 2000;
|
||||
|
||||
export class UpdateAuthorityNoticeDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Die angezeigte Zahl; bleibt korrigierbar',
|
||||
})
|
||||
@IsOptionalNotNull()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
totalColonies?: number;
|
||||
|
||||
@ApiPropertyOptional({ example: '2027-01-01' })
|
||||
@IsOptionalNotNull()
|
||||
@Matches(DAY_PATTERN, { message: `referenceDate ${DAY_MESSAGE}` })
|
||||
@IsDateString({ strict: true })
|
||||
referenceDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true, example: '2027-01-15' })
|
||||
@IsOptional()
|
||||
@Matches(DAY_PATTERN, { message: `sentOn ${DAY_MESSAGE}` })
|
||||
@IsDateString({ strict: true })
|
||||
sentOn?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true, description: 'z. B. E-Mail, Post' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NOTICE_TEXT_LENGTH)
|
||||
sentVia?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true, description: 'Aktenzeichen' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NOTICE_TEXT_LENGTH)
|
||||
reference?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NOTICE_NOTES_LENGTH)
|
||||
notes?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ColonyType } from '../../../generated/prisma';
|
||||
import { IsOptionalNotNull } from '../../common/validation/is-optional-not-null';
|
||||
|
||||
const MONTH_DAY_PATTERN = /^\d{2}-\d{2}$/;
|
||||
const MONTH_DAY_MESSAGE = 'muss ein Tag im Format MM-TT sein';
|
||||
const MAX_NAME_LENGTH = 200;
|
||||
const MAX_ADDRESS_LENGTH = 300;
|
||||
|
||||
/** One row of the label digit height table. Annahme - vom Inhaber zu prüfen. */
|
||||
export class NetQuantityDigitRuleDto {
|
||||
@ApiProperty({
|
||||
nullable: true,
|
||||
description: 'Obergrenze in g, null = alles darüber',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false })
|
||||
maxGrams!: number | null;
|
||||
|
||||
@ApiProperty({ description: 'Mindesthöhe der Ziffern in mm' })
|
||||
@IsNumber({ allowNaN: false, allowInfinity: false })
|
||||
minMm!: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legal values (deadlines, digit heights) are settings, not code. Every value
|
||||
* here is an assumption the owner has to check.
|
||||
*/
|
||||
export class UpdateComplianceSettingsDto {
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NAME_LENGTH)
|
||||
veterinaryOfficeName?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_ADDRESS_LENGTH)
|
||||
veterinaryOfficeAddress?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NAME_LENGTH)
|
||||
registrationNumber?: string | null;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptionalNotNull()
|
||||
@IsBoolean()
|
||||
noticeOnColonyCountChange?: boolean;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptionalNotNull()
|
||||
@IsBoolean()
|
||||
noticeOnApiaryChange?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ example: '01-01', description: 'MM-TT, ohne 02-29' })
|
||||
@IsOptionalNotNull()
|
||||
@IsString()
|
||||
@Matches(MONTH_DAY_PATTERN, {
|
||||
message: `tskReferenceDay ${MONTH_DAY_MESSAGE}`,
|
||||
})
|
||||
tskReferenceDay?: string;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true, example: '03-31' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(MONTH_DAY_PATTERN, {
|
||||
message: `tskReportDueDay ${MONTH_DAY_MESSAGE}`,
|
||||
})
|
||||
tskReportDueDay?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(MAX_NAME_LENGTH)
|
||||
tskMemberNumber?: string | null;
|
||||
|
||||
@ApiPropertyOptional({ enum: ColonyType, isArray: true })
|
||||
@IsOptionalNotNull()
|
||||
@IsArray()
|
||||
@IsEnum(ColonyType, { each: true })
|
||||
tskExcludedColonyTypes?: ColonyType[];
|
||||
|
||||
@ApiPropertyOptional({ type: [NetQuantityDigitRuleDto] })
|
||||
@IsOptionalNotNull()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => NetQuantityDigitRuleDto)
|
||||
netQuantityDigitRules?: NetQuantityDigitRuleDto[];
|
||||
}
|
||||
Reference in new issue
Block a user