Files
ApiCore/docs/superpowers/specs/2026-10-13-t6-compliance-design.md
T
Nick SzaunigandClaude Opus 5.5 cdd30cf3ee feat: platform backbone and field capture complete (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
  harvests, colonies, wintering, stock and bottling record domain events
  in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
  lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
  daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices

Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 11:36:38 +02:00

67 KiB
Raw Blame History

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:

  1. Eine Behandlung aus einer vollständig erfassten Packung ergibt eine Bestandsbuch-Zeile ohne „FEHLT“.
  2. GET /label-templates/:id/print/:bottlingBatchId liefert bei fehlender Pflichtangabe 409 mit Lückenliste und sonst das PDF.
  3. Eine geänderte Völkerzahl erzeugt nach dem Abarbeiten der Ereignisse genau eine ungelesene Meldung je berechtigtem Nutzer.
  4. GET /compliance/readiness ist 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 Funktion summarizeLot in src/veterinary-medicines/medicine-ledger.ts.
  • Rundung: gemeinsamer Helfer roundToThousandths in src/common/quantity.ts. roundKg in bottling.service.ts nutzt ihn ebenfalls, statt eine eigene Kopie zu haben.
  • Vorschlag der Packung: fifoOrder aus src/stock/stock-ledger.ts wird wiederverwendet (expiresOn als Haltbarkeit, purchasedOn als Datum). Wählbar sind Packungen mit Rest > 0 und die bereits gewählte Packung der Behandlung.
  • Bewegungen:
    • TREATMENT entsteht 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. reason ist 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 allocateSale beim Shop).
  • Änderungssperren: VeterinaryMedicine.unit ist änderbar, solange keine Packung existiert. lot.medicineId ist änderbar, solange keine Bewegung existiert. Sonst 409. Alle anderen Felder sind jederzeit änderbar (Null-Regel, Annahme 20).
  • Übernahme-Vorschläge: GET /veterinary-medicines/suggestions liefert die eigenen, noch nicht katalogisierten Handelsnamen aus Treatment.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) in export/treatment-log.ts. Er liefert die fehlenden Pflichtfelder: Lieferant, Beleg, Menge, Wartezeit, und Tierarzt nur bei medicine.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 PRINT am BottlingBatch: { 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: totalLiving anders. 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: REGISTRATION nur ohne Bezugsstand. HERD_CHANGE nur mit Bezugsstand. TSK_REPORT braucht year. Mit sentOn im Anlegen entsteht die Anzeige gleich als SENT. 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 kommt openKey als 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 ≤ endsOn gilt (oder endsOn leer ist). „Tag“ ist der UTC-Tag wie in der täglichen Auswertung (utcDay).
  • Gemeinsamer Helfer assertNoActiveRestriction(tx, apiaryIds, day, override) in src/compliance/restricted-zone.ts. Er wirft 409 { code: 'RESTRICTED_ZONE', message, restrictionIds }.
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) an CreateColonyDto, UpdateColonyDto, MoveColonyDto und MergeColonyDto.
    • Nur mit compliance:manage. Der Service prüft das über PermissionResolverService.getPermissions, sonst 403.
    • 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 null wie weggelassen. update-dto-null-handling.spec.ts hält das fest.
  • Erfassen: POST /apiary-restrictions mit apiaryIds (1–50) legt eine Zeile je Stand in einer Transaktion an. Beendet wird eine Sperre über endsOn. 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, sonst OPEN. 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 subjects mit 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 zu relatedType = 'ComplianceItem' und derselben relatedId (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.ts deckt 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), entsteht TskReferenceDayReached mit dailyDedupeKey('TskReferenceDayReached', 'tsk:<Jahr>', Tag). Es entsteht höchstens einmal.
  • Das Ergebnis im JobRun bekommt TskReferenceDayReached: 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 über aria-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 ergeben treatmentLogGaps leer für alle Zeilen.
  • test/label-compliance.e2e-spec.ts: 409 → Stammdaten ergänzen → 200 und Audit PRINT.
  • 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üfpunkt herd-change weg; PDF application/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.ts für alle neuen Update-DTOs und restrictionOverrideReason.
  • handler-contract.spec.ts deckt compliance.handlers.ts ab.
  • Neu seed-no-medicines.spec.ts: Der Seed legt keine Tierarzneimittel an.
  • Platzhalter: Backend- und Frontend-Liste PLACEHOLDER_KEYS sind 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-disabled mit 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

  1. Voraussetzungen: Rückgrat-Runbook bis Schritt 3 live (Worker, CronJobs). T3 Task 4 ist gemergt.
  2. 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.
  3. Checkliste für den Inhaber, direkt nach dem Rollout (ab dann sperrt der Etikettendruck bei Lücken; Probedruck bleibt):
    1. Stammdaten: Lebensmittelunternehmer eintragen.
    2. Produkte: Bezeichnung eintragen.
    3. Etikettenvorlagen im Designer öffnen, „Pflichtangaben-Block einfügen“ oder Lücken schließen.
    4. Einstellungen: Veterinäramt, Registriernummer. Stichtag und Frist der Tierseuchenkasse prüfen (Annahme).
    5. Katalog aus den bisherigen Behandlungen übernehmen, aktuelle Packungen erfassen.
    6. Erstanzeige: „bereits angezeigt am …“ mit dem echten Datum.
    7. Prüfbereitschaft durchgehen, bis sie grün ist.
  4. Rückweg: Ein Rollback des Codes lässt die neuen Tabellen ungenutzt. Es geht nichts verloren.
  5. 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.md um 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).