- 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]>
68 KiB
T6 Compliance-Autopilot „prüfungsfest“ — Design
Stand: 2026-10-13 · Betrifft: ApiCore (Backend, Frontend) · Grundlage: Spec 2026-10-12-plattform-rueckgrat-und-fahrplan-design.md §10 „T6“, Inhaber-Entscheidungen vom 2026-10-13
1. Ziel
Bei einer Kontrolle durch Veterinäramt oder Lebensmittelüberwachung soll Imkerei Wabenhain alles Verlangte sofort vorlegen können. ApiCore hält die Pflichten nach, die aus der Arbeit am Stand folgen:
- Bestandsbuch ohne „FEHLT“: Ein Tierarzneimittel-Katalog und erfasste Packungen füllen die Behandlung vor. Die Behandlung bucht die verbrauchte Menge von der Packung ab.
- Etiketten nur mit Pflichtangaben: Vor dem Druck prüft die App das fertige Etikett der konkreten Abfüllung. Fehlt eine Pflichtangabe, sperrt sie den Druck und nennt die Lücke.
- Behördenpaket: Ändern sich Völkerzahl oder Stände, kommt ein Hinweis. Die Anzeige ans Veterinäramt entsteht als PDF. Zum Stichtag der Tierseuchenkasse zählt die App die Völker selbst. Ein Stand im Sperrbezirk sperrt das Umsetzen.
- Seite „Prüfbereitschaft“: Jeder offene Punkt steht rot da, mit einem Knopf zum Beheben.
Die App warnt und listet Lücken. Sie gibt keine Rechtsberatung. Wo ein konkreter Wert nicht sicher belegt ist, ist er eine Einstellung mit Vorgabe und dem Vermerk „Annahme – vom Inhaber zu prüfen“.
Messbar fertig ist T6, wenn:
- Eine Behandlung aus einer vollständig erfassten Packung ergibt eine Bestandsbuch-Zeile ohne „FEHLT“.
GET /label-templates/:id/print/:bottlingBatchIdliefert bei fehlender Pflichtangabe 409 mit Lückenliste und sonst das PDF.- Eine geänderte Völkerzahl erzeugt nach dem Abarbeiten der Ereignisse genau eine ungelesene Meldung je berechtigtem Nutzer.
GET /compliance/readinessist bei vollständigen Daten leer und meldet „prüfbereit“.
Entscheidungen
| Frage | Entscheidung |
|---|---|
| Fremdhonig und Mischungen | Nicht im Umfang (Inhaber 2026-10-13). Nur eigener deutscher Honig. Länderanteile in Prozent entfallen. Fremdhonig ist im Modell ohnehin nicht abbildbar: HoneyBatch hängt immer an einer eigenen Extraction. |
| Wanderung (BienSeuchV § 5) | Entfällt (Inhaber 2026-10-13: keine Wanderimkerei). Keine Bescheinigungen, kein Stand-Schild. Ein fester Hinweistext im Umsetzen-Dialog bleibt, siehe 2.2. |
| Wo liegt der Lagerbestand der Tierarzneimittel? | Eigene Packungen mit eigenem Bewegungsbuch (VeterinaryMedicineLot + VeterinaryMedicineMovement). Nicht StockItem: dort gibt es keine Charge, keinen Lieferanten und keinen Beleg je Kauf. Nicht BatchMovement: das gehört zum Shop-Bestand, löst StockChanged und Webhooks aus und rechnet in ganzen Stück bzw. Gramm. Inhaber 2026-10-13. Ein Lagerartikel ohne Charge hätte etwa 1,25 PT gespart, aber Lieferant und Beleg wären Freitext je Behandlung geblieben. |
| Lieferant, Beleg, Ch.-B. und Tierarzt bei Behandlung aus einer Packung | Werden live aus der Packung gelesen, nicht kopiert. Ein Tippfehler im Beleg wird einmal an der Packung korrigiert und ist in jedem Bestandsbuch richtig. Die Freitextfelder der Behandlung gelten nur noch ohne Packung. |
| Vorbelegung aus dem Katalog | Nur sichtbar im Formular, mit Herkunftsnotiz wie in T3. Der Server leitet nichts still ab. Einzige Ausnahme: product wird zum Handelsnamen des gewählten Katalogeintrags (ein Verweis, keine Beobachtung). |
| Wie wird ein Etikett geprüft? | Am aufgelösten Inhalt des Etiketts für die konkrete Abfüllung: alle Texte mit eingesetzten Platzhaltern. So zählt auch fest getippter Text, und ein festes „500 g“ auf einer 250-g-Abfüllung fällt auf. |
| Was sperrt den Etikettendruck? | Fehlende Pflichtangabe (auch eine abgeschnittene) → 409. Formmängel (x-Höhe, Ziffernhöhe der Füllmenge, fehlendes „L“) → Warnung. Probedruck mit Aufdruck „MUSTER“ bleibt immer möglich. Inhaber 2026-10-13. Grund: Die Ziffernhöhen sind eine Annahme; eine Sperre darauf würde echten Druck verhindern. |
| Name und Anschrift auf dem Etikett | Eigene Felder „Lebensmittelunternehmer“ (SiteSettings.foodBusinessName, foodBusinessAddress) mit Knopf „Vom Bienenhalter übernehmen“. Nicht die Bienenhalter-Felder des Bestandsbuchs. Inhaber 2026-10-13. Auf das Glas gehört die Marke, ins Bestandsbuch die Person. |
| Rechtswerte im Code | Keine. Der Katalog startet leer (Annahme 10). Stichtag der Tierseuchenkasse, Auslöser der Änderungsanzeige und Ziffernhöhen der Füllmenge sind Einstellungen mit markierter Annahme. |
| Zustand der Prüfbereitschaft | Live berechnet aus den Fachdaten. Gespeichert werden nur Quittungen für „Prüfen“-Punkte. Kein zweiter Datenstand, der veralten kann. |
| Farbe | Jeder offene Punkt ist rot (Inhaber). Die Schwere steht als Wort daneben: „Pflicht“ oder „Prüfen“. „Prüfbereit“ (grün) heißt: nichts offen. |
| Sperrbezirk | Harte Sperre für Umsetzen, Anlegen im Stand, Standkorrektur und Vereinigen über Stände hinweg. Ausnahme nur mit Begründung, Recht compliance:manage und Audit-Eintrag. Inhaber 2026-10-13. |
| Ernte am Stand im Sperrbezirk | Nur Warnung, keine Sperre. Inhaber 2026-10-13. Was erlaubt ist, steht in der Verfügung des Amts. Eine Ernte nachträglich zu dokumentieren muss möglich bleiben. |
| Ereigniskatalog | Wird um vier Typen erweitert (ColonyCreated, ColonyMoved, ApiaryChanged, TskReferenceDayReached). Der Rückgrat-Plan nennt seinen Katalog „vollständig“ für die erste Stufe; T6 ist die nächste Stufe. |
| Meldungen | Höchstens eine ungelesene Meldung je Thema und Nutzer. Sonst meldet jede Ablegerbildung im Juni neu. |
Abhängigkeiten
- Rückgrat: Task 3 (
events.record,recordForPermission), Task 13 (Völker schreiben in der Transaktion mit Ereignis), Task 15 (JobRun,cli.ts), Task 18 (daily-evaluation,dailyDedupeKey,inCatchUpWindow). Im Cluster: Runbook-Schritt 3 (Worker und CronJobs) ist live. - T3 Task 4 (
POST /treatments/bulk, gemeinsame private Datenaufbereitung von Einzel- und Sammel-Anlegen) und Task 2 (last-by-product). T6 erweitert genau diese gemeinsame Funktion. - T4 ist keine Voraussetzung. T5-Farbtokens werden genutzt, wenn vorhanden.
2. Umfang
2.1 Im Umfang
| Baustein | Inhalt |
|---|---|
| Tierarzneimittel-Katalog | Handelsname, Wirkstoff, Zulassungsnummer, verschreibungspflichtig ja/nein, Wartezeit Honig, Einheit, Dosis je Volk mit Hinweis, Anwendungsart, Temperaturfenster, Standard-Lieferant. Vom Imker gepflegt. Übernahme-Vorschläge aus den eigenen bisherigen Behandlungen. |
| Packungen und Bewegungsbuch | Erwerb mit Ch.-B., Datum, Menge, Verfall, Lieferant, Beleg (Text und Scan), Tierarzt. Rest aus dem Bewegungsbuch. Abgang je Behandlung, Inventur-Korrektur, Entsorgung. |
| Behandlung | Mittel aus dem Katalog, Packung (älteste zuerst vorgeschlagen), Dosis je Volk. Lagerabgang in derselben Transaktion, auch bei der Sammelerfassung. Korrektur bucht gegen. Hinweis zum Temperaturfenster aus dem Stand-Wetter. |
| Bestandsbuch | Spalten aus der Packung, Menge aus der Dosis, Tierarzt nur bei verschreibungspflichtigen Mitteln Pflicht. Ein gemeinsamer Lücken-Helfer für PDF und Prüfbereitschaft. Stand am Behandlungstag korrigierbar. |
| Etikett-Pflichtangaben | Neue Platzhalter, Prüfregeln für Bezeichnung, Füllmenge, MHD, Los, Ursprungsland, Name und Anschrift. Warnungen für Form. Live-Liste im Designer, Knopf „Pflichtangaben-Block einfügen“, Druck-Sperre, Probedruck, Audit je Druck. |
| Behördenpaket | Einstellungen (Veterinäramt, Registriernummer, Auslöser, Stichtag). Erstanzeige und Änderungsanzeige als PDF. Zählnachweis zum TSK-Stichtag, automatisch als Entwurf. Sperrbezirk je Stand mit Verfügung als Dokument. |
| Prüfbereitschaft | 18 Prüfpunkte in fünf Bereichen, Quittieren von „Prüfen“-Punkten mit Notiz, Kurzstatus fürs Dashboard, Bericht als PDF. |
2.2 Nicht im Umfang
| Thema | Grund |
|---|---|
| Mischungen, Länderanteile, Fremdhonig | Inhaber-Entscheidung 2026-10-13. Die geänderte HonigV (Länderanteile ab 14.06.2026, siehe Fahrplan-Spec) betrifft nur Mischungen. |
| Wanderbescheinigung, Stand-Schild, Rückkehr-Aufgabe | Inhaber-Entscheidung 2026-10-13. Rest-Risiko: Auch das Umsetzen zwischen eigenen festen Ständen kann eine Gesundheitsbescheinigung verlangen (BienSeuchV § 5). Der Umsetzen-Dialog zeigt dazu einen festen Hinweis: „Beim Umsetzen an einen anderen Standort kann eine Gesundheitsbescheinigung nötig sein (BienSeuchV § 5). Im Zweifel beim Veterinäramt nachfragen.“ Keine Logik dahinter. |
| Prüfung, ob die Bezeichnung zulässig ist | Die App prüft nur, ob eine Bezeichnung mit „Honig“ da ist. Ob z. B. „Rapshonig“ stimmt (überwiegend aus dieser Tracht, HonigV), weiß nur der Imker oder ein Labor. |
| Sichtfeld-Regel (LMIV Art. 13 Abs. 5) | Bezeichnung und Füllmenge im selben Sichtfeld: Die Glasform kennt die App nicht. Ein Etikett je Glas wird angenommen. |
| Nährwertdeklaration | Annahme – vom Inhaber zu prüfen: Honig ist als unverarbeitetes Erzeugnis aus einer Zutat davon befreit (LMIV Anhang V Nr. 1; Direktabgabe kleiner Mengen Anhang V Nr. 19). Die App prüft das nicht. |
| Pflichtangaben im Online-Shop (LMIV Art. 14, Fernabsatz) | Gehört zum Wabenhain-Shop. Eine spätere Erweiterung von shop-sync kann legalName, Ursprungsland und Lebensmittelunternehmer liefern. |
| Registrierung als Lebensmittelunternehmer (VO (EG) 852/2004 Art. 6) | Einmaliger Vorgang ohne Datenbezug zur App. |
| Meldung an die TSK selbst (Portal oder Meldebogen) | Die App zählt und dokumentiert. Melden tut der Inhaber. Das PDF sagt das ausdrücklich. |
| Arzneimitteldaten von außen (Datenbanken, Beipackzettel) | Annahme 10: keine fremden, rechtlich relevanten Daten im Code oder im Seed. |
3. Rechtliche Leitplanken
Spalte „Stand“: Norm = die App setzt die zitierte Vorschrift um. Annahme = Wert oder Auslegung ist eine Einstellung, „Annahme – vom Inhaber zu prüfen“.
Die Paragraphen-Nummern wurden nicht gegen die amtliche Fassung abgeglichen. Vor dem Rollout gleicht sie jemand einmal gegen gesetze-im-internet.de bzw. EUR-Lex ab. Die Texte in der App nennen die Norm nur als Hinweis.
| Pflicht | Norm | Umsetzung in der App | Stand |
|---|---|---|---|
| Bestandsbuch: Datum, Mittel, Menge, Lieferant, Erwerbsbeleg, Tiere, verschreibender Tierarzt (falls zutreffend), Wartezeit auch bei 0, Dauer; 5 Jahre aufbewahren | Art. 108 VO (EU) 2019/6 | Bestehendes PDF (Annahme 15), Felder aus der Packung, Lücken-Helfer, Prüfbereitschaft über 6 Kalenderjahre (laufendes + 5) | Norm |
| Anzeige der Bienenhaltung mit Völkerzahl und Standort | BienSeuchV § 1a | Erstanzeige als PDF oder „bereits angezeigt am …“ | Norm |
| Anzeige von Änderungen (Völkerzahl, Stände) | BienSeuchV § 1a; ob jede Änderung der Völkerzahl anzuzeigen ist, regelt das Amt unterschiedlich | Einstellungen noticeOnColonyCountChange, noticeOnApiaryChange, beide Vorgabe an |
Annahme |
| Registriernummer des Halters | ViehVerkV § 26; ob und wie sie für Bienen vergeben wird, hängt vom Landratsamt ab | nur ein Feld, wird ins PDF gedruckt | Annahme |
| Meldestichtag und Meldefrist der Bayerischen Tierseuchenkasse, gezählte Völkerarten | Landesrecht Bayern (Ausführungsgesetz zum TierGesG, Beitragssatzung der Tierseuchenkasse) | tskReferenceDay Vorgabe 01-01, tskReportDueDay leer, tskExcludedColonyTypes leer (alle zählen) |
Annahme |
| Sperrbezirk bei Amerikanischer Faulbrut: Völker dürfen nicht entfernt und nicht hineingebracht werden; weitere Auflagen (u. a. Honig, Waben) | BienSeuchV § 10; Einzelheiten in der Verfügung des Amts | harte Sperre für Bewegungen von Völkern; Warnung bei Ernte; Verfügung als Dokument | Norm (Sperre); Ernte nur Warnung (Inhaber 2026-10-13) |
| Bezeichnung des Lebensmittels | LMIV Art. 9 Abs. 1 lit. a; HonigV § 3 mit Anlage 1 | Pflicht: Text mit „honig“ | Norm |
| Kein Zutatenverzeichnis bei Honig | LMIV Art. 19 Abs. 1 lit. e (eine Zutat, Name identisch) | wird nicht geprüft | Norm |
| Nettofüllmenge | LMIV Art. 9 Abs. 1 lit. e, Art. 23, Anhang IX | Pflicht: „{Glasgröße} g“ im Text | Norm |
| Mindestziffernhöhe der Nennfüllmenge | Fertigpackungsverordnung, Grundlage RL 76/211/EWG Anhang I Nr. 3.1 | Warnung, Werte als Einstellung: bis 50 g 2 mm, bis 200 g 3 mm, bis 1000 g 4 mm, darüber 6 mm | Annahme |
| Mindesthaltbarkeitsdatum mit „mindestens haltbar bis …“ | LMIV Art. 9 Abs. 1 lit. f, Art. 24, Anhang X | Pflicht: Wortlaut und Datum der Abfüllung | Norm |
| Los-Kennzeichnung, Buchstabe „L“ vorangestellt, außer deutlich unterscheidbar | Los-Kennzeichnungs-Verordnung (LKV), Grundlage RL 2011/91/EU. Die frühere LMKV ist durch die LMIV abgelöst. | Pflicht: Losnummer im Text; Warnung ohne „L“. Bewusst Pflicht, obwohl die LKV bei MHD mit Tag und Monat befreit: Rückverfolgbarkeit nach Art. 18 VO (EG) 178/2002 | Norm |
| Ursprungsland | HonigV § 3 | Pflicht: SiteSettings.originCountry im Text |
Norm |
| Name oder Firma und Anschrift des Lebensmittelunternehmers | LMIV Art. 8, Art. 9 Abs. 1 lit. h | Pflicht: beide Werte aus den Stammdaten im Text | Norm |
| Mindestschriftgröße: x-Höhe 1,2 mm, bei größter Verpackungsfläche unter 80 cm² 0,9 mm | LMIV Art. 13 Abs. 2 und 3, Anhang IV | Warnung, Grenzwert je Vorlage (minXHeightMm, Vorgabe 1,2) |
Norm |
4. Datenmodell
4.1 Tierarzneimittel
enum MedicineUnit {
ML
G
PIECE
}
// Katalog des Imkers. Jeder Wert kommt aus Packungsbeilage oder Rezept und
// wird vom Imker eingetragen; der Code liefert keine Arzneimitteldaten
// (ARCHITECTURE Annahme 10).
model VeterinaryMedicine {
id String @id @default(cuid())
name String // Handelsname
activeIngredient String?
authorisationNumber String? // Zulassungsnummer laut Packung
prescriptionOnly Boolean @default(false)
withdrawalPeriodDays Int? // Wartezeit Honig in Tagen, auch 0
unit MedicineUnit // nur änderbar, solange keine Packung existiert
dosePerColony Float? // in `unit`
doseNote String? // z. B. "5 ml je besetzter Wabengasse"
applicationMethod String? // z. B. Träufeln, Verdunsten
minTemperatureC Float?
maxTemperatureC Float?
defaultSupplier String?
notes String?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lots VeterinaryMedicineLot[]
treatments Treatment[]
@@index([active])
}
// Eine gekaufte Packung. Ch.-B., Lieferant, Beleg und Tierarzt stehen hier
// einmal und gelten für jede Behandlung aus dieser Packung.
model VeterinaryMedicineLot {
id String @id @default(cuid())
medicineId String // nur änderbar, solange keine Bewegung existiert
medicine VeterinaryMedicine @relation(fields: [medicineId], references: [id], onDelete: Restrict)
batchNumber String? // Ch.-B.
purchasedOn DateTime
quantity Float // Anfangsmenge in medicine.unit, > 0
expiresOn DateTime?
supplier String? // Name, Anschrift
purchaseProof String? // z. B. "Rechnung 4711 vom 01.08.2026"
prescribingVet String?
notes String?
userId String?
user User? @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
movements VeterinaryMedicineMovement[]
treatments Treatment[]
@@index([medicineId, expiresOn])
}
enum VeterinaryMedicineMovementType {
TREATMENT
CORRECTION
DISPOSAL
}
// Bewegungsbuch der Packungen: nie geändert, nie gelöscht; Fehler werden
// gegengebucht. Rest = quantity der Packung + Summe aller quantity.
model VeterinaryMedicineMovement {
id String @id @default(cuid())
lotId String
lot VeterinaryMedicineLot @relation(fields: [lotId], references: [id], onDelete: Restrict)
type VeterinaryMedicineMovementType
quantity Float // mit Vorzeichen
treatmentId String?
treatment Treatment? @relation(fields: [treatmentId], references: [id], onDelete: Restrict)
reason String?
userId String?
user User? @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
@@index([lotId])
@@index([treatmentId])
}
CHECK-Constraints in der Migration (Stil wie BatchMovement): quantity > 0 an der Packung; quantity <> 0 an der Bewegung; (type = 'TREATMENT') = ("treatmentId" IS NOT NULL).
4.2 Behandlung
Treatment bekommt:
| Feld | Typ | Bedeutung |
|---|---|---|
medicineId |
String? → VeterinaryMedicine |
Katalogeintrag |
medicineLotId |
String? → VeterinaryMedicineLot |
Packung, aus der behandelt wurde |
doseQuantity |
Float? |
Menge je Volk in der Einheit des Mittels |
medicineMovements |
Relation | Buchungen dieser Behandlung |
Dazu @@index([medicineLotId]). amount (Freitext) bleibt. apiaryId (Stand am Behandlungstag) kommt ins Update-DTO, damit eine mit * markierte Zeile korrigiert werden kann.
4.3 Etikett
| Modell | Feld | Bedeutung |
|---|---|---|
SiteSettings |
foodBusinessName String? |
Name oder Firma auf dem Etikett, z. B. „Imkerei Wabenhain · Nick Rosenlöcher“ |
SiteSettings |
foodBusinessAddress String? |
Postanschrift auf dem Etikett |
Product |
legalName String? |
Bezeichnung des Lebensmittels, z. B. „Blütenhonig“ |
LabelTemplate |
minXHeightMm Float @default(1.2) |
Grenzwert der x-Höhe für diese Vorlage, 0,5–5 |
AuditAction |
neuer Wert PRINT |
Etikettendruck im Audit-Log |
Neue Platzhalter (PLACEHOLDER_KEYS, Backend und Frontend):
| Schlüssel | Wert | Beispiel |
|---|---|---|
legalName |
Product.legalName |
Blütenhonig |
originCountry |
SiteSettings.originCountry |
Deutschland |
foodBusinessName |
SiteSettings.foodBusinessName |
Imkerei Wabenhain · Nick Rosenlöcher |
foodBusinessAddress |
SiteSettings.foodBusinessAddress |
Bühlweg 10, 86450 Altenmünster |
bestBeforeLine |
„Mindestens haltbar bis “ + MHD | Mindestens haltbar bis 05.03.2028 |
lotLine |
„L “ + Losnummer | L 2026-0007 |
jarSize bleibt („500 g“) und ist die Nettofüllmenge. buildPlaceholderValues bekommt die Einstellungen als zweiten Parameter. Druck und Prüfung nutzen dieselbe Funktion.
4.4 Behörden und Sperrbezirk
// Singleton wie SiteSettings. Werte mit "Annahme" siehe Abschnitt 3.
model ComplianceSettings {
id String @id @default("singleton")
veterinaryOfficeName String? // Empfänger der Anzeige
veterinaryOfficeAddress String?
registrationNumber String?
noticeOnColonyCountChange Boolean @default(true) // Annahme
noticeOnApiaryChange Boolean @default(true) // Annahme
tskReferenceDay String @default("01-01") // MM-TT, Annahme
tskReportDueDay String? // MM-TT
tskMemberNumber String?
tskExcludedColonyTypes ColonyType[] @default([]) // Annahme: alle zählen
netQuantityDigitRules Json // Annahme, Vorgabe in der Migration (5.4)
updatedAt DateTime @updatedAt
}
enum AuthorityNoticeKind {
REGISTRATION // Erstanzeige
HERD_CHANGE // Änderungsanzeige
TSK_REPORT // Zählnachweis zum TSK-Stichtag
}
enum AuthorityNoticeStatus {
DRAFT
SENT
CANCELLED
}
model AuthorityNotice {
id String @id @default(cuid())
kind AuthorityNoticeKind
status AuthorityNoticeStatus @default(DRAFT)
year Int? // nur TSK_REPORT
referenceDate DateTime // "Stand am" bzw. Stichtag
countedAt DateTime // wann gezählt wurde
snapshot Json // HerdSnapshot, siehe 5.5
totalColonies Int // aus dem Snapshot, korrigierbar
sentOn DateTime?
sentVia String? // Post, E-Mail, Portal
reference String? // Aktenzeichen, Eingangsbestätigung
notes String?
// Nur bei DRAFT gesetzt ("REGISTRATION", "HERD_CHANGE", "TSK_REPORT:2027"),
// sonst null: höchstens ein offener Entwurf je Art und Jahr. Nullbares
// @unique statt partiellem Index, wie dedupeKey im Rückgrat.
openKey String? @unique
createdByUserId String?
createdBy User? @relation(fields: [createdByUserId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kind, status])
@@index([year])
}
model ApiaryRestriction {
id String @id @default(cuid())
apiaryId String
apiary Apiary @relation(fields: [apiaryId], references: [id], onDelete: Restrict)
disease String // z. B. "Amerikanische Faulbrut"
authority String? // anordnende Behörde
reference String? // Aktenzeichen der Verfügung
startsOn DateTime
endsOn DateTime? // null = gilt bis auf Weiteres
notes String?
createdByUserId String?
createdBy User? @relation(fields: [createdByUserId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([apiaryId, startsOn])
}
CHECK "endsOn" IS NULL OR "endsOn" >= "startsOn".
4.5 Prüfbereitschaft
// "Geprüft, in Ordnung" für einen Prüfen-Punkt. Gilt nur, solange der
// Fingerabdruck zum aktuellen Befund passt; ändern sich die Daten, ist der
// Punkt wieder offen.
model ComplianceAcknowledgement {
id String @id @default(cuid())
itemKey String
subjectId String
fingerprint String
note String
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
revokedAt DateTime?
revokedByUserId String?
@@index([itemKey, subjectId])
}
4.6 Dokumente
DocumentEntityType bekommt MEDICINE_LOT (Erwerbsbeleg, Rezept), AUTHORITY_NOTICE (Kopie, Eingangsbestätigung) und APIARY_RESTRICTION (Verfügung). DocumentsService.assertEntityExists kennt die drei neuen Arten. Upload, MIME-Allowlist und Signaturprüfung bleiben unverändert.
4.7 Migrationen
Zeitstempel beim Bau. Reihenfolge fest, alles additiv. Erzeugt wie im Rückgrat-Plan mit prisma migrate diff, dann von Hand nachgearbeitet.
| Migration | Inhalt |
|---|---|
<ts>_t6_veterinary_medicines |
Enums MedicineUnit, VeterinaryMedicineMovementType; drei Tabellen mit CHECKs; Treatment.medicineId, medicineLotId, doseQuantity mit FKs und Index; DocumentEntityType + MEDICINE_LOT; Rechte veterinary-medicines:* samt Vergabe |
<ts>_t6_label_particulars |
SiteSettings.foodBusinessName, foodBusinessAddress; Product.legalName; LabelTemplate.minXHeightMm; AuditAction + PRINT |
<ts>_t6_authority_compliance |
ComplianceSettings mit Vorgabe für netQuantityDigitRules; AuthorityNotice mit Enums; ApiaryRestriction mit CHECK; ComplianceAcknowledgement; DocumentEntityType + AUTHORITY_NOTICE, APIARY_RESTRICTION; Rechte compliance:* samt Vergabe |
ALTER TYPE … ADD VALUE läuft in der Migrationstransaktion (Postgres ≥ 12). Kein neuer Wert wird in derselben Migration benutzt.
4.8 Rechte
Der Katalog-Sync berechtigt nur System-Rollen. Die Migrationen vergeben die neuen Rechte darum selbst, im Stil von 20260928091348_winter_checks (Annahme 16).
| Recht | Gruppe (groupDe) |
ADMIN | BEEKEEPER | STAFF | READONLY | API_CLIENT |
|---|---|---|---|---|---|---|
veterinary-medicines:read |
Tierarzneimittel | ✓ | ✓ | ✓ | ✓ | – |
veterinary-medicines:create |
Tierarzneimittel | ✓ | ✓ | ✓ | – | – |
veterinary-medicines:update |
Tierarzneimittel | ✓ | ✓ | ✓ | – | – |
compliance:read |
Prüfbereitschaft & Behörden | ✓ | ✓ | ✓ | ✓ | – |
compliance:manage |
Prüfbereitschaft & Behörden | ✓ | ✓ | – | – | – |
API_CLIENT bekommt nichts: Belege, Rechnungsnummern und Anschriften braucht der Shop nicht. Eine Behandlung aus einer Packung bucht mit treatments:create ab. Ein eigenes Recht dafür gibt es nicht.
4.9 Backup
Alle neuen Modelle kommen ins Backup. Keines steht in MODELS_EXCLUDED_FROM_BACKUP, keines in LEGACY_UNSEALED_TABLES: Sie kommen nur aus versiegelten Dateien. Der bestehende Wächter-Test erzwingt das.
5. Fachlogik
5.1 Tierarzneimittel und Packungen
- Rest =
lot.quantity + Σ movement.quantity, gerundet auf Tausendstel. Er wird immer berechnet, nie gespeichert. Pure FunktionsummarizeLotinsrc/veterinary-medicines/medicine-ledger.ts. - Rundung: gemeinsamer Helfer
roundToThousandthsinsrc/common/quantity.ts.roundKginbottling.service.tsnutzt ihn ebenfalls, statt eine eigene Kopie zu haben. - Vorschlag der Packung:
fifoOrderaussrc/stock/stock-ledger.tswird wiederverwendet (expiresOnals Haltbarkeit,purchasedOnals Datum). Wählbar sind Packungen mit Rest > 0 und die bereits gewählte Packung der Behandlung. - Bewegungen:
TREATMENTentsteht nur über die Behandlung (5.2).CORRECTION(Inventur): Der Nutzer gibt den tatsächlichen Rest ein. Der Server sperrt die Packung (SELECT … FOR UPDATE), berechnet das Delta und bucht es. Ein Delta von 0 bucht nichts.DISPOSAL(Entsorgung): positive Menge, gebucht als negativ.reasonist Pflicht.
- Negativer Rest ist erlaubt. Die Behandlung hat stattgefunden. Sie abzulehnen würde die Lücke nur verstecken. Die Prüfbereitschaft meldet den negativen Rest (gleiche Haltung wie
allocateSalebeim Shop). - Änderungssperren:
VeterinaryMedicine.unitist änderbar, solange keine Packung existiert.lot.medicineIdist änderbar, solange keine Bewegung existiert. Sonst 409. Alle anderen Felder sind jederzeit änderbar (Null-Regel, Annahme 20). - Übernahme-Vorschläge:
GET /veterinary-medicines/suggestionsliefert die eigenen, noch nicht katalogisierten Handelsnamen ausTreatment.product(ohne Groß-/Kleinschreibung) mit Anzahl und den Werten der jüngsten Behandlung. Angelegt wird erst nach Bestätigung im Formular.
5.2 Behandlung
Einzel- und Sammel-Anlegen nutzen die gemeinsame Datenaufbereitung aus T3 Task 4. Dort kommen Prüfung und Buchung hinein, also für beide Wege gleich.
| Fall | Verhalten |
|---|---|
medicineId gesetzt, product fehlt |
product = medicine.name |
medicineId und product weichen voneinander ab (ohne Groß-/Kleinschreibung) |
400 „Mittel und Katalogeintrag passen nicht zusammen“ |
| Katalogeintrag inaktiv | Anlegen: 400. Bestehende Behandlungen bleiben bearbeitbar. |
medicineLotId ohne medicineId |
medicineId = lot.medicineId |
| Packung gehört zu einem anderen Mittel | 400 |
Packung plus supplier, purchaseProof, batchNumber oder prescribingVet |
400 „Diese Angabe kommt aus der Packung“ |
doseQuantity ≤ 0 |
400 |
| Packung und Dosis gesetzt | Buchung TREATMENT, Menge −Dosis, in derselben Transaktion wie Behandlung, Audit und TreatmentRecorded |
| Dosis ohne Packung | wird gespeichert, keine Buchung |
Bearbeiten ändert Packung oder Dosis, oder setzt eine davon auf null |
Delta je Packung: Soll minus bisherige Summe dieser Behandlung. Gegenbuchung auf der alten, neue Buchung auf der neuen Packung. Pure Funktion treatmentBookingDelta(current, desired). |
medicineId: null bei gesetzter Packung |
400 |
| Behandlungsdatum nach Verfall der Packung | wird gespeichert; Prüfbereitschaft „Prüfen“ |
| Sammelerfassung | je Volk eigene Buchung, alles in der einen Transaktion; fehlt ein Volk, wird wie in T3 nichts gespeichert |
Volk wird umgebucht (colonyId) |
keine Wirkung auf den Bestand |
Die Vorbelegung bleibt im Formular. Der Server füllt activeIngredient, withdrawalPeriodDays, method und doseQuantity nie still nach. Fehlt die Wartezeit, steht im Bestandsbuch „FEHLT“, und die Prüfbereitschaft meldet es. Das ist ehrlich.
5.3 Bestandsbuch
TreatmentLogSource bekommt Packung und Mittel. Die Spalten kommen aus genau einer Quelle:
| Spalte | ohne Packung | mit Packung |
|---|---|---|
| Arzneimittel, Ch.-B. | product, batchNumber |
product, lot.batchNumber |
| Lieferant; Tierarzt | supplier; prescribingVet |
lot.supplier; lot.prescribingVet |
| Beleg für den Erwerb | purchaseProof |
lot.purchaseProof |
| Verabreichte Menge | amount |
amount, sonst doseQuantity + Einheit („30 ml“) |
| Wartezeit | withdrawalPeriodDays |
gleich |
- Gemeinsamer Lücken-Helfer
treatmentLogGaps(row)inexport/treatment-log.ts. Er liefert die fehlenden Pflichtfelder: Lieferant, Beleg, Menge, Wartezeit, und Tierarzt nur beimedicine.prescriptionOnly. Das PDF druckt „FEHLT“ genau dort, wo der Helfer eine Lücke meldet. Die Prüfbereitschaft nutzt denselben Helfer. Ein Test hält fest, dass beide übereinstimmen. - Gruppierung wie bisher, aber über die aufgelösten Werte. Zwei Völker aus derselben Packung am selben Tag stehen in einer Zeile.
- Layout unverändert. Fußnote neu: „Lieferant, Beleg und Ch.-B. stammen aus der erfassten Packung.“
5.4 Etikett-Pflichtangaben
Aufgelöster Inhalt. Alle Text-Elemente der Vorlage, Platzhalter eingesetzt für die konkrete Abfüllung. Sortiert nach Position (oben, dann links) und mit Leerzeichen verbunden.
Normalisieren: Kleinbuchstaben, Unicode NFKC, jedes Zeichen außer Buchstaben und Ziffern wird zu einem Leerzeichen. So passt „Bühlweg 10, 86450 Altenmünster“ auch zu „Bühlweg 10 · 86450 Altenmünster“ oder einem Zeilenumbruch.
Abgeschnittener Text zählt nicht. Passt der Text eines Elements nicht in seine Box (pdfkit heightOfString bei Elementbreite größer als Elementhöhe + 0,5 pt), trägt das Element nichts zur Prüfung bei. Es gibt die Warnung TEXT_CLIPPED. Eine nur abgeschnitten vorhandene Pflichtangabe fehlt also.
| Code | Art | Erfüllt, wenn | Norm |
|---|---|---|---|
LEGAL_NAME |
Pflicht | ein Wort enthält „honig“ | LMIV Art. 9 Abs. 1 lit. a; HonigV § 3, Anlage 1 |
NET_QUANTITY |
Pflicht | „{Glasgröße} g“ steht im Text; bei vollen Kilogramm auch „{n} kg“; Dezimalkomma | LMIV Art. 9 Abs. 1 lit. e, Art. 23, Anhang IX |
BEST_BEFORE |
Pflicht | „mindestens haltbar bis“ und das MHD der Abfüllung (TT.MM.JJJJ, führende Null egal) | LMIV Art. 9 Abs. 1 lit. f, Art. 24, Anhang X |
LOT |
Pflicht | die Losnummer der Abfüllung | LKV; Art. 18 VO (EG) 178/2002 |
ORIGIN |
Pflicht | SiteSettings.originCountry |
HonigV § 3 |
FOOD_BUSINESS |
Pflicht | foodBusinessName und foodBusinessAddress sind gesetzt und beide im Text |
LMIV Art. 8, Art. 9 Abs. 1 lit. h |
LOT_PREFIX |
Warnung | vor der Losnummer steht das Wort „L“ | LKV |
X_HEIGHT |
Warnung | jedes Element mit einer Pflichtangabe hat eine x-Höhe ≥ minXHeightMm |
LMIV Art. 13 Abs. 2, 3, Anhang IV |
NET_QUANTITY_HEIGHT |
Warnung | das Element mit der Füllmenge hat eine Versalhöhe ≥ Regel aus netQuantityDigitRules |
Fertigpackungsverordnung / RL 76/211/EWG (Annahme) |
TEXT_CLIPPED |
Warnung | kein Element mit Pflichtangabe ist abgeschnitten | – |
Schriftmetriken kommen aus den eingebauten PDF-Schriften (AFM: x-Höhe, Versalhöhe je Schnitt). Nichts davon ist hartkodiert. Formel: Höhe in mm = Schriftgröße in pt × Metrik / 1000 × 25,4 / 72. Beispiele (Helvetica, x-Höhe 523, Versalhöhe 718):
- 6 pt → x-Höhe 1,11 mm → Warnung. 7 pt → 1,29 mm → in Ordnung.
- 500-g-Glas, Regel 4 mm → Füllmenge ab 15,8 pt.
netQuantityDigitRules (Vorgabe in der Migration, Annahme – vom Inhaber zu prüfen): [{"maxGrams":50,"minMm":2},{"maxGrams":200,"minMm":3},{"maxGrams":1000,"minMm":4},{"maxGrams":null,"minMm":6}].
Ergebnis (LabelCheckResult): { printable: boolean; gaps: LabelFinding[]; warnings: LabelFinding[]; sample: boolean }, mit LabelFinding = { code; title; detail; elementIds: string[] }. Titel auf Deutsch, kurz: „Füllmenge fehlt“, „Text zu klein (x-Höhe 1,1 mm)“. sample: true heißt: Prüfung mit Beispielwerten, weil keine Abfüllung gewählt ist.
Druck. renderLabelsPdf ruft zuerst die Prüfung auf.
- Pflichtlücke und kein Probedruck →
409 { code: 'LABEL_PARTICULARS_MISSING', gaps }. - Probedruck (
?proof=true) → PDF mit diagonalem Aufdruck „MUSTER – nicht verkaufsfähig“ in 50 % Grau auf jedem Etikett. - Jeder Druck schreibt ein Audit
PRINTamBottlingBatch:{ templateId, copies, proof, gapCodes, warningCodes }. So ist belegt, mit welcher Vorlage welches Los gedruckt wurde.
5.5 Behördenpaket
Bestandsbild (HerdSnapshot, pure Funktion buildHerdSnapshot in src/compliance/herd-snapshot.ts):
interface HerdSnapshot {
version: 1;
takenAt: string; // ISO
apiaries: {
apiaryId: string; name: string; address: string | null; location: string | null;
latitude: number | null; longitude: number | null;
livingColonies: number; byType: Partial<Record<ColonyType, number>>;
}[];
totalLiving: number;
}
Gezählt werden lebende Völker über LIVING_COLONY_WHERE (Annahme 21) an aktiven Ständen.
Bezugsstand ist die jüngste SENT-Anzeige der Art REGISTRATION oder HERD_CHANGE (nach sentOn). TSK-Zählnachweise zählen nicht: Sie gehen an einen anderen Empfänger.
Abweichung (herdDeviation(baseline, current, settings)):
noticeOnApiaryChange: ein Stand neu, aufgegeben, oder Adresse, Ort bzw. Koordinaten geändert.noticeOnColonyCountChange:totalLivinganders. Ein Umsetzen zwischen Ständen ändert die Summe nicht und löst darum nichts aus. Das PDF zeigt die Verteilung trotzdem.- Ohne Bezugsstand gibt es keine Abweichung, sondern den Punkt „Erstanzeige fehlt“.
Lebenszyklus einer Anzeige:
mark-sent reopen
DRAFT ───────────────▶ SENT ───────────────▶ DRAFT
│
└── cancel ──▶ CANCELLED
- Anlegen:
REGISTRATIONnur ohne Bezugsstand.HERD_CHANGEnur mit Bezugsstand.TSK_REPORTbrauchtyear. MitsentOnim Anlegen entsteht die Anzeige gleich alsSENT. Das ist der Weg „bereits angezeigt am …“ für den Altbestand. - Neu zählen ersetzt Bestandsbild und Summe, nur im Entwurf.
- Mehrfach-Sicherheit: Anlegen sperrt die
ComplianceSettings-Zeile (SELECT … FOR UPDATE) als Mutex. Dazu kommtopenKeyals nullbares@unique. Ein zweiter Entwurf derselben Art und desselben Jahres ergibt 409 mit der ID des vorhandenen.
TSK-Zählung. Die tägliche Auswertung erzeugt am Stichtag TskReferenceDayReached. Der Handler legt den Entwurf an und zählt sofort. Liegt der Zähltag nach dem Stichtag (Job ausgefallen, Nachholen bis 7 Tage), steht im PDF: „Gezählt am {countedAt}, nicht am Stichtag. Bitte prüfen.“ Gibt es für das Jahr schon eine SENT-Zählung, passiert nichts. Fehlt der Entwurf ganz (mehr als 7 Tage Ausfall oder neu eingeführt), zeigt die Prüfbereitschaft den Punkt trotzdem. Der Knopf „Jetzt zählen“ legt ihn an. Völkerarten in tskExcludedColonyTypes zählen nicht.
5.6 Sperrbezirk
- Aktiv ist eine Sperre, wenn
startsOn≤ Tag ≤endsOngilt (oderendsOnleer ist). „Tag“ ist der Berliner Kalendertag (berlinToday(), Inhaber-Entscheidung 2026-10-15; nicht UTC). Es gibt keinendbUtcDay. - Gemeinsamer Helfer
assertNoActiveRestriction(tx, apiaryIds, day, override)insrc/compliance/restricted-zone.ts. Er wirft409 { code: 'RESTRICTED_ZONE', message, restrictionIds, restrictions }.
| Vorgang | Wirkung |
|---|---|
Volk umsetzen (move) |
Sperre, wenn Quell- oder Zielstand gesperrt ist |
| Volk anlegen im gesperrten Stand | Sperre (Hineinbringen) |
| Stand in „Volk bearbeiten“ korrigieren | Sperre, wenn alter oder neuer Stand gesperrt ist |
| Vereinigen | Sperre, wenn die Völker an verschiedenen Ständen stehen und einer davon gesperrt ist |
| Ableger bilden | keine Sperre: Der Ableger bleibt am Stand |
| Ernte am gesperrten Stand | nur Warnung im Formular (Inhaber 2026-10-13) |
- Ausnahme: Feld
restrictionOverrideReason(10–500 Zeichen) anCreateColonyDto,UpdateColonyDto,MoveColonyDtoundMergeColonyDto.- Nur mit
compliance:manage. Der Service prüft das überPermissionResolverService.getPermissions, sonst 403 - immer, auch wenn gar keine Sperre aktiv ist, und gleich zu Beginn der Anfrage. Ohne Passwort-Abfrage (Inhaber-Entscheidung 2026-10-15). - Die Begründung landet im Audit-Eintrag und in
ColonyApiaryHistory.notes(„Ausnahme Sperrbezirk: …“). - Das Feld wird nicht am Volk gespeichert. Für die Null-Regel gilt
nullwie weggelassen.update-dto-null-handling.spec.tshält das fest.
- Nur mit
- Gleichzeitigkeit: Die Völker-Pfade sperren zuerst die Stände (
FOR SHARE, aufsteigend), dann das Volk; ändert ein Volk zwischen ungesperrtem Lesen und Sperre seinen Stand, antwortet der Server 409 „Der Stand wurde gerade geändert. Bitte neu laden.“ (kein automatischer Neuversuch).ApiaryRestrictionsServicesperrt die StändeFOR UPDATE. - CSV-Import: Völker, die der Import in einem gesperrten Stand anlegen würde, werden als Zeilenfehler gemeldet (Sperrbezirk-Meldung, nichts geschrieben); der Import hat keine Ausnahme.
- Lesen:
GET /apiary-restrictionsundGET /apiaries/:id/restrictionsliefern Behörde, Aktenzeichen, Notiz und Ersteller nur mitcompliance:read; mit nurapiaries:readgibt esid,apiaryId,disease,startsOn,endsOn. - Erfassen:
POST /apiary-restrictionsmitapiaryIds(1–50) legt eine Zeile je Stand in einer Transaktion an. Beendet wird eine Sperre überendsOn. Gelöscht wird nichts.
5.7 Prüfbereitschaft
ComplianceReadinessService.evaluate(now) lädt die Daten begrenzt und ruft je Bereich eine pure Regel in src/compliance/rules/*.rule.ts auf:
type ReadinessSeverity = 'PFLICHT' | 'PRUEFEN';
interface ReadinessItem {
key: string; // z. B. 'treatment-log-gaps'
area: 'STAMMDATEN' | 'BEHOERDEN' | 'TIERARZNEIMITTEL' | 'ETIKETTEN' | 'ERNTE';
severity: ReadinessSeverity;
subjectId: string; // z. B. Jahr, Packung, Vorlage; 'global' bei Einzelpunkten
title: string; // Deutsch, ein kurzer Satz
detail: string;
legalBasis: string | null;
acknowledgeable: boolean;
fingerprint: string; // sha256 der für den Befund maßgeblichen Werte
subjects: { type: string; id: string; label: string; missing?: string[] }[]; // höchstens 50, Rest in `more`
more: number;
}
- Gesamtstatus:
READY, wenn kein Punkt offen ist, sonstOPEN. Offen ist jeder Punkt ohne gültige Quittung. - Quittieren geht nur bei
acknowledgeable: true. Die Notiz ist Pflicht (mindestens 10 Zeichen). Der Fingerabdruck muss zum aktuell berechneten passen, sonst 409. Ändern sich die Daten, ist der Punkt wieder offen. Eine Quittung kann zurückgenommen werden (revokedAt). - Links: Das Backend liefert
subjectsmit Typ und ID, keine UI-Pfade. Das Frontend bildet sie an einer Stelle auf Seiten ab (lib/compliance/fix-links.ts). - Begrenzung: Behandlungen der letzten 6 Kalenderjahre, Ernten der letzten 2 Jahre (
HARVEST_WITHDRAWAL_LOOKBACK_YEARS = 2; Annahme: Honig wird innerhalb von 2 Jahren verkauft), nur benötigte Spalten.
| Schlüssel | Bereich | Schwere | Offen, wenn | Beheben | Norm | quittierbar |
|---|---|---|---|---|---|---|
holder-data |
Stammdaten | Pflicht | Name oder Anschrift des Bienenhalters leer | Stammdaten | Art. 108 VO (EU) 2019/6 | nein |
food-business-data |
Stammdaten | Pflicht | Name oder Anschrift des Lebensmittelunternehmers leer | Stammdaten („Vom Bienenhalter übernehmen“) | LMIV Art. 8 | nein |
vet-office |
Stammdaten | Prüfen | Veterinäramt (Empfänger) leer | Einstellungen | – | nein |
registration |
Behörden | Pflicht | keine SENT-Erstanzeige |
Anzeige erstellen oder „bereits angezeigt am …“ | BienSeuchV § 1a | nein |
herd-change |
Behörden | Pflicht | Abweichung zum Bezugsstand laut Einstellung | Änderungsanzeige | BienSeuchV § 1a (Umfang: Annahme) | nein |
tsk-report |
Behörden | Pflicht | Stichtag des Jahres erreicht, keine SENT-Zählung; nach tskReportDueDay mit „überfällig“ |
Zählung öffnen oder anlegen | Landesrecht Bayern (Annahme) | nein |
restriction-active |
Behörden | Pflicht | Stand mit aktiver Sperre | Verfügung ansehen; Ende eintragen, wenn aufgehoben | BienSeuchV § 10 | nein |
treatment-log-gaps |
Tierarzneimittel | Pflicht | Bestandsbuch-Zeilen eines Jahres mit Lücke (ein Punkt je Jahr; Behandlungen aus derselben lückenhaften Packung erscheinen einmal als Packung) | Behandlung oder Packung bearbeiten | Art. 108 VO (EU) 2019/6 | nein |
treatment-location-uncertain |
Tierarzneimittel | Prüfen | Zeile mit * (Stand am Behandlungstag unsicher) |
Stand bestätigen (setzt Treatment.apiaryId) |
Art. 108 (Identifizierung) | nein |
lot-gaps |
Tierarzneimittel | Pflicht | Packung ohne Lieferant oder Beleg | Packung bearbeiten | Art. 108 | nein |
lot-prescription |
Tierarzneimittel | Pflicht | verschreibungspflichtiges Mittel, Packung ohne Tierarzt | Packung bearbeiten | Art. 108 | nein |
medicine-withdrawal-missing |
Tierarzneimittel | Prüfen | aktiver Katalogeintrag ohne Wartezeit | Mittel bearbeiten | Annahme 10 | nein |
lot-expired-stock |
Tierarzneimittel | Prüfen | Verfall überschritten, Rest > 0 | Entsorgung buchen | – | ja |
lot-negative |
Tierarzneimittel | Prüfen | Rest < 0 | Inventur oder fehlenden Erwerb erfassen | – | nein |
treatment-expired-lot |
Tierarzneimittel | Prüfen | Behandlung nach Verfall der Packung | prüfen | – | ja |
label-template-gaps |
Etiketten | Pflicht | aktive Vorlage mit Pflichtlücke, geprüft gegen die jüngste Abfüllung mit Restbestand (sonst die jüngste, sonst Beispielwerte) | Designer | LMIV, HonigV, LKV | nein |
label-template-form |
Etiketten | Prüfen | aktive Vorlage mit Warnung | Designer | LMIV Art. 13; Fertigpackungsverordnung (Annahme) | ja |
harvest-withdrawal |
Ernte | Prüfen | Ernte in einem aktiven Wartezeit-Fenster eines beteiligten Volks (Logik von getWithdrawalStatus) |
prüfen, Notiz | Wartezeit laut Behandlung (Annahme 10) | ja |
6. Ereignisse, Handler, tägliche Auswertung
6.1 Neue Ereignistypen
DomainEventPayloads, CATALOG und AggregateType (+ 'Apiary', 'Compliance') in src/domain-events/domain-event-types.ts werden erweitert. Payloads enthalten nur IDs und Werte, Daten als YYYY-MM-DD.
| Ereignis | Payload | Aggregat | Erzeuger |
|---|---|---|---|
ColonyCreated |
{ apiaryId } |
Colony | ColoniesService.create, split (für den Ableger) |
ColonyMoved |
{ fromApiaryId, toApiaryId } |
Colony | move, update mit Standwechsel |
ApiaryChanged |
{ change: 'CREATED' | 'DEACTIVATED' | 'REACTIVATED' | 'LOCATION_CHANGED' } |
Apiary | ApiariesService.create, deactivate, reactivate, update (Adresse, Ort, Koordinaten) |
TskReferenceDayReached |
{ year, referenceDay } |
Compliance, ID tsk:<year> |
Job daily-evaluation |
ApiariesService schreibt dafür wie die anderen Fachservices: Datensatz, Audit (auditLog.record(…, tx)) und Ereignis in einer Transaktion. Der Webhook colony.created bleibt im Altpfad (Rückgrat Task 13). Die neuen Ereignisse gehen nicht an Webhooks: Behördendaten verlassen das System nicht.
6.2 Handler (src/compliance/compliance.handlers.ts)
| Handler-ID | Ereignisse | Wirkung (nur über tx) |
|---|---|---|
compliance.herd-change |
ColonyCreated, ColonyMoved, ColonyEnded, ColonyStatusChanged (nur beim Wechsel lebend ↔ beendet), ApiaryChanged |
Bestandsbild und Abweichung berechnen. Bei Abweichung: Meldung HINWEIS „Bestand geändert: jetzt 14 Völker an 3 Ständen. Angezeigt: 12 an 2. Anzeige ans Veterinäramt prüfen.“ an compliance:manage. |
compliance.tsk-reference-day |
TskReferenceDayReached |
Entwurf TSK_REPORT anlegen (Mutex, openKey), sofort zählen, Meldung WARNUNG „TSK-Stichtag 01.01.2027: 14 Völker gezählt. Bitte prüfen und melden.“ |
- Dubletten: Neuer Helfer
NotificationsService.recordForPermissionOnce(permission, input, tx). Er überspringt Nutzer, die zurelatedType = 'ComplianceItem'und derselbenrelatedId(herd-change,tsk-report:2027) schon eine ungelesene Meldung haben. Derselbe Helfer passt für die geplante Dublettensperre bei gescheiterten Jobs (Rückgrat Task 15, Carry-forward). - Handler-Vertrag: keine Netzwerk-Clients; der Wächter
handler-contract.spec.tsdeckt die Datei ab. - Zustellweg: Die Meldungen sind normale
Notification-Zeilen. Später kann sie der Telegram-Bot-Kanal aus der T4-Spec zustellen. In T6 ist dafür nichts zu bauen.
6.3 Tägliche Auswertung
Neue Regel in src/jobs/daily-evaluation.rules.ts:
- Für das laufende und das vorige Jahr gilt Tag =
<Jahr>-<tskReferenceDay>. - Liegt der Tag im Nachholfenster (
inCatchUpWindow), entstehtTskReferenceDayReachedmitdailyDedupeKey('TskReferenceDayReached', 'tsk:<Jahr>', Tag). Es entsteht höchstens einmal. - Das Ergebnis im
JobRunbekommtTskReferenceDayReached: n. - Abgelaufene Packungen und endende Sperren brauchen kein Ereignis. Die Prüfbereitschaft rechnet sie live.
7. API
Alle Pfade unter /api/v1. Anlege-Endpunkte, die Formulare und die Offline-Warteschlange wiederholen können, tragen @Idempotent() (Annahme 11). Update-DTOs folgen der Null-Regel (Annahme 20).
| Methode | Pfad | Recht | Zweck |
|---|---|---|---|
| GET | /veterinary-medicines?active= |
veterinary-medicines:read |
Katalog mit Gesamtrest und nächstem Verfall |
| GET | /veterinary-medicines/suggestions |
veterinary-medicines:read |
Übernahme-Vorschläge aus eigenen Behandlungen |
| GET | /veterinary-medicines/:id |
veterinary-medicines:read |
Eintrag mit Packungen und Rest |
| POST | /veterinary-medicines |
veterinary-medicines:create |
anlegen, idempotent |
| PATCH | /veterinary-medicines/:id |
veterinary-medicines:update |
bearbeiten |
| POST | /veterinary-medicines/:id/deactivate, /reactivate |
veterinary-medicines:update |
Historie statt Löschen |
| GET | /veterinary-medicine-lots?medicineId=&selectable=true |
veterinary-medicines:read |
Packungen, älteste zuerst (für das Behandlungsformular) |
| GET | /veterinary-medicine-lots/:id |
veterinary-medicines:read |
Packung mit Bewegungsbuch |
| POST | /veterinary-medicine-lots |
veterinary-medicines:create |
Erwerb erfassen, idempotent |
| PATCH | /veterinary-medicine-lots/:id |
veterinary-medicines:update |
bearbeiten |
| POST | /veterinary-medicine-lots/:id/movements |
veterinary-medicines:update |
{ type: 'CORRECTION', actualRemaining } oder { type: 'DISPOSAL', quantity, reason }, idempotent |
| POST, PATCH | /treatments, /treatments/bulk, /treatments/:id |
wie bisher | + medicineId, medicineLotId, doseQuantity; PATCH + apiaryId |
| POST | /label-templates/check |
label-templates:read |
Prüfung eines ungespeicherten Entwurfs: Text-Elemente, Maße, minXHeightMm, optional bottlingBatchId |
| GET | /label-templates/:id/check/:bottlingBatchId |
label-templates:read |
Prüfung vor dem Druck |
| GET | /label-templates/:id/print/:bottlingBatchId?startPosition=&proof= |
label-templates:print |
Druck; 409 bei Pflichtlücke ohne proof |
| GET | /compliance/readiness |
compliance:read |
alle Punkte mit Status |
| GET | /compliance/readiness/summary |
compliance:read |
{ status, openCount, firstItemKey } fürs Dashboard |
| GET | /compliance/readiness/report.pdf |
compliance:read |
Prüfbericht |
| POST | /compliance/acknowledgements |
compliance:manage |
{ itemKey, subjectId, fingerprint, note }, idempotent |
| POST | /compliance/acknowledgements/:id/revoke |
compliance:manage |
Quittung zurücknehmen |
| GET, PATCH | /compliance/settings |
compliance:read / compliance:manage |
Einstellungen |
| GET | /authority-notices?kind=&year= |
compliance:read |
Liste |
| GET | /authority-notices/:id, /authority-notices/:id/pdf |
compliance:read |
Anzeige, PDF |
| POST | /authority-notices |
compliance:manage |
{ kind, year?, referenceDate?, sentOn? }, idempotent |
| PATCH | /authority-notices/:id |
compliance:manage |
totalColonies, referenceDate, sentOn, sentVia, reference, notes |
| POST | /authority-notices/:id/recount, /mark-sent, /reopen, /cancel |
compliance:manage |
Zustandswechsel nach 5.5 |
| GET | /apiary-restrictions?active=true |
apiaries:read |
aktive Sperren (für Umsetzen- und Ernte-Dialog) |
| GET | /apiaries/:id/restrictions |
apiaries:read |
Sperren eines Stands |
| POST | /apiary-restrictions |
compliance:manage |
{ apiaryIds, disease, authority?, reference?, startsOn, endsOn?, notes? }, idempotent |
| PATCH | /apiary-restrictions/:id |
compliance:manage |
bearbeiten, beenden |
| POST, PATCH | Völker create, update, move, merge |
wie bisher | + restrictionOverrideReason |
Sperren lesen mit apiaries:read: Wer Völker bewegt, muss die Sperre sehen. Schreiben erfordert compliance:manage.
8. PDFs
Alle PDFs entstehen mit pdfkit und den Helfern aus src/export/pdf-table.ts (PDF_MARGIN, drawTable, formatDay, orDash). Fehlende Pflichtangaben stehen als „FEHLT“ da, wie im Bestandsbuch. Antworten bleiben Cache-Control: no-store.
8.1 Anzeige ans Veterinäramt (REGISTRATION, HERD_CHANGE)
A4 hoch. Quellen: SiteSettings (Halter), ComplianceSettings (Empfänger, Registriernummer), AuthorityNotice.snapshot, Bestandsbild des Bezugsstands.
┌──────────────────────────────────────────────────────┐
│ {holderName} · {holderAddress} │
│ Registriernummer: {registrationNumber | –} │
│ │
│ An {veterinaryOfficeName | FEHLT} │
│ {veterinaryOfficeAddress} │
│ {Ort}, {Datum} │
│ Änderungsanzeige Bienenhaltung (§ 1a BienSeuchV) │
│ Stand der Völker am {referenceDate} │
│ ┌───────────┬────────────────────────┬──────┬───────┐ │
│ │ Stand │ Lage (Adresse/Koord.) │Völker│ vorher│ │
│ ├───────────┼────────────────────────┼──────┼───────┤ │
│ │ {name} │ {address | lat, long} │ 8 │ 6 │ │
│ │ … │ │ │ │ │
│ │ Summe │ │ 14 │ 12 │ │
│ └───────────┴────────────────────────┴──────┴───────┘ │
│ Änderungen seit der Anzeige vom {sentOn}: │
│ – Stand „{name}“ neu – Stand „{name}“ aufgegeben │
│ │
│ _________________________ Unterschrift │
│ Stand der Daten {updatedAt} · erstellt mit ApiCore │
└──────────────────────────────────────────────────────┘
Die Erstanzeige hat die Überschrift „Anzeige der Bienenhaltung (§ 1a BienSeuchV)“ und keine Spalte „vorher“. Koordinaten mit 5 Nachkommastellen, nur wenn keine Adresse da ist.
8.2 Zählnachweis zum TSK-Stichtag (TSK_REPORT)
A4 hoch. Kopf wie 8.1, mit tskMemberNumber. Überschrift „Zählnachweis Bienenvölker zum Stichtag {Datum}“. Tabelle je Stand mit Spalten je gezählter Völkerart und Summe. Darunter „Gezählt am {countedAt}“, gegebenenfalls der Hinweis aus 5.5, und fest: „Ersetzt nicht die Meldung an die Tierseuchenkasse.“
8.3 Bestandsbuch
Layout bleibt. Neu sind die Quellen aus 5.3 und die Fußnote. Ein Bestandsbuch mit vollständigen Daten enthält kein „FEHLT“.
8.4 Probedruck der Etiketten
Wie der normale Druck, plus diagonaler Aufdruck „MUSTER – nicht verkaufsfähig“ je Etikett. Zum Ausrichten am Drucker, nie zum Verkauf.
8.5 Prüfbericht
A4 hoch: Kopf mit Halter und Datum, Gesamtstatus, dann je Bereich die Punkte als Tabelle (Status, Schwere, Titel, Norm, bei Quittungen Notiz, Nutzer, Datum). Für den Kontrolltermin.
9. Oberfläche
Grundsätze: Deutsch, kurze Sätze, eine Hauptaktion je Ansicht (DetailHeader), seltene Felder eingeklappt (FormSection), Felder 44 px. WCAG 2.2 AA:
- Status nie nur über Farbe, immer Symbol plus Wort („Offen“, „Erledigt“).
- Roter Text mit mindestens 4,5:1 Kontrast (Token
destructive, mit T5 geprüft). - Die Prüfliste im Designer meldet Änderungen über
aria-live="polite". - Ein gesperrter Druckknopf bleibt fokussierbar (
aria-disabled) und nennt den Grund überaria-describedby. - Nach einem 409 springt der Fokus auf die Lückenliste.
| Seite / Stelle | Inhalt | Hauptaktion |
|---|---|---|
/compliance „Prüfbereitschaft“ (neu, Nav „Betrieb“, Symbol ShieldCheck, Recht compliance:read) |
Status oben („Prüfbereit“ grün oder „3 offen“ rot). Punkte nach Bereich, jeder offene rot mit Symbol, „Offen“, Schwere-Chip „Pflicht“/„Prüfen“, Titel, eine Zeile Detail, Norm klein. Aufklappbar die betroffenen Datensätze mit „Beheben“. „Erledigt (n)“ eingeklappt. Unter „Mehr“: Bericht als PDF, Bestandsbuch, Einstellungen. | „Nächsten Punkt lösen“ (springt zum ersten offenen Pflicht-Punkt). Alles grün: „Bestandsbuch {Jahr} drucken“. |
/compliance/notices, /compliance/notices/[id] |
Liste; Detail mit drei Schritten „1. PDF öffnen · 2. ans Amt schicken · 3. hier bestätigen“ | je nach Schritt „PDF öffnen“ oder „Als gesendet markieren“ |
/compliance/restrictions |
aktive und beendete Sperren; Formular mit Mehrfachauswahl der Stände, Krankheit über ChoiceGroup (Amerikanische Faulbrut, Kleiner Beutenkäfer, Andere mit Freitext), Verfügung per DocumentUploadCard |
„Sperrbezirk eintragen“ |
/compliance/settings |
Veterinäramt, Registriernummer, Auslöser, TSK-Stichtag, Frist, Mitgliedsnummer; „Erweitert“ eingeklappt: gezählte Völkerarten, Ziffernhöhen. Jede Annahme mit Vermerk „Annahme – bitte prüfen“. | „Speichern“ |
/medicines, /medicines/[id], /medicines/lots/[id] (neu; erreichbar über Lager, Behandlungsformular und Prüfbereitschaft; kein eigener Nav-Eintrag) |
Katalog mit Rest und Verfall; leerer Zustand „Aus deinen Behandlungen übernehmen?“ mit Vorschlägen; Packung mit Bewegungsbuch und Beleg-Upload | Liste: „Mittel anlegen“; Mittel: „Packung erfassen“; Packung: „Inventur“ |
| Behandlung neu und Sammelerfassung | „Mittel“ als RecordSelect aus dem Katalog mit letzter Option „Anderes Mittel“ (dann Freitext wie heute, T3-Vorbelegung bleibt). Bei Katalog: Wirkstoff, Wartezeit, Methode, Dosis vorbelegt mit PrefillNote „aus Katalog“. Packung als RecordSelect („Ch.-B. 4711 · Rest 180 ml · verw. bis 03/2027“), älteste vorgewählt mit Notiz. Dosis im NumberStepper mit Einheit. Statt Lieferant, Beleg, Tierarzt eine Zeile „aus Packung: …“. Über dem Speichern: „Bucht 30 ml je Volk ab (× 6 = 180 ml).“ Temperatur: „Heute 9–17 °C. Laut Katalog 12–25 °C.“ mit Warnsymbol, wenn außerhalb. |
„Speichern“ |
| Etiketten-Designer | neue Komponente LabelCompliancePanel (eigene Datei, der Designer hat schon 695 Zeilen): Liste der Pflichtangaben mit ✓/✗ und Warnungen, Prüfung entprellt 500 ms über POST /label-templates/check (nur Text-Elemente); Knopf „Pflichtangaben-Block einfügen“ setzt einen Block mit allen neuen Platzhaltern in Mindestgrößen ein (Rückgängig wie gewohnt); Feld „Mindest-x-Höhe“ mit Hinweis auf 0,9 mm bei kleinen Gläsern |
„Speichern“ |
| Druckkarte der Abfüllung | prüft vorher; bei Lücken rote Liste mit Links (Stammdaten, Produkt, Designer) | „Etiketten als PDF“, bei Lücken gesperrt mit Grund; daneben „Probedruck (MUSTER)“ |
| Stammdaten & Web (bestehender Tab) | Felder Lebensmittelunternehmer, Knopf „Vom Bienenhalter übernehmen“ (füllt sichtbar, speichert erst beim Speichern) | wie bisher |
| Produkt-Formular | Feld „Bezeichnung auf dem Etikett (z. B. Blütenhonig)“ | wie bisher |
| Umsetzen-Dialog, Volk anlegen, Vereinigen | rote Box „Stand {Name} liegt im Sperrbezirk ({Krankheit}, seit {Datum}). Umsetzen ist gesperrt.“; mit compliance:manage aufklappbar „Ausnahme mit Begründung“ (Textfeld, Rückfrage im ConfirmDialog); fester Hinweis zu BienSeuchV § 5 (2.2) |
„Umsetzen“ (gesperrt, solange die Sperre gilt und keine Ausnahme begründet ist) |
| Stand-Detail | rotes Band bei aktiver Sperre mit Link | wie bisher |
| Ernte-Formular | Warnung bei gesperrtem Stand: „Im Sperrbezirk gelten Auflagen auch für Honig und Waben. Verfügung prüfen.“ | „Speichern“ (nicht gesperrt) |
| Dashboard „Heute“ | eine Zeile „Prüfbereitschaft: 3 offen“ mit Link, nur bei openCount > 0 und compliance:read |
– |
| Lager | Karte „Tierarzneimittel“ mit Link zu /medicines |
– |
10. Fehlerfälle
| Fall | Verhalten |
|---|---|
| Packung reicht nicht für die Dosis | Behandlung wird gespeichert, Rest wird negativ, Prüfbereitschaft „Rest negativ“ |
| Packung abgelaufen | Behandlung wird gespeichert; Formular warnt vorher; Prüfbereitschaft „Prüfen“ |
| Zwei Inventuren gleichzeitig auf einer Packung | FOR UPDATE auf die Packung; die zweite rechnet mit dem neuen Rest |
| Einheit eines Mittels mit Packungen ändern | 409 „Einheit ist festgelegt, sobald Packungen erfasst sind“ |
Nutzer mit treatments:create, ohne veterinary-medicines:read |
Formular zeigt nur „Anderes Mittel“ (Freitext); kein Fehler |
| Etikett ohne Produkt an der Abfüllung | legalName leer; erfüllt nur, wenn ein fester Text „honig“ enthält |
| Stammdaten Lebensmittelunternehmer leer | Pflichtlücke FOOD_BUSINESS mit Link zu den Stammdaten |
| Druck mit Pflichtlücke | 409 LABEL_PARTICULARS_MISSING mit gaps; Probedruck bleibt möglich |
| Check-Anfrage mit Bild-Elementen | DTO nimmt nur Text-Elemente an; 400 bei anderem Typ. Das Frontend filtert vorher. |
| Umsetzen in oder aus gesperrtem Stand | 409 RESTRICTED_ZONE mit Stand, Krankheit, Beginn |
| Ausnahme ohne Recht / zu kurze Begründung | 403 / 400 |
| Zweiter Entwurf derselben Anzeige | 409 mit existingId |
recount auf SENT, mark-sent auf CANCELLED |
409 |
| Änderungsanzeige ohne Erstanzeige | 409 „Erst die Erstanzeige erfassen“ |
Stichtag ungültig (02-30, 02-29, 1-1) |
400; 02-29 bewusst abgelehnt, weil es nicht jedes Jahr gibt |
Job daily-evaluation fällt länger als 7 Tage aus |
kein Ereignis; Prüfbereitschaft zeigt tsk-report trotzdem; „Jetzt zählen“ legt den Entwurf an |
Handler compliance.tsk-reference-day läuft doppelt |
Mutex + openKey: ein Entwurf; SENT vorhanden → nichts |
| Handler scheitert | Wiederholung nach Rückgrat-Regel; nach 8 Versuchen FAILED in „Verwaltung > System“ |
| Quittung mit veraltetem Fingerabdruck / für einen Pflicht-Punkt | 409 / 400 |
| Dokument zu neuer Art, Objekt fehlt | 404 wie bisher |
| Veterinäramt nicht eingetragen | PDF druckt „FEHLT“; Prüfbereitschaft vet-office |
11. Sicherheit
| Thema | Maßnahme |
|---|---|
| Anschriften, Koordinaten, Belege, Aktenzeichen | nur mit compliance:read bzw. veterinary-medicines:read; nie in öffentlichen Endpunkten, nie im Shop-Feed, nie in Webhooks oder Ereignis-Payloads. Test in public.service.spec.ts: Herkunftsseite enthält keine Sperr- oder Anzeigedaten. |
| Ausnahme vom Sperrbezirk | eigenes Recht, Pflicht-Begründung, Audit in derselben Transaktion |
| Rechte | fail-closed PermissionsGuard je Endpunkt; API_CLIENT bekommt keine T6-Rechte |
| Eingaben | class-validator mit MaxLength für jeden Freitext (Namen 200, Anschriften 300, Begründungen 500, Notizen 2000); Mengen IsPositive bzw. endlich; apiaryIds höchstens 50, IDs im cuid-Zeichensatz wie in WithdrawalStatusQueryDto |
| Check-Endpunkt | nur Text-Elemente, Body-Limit 5 MB wie global; Standard-Rate-Limit 100/min reicht bei 500 ms Entprellung |
Logs, lastError |
nur loggableError(), nie Payload oder Freitext |
| PDFs | no-store; Dateinamen ohne Nutzereingaben (anzeige-<id>.pdf) |
| Gesetzesdaten | Wächter: Der Seed legt keine Tierarzneimittel an. |
12. Tests
TDD wie im Rückgrat-Plan: erst der fehlschlagende Test.
Unit (Jest)
medicine-ledger.spec.ts:summarizeLot rounds to thousandths,treatmentBookingDelta books the full dose on create,moves the booking when the lot changes,reverses the booking when dose or lot is cleared,books nothing when nothing changed.treatments.service.spec.ts:create books the dose from the lot in the same transaction,rejects a lot of another medicine,rejects supplier text together with a lot,takes the product name from the catalogue,never fills withdrawal or ingredient from the catalogue,bulk books one movement per colony.treatment-log.spec.ts:takes supplier, proof, batch and vet from the lot,prints the dose with unit when the amount text is empty,requires the vet only for prescription-only medicines,prints FEHLT exactly where treatmentLogGaps reports a gap.label-particulars.spec.ts:passes a label with all particulars,reports each missing particular by code,matches the address across line breaks and separators,flags a static "500 g" on a 250 g bottling,treats clipped text as missing,Helvetica 6 pt is x-height 1.11 mm (warning), 7 pt is 1.29 mm,needs 4 mm digits for 500 g with the default rules,warns on a lot without a leading L.label-templates.service.spec.ts:print refuses with 409 and the gap list,proof print adds the watermark and writes a PRINT audit entry.restricted-zone.spec.ts:active on the start and end day, inactive the day after,refuses moving into and out of a restricted apiary,override needs compliance:manage and a reason,split inside the apiary is allowed,merge across apiaries is refused.herd-snapshot.spec.ts:no deviation without changes,count change triggers only when enabled,moving between apiaries keeps the total,new, removed or relocated apiary triggers,no baseline means registration, not herd change.authority-notices.service.spec.ts:one open draft per kind and year,create with sentOn is SENT at once,recount only in DRAFT,herd change needs a baseline.daily-evaluation.rules.spec.ts:emits TskReferenceDayReached on the configured day,catches up within 7 days exactly once,covers the previous year's day in early January.compliance.handlers.spec.ts:herd-change notifies each permitted user once while unread,ignores a status change within living states,tsk handler creates one draft and skips a year already sent.rules/*.rule.spec.ts: je Prüfpunkt offen, erledigt und quittiert.compliance-acknowledgements.service.spec.ts:refuses a stale fingerprint,refuses a PFLICHT item,a revoked acknowledgement reopens the item.
e2e gegen die Test-DB
test/veterinary-medicines.e2e-spec.ts: Packung → Behandlung → Rest; Korrektur bucht gegen; Sammelerfassung; Rechte (STAFF darf Packung anlegen, READONLY nicht, API_CLIENT nichts).test/treatment-log.e2e-spec.ts(Erweiterung): vollständiger Katalog und Packung ergebentreatmentLogGapsleer für alle Zeilen.test/label-compliance.e2e-spec.ts: 409 → Stammdaten ergänzen → 200 und AuditPRINT.test/restricted-zone.e2e-spec.ts: 409 beim Umsetzen; Ausnahme mit Recht ergibt 200, Audit und Historien-Notiz.test/authority-notices.e2e-spec.ts: Volk anlegen →drainDomainEvents→ eine Meldung; zweites Volk → keine zweite, solange ungelesen; Anzeige anlegen und senden → Prüfpunktherd-changeweg; PDFapplication/pdf.test/daily-evaluation.e2e-spec.ts(Erweiterung): Job zweimal am Stichtag → ein Ereignis, ein Entwurf.test/compliance-permissions.e2e-spec.ts: Vergabe der Migration je Rolle.
Wächter
- Backup-Wächter (bestehend) für alle neuen Modelle.
update-dto-null-handling.spec.tsfür alle neuen Update-DTOs undrestrictionOverrideReason.handler-contract.spec.tsdecktcompliance.handlers.tsab.- Neu
seed-no-medicines.spec.ts: Der Seed legt keine Tierarzneimittel an. - Platzhalter: Backend- und Frontend-Liste
PLACEHOLDER_KEYSsind gleich (Vitest liest die Swagger-Enum-Werte aus einer Fixture).
Frontend (Vitest + RTL)
- Prüfbereitschaft: offene Punkte mit Symbol und Wort, Hauptaktion zeigt auf den ersten Pflicht-Punkt, Quittieren verlangt eine Notiz.
- Druckkarte: Hauptknopf
aria-disabledmit Grund, Probedruck möglich. - Designer-Panel: Live-Liste,
aria-live, „Pflichtangaben-Block einfügen“. - Behandlungsformular: Vorbelegung „aus Katalog“, älteste Packung vorgewählt, Lieferant-Felder ausgeblendet, Buchungszeile.
- Umsetzen-Dialog: Sperre sichtbar; Ausnahme nur mit Recht.
- Mittel-Liste: leerer Zustand mit Vorschlägen.
Manuell (MP): Desktop 1366 px und Handy 390 px: Prüfbereitschaft, Behandlung mit Packung, Etikett drucken, Umsetzen im Sperrbezirk.
13. Rollout
- Voraussetzungen: Rückgrat-Runbook bis Schritt 3 live (Worker, CronJobs). T3 Task 4 ist gemergt.
- Image mit den drei Migrationen. Alles additiv: neue Tabellen, nullbare Spalten, Enum-Werte. Der alte Pod läuft während des RollingUpdates mit dem alten Code weiter.
- Checkliste für den Inhaber, direkt nach dem Rollout (ab dann sperrt der Etikettendruck bei Lücken; Probedruck bleibt):
- Stammdaten: Lebensmittelunternehmer eintragen.
- Produkte: Bezeichnung eintragen.
- Etikettenvorlagen im Designer öffnen, „Pflichtangaben-Block einfügen“ oder Lücken schließen.
- Einstellungen: Veterinäramt, Registriernummer. Stichtag und Frist der Tierseuchenkasse prüfen (Annahme).
- Katalog aus den bisherigen Behandlungen übernehmen, aktuelle Packungen erfassen.
- Erstanzeige: „bereits angezeigt am …“ mit dem echten Datum.
- Prüfbereitschaft durchgehen, bis sie grün ist.
- Rückweg: Ein Rollback des Codes lässt die neuen Tabellen ungenutzt. Es geht nichts verloren.
- Doku im selben Commit: ARCHITECTURE bekommt vier neue Annahmen, jede mit der nächsten freien Annahme-Nummer zum Zeitpunkt des Commits (die Nummerierung teilen sich mehrere Teilprojekte): Katalog und Packungen, Etikett-Pflichtangaben, Behördenpaket und Sperrbezirk, Prüfbereitschaft. Außerdem Abschnitt 10 um die neuen Ereignisse;
docs/pruefung-2026-09.mdum einen Abschnitt „Prüfbereitschaft“; TASKS.md.
14. Aufwand
| Paket | PT |
|---|---|
| Datenmodell, drei Migrationen, Rechte, Backup | 0,5 |
| Katalog, Packungen, Bewegungsbuch, Vorschläge (Backend) | 1,0 |
| Behandlung: Prüfung, Buchung, Sammelerfassung, Bestandsbuch, Lücken-Helfer | 0,75 |
| Etikett: Platzhalter, Prüfregeln, Schriftmetriken, Druck-Sperre, Probedruck, Audit | 1,0 |
| Sperrbezirk: Modell, Wächter-Helfer in vier Völker-Pfaden, Ausnahme | 0,5 |
| Behördenpaket: Einstellungen, Anzeigen, zwei PDFs, Ereignisse, zwei Handler, Job-Regel | 1,25 |
| Prüfbereitschaft: 18 Regeln, Service, Quittungen, Bericht | 0,75 |
| Frontend: sechs neue Seiten, Behandlungsformular, Designer-Panel, Druckkarte, Dialoge, Banner | 2,5 |
| e2e, Wächter, Doku | 0,5 |
| Summe | 8,75 (≈ 9) |
Der Fahrplan nannte etwa 6 PT. Die Wanderung (≈ 1 PT) entfällt. Dazu kommen:
- das Bewegungsbuch der Packungen (+1,5),
- die Form-Prüfung der Etiketten mit Schriftmetriken (+1),
- Quittungen und Prüfbericht (+0,5),
- die Ereignis-Erzeuger für Stände und Völker samt Sperrbezirk-Prüfung (+0,75).
Ein Lagerartikel statt Packungen hätte etwa 7,5 PT ergeben. Der Inhaber hat sich am 2026-10-13 für die Packungen entschieden.
15. Offene Fragen an den Inhaber
Keine. Alle fünf Fragen hat der Inhaber am 2026-10-13 entschieden. Die Entscheidungen stehen in der Tabelle in Abschnitt 1:
- Packungen mit eigenem Bewegungsbuch,
- Druck-Sperre nur bei fehlender oder abgeschnittener Pflichtangabe,
- eigene Felder „Lebensmittelunternehmer“,
- Ausnahme vom Sperrbezirk nur mit Begründung, Recht und Audit,
- Ernte im Sperrbezirk nur mit Warnung.
Offen bleiben nur die Annahmen aus Abschnitt 3. Sie sind Einstellungen, ändern also nichts am Bau. Der Inhaber prüft sie beim Rollout (Abschnitt 13, Schritt 3).