fix: T4 browser-check findings, interval on season state, T6 plan refresh
- Honey batch create omits an empty batch number (was 400); product form
labels the legacy total weight, detail shows jar weight and shelf life;
bottling hints next to their fields, values cleared on product change,
post-save water-content wording; withdraw reason placeholder not
selectable; honey batch actions offline-disabled with reason, 44 px.
- Offline signal: one slow request no longer flips the app offline; two
consecutive timeouts do, any HTTP answer clears it, aborts never count.
- Phenology sheet as a side card on desktop with focus return; confirm
dialogs as alertdialog; step-up texts in du-form ("Das Passwort stimmt
nicht."); backup import gives its disabled reason; missing event labels
plus a guard test; profile buttons 44 px.
- GET /phenology/state returns intervalDays via the shared
inspectionIntervalOf (autopilot, digest, season state use one source).
- T6 plan aligned with the code after T4, plus owner decisions of
2026-10-15 (Berlin day for restrictions, COMPLIANCE category, override
without step-up).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
9760ce154f
commit
7a1480138d
63 files changed
+1076
-251
No files matched your search
@@ -6,7 +6,7 @@
|
||||
|
||||
**Architecture:** Neue Module `veterinary-medicines` (Katalog, Packungen, Bewegungsbuch), `apiary-restrictions` (Sperrbezirk) und `compliance` (Einstellungen, Anzeigen, Handler, Prüfbereitschaft mit einer reinen Regel-Datei je Prüfpunkt). Folgen laufen über Ereignisse des Rückgrats (`ColonyCreated`, `ColonyMoved`, `ApiaryChanged`, `TskReferenceDayReached`) und Handler, die nur über `tx` schreiben. Die Prüfbereitschaft wird live aus den Fachdaten berechnet; gespeichert werden nur Quittungen. Die Etikett-Prüfung und das Bestandsbuch nutzen je eine gemeinsame reine Funktion für Druck, Designer und Prüfbereitschaft. Rechtswerte sind Einstellungen mit Vermerk „Annahme – vom Inhaber zu prüfen“, nie Code.
|
||||
|
||||
**Tech Stack:** NestJS 11, Prisma 7.9 (`@prisma/adapter-pg`), PostgreSQL 18, pdfkit 0.19, Jest 30 (Unit + e2e gegen Test-DB), Next.js 16.3.1 App Router, React 19.2, TanStack Query 5, Vitest 4 + RTL. Keine neue Abhängigkeit.
|
||||
**Tech Stack:** NestJS 11, Prisma 7.9 (`@prisma/adapter-pg`), PostgreSQL (Betrieb 18.6; die Test-DB auf Port 5433 ist **17**, also keine Funktion nutzen, die erst ab 18 existiert), pdfkit 0.19, Jest 30 (Unit + e2e gegen Test-DB), Next.js 16.3.1 App Router, React 19.2, TanStack Query 5, Vitest 4 + RTL. Keine neue Abhängigkeit.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-13-t6-compliance-design.md` (bindend, mit Inhaber-Entscheidungen vom 2026-10-13). Bausteine aus `docs/superpowers/plans/2026-10-13-plattform-rueckgrat.md` (Rückgrat), `docs/superpowers/plans/2026-10-13-t3-erfassung-am-stand.md` (T3) und `docs/superpowers/plans/2026-10-13-t4-logik-ketten.md` (T4, läuft vor T6).
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
|
||||
**Voraussetzungen (nicht neu bauen, genau so nutzen)**
|
||||
- Rückgrat Tasks 1–26 außer 17 und 25 sind im Arbeitsbaum. Genutzt werden: `DomainEventsService.record/recordAll(tx, …)`, `@OnDomainEvent(types, '<consumer>.<name>')` (Handler-ID `^[a-z0-9-]+\.[a-z0-9-]+$`), `dailyDedupeKey`, `utcDay`, `inCatchUpWindow`, `DAILY_EVALUATION_RULES` (`src/jobs/daily-evaluation.rules.ts`), `AuditLogService.record(entry, tx)`, `lockRowForUpdate`/`lockRowsForShare` (`src/prisma/row-lock.ts`), `runBulk` (`src/common/bulk/run-bulk.ts`, Callback `(tx, entry, colony, bulkId)`), `lockColony`/`colonyTransitionEvents` (`src/colonies/colony-events.ts`), `LIVING_COLONY_WHERE`/`isLivingColonyStatus`, `loggableError`, `PermissionResolverService.getPermissions(userId)`, `drainDomainEvents` (`test/utils/domain-events.ts`), `@Idempotent()`, `IsOptionalNotNull`, `optionalDate`, `fifoOrder` (`src/stock/stock-ledger.ts`), `PDF_MARGIN`/`drawTable`/`fillWidth`/`formatDay`/`orDash` (`src/export/pdf-table.ts`).
|
||||
- T3 ist gemergt: `TreatmentsService.createBulk` mit der gemeinsamen privaten Aufbereitung `treatmentRow` + `recordIn` (T6 erweitert genau diese Funktionen), `GET /treatments/last-by-product`, Frontend `PrefillNote`, `NumberStepper`, `RecordSelect`, `useCaptureMutation`, `applyPrefill`.
|
||||
- T4 ist gemergt: Meldungen je Nutzer (`NotificationsService.record/recordForUsers/recordForPermission(permission | permissions[], FanOutNotificationInput, tx)`, `NotificationCategory`, `assertAppLink`), `LOCKABLE_TABLES` als Einträge `{ table, key }`, `src/treatments/withdrawal-window.ts` (`withdrawalWindowOf`, `isInWindow`, `WithdrawalWindow`), `TimeEventsStep` über `DAILY_EVALUATION_RULES`, `berlinToday()` (`src/common/dates.ts`), `ColonySplit`.
|
||||
- T3 ist gemergt: `TreatmentsService.createBulk` (über `runBulk`) mit der gemeinsamen Aufbereitung `treatmentRow(dto, apiaryId, actingUserId)` (**freie Modulfunktion** in `treatments.service.ts`, kein Klassenmitglied; sie spreizt `...dto`) und der privaten Methode `recordIn(tx, data, actingUserId, bulkId?)` (T6 erweitert genau diese beiden), `GET /treatments/last-by-product`, Frontend `PrefillNote` (`src/components/treatments/prefill-note.tsx`), `NumberStepper`, `RecordSelect`, `useCaptureMutation`, `applyPrefill`. `create` sperrt das Volk mit `lockRowForUpdate(tx, 'Colony', …)`; `update` kennt `apiaryId` heute nicht und spreizt `...dto`.
|
||||
- T4 ist gemergt: Meldungen je Nutzer (`NotificationsService.record(input, tx?)`, `recordForUsers(userIds, input, tx?)`, `recordForPermission(permission | permissions[], FanOutNotificationInput, tx?)`; `NotificationCategory` ist ein Prisma-Enum `TASK_DUE | DIGEST | SUGGESTION | WITHDRAWAL | SEASON | SYSTEM`; `assertAppLink` und `notificationDedupeKey` in `src/notifications/notification-link.ts`; die Empfängerabfrage ist die **private** Methode `recipients(db, keys, userIds)`; jede neue Zeile plant über `NotificationDeliveryPlanner.planIn` die Telegram-Zustellung in derselben Transaktion), `LOCKABLE_TABLES` als Einträge `{ table, key }`, `src/treatments/withdrawal-window.ts` (`withdrawalWindowOf(t)` → `WithdrawalWindow { treatmentId, colonyId, product, fromDay, untilDay }` oder `null`, `isInWindow(window, day)`: `fromDay <= day < untilDay`, `withdrawalUntilDay`), `src/withdrawal/withdrawal-text.ts` (`lastBlockedDay`, `withdrawalRangeText`) und `WithdrawalCheckService.conflictsForHarvests` (`src/withdrawal/`), `TimeEventsStep` (`src/jobs/steps/time-events.step.ts`) über `DAILY_EVALUATION_RULES`, Berliner Tage in `src/common/dates.ts` (`berlinToday`, `berlinDayOf`, `dayToDate`, `addDays`, `daysBetween`, `addMonthsClamped`; Annahme 41), `DbClient` (`src/prisma/db-client.ts`), `ColonySplit`, `Product.shelfLifeMonths` (Migration `20261015100200_t4_product_shelf_life`, T6 baut es nicht), `roundKg` (exportiert aus `src/honey-batches/honey-plausibility.ts`), Frontend `StepUpDialog`, `PrefillStatus`, `describeSaveError`/`saveErrorText` (`src/lib/honey-batches/errors.ts`), `refreshOn401: false`, `ConfirmDialog` mit `triggerDescribedBy`/`triggerDisabled`, `ToggleChip` mit `disabled`, `NEVER_CACHED_API_PATHS` (`public/sw-routes.js`), `DOMAIN_EVENT_LABELS` (`src/lib/system/labels.ts`).
|
||||
- Tagesauswertung: Eine Regel ist `DailyRule<T>` mit `occurrences(db: PrismaService, today: Date, ctx: { seasons, day })` (kein `evaluate`); `ctx.day` ist der **Berliner** Tag des Laufs, `today = dayToDate(ctx.day)`; `CATCH_UP_DAYS = 7` ist exportiert; neue Typen kommen in die Union `DailyEventType`.
|
||||
- Alle Backend-Pfade relativ zu `ApiCore-Backend/`, alle Frontend-Pfade relativ zu `ApiCore-Frontend/`, sofern nicht anders angegeben.
|
||||
|
||||
**Migrationen (Zeitstempel fest, alle nach der höchsten T4-Migration `20261015110000_t4_telegram`; alles additiv)**
|
||||
@@ -28,6 +29,7 @@
|
||||
| 20261016100100 | `t6_label_particulars` | `SiteSettings.foodBusinessName`, `foodBusinessAddress`; `Product.legalName`; `LabelTemplate.minXHeightMm Float @default(1.2)` + CHECK 0,5–5; `AuditAction` + `PRINT` |
|
||||
| 20261016100200 | `t6_authority_compliance` | `ComplianceSettings` (Vorgabe `netQuantityDigitRules` = `[{"maxGrams":50,"minMm":2},{"maxGrams":200,"minMm":3},{"maxGrams":1000,"minMm":4},{"maxGrams":null,"minMm":6}]`); `AuthorityNotice` mit Enums; `ApiaryRestriction` + CHECK `"endsOn" IS NULL OR "endsOn" >= "startsOn"`; `ComplianceAcknowledgement`; `DocumentEntityType` + `AUTHORITY_NOTICE`, `APIARY_RESTRICTION`; Rechte `compliance:*` samt Vergabe |
|
||||
|
||||
- Geprüft am 2026-10-15 (HEAD 9760ce1): Die neueste Migration ist `20261015110000_t4_telegram`, alle drei T6-Stempel liegen dahinter. T4 hat weder `AuditAction.PRINT` noch eine der drei `DocumentEntityType`-Werte noch Rechte `veterinary-medicines:*`/`compliance:*` angelegt (`Product.shelfLifeMonths` gibt es seit T4, T6 ergänzt dort nur `legalName`). Doppelt oder widersprüchlich ist nichts. Hängt ein weiterer Strang vor T6 eine Migration an, rücken die Stempel hinter dessen höchsten.
|
||||
- Erzeugt mit `npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --script` gegen die Test-DB, von Hand nachgearbeitet (Kopfkommentar wie `20261012100000_calendar_sync_claims`). Nie `db push`, nie `migrate dev`. `ALTER TYPE … ADD VALUE` läuft in der Migrationstransaktion; kein neuer Wert wird in derselben Migration benutzt.
|
||||
- Test-DB migrieren (Git Bash, in `ApiCore-Backend/`): `export DATABASE_URL="$(grep '^DATABASE_URL=' .env.test | cut -d= -f2- | tr -d '"')" && npx prisma migrate deploy && npx prisma generate`. Läuft Task 1 in einer Welle mit anderen Tasks, wendet er seine Migrationen zuerst an.
|
||||
|
||||
@@ -44,26 +46,27 @@
|
||||
- Belege, Rechnungsnummern, Anschriften, Koordinaten und Aktenzeichen nur mit `compliance:read` bzw. `veterinary-medicines:read`; nie in öffentlichen Endpunkten, Shop-Feed, Webhooks oder Ereignis-Payloads.
|
||||
|
||||
**Fachwerte aus der Spec**
|
||||
- Rest einer Packung = `lot.quantity + Σ movement.quantity`, gerundet auf Tausendstel (`roundToThousandths`, `src/common/quantity.ts`; `roundKg` in `bottling.service.ts` nutzt ihn). Immer berechnet, nie gespeichert. Negativer Rest ist erlaubt und wird in der Prüfbereitschaft gemeldet. Bewegungen werden nie geändert oder gelöscht, Fehler werden gegengebucht.
|
||||
- Rest einer Packung = `lot.quantity + Σ movement.quantity`, gerundet auf Tausendstel (`roundToThousandths`, `src/common/quantity.ts`; `roundKg` in `honey-plausibility.ts` und `bottling.service.ts` nutzen ihn). Immer berechnet, nie gespeichert. Negativer Rest ist erlaubt und wird in der Prüfbereitschaft gemeldet. Bewegungen werden nie geändert oder gelöscht, Fehler werden gegengebucht.
|
||||
- Behandlung (Spec 5.2): Packung plus `supplier`/`purchaseProof`/`batchNumber`/`prescribingVet` → 400 „Diese Angabe kommt aus der Packung“; `medicineId` und `product` weichen ab (ohne Groß-/Kleinschreibung) → 400 „Mittel und Katalogeintrag passen nicht zusammen“; Katalogeintrag inaktiv → Anlegen 400; Packung eines anderen Mittels → 400; `doseQuantity` ≤ 0 → 400; `medicineId: null` bei gesetzter Packung → 400. Der Server füllt `activeIngredient`, `withdrawalPeriodDays`, `method`, `doseQuantity` nie still nach; nur `product` wird zum Handelsnamen.
|
||||
- Etikett (Spec 5.4): Pflicht `LEGAL_NAME`, `NET_QUANTITY`, `BEST_BEFORE`, `LOT`, `ORIGIN`, `FOOD_BUSINESS` sperren den Druck (409 `LABEL_PARTICULARS_MISSING`, `{ gaps }`); Warnungen `LOT_PREFIX`, `X_HEIGHT`, `NET_QUANTITY_HEIGHT`, `TEXT_CLIPPED` sperren nie. Probedruck `?proof=true` immer möglich, Aufdruck „MUSTER – nicht verkaufsfähig“ in 50 % Grau. Jeder Druck schreibt Audit `PRINT` am `BottlingBatch`: `{ templateId, copies, proof, gapCodes, warningCodes }`. Höhe in mm = Schriftgröße in pt × AFM-Metrik / 1000 × 25,4 / 72 (Helvetica x-Höhe 523, Versalhöhe 718); 6 pt → 1,11 mm Warnung, 7 pt → 1,29 mm in Ordnung; 500-g-Glas mit Regel 4 mm → Füllmenge ab 15,8 pt. `minXHeightMm` Vorgabe 1,2, Bereich 0,5–5.
|
||||
- Sperrbezirk (Spec 5.6): aktiv bei `startsOn` ≤ Tag ≤ `endsOn` (oder `endsOn` leer), Tag = UTC-Tag der DB-Uhr. Sperre bei `move` (Quell- oder Zielstand), `create` im Stand, Stand-Korrektur über `update` (alter oder neuer Stand), `merge` über verschiedene Stände. Keine Sperre bei `split`. Ernte nur Warnung. `restrictionOverrideReason` 10–500 Zeichen, nur mit `compliance:manage` (sonst 403), landet im Audit und in `ColonyApiaryHistory.notes` („Ausnahme Sperrbezirk: …“), wird nie am Volk gespeichert. Fehler: 409 `{ code: 'RESTRICTED_ZONE', message, restrictionIds }`. `apiaryIds` 1–50.
|
||||
- Sperrbezirk (Spec 5.6): aktiv bei `startsOn` ≤ Tag ≤ `endsOn` (oder `endsOn` leer), Tag = UTC-Tag der DB-Uhr (Spec 5.6 sagt ausdrücklich UTC, begründet mit der Tagesauswertung, die seit T4 aber Berliner Tage nutzt, Annahme 41; **bleibt UTC bis zur Entscheidung des Inhabers**, siehe Aktualisierung, Frage 1; der Tag steht nur in `dbUtcDay`, ein Wechsel ist eine Zeile). Alle **anderen** Tage der T6 sind Berliner Tage (`berlinToday()`, `berlinDayOf`, `addDays` aus `src/common/dates.ts`): Vorgabe von `referenceDate`/`sentOn`, Stichtag, „überfällig“, `today` der Prüfbereitschaft. Sperre bei `move` (Quell- oder Zielstand), `create` im Stand, Stand-Korrektur über `update` (alter oder neuer Stand), `merge` über verschiedene Stände. Keine Sperre bei `split`. Ernte nur Warnung. `restrictionOverrideReason` 10–500 Zeichen, nur mit `compliance:manage` (sonst 403), landet im Audit und in `ColonyApiaryHistory.notes` („Ausnahme Sperrbezirk: …“), wird nie am Volk gespeichert. Fehler: 409 `{ code: 'RESTRICTED_ZONE', message, restrictionIds }`. `apiaryIds` 1–50.
|
||||
- Behördenpaket (Spec 5.5): Bezugsstand = jüngste `SENT`-Anzeige der Art `REGISTRATION`/`HERD_CHANGE` (nach `sentOn`); `REGISTRATION` nur ohne Bezugsstand, `HERD_CHANGE` nur mit („Erst die Erstanzeige erfassen“, 409); `TSK_REPORT` braucht `year`; mit `sentOn` im Anlegen entsteht `SENT` gleich. Neu zählen und Löschen nur im Entwurf; `openKey` = `REGISTRATION` | `HERD_CHANGE` | `TSK_REPORT:<Jahr>` nur bei `DRAFT`; zweiter Entwurf → 409 `{ code: 'DRAFT_EXISTS', existingId }`. Mutex: Zeile `ComplianceSettings` per `lockRowForUpdate`. Einstellungen: `noticeOnColonyCountChange`/`noticeOnApiaryChange` Vorgabe `true`, `tskReferenceDay` Vorgabe `01-01` (Format `MM-TT`, `02-29` und `1-1` → 400), `tskReportDueDay` leer, `tskExcludedColonyTypes` leer. Lebende Völker über `LIVING_COLONY_WHERE` an aktiven Ständen. Nachholfenster der Tagesauswertung 7 Tage.
|
||||
- Prüfbereitschaft (Spec 5.7): 18 Prüfpunkte in fünf Bereichen (`STAMMDATEN`, `BEHOERDEN`, `TIERARZNEIMITTEL`, `ETIKETTEN`, `ERNTE`), Schwere `PFLICHT` | `PRUEFEN`; Quittieren nur bei `acknowledgeable`, Notiz 10–2000 Zeichen, Fingerabdruck muss zum aktuell berechneten passen (sonst 409), Quittung zurücknehmbar; Behandlungen der letzten 6 Kalenderjahre, Ernten der letzten 2 Jahre (`HARVEST_WITHDRAWAL_LOOKBACK_YEARS = 2`); `subjects` höchstens 50, Rest in `more`. Jeder offene Punkt ist rot, Schwere steht als Wort („Pflicht“/„Prüfen“) daneben.
|
||||
- Meldungen: höchstens eine ungelesene je Thema und Nutzer (`relatedType = 'ComplianceItem'`, `relatedId` `herd-change` bzw. `tsk-report:<Jahr>`). Texte exakt: „Bestand geändert: jetzt {n} Völker an {m} Ständen. Angezeigt: {a} an {b}. Anzeige ans Veterinäramt prüfen.“ (Priorität `HINWEIS`), „TSK-Stichtag {TT.MM.JJJJ}: {n} Völker gezählt. Bitte prüfen und melden.“ (`WARNUNG`).
|
||||
- Meldungen: höchstens eine ungelesene je Thema und Nutzer (`relatedType = 'ComplianceItem'`, `relatedId` `herd-change` bzw. `tsk-report:<Jahr>`), Kategorie `SYSTEM` (T4 hat keine Behörden-Kategorie; Telegram bekommt sie nur, wenn der Nutzer `SYSTEM` unter seinen Telegram-Kategorien gewählt hat, Vorgabe sind `TASK_DUE` und `DIGEST`; Texte unter 600 Zeichen, ohne Adresse, also im Telegram-Format; der Link steht in `link`, nicht im Text). Texte exakt: „Bestand geändert: jetzt {n} Völker an {m} Ständen. Angezeigt: {a} an {b}. Anzeige ans Veterinäramt prüfen.“ (Priorität `HINWEIS`), „TSK-Stichtag {TT.MM.JJJJ}: {n} Völker gezählt. Bitte prüfen und melden.“ (`WARNUNG`).
|
||||
- Eingaben: `MaxLength` für jeden Freitext (Namen 200, Anschriften 300, Begründungen 500, Notizen 2000); Mengen `IsPositive` und endlich; IDs im cuid-Zeichensatz (`/^[A-Za-z0-9_-]+$/`).
|
||||
- Keine Arzneimittel- oder Rechtsdaten im Code oder Seed: Der Katalog startet leer; Wächter `seed-no-medicines.spec.ts`.
|
||||
|
||||
**Projektregeln**
|
||||
- Sicherheit hat Priorität 1. Konsistenz ist oberstes Gesetz: ein Muster, gemeinsame Helfer, keine Kopien. Pflicht-`tx` als erster Parameter, optionaler `tx` als letzter.
|
||||
- Datensatz, Audit und Ereignis in **einer** Transaktion. Handler sind reine DB-Schreiber (nur `tx`, keine Netzaufrufe, `handler-contract.spec.ts` deckt `compliance.handlers.ts` ab). Zeitvergleiche in SQL gegen `now()`.
|
||||
- **Read-modify-write nur unter Zeilensperre:** `before` wird **innerhalb** der Transaktion nach `lockRowForUpdate(tx, table, id)` gelesen. Alle neuen Tabellen kommen in `LOCKABLE_TABLES` – in **einem** Task (Task 1). Sperr-Reihenfolge = Reihenfolge der Map; die T6-Tabellen stehen hinter den bisherigen (`Colony` → … → `Treatment` → `VeterinaryMedicine` → `VeterinaryMedicineLot` → `ComplianceSettings` → `AuthorityNotice` → …). Bestehende Services, die T6 anfasst und `before` außerhalb lesen (`SiteSettingsService.update`, `ProductsService.update`, `LabelTemplatesService.update`, `ApiariesService.*`), stellt der jeweilige Task auf die Sperre um.
|
||||
- **Read-modify-write nur unter Zeilensperre:** `before` wird **innerhalb** der Transaktion nach `lockRowForUpdate(tx, table, id)` gelesen. Alle neuen Tabellen kommen in `LOCKABLE_TABLES` – in **einem** Task (Task 1). Sperr-Reihenfolge = Reihenfolge der Map. **Offene Entscheidung des Inhabers, hier umgesetzt: `Apiary` sperrt vor `Colony`.** Task 1 zieht `Apiary` an die erste Stelle der Map (heute steht es hinter `NotificationPreference`); die übrigen T6-Tabellen stehen hinter den bisherigen (`Apiary` → `Colony` → … → `Treatment` → … → `NotificationDelivery` → `LabelTemplate` → `SiteSettings` → `VeterinaryMedicine` → `VeterinaryMedicineLot` → `ComplianceSettings` → `AuthorityNotice` → `ApiaryRestriction` → `ComplianceAcknowledgement`; `Product` steht schon in der Map). Die Völker-Pfade, die Stände und Völker zusammen sperren, nehmen zuerst die Stände (`lockRowsForShare(tx, 'Apiary', ids)`, aufsteigend), dann das Volk (Task 8). `ApiaryRestrictionsService` sperrt die betroffenen Stände mit `lockRowsForUpdate`, damit eine Sperre und ein laufendes Umsetzen sich hintereinander einreihen. Bestehende Services, die T6 anfasst und `before` außerhalb lesen (`SiteSettingsService.update`, `ProductsService.update`, `LabelTemplatesService.update`, `ApiariesService.*`), stellt der jeweilige Task auf die Sperre um (`LabelTemplatesService.update` liest schon in der Transaktion; prüfen, ob `before` nach der Sperre kommt).
|
||||
- Jedes Feld bleibt bearbeitbar (Null-Regel, Annahme 20). Neue oder geänderte Update-DTOs stehen in einem Null-Vertrags-Spec (Task 1 liefert dafür `describeNullContracts`; jede T6-Task legt ihre Verträge in eine **eigene** Spec-Datei, damit parallele Tasks nicht dieselbe Datei ändern).
|
||||
- Worker und Jobs nutzen den resolver-only `JobPermissionsModule`, nicht `PermissionsModule`: Kein Provider eines Moduls, das `WorkerModule` importiert, darf `PermissionResolverService` injizieren (die Prüfung von `restrictionOverrideReason` liegt darum in `ColoniesService`). Jedes Modul mit Handlern wird von `AppModule` **und** `WorkerModule` importiert.
|
||||
- UI deutsch, kurze Sätze, ein Hauptknopf je Ansicht (`DetailHeader`), seltene Felder eingeklappt (`FormSection`), Felder 44 px; Zustand nie nur über Farbe (Symbol + Wort); roter Text mit ≥ 4,5:1 (Token `destructive`); WCAG 2.2 AA; `aria-live="polite"` für Live-Listen; ein gesperrter Druckknopf bleibt fokussierbar (`aria-disabled` + `aria-describedby`); nach einem 409 springt der Fokus auf die Lückenliste. Code-Bezeichner, Log- und Fehlertexte (außer Nutzertexten) englisch.
|
||||
- Fehler und Logs nur über `loggableError()`, nie Payload oder Freitext. PDFs mit `Cache-Control: no-store`, Dateinamen ohne Nutzereingaben (`anzeige-<id>.pdf`, `pruefbericht.pdf`).
|
||||
- TDD: zuerst der fehlschlagende Test; Testnamen englisch. **Agents committen nicht.** Jeder Task endet mit „Verifikation“, „kein Commit“ und einer Dateiliste an den Orchestrator.
|
||||
- Backend-Checks: `npm run typecheck && npx eslint "{src,test}/**/*.ts" && npm test -- --ci`; einzelne e2e-Datei `npx jest --config ./test/jest-e2e.json test/<datei>.e2e-spec.ts --runInBand`. Frontend: vor Next.js-Code `node_modules/next/dist/docs/` lesen (`AGENTS.md`); Checks `npm run lint && npm run typecheck && npx vitest run <pfade>`.
|
||||
- Backend-Checks: `npm run typecheck && npx eslint "{src,test}/**/*.ts" && npm test -- --ci` (nie `npm run lint`, das Skript hat `--fix`); einzelne e2e-Datei `npx jest --config ./test/jest-e2e.json test/<datei>.e2e-spec.ts --runInBand`; alle e2e `npm run test:e2e -- --runInBand` (die Konfiguration hat `maxWorkers: 4` gegen **eine** Test-DB, `testTimeout` 20 s je Test: Tests mit PDF, Sammelerfassung oder `drainDomainEvents` schlank halten). Frontend: vor Next.js-Code `node_modules/next/dist/docs/` lesen (`AGENTS.md`); Checks `npm run lint && npm run typecheck && npx vitest run <pfade>` (`npm run test` = `vitest run`).
|
||||
- **Die Test-DB ist nicht leer** (Reste früherer Läufe, Völker, Stände, Nutzer mit Rechten). E2E-Tests legen ihre Daten selbst an, erwarten nur das, was sie selbst erzeugt haben (Zahlen als Differenz vor/nach, Prüfpunkte über `key` + `subjectId`, Meldungen für die eigenen Testnutzer), und räumen T6-eigene Tabellen (`ComplianceSettings`, `AuthorityNotice`, `ApiaryRestriction`, `ComplianceAcknowledgement`, `VeterinaryMedicine*`) in `beforeEach` per `deleteMany` auf. Sätze wie „ein leeres System“ oder „READY“ gelten für den **eigenen Ausschnitt** (die eigenen Stände/Völker/Vorlagen), nie für die ganze Datenbank; wo der Gesamtstatus gebraucht wird, prüft der Test die Regeln einzeln über `READINESS_RULES` mit einem selbst gebauten `ReadinessContext`.
|
||||
- `ARCHITECTURE.md` und `TASKS.md` trägt der Orchestrator ein; jeder Task liefert seinen Doku-Text im Bericht („Docs“). Neue Annahmen bekommen die **nächste freie Nummer** zum Zeitpunkt des Eintrags.
|
||||
|
||||
---
|
||||
@@ -110,11 +113,12 @@
|
||||
Geteilte Dateien, deshalb je Welle nur ein Task:
|
||||
- `prisma/schema.prisma`, `src/backup/backup.service.ts` (+ Spec), `src/prisma/row-lock.ts` (+ Spec), `src/auth/permissions/permission-catalog.ts`: nur Task 1.
|
||||
- `src/app.module.ts`: 2 (W2), 9 (W3), 12 (W4); `src/worker/worker.module.ts`: nur 11.
|
||||
- `src/domain-events/domain-event-types.ts`, `src/colonies/colonies.service.ts`, `src/apiaries/apiaries.service.ts`: 7 (W2), dann 8 (W3, nur `colonies.service.ts` und DTOs).
|
||||
- `src/domain-events/domain-event-types.ts`, `src/colonies/colonies.service.ts`, `src/apiaries/apiaries.service.ts`: 7 (W2), dann 8 (W3, nur `colonies.service.ts` und DTOs). Von T4 geteilt und hier nur gelesen: `src/inspections/inspections.service.ts` (ruft `colonies.updateIn`; Task 8 ändert es nicht, nur den Vertrag von `updateIn`), `src/autopilot/autopilot.service.ts` (`syncApiaryIn`, Reihenfolge Stand → Volk ist schon die neue; nur Task 1 passt den Spec an), `src/honey-batches/honey-plausibility.ts` (`roundKg`): nur 2.
|
||||
- `src/notifications/notifications.service.ts`, `src/jobs/daily-evaluation.rules.ts` (+ `src/jobs/steps/time-events.step.ts`, falls die Union der Ereignistypen dort gespiegelt wird): nur 11; `src/worker/worker.module.ts`: nur 11.
|
||||
- `src/compliance/compliance.module.ts`: 9 (W3), 11 (W4); `authority-notices.controller.ts`: 9 (W3), 10 (W4); `src/compliance/readiness/readiness-context.ts` und `rules/index.ts`: 12 (W4), 13 (W5).
|
||||
- `src/label-templates/label-templates.service.ts`, `label-element.types.ts`: 5 (W2), 6 (W3).
|
||||
- `src/export/treatment-log.ts`, `src/treatments/*`: nur 3. `src/documents/*`: nur 4. `src/notifications/notifications.service.ts`, `src/jobs/daily-evaluation.rules.ts`: nur 11.
|
||||
- Frontend: `src/app/(app)/nav-config.ts` nur 14; `src/lib/compliance/types.ts` 14 (W5), danach nur lesen; `src/app/(app)/colonies/[id]/*`: 16 (W6, nur `treatment-edit-dialog.tsx` und `page.tsx`) und 19 (W6, nur `colony-actions.tsx`, `colony-edit-card.tsx`) – disjunkte Dateien; `src/app/(app)/storage/page.tsx`: nur 15.
|
||||
- Frontend: `src/app/(app)/nav-config.ts`, `src/lib/system/labels.ts` (+ `labels.spec.ts`), `public/sw-routes.js` + `public/sw.js` (`SW_ROUTES_VERSION`) + `src/lib/offline/sw-routes.spec.ts`, `src/lib/compliance/errors.ts`: nur 14 (18 und 19 lesen nur); `src/lib/documents/types.ts` (`DocumentEntityType` += die drei neuen Arten): nur 15 (W5; 18 und 19 in W6 lesen nur); `src/lib/compliance/types.ts` 14 (W5), danach nur lesen; `src/app/(app)/colonies/[id]/*`: 16 (W6, nur `treatment-edit-dialog.tsx` und `page.tsx`) und 19 (W6, nur `colony-actions.tsx`, `colony-edit-card.tsx`) – disjunkte Dateien; `src/app/(app)/storage/page.tsx`: nur 15.
|
||||
- `ARCHITECTURE.md`, `TASKS.md`, `docs/pruefung-2026-09.md`: nur der Orchestrator.
|
||||
|
||||
---
|
||||
@@ -149,10 +153,15 @@ Geteilte Dateien, deshalb je Welle nur ein Task:
|
||||
export interface NullContract { dto: new () => object; notNull: string[]; clearable: string[] }
|
||||
export function describeNullContracts(title: string, contracts: Record<string, NullContract>): void; // registriert die drei Prüfungen je Vertrag (leerer Patch, null auf notNull -> 400 genau dieses Felds, null auf clearable -> ok)
|
||||
// permission-catalog.ts: veterinary-medicines {read 'Ansehen', create 'Anlegen', update 'Bearbeiten'} Gruppe 'Tierarzneimittel';
|
||||
// compliance {read 'Ansehen', manage 'Verwalten'} Gruppe 'Prüfbereitschaft & Behörden'; sortOrder hinter dem höchsten vorhandenen Wert
|
||||
// row-lock.ts: LOCKABLE_TABLES += Apiary, LabelTemplate, Product, SiteSettings, VeterinaryMedicine, VeterinaryMedicineLot,
|
||||
// ComplianceSettings, AuthorityNotice, ApiaryRestriction, ComplianceAcknowledgement (alle Schlüssel "id"), in dieser Reihenfolge hinter den bisherigen Einträgen
|
||||
// compliance {read 'Ansehen', manage 'Verwalten'} Gruppe 'Prüfbereitschaft & Behörden'; sortOrder 3700 und 3800 (der höchste vorhandene Wert ist 3600, `erp-inventory`)
|
||||
// row-lock.ts (Schlüssel jeweils "id"; ComplianceSettings und SiteSettings haben die Zeile "singleton"):
|
||||
// 1. Apiary rückt an die ERSTE Stelle der Map (vor Colony) und verschwindet aus der alten Position hinter NotificationPreference
|
||||
// 2. LOCKABLE_TABLES += LabelTemplate, SiteSettings, VeterinaryMedicine, VeterinaryMedicineLot, ComplianceSettings, AuthorityNotice,
|
||||
// ApiaryRestriction, ComplianceAcknowledgement, in dieser Reihenfolge hinter NotificationDelivery (Product und Apiary stehen schon in der Map, nicht doppelt eintragen)
|
||||
// neue Reihenfolge: Apiary, Colony, Suggestion, Task, Queen, AutomationRule, BottlingBatch, Feeding, Harvest, Inspection, Product, ProductionBatch,
|
||||
// RawMaterialLot, StockItem, Treatment, VarroaMeasurement, NotificationPreference, PhenologyReport, Extraction, HoneyBatch, NotificationDelivery, <die acht T6-Tabellen>
|
||||
```
|
||||
- **Lock-Reihenfolge Stand → Volk.** Heute sperrt genau ein Pfad Stand und Volk in einer Transaktion: `AutopilotService.syncApiaryIn` (`src/autopilot/autopilot.service.ts`), und zwar schon Stand zuerst, dann alle Völker in einer Anweisung. Er braucht keine Codeänderung; nur der Kommentar der Map und `row-lock.spec.ts` ziehen nach. Kein bestehender Pfad sperrt Volk vor Stand. Die Aufrufer, die T6 für die neue Reihenfolge anpasst (vollständige Liste, alle in Task 8, Task 7 nur für Apiaries): `ColoniesService.create` (Stand vor dem Anlegen, `assertApiaryActive` läuft dabei über `tx` statt vor der Transaktion), `ColoniesService.update` (liest den Stand ungesperrt, sperrt alter und neuer Stand, dann `lockColony`, prüft unter der Sperre, dass der Stand unverändert ist, sonst 409), `ColoniesService.move` (ebenso), `ColoniesService.merge` (Stände aller beteiligten Völker, dann `lockRowsForUpdate` der Völker), `ApiariesService.update/deactivate/reactivate` (nur Stand, Task 7), `ApiaryRestrictionsService.create/update` (nur Stände, `FOR UPDATE`). Nicht geändert, aber zu schützen: `InspectionsService.create` → `ColoniesService.updateIn` läuft schon unter der Volkssperre, der Übernahme-Patch enthält nie `apiaryId` (`colony-takeover.ts`), `updateIn` sperrt darum **keinen** Stand; ein Test hält das fest.
|
||||
|
||||
- [ ] **Schritt 1: Failing Tests**
|
||||
```ts
|
||||
@@ -171,8 +180,10 @@ it('STAFF holds compliance:read and veterinary-medicines:create but not complian
|
||||
it('READONLY holds the two read permissions only', ...);
|
||||
it('API_CLIENT holds none of the five new permissions', ...);
|
||||
// row-lock.spec.ts
|
||||
it('lists the T6 tables after Treatment in lock order', ...);
|
||||
it('locks ComplianceSettings by id', ...);
|
||||
it('keeps the map in lock order: Apiary before Colony, the T6 tables last', ...); // ersetzt den bestehenden Test 'keeps the map in lock order: Colony, Suggestion, ...' (Liste LOCK_ORDER vollständig)
|
||||
it('every table of the map is lockable by id and each appears once', ...); // erfasst das Doppel-Eintragen von Product/Apiary
|
||||
it.each([...T6-Tabellen])('locks %s by "id"', ...); // ergänzt die bestehende it.each-Liste
|
||||
// autopilot.service.spec.ts: der bestehende Test, der ['Apiary','api-1'] vor den Völkern erwartet, bleibt grün (Wächter für die Reihenfolge)
|
||||
// backup.service.spec.ts
|
||||
it('no T6 model is excluded from the backup or listed as legacy-unsealed', ...);
|
||||
it('restores medicine, lot, treatment and movement in foreign-key order', ...);
|
||||
@@ -197,7 +208,7 @@ it('keeps every existing contract green after the move to describeNullContracts'
|
||||
|
||||
**Files**
|
||||
- Create: `src/common/quantity.ts` (+ Spec), `src/veterinary-medicines/medicine-ledger.ts` (+ Spec), `veterinary-medicines.module.ts`, `veterinary-medicines.service.ts` (+ Spec), `veterinary-medicine-lots.service.ts` (+ Spec), `veterinary-medicines.controller.ts`, `veterinary-medicine-lots.controller.ts`, `dto/create-veterinary-medicine.dto.ts`, `update-veterinary-medicine.dto.ts`, `create-lot.dto.ts`, `update-lot.dto.ts`, `create-movement.dto.ts`, `medicine-list-query.dto.ts`, `lot-list-query.dto.ts`, `veterinary-medicines-null-contract.spec.ts`, `src/seed-no-medicines.spec.ts` (Wächter: `prisma/seed.ts` legt keine Tierarzneimittel an)
|
||||
- Modify: `src/bottling/bottling.service.ts` (`roundKg` → `roundToThousandths`), `src/app.module.ts`
|
||||
- Modify: `src/bottling/bottling.service.ts` (die private `roundKg` entfällt, Aufrufer nehmen `roundToThousandths`), `src/honey-batches/honey-plausibility.ts` (T4 hat dort ein **exportiertes** `roundKg` mit gleichem Rumpf angelegt, Aufrufer u. a. `extractions.service.ts`: `export const roundKg = roundToThousandths;`, damit es nicht drei Kopien gibt; die private Kopie in `src/backup/legacy-honey-ledger.ts` bleibt, sie ist ein eingefrorener Altpfad), `src/app.module.ts`
|
||||
- Test: neu `test/veterinary-medicines.e2e-spec.ts`; `test/bottling.e2e-spec.ts` (läuft unverändert grün)
|
||||
- Docs: neue Annahme „Katalog und Packungen“ (eigenes Bewegungsbuch statt `StockItem`/`BatchMovement`, Rest berechnet, negativer Rest erlaubt, Katalog startet leer – Annahme 10).
|
||||
|
||||
@@ -271,7 +282,7 @@ it('a movement can neither be changed nor deleted (no such routes)', ...);
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
- [ ] **Schritt 5: Verifikation**
|
||||
- typecheck, lint, `npm test -- --ci`
|
||||
- `npx jest --config ./test/jest-e2e.json test/veterinary-medicines.e2e-spec.ts test/bottling.e2e-spec.ts test/authorization-coverage.e2e-spec.ts --runInBand` → PASS
|
||||
- `npx jest --config ./test/jest-e2e.json test/veterinary-medicines.e2e-spec.ts test/bottling.e2e-spec.ts test/extractions.e2e-spec.ts test/authorization-coverage.e2e-spec.ts --runInBand` → PASS
|
||||
- kein Commit
|
||||
|
||||
---
|
||||
@@ -342,7 +353,7 @@ it('a complete catalogue entry and lot give a Bestandsbuch PDF without FEHLT and
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx jest src/treatments src/export` und die e2e-Dateien → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- `treatmentRow` bleibt die einzige Datenaufbereitung für `create`, `createBulk` und (Teilmenge) `update`; `resolveMedicineIn` wird aus allen drei Pfaden aufgerufen. Bulk: Prüfung einmal, Buchung je Volk in `recordIn` (nach dem `tx.treatment.create`, vor Audit).
|
||||
- `treatmentRow` (freie Modulfunktion, spreizt `...dto`) bleibt die einzige Datenaufbereitung für `create` und `createBulk`; `update` baut seine Daten weiter selbst (`...dto`, plus neu `apiaryId`), ruft aber wie die beiden anderen `resolveMedicineIn`. Die neuen Spalten `medicineId`/`medicineLotId`/`doseQuantity` fließen über das Spreizen; `supplier`/`purchaseProof`/`batchNumber`/`prescribingVet` bleiben bei gewählter Packung leer (der Server lehnt sie sonst ab, der Aufbereiter legt sie nie still an). Bulk: Prüfung einmal (vor `runBulk`), Buchung je Volk in `recordIn(tx, data, actingUserId, bulkId?)` (nach dem `tx.treatment.create`, vor Audit); `create` sperrt das Volk weiter mit `lockRowForUpdate(tx, 'Colony', …)` und die Packung danach (Reihenfolge der Map: Volk vor Packung).
|
||||
- `update`: nach `lockRowForUpdate(tx, 'Treatment', id)` und gelesenem `before` die betroffenen Packungen (alte und neue) in aufsteigender ID sperren (`lockRowForUpdate(tx, 'VeterinaryMedicineLot', …)`), Summe der bisherigen `TREATMENT`-Bewegungen je Packung lesen, `treatmentBookingDelta` anwenden. Der zusammengeführte Zustand (`before` + DTO) wird geprüft, nicht das DTO allein; `null` leert das Feld.
|
||||
- Buchung: `tx.veterinaryMedicineMovement.create({ type: 'TREATMENT', quantity, treatmentId, lotId, userId })`; Audit der Behandlung bleibt unverändert, `afterData` enthält `medicineLotId`/`doseQuantity`.
|
||||
- `treatment-log.ts`: `buildTreatmentLog` lädt `medicine` und `medicineLot` per `include`, ruft `resolveSourceValues`, Gruppierungsschlüssel über die aufgelösten Werte; `renderSheet` ruft `treatmentLogGaps(row)` und druckt `MISSING` genau bei gemeldeter Lücke (Tierarzt: Spalte „a) Lieferant …“). Fußnote ergänzen. Layout unverändert.
|
||||
@@ -371,7 +382,7 @@ export const DOCUMENT_ENTITY_PERMISSIONS: Partial<Record<DocumentEntityType, { r
|
||||
APIARY_RESTRICTION: { read: 'compliance:read', write: 'compliance:manage' },
|
||||
};
|
||||
DocumentsService.assertEntityAccess(userId: string, entityType: DocumentEntityType, mode: 'read' | 'write'): Promise<void>; // 403 ohne Recht; Arten ohne Eintrag: nichts zusätzlich
|
||||
// findAll/create/readFile/update/delete bekommen `actingUserId`; update prüft alte UND neue Art; assertEntityExists kennt die drei neuen Arten (404 wie bisher)
|
||||
// create/update/delete haben `actingUserId` schon (für Audit und Upload); neu bekommen ihn `findAll` und `readFile` (Controller reicht `actor.id` durch); update prüft alte UND neue Art; assertEntityExists kennt die drei neuen Arten (404 wie bisher)
|
||||
```
|
||||
|
||||
- [ ] **Schritt 1: Failing Tests**
|
||||
@@ -506,6 +517,7 @@ it('the public origin page exposes neither foodBusinessAddress nor legalName', .
|
||||
|
||||
**Interfaces**
|
||||
- Consumes: Task 1, Task 5 (`checkLabel`, `SAMPLE_BOTTLING`, `buildPlaceholderValues`).
|
||||
- Ändert eine bestehende Signatur: `renderLabelsPdf(templateId, bottlingBatchId, startPosition = 1)` wird zu `renderLabelsPdf(templateId, bottlingBatchId, options, actingUserId)`. Aufrufer: `label-templates.controller.ts` und `label-templates.service.spec.ts` (beide in diesem Task anpassen); `buildPlaceholderValues` ist eine freie exportierte Funktion mit einem Parameter (Task 5 gibt ihr den zweiten).
|
||||
- Produces:
|
||||
```ts
|
||||
export class CheckLabelTemplateDto { // POST /label-templates/check; nur Text-Elemente
|
||||
@@ -574,7 +586,11 @@ ColonyCreated: { apiaryId: string };
|
||||
ColonyMoved: { fromApiaryId: string; toApiaryId: string };
|
||||
ApiaryChanged: { change: 'CREATED' | 'DEACTIVATED' | 'REACTIVATED' | 'LOCATION_CHANGED' };
|
||||
TskReferenceDayReached: { year: number; referenceDay: string }; // referenceDay = YYYY-MM-DD des Stichtags
|
||||
// AggregateType += 'Apiary' | 'Compliance'; CATALOG += die vier Typen
|
||||
// AggregateType += 'Compliance' ('Apiary' gibt es seit T4b schon); CATALOG += die vier Typen (der Record über die Union erzwingt es);
|
||||
// Handler-Registrierung: die Erzeuger (ColoniesService, ApiariesService) laufen nur in der API, die Handler (Task 11) liegen in `ComplianceModule`,
|
||||
// das AppModule UND WorkerModule importieren; `@OnDomainEvent` steht auf einem Singleton-Provider (Handler-ID `compliance.herd-change`, `compliance.tsk-reference-day`)
|
||||
// Frontend: DOMAIN_EVENT_LABELS (src/lib/system/labels.ts) bekommt die vier Typen in Task 14: ColonyCreated 'Volk angelegt', ColonyMoved 'Volk umgesetzt',
|
||||
// ApiaryChanged 'Stand geändert', TskReferenceDayReached 'TSK-Stichtag erreicht'
|
||||
// Erzeuger: ColoniesService.create -> ColonyCreated (in der Transaktion); split -> ColonyCreated für den Ableger ZUSÄTZLICH zu ColonySplit (T4);
|
||||
// move und update mit Standwechsel -> ColonyMoved; ApiariesService.create/update/deactivate/reactivate: Datensatz + Audit(tx) + ApiaryChanged in EINER Transaktion
|
||||
```
|
||||
@@ -604,7 +620,7 @@ it('updating only the name of an apiary leaves no ApiaryChanged', ...); // tes
|
||||
- [ ] **Schritt 2:** `npx jest src/domain-events src/colonies src/apiaries src/common/after-commit-usage.spec.ts src/webhooks` und die e2e-Dateien → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- Der Webhook `colony.created` bleibt im Altpfad nach dem Commit. `assertApiaryActive` bleibt vor der Transaktion (Fehlerbild unverändert). Ereignisse werden über `this.events.record(tx, …)`/`recordAll` im selben `$transaction`-Block geschrieben.
|
||||
- `ApiariesService` übernimmt `DomainEventsService` und `lockRowForUpdate` (Apiary); `ApiariesModule` importiert `DomainEventsModule`. `deactivate` zählt lebende Völker nach der Sperre in der Transaktion.
|
||||
- `ApiariesService` hat heute keine Transaktion (`prisma.apiary.update` + `auditLog.record` getrennt, `deactivate` zählt außerhalb): alle vier Schreibpfade werden `$transaction`. Er übernimmt `DomainEventsService` und `lockRowForUpdate(tx, 'Apiary', id)`; `ApiariesModule` importiert `DomainEventsModule`. `deactivate` zählt lebende Völker nach der Sperre in der Transaktion; weil Völker-Pfade den Stand mit `FOR SHARE` halten (Task 8), wartet das Deaktivieren auf ein laufendes Umsetzen in diesen Stand.
|
||||
- Die Vergleichsfelder für `LOCATION_CHANGED` kommen aus `before` (unter Sperre) gegen den gespeicherten Stand.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
- [ ] **Schritt 5: Verifikation**
|
||||
@@ -635,7 +651,7 @@ export function assertNoActiveRestriction(tx: Prisma.TransactionClient, apiaryId
|
||||
export function dbUtcDay(db: DbClient): Promise<string>; // utcDay(SELECT now())
|
||||
// DTOs: restrictionOverrideReason?: string (@IsOptional @IsString @MinLength(10) @MaxLength(500)) an Create/Update/Move/Merge; null gilt wie weggelassen
|
||||
// ApiaryRestrictionsService
|
||||
create(dto: CreateApiaryRestrictionDto, userId: string): Promise<ApiaryRestriction[]>; // apiaryIds 1-50, eine Zeile je Stand, eine Transaktion, Audit je Zeile; @Idempotent
|
||||
create(dto: CreateApiaryRestrictionDto, userId: string): Promise<ApiaryRestriction[]>; // apiaryIds 1-50, eine Zeile je Stand, eine Transaktion, Audit je Zeile; @Idempotent; zuerst `lockRowsForUpdate(tx, 'Apiary', apiaryIds)` (unbekannte Stände: 404 nach der Sperre, alles zurück); `update` sperrt den Stand der Sperre ebenso, dann `ApiaryRestriction`
|
||||
update(id: string, dto: UpdateApiaryRestrictionDto, userId: string): Promise<ApiaryRestriction>; // endsOn >= startsOn auf dem zusammengeführten Stand (400), unter Sperre
|
||||
findActive(): Promise<ActiveRestrictionInfo[]>; findForApiary(apiaryId: string): Promise<ApiaryRestriction[]>;
|
||||
// GET /apiary-restrictions?active=true, GET /apiaries/:id/restrictions (apiaries:read); POST, PATCH (compliance:manage)
|
||||
@@ -657,7 +673,10 @@ it('update with a changed apiary checks old and new apiary, a name change checks
|
||||
it('create in a restricted apiary is refused', ...);
|
||||
it('the override reason is never part of the data written to the colony', ...);
|
||||
it('an override writes the reason into the audit entry and into ColonyApiaryHistory.notes', ...);
|
||||
it('the restriction check runs after the colony row is locked, inside the transaction', ...);
|
||||
it('the restriction check runs inside the transaction after the apiary rows (share, ascending) and then the colony row are locked', ...); // Reihenfolge der Aufrufe auf einem Fake-tx
|
||||
it('update, move and merge answer 409 when the colony changed its apiary between the unlocked read and the lock', ...);
|
||||
it('updateIn called from an inspection takeover locks no apiary', ...);
|
||||
it('a restriction created while a move into that apiary is in flight: the move commits first, the restriction waits (FOR UPDATE vs FOR SHARE)', ...); // e2e, zwei Verbindungen
|
||||
// apiary-restrictions.service.spec.ts
|
||||
it('creates one row per apiary in one transaction and rolls all back when one apiary is unknown', ...);
|
||||
it('rejects more than 50 apiaries and an endsOn before startsOn', ...);
|
||||
@@ -674,7 +693,7 @@ it('READONLY may read restrictions but not create them (403)', ...);
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx jest src/compliance src/apiary-restrictions src/colonies` und die e2e-Dateien → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- In `ColoniesService.create/update/move/merge`: `const { restrictionOverrideReason, ...data } = dto;` – nur `data` geht an Prisma (sonst „Unknown argument“; `UpdateColonyDto` ist per `PickType` gebaut und bekommt das Feld ausdrücklich). Berechtigung einmal pro Anfrage: `canOverride = (await resolver.getPermissions(userId)).has('compliance:manage')`. Prüfung **innerhalb** der Transaktion nach `lockColony` (bei `create` vor `tx.colony.create`), Tag aus `dbUtcDay(tx)`.
|
||||
- In `ColoniesService.create/update/move/merge`: `const { restrictionOverrideReason, ...data } = dto;` – nur `data` geht an Prisma (sonst „Unknown argument“; `UpdateColonyDto` ist per `PickType` gebaut und bekommt das Feld ausdrücklich). Berechtigung einmal pro Anfrage: `canOverride = (await resolver.getPermissions(userId)).has('compliance:manage')`. Prüfung **innerhalb** der Transaktion, **nach den Sperren Stand → Volk** (bei `create` nach der Standsperre, vor `tx.colony.create`), Tag aus `dbUtcDay(tx)`. Sperrmuster (Lock-Reihenfolge, Task 1): (1) Stand-IDs ungesperrt lesen (`move`/`update`: `colony.apiaryId` plus Ziel; `merge`: Stände von Überlebendem und allen vereinigten Völkern), (2) `lockRowsForShare(tx, 'Apiary', ids)` aufsteigend, (3) `lockColony` bzw. `lockRowsForUpdate(tx, 'Colony', …)`, (4) unter der Sperre prüfen, dass `apiaryId` noch der gelesene ist, sonst 409 „Das Volk wurde inzwischen umgesetzt. Bitte neu laden.“, (5) `assertNoActiveRestriction`. `assertApiaryActive` läuft dabei über `tx` (heute über `this.prisma` vor der Transaktion). `updateIn(tx, …)` sperrt keinen Stand selbst: `update()` macht Schritte 1–2 und ruft `updateIn` danach; der Aufruf aus `InspectionsService` (unter Volkssperre, Patch ohne `apiaryId`) bleibt unverändert.
|
||||
- `move`: Quell- und Zielstand; `update`: alter und neuer Stand nur bei Wechsel; `merge`: alle beteiligten Stände, nur wenn mehr als ein verschiedener Stand beteiligt ist; `split`: keine Prüfung.
|
||||
- Override: `notes` der `ColonyApiaryHistory` = `Ausnahme Sperrbezirk: <Begründung>` (bei `update`/`move` vor ein vorhandenes `notes` gestellt), Audit `afterData` bekommt `restrictionOverride: { reason, restrictionIds }`.
|
||||
- `ApiaryRestrictionsModule` injiziert selbst keinen Resolver und wird nicht in `WorkerModule` benötigt.
|
||||
@@ -696,7 +715,7 @@ it('READONLY may read restrictions but not create them (403)', ...);
|
||||
- Docs: Annahme „Behördenpaket und Sperrbezirk“ (Bezugsstand, Abweichung, Lebenszyklus, Mutex, `totalColonies` ist die Zahl der Anzeige und bleibt korrigierbar).
|
||||
|
||||
**Interfaces**
|
||||
- Consumes: Task 1, `LIVING_COLONY_WHERE`, `utcDay`.
|
||||
- Consumes: Task 1, `LIVING_COLONY_WHERE`, `berlinToday`/`dayToDate` (`src/common/dates.ts`). Tests: Die Test-DB ist nicht leer (Projektregeln): „REGISTRATION nur ohne Bezugsstand“ und „Bezugsstand“ laufen nach `authorityNotice.deleteMany()`; Zählungen (`totalLiving`) vergleicht der Test gegen die Zahl, die `loadHerdApiaryRows` im selben Test liefert.
|
||||
- Produces:
|
||||
```ts
|
||||
// tsk-day.ts
|
||||
@@ -765,9 +784,9 @@ it('a SENT registration with sentOn in the past is the baseline of the next herd
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx jest src/compliance` und `test/authority-notices.e2e-spec.ts` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- `create`: Transaktion, Einstellungszeile per Upsert sicherstellen, `lockRowForUpdate(tx, 'ComplianceSettings', 'singleton')` als Mutex, Bezugsstand lesen, Regeln nach Global Constraints prüfen, Bestandsbild zählen (`loadHerdApiaryRows(tx)` → `buildHerdSnapshot`), `openKey` nur bei `DRAFT`; Konflikt (vorhandener Entwurf oder `P2002` auf `openKey`) → 409 `DRAFT_EXISTS` mit `existingId`. `referenceDate` Vorgabe: heutiger UTC-Tag der DB-Uhr.
|
||||
- `create`: Transaktion, Einstellungszeile per Upsert sicherstellen, `lockRowForUpdate(tx, 'ComplianceSettings', 'singleton')` als Mutex, Bezugsstand lesen, Regeln nach Global Constraints prüfen, Bestandsbild zählen (`loadHerdApiaryRows(tx)` → `buildHerdSnapshot`), `openKey` nur bei `DRAFT`; Konflikt (vorhandener Entwurf oder `P2002` auf `openKey`) → 409 `DRAFT_EXISTS` mit `existingId`. `referenceDate` Vorgabe: `berlinToday()` (Annahme 41; kein UTC-Tag).
|
||||
- Zustandswechsel: `lockRowForUpdate(tx, 'AuthorityNotice', id)`, `before` danach lesen, Übergang prüfen (409 mit deutscher Meldung), schreiben, Audit `UPDATE` mit `beforeData`/`afterData`. `reopen` setzt `openKey` neu und prüft den Mutex; `cancel` und `mark-sent` setzen `openKey = null`.
|
||||
- `markSent` verlangt `sentOn` (Vorgabe: heutiger UTC-Tag); `PATCH` ändert `totalColonies`, `referenceDate`, `sentOn`, `sentVia`, `reference`, `notes` (nicht `kind`/`status`); auf `CANCELLED` 409.
|
||||
- `markSent` verlangt `sentOn` (Vorgabe: `berlinToday()`); `PATCH` ändert `totalColonies`, `referenceDate`, `sentOn`, `sentVia`, `reference`, `notes` (nicht `kind`/`status`); auf `CANCELLED` 409.
|
||||
- `ComplianceModule` exportiert die Services; **kein** Provider mit `PermissionResolverService`.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
- [ ] **Schritt 5: Verifikation**
|
||||
@@ -831,7 +850,7 @@ it('the pdf route needs compliance:read', ...);
|
||||
|
||||
**Files**
|
||||
- Create: `src/compliance/compliance.handlers.ts` (+ `compliance.handlers.spec.ts`)
|
||||
- Modify: `src/notifications/notifications.service.ts` (+ Spec; `recipientsFor`, `recordForPermissionOnce`), `src/jobs/daily-evaluation.rules.ts` (+ Spec), `src/compliance/compliance.module.ts` (Provider), `src/worker/worker.module.ts` (importiert `ComplianceModule`)
|
||||
- Modify: `src/notifications/notifications.service.ts` (+ Spec; nur `recordForPermissionOnce`, die vorhandene private `recipients` wird mitgenutzt), `src/jobs/daily-evaluation.rules.ts` (+ Spec), `src/compliance/compliance.module.ts` (Provider), `src/worker/worker.module.ts` (importiert `ComplianceModule`)
|
||||
- Test: `test/authority-notices.e2e-spec.ts` (ergänzen), `test/daily-evaluation.e2e-spec.ts`
|
||||
- Docs: ARCHITECTURE §10 Handler-Tabelle: `compliance.herd-change`, `compliance.tsk-reference-day`; Meldungen ohne Dublette.
|
||||
|
||||
@@ -839,17 +858,20 @@ it('the pdf route needs compliance:read', ...);
|
||||
- Consumes: Task 7 (Ereignisse), Task 9 (`herdStatusIn`, `createDraftIn`, `tskReferenceDayOf`), T4 (`recordForUsers`, Kategorie `SYSTEM`, `DAILY_EVALUATION_RULES`).
|
||||
- Produces:
|
||||
```ts
|
||||
NotificationsService.recipientsFor(db: DbClient, permissions: readonly PermissionKey[]): Promise<string[]>; // die Abfrage aus recordForPermission, herausgezogen (keine zweite Kopie)
|
||||
// Die Empfängerabfrage gibt es schon als private Methode `recipients(db, keys, userIds)` (T4); kein `recipientsFor`, keine zweite Kopie:
|
||||
// recordForPermissionOnce liegt in derselben Klasse und nutzt `recipients` und `fanOut`/`recordForUsers`.
|
||||
NotificationsService.recordForPermissionOnce(permission: PermissionKey | readonly PermissionKey[],
|
||||
input: Omit<FanOutNotificationInput, 'dedupeKeyPrefix'> & { relatedType: string; relatedId: string }, tx: Prisma.TransactionClient): Promise<Notification[]>;
|
||||
// pg_advisory_xact_lock(hashtextextended('notify-once:' || relatedType || ':' || relatedId, 0)), dann Empfänger ohne ungelesene Zeile derselben relatedType/relatedId, dann recordForUsers ohne dedupeKey
|
||||
// pg_advisory_xact_lock(hashtextextended('notify-once:' || relatedType || ':' || relatedId, 0)), dann `recipients` (Rechte, aktiv), davon die ohne ungelesene Zeile
|
||||
// derselben relatedType/relatedId (read = false), dann recordForUsers(neueEmpfänger, { ...input, requiredPermissions: <die Empfängerrechte> }) ohne dedupeKeyPrefix
|
||||
// (die Telegram-Zustellung plant `write` → `planIn` selbst; Wächter no-broadcast-notifications.spec.ts bleibt grün, weil nur notifications.service.ts Zeilen schreibt)
|
||||
// compliance.handlers.ts
|
||||
@OnDomainEvent(['ColonyCreated','ColonyMoved','ColonyEnded','ColonyStatusChanged','ApiaryChanged'], 'compliance.herd-change')
|
||||
herdChange(event: AnyDomainEventEnvelope<…>, tx: Prisma.TransactionClient): Promise<void>; // ColonyStatusChanged nur beim Wechsel lebend <-> beendet
|
||||
@OnDomainEvent('TskReferenceDayReached', 'compliance.tsk-reference-day')
|
||||
tskReferenceDay(event: AnyDomainEventEnvelope<'TskReferenceDayReached'>, tx: Prisma.TransactionClient): Promise<void>;
|
||||
// daily-evaluation.rules.ts: DailyEventType += 'TskReferenceDayReached'; neue Regel in DAILY_EVALUATION_RULES
|
||||
export const tskReferenceDayRule: DailyRule<'TskReferenceDayReached'>; // Jahr und Vorjahr, Tag = tskReferenceDayOf(Jahr, settings.tskReferenceDay), Vorgabe 01-01 ohne Einstellungszeile
|
||||
export const tskReferenceDayRule: DailyRule<'TskReferenceDayReached'>; // occurrences(db: PrismaService, today, { day }): Jahr von ctx.day (Berliner Tag) und Vorjahr, Tag = tskReferenceDayOf(Jahr, settings.tskReferenceDay), Vorgabe 01-01 ohne Einstellungszeile (db.complianceSettings.findUnique({ where: { id: 'singleton' } })); catchUpEvents filtert auf CATCH_UP_DAYS = 7
|
||||
```
|
||||
|
||||
- [ ] **Schritt 1: Failing Tests**
|
||||
@@ -867,7 +889,8 @@ it('a second handler run for the same event adds no second draft or notification
|
||||
it('recordForPermissionOnce skips users with an unread notification of the same topic', ...);
|
||||
it('recordForPermissionOnce writes again after the user read the earlier one', ...);
|
||||
it('recordForPermissionOnce takes the advisory lock before reading the unread rows', ...);
|
||||
it('recordForPermission still works through recipientsFor', ...);
|
||||
it('recordForPermissionOnce plans the Telegram delivery for a user with SYSTEM chosen and none for a user without', ...);
|
||||
it('recordForPermission is unchanged', ...);
|
||||
// daily-evaluation.rules.spec.ts
|
||||
it('emits TskReferenceDayReached on the configured day', ...);
|
||||
it('catches up within 7 days exactly once', ...);
|
||||
@@ -875,7 +898,7 @@ it('covers the previous year\'s day in early January', ...);
|
||||
it('uses 01-01 when no settings row exists', ...);
|
||||
// handler-contract.spec.ts läuft unverändert und deckt die neue Datei ab
|
||||
// e2e
|
||||
it('a new colony changes the herd: after draining one unread notification per permitted user', ...); // Messkriterium 3
|
||||
it('a new colony changes the herd: after draining one unread notification per permitted user', ...); // Messkriterium 3; "permitted" = die im Test angelegten Nutzer mit compliance:manage, andere Nutzer der Test-DB zählen nicht; Baseline über eine SENT-Anzeige mit totalColonies = aktueller Stand, damit die Abweichung vom selbst gesetzten Stand ausgeht
|
||||
it('a second colony within one unread notice adds no second notification', ...);
|
||||
it('creating and sending a change notice removes the deviation; the next colony after reading notifies again', ...);
|
||||
it('daily evaluation twice on the reference day: one event, one draft', ...);
|
||||
@@ -883,11 +906,11 @@ it('a user without compliance:manage gets no herd notification', ...);
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx jest src/compliance src/notifications src/jobs src/domain-events` und die e2e-Dateien → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- Meldungen: `category: 'SYSTEM'` (T4 hat keinen Wert für Behörden; die Telegram-Kategorienliste bleibt unverändert), `relatedType: 'ComplianceItem'`, `relatedId: 'herd-change'` bzw. `tsk-report:<Jahr>`, `link: '/compliance'` bzw. `/compliance/notices/<id>`, Empfänger `compliance:manage`. T4 vergibt `dedupeKey` dauerhaft eindeutig; hier bewusst **keiner**, die Dublette steuert nur „ungelesen“.
|
||||
- Meldungen: `category: 'SYSTEM'` (T4 hat keinen Wert für Behörden; die Telegram-Kategorienliste bleibt unverändert, Zustellung per Telegram nur für Nutzer, die `SYSTEM` gewählt haben, Frage 2), `relatedType: 'ComplianceItem'`, `relatedId: 'herd-change'` bzw. `tsk-report:<Jahr>`, `link: '/compliance'` bzw. `/compliance/notices/<id>`, Empfänger `compliance:manage`. T4 vergibt `dedupeKey` dauerhaft eindeutig; hier bewusst **keiner**, die Dublette steuert nur „ungelesen“.
|
||||
- Handler lesen nur über `tx`; `herdStatusIn(tx, now)` mit `now` aus `SELECT now()` über `tx`. `herdChange` legt nie eine Anzeige an.
|
||||
- `tskReferenceDay`: `lockRowForUpdate(tx, 'ComplianceSettings', 'singleton')`; gibt es für das Jahr `SENT` oder einen offenen Entwurf, kehrt der Handler zurück; sonst `createDraftIn(tx, { kind: 'TSK_REPORT', year, referenceDate }, null)` und Meldung.
|
||||
- Regel: je Jahr eine `DailyOccurrence` mit `aggregateType: 'Compliance'`, `aggregateId: 'tsk:<Jahr>'`, `payload: { year, referenceDay: <Tag> }`; Schlüssel über `catchUpEvents` → `dailyDedupeKey`.
|
||||
- `WorkerModule` importiert `ComplianceModule`; `AppModule` hat es seit Task 9.
|
||||
- `WorkerModule` importiert `ComplianceModule`; `AppModule` hat es seit Task 9 (Worker = API: der Dispatcher findet die Handler per DI-Entdeckung in beiden Wurzeln; `test/worker-runtime.e2e-spec.ts` und `domain-event-registry.spec.ts` sollen `compliance.herd-change` und `compliance.tsk-reference-day` in beiden Kontexten sehen). Weder `ComplianceModule` noch ein davon importiertes Modul darf `PermissionsModule` importieren; der Worker hat nur `JobPermissionsModule`.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
- [ ] **Schritt 5: Verifikation**
|
||||
- typecheck, lint, `npm test -- --ci`
|
||||
@@ -953,12 +976,13 @@ it('revoke locks the acknowledgement row and refuses an already revoked one', ..
|
||||
// tsk-report: open from the reference day without a SENT report; "überfällig" after tskReportDueDay; a DRAFT of last year keeps last year's item open
|
||||
// restriction-active: one finding listing the apiaries with an active restriction
|
||||
// test/compliance-readiness.e2e-spec.ts
|
||||
it('an empty system reports the Stammdaten and registration items open and status OPEN', ...);
|
||||
it('without site data and without a SENT registration the Stammdaten and registration items are open and the status is OPEN', ...); // T6-Tabellen vorher leeren, SiteSettings im Test setzen/leeren; nichts über fremde Daten behaupten
|
||||
it('complete master data and a SENT registration make those items DONE', ...);
|
||||
it('READONLY may read the report but gets 403 acknowledging; STAFF gets 403 on acknowledgements; API_CLIENT gets 403 everywhere', ...);
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx jest src/compliance/readiness` und `test/compliance-readiness.e2e-spec.ts` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- `today` des Kontexts ist `berlinToday()`; „Stichtag erreicht“, „überfällig“ und die Rückblickfenster rechnen mit Berliner Tagen (`addDays`, `daysBetween`); nur `restriction-active` nutzt denselben Tag wie der Wächter (`dbUtcDay`, Frage 1), damit Anzeige und Sperre nie auseinanderlaufen.
|
||||
- `evaluate`: Kontext einmal laden (begrenzt, nur benötigte Spalten), jede Regel ausführen, Findings zu Items (`fingerprint = fingerprintOf({ key, subjectId, parts })`), gültige Quittung (gleicher `itemKey`, `subjectId`, `fingerprint`, `revokedAt` null; neueste gewinnt) → `ACKNOWLEDGED`. Regeln ohne Findings → ein `DONE`-Item mit dem Regeltitel.
|
||||
- Maskierung: Hat der Aufrufer `rule.subjectPermission` nicht, bleiben `subjects` leer und `more` = Anzahl; Titel und Detailtexte der Regeln enthalten keine Namen.
|
||||
- Quittieren: Item aus der aktuellen Auswertung nehmen (`key` + `subjectId`), `acknowledgeable` prüfen, Fingerabdruck vergleichen, in einer Transaktion schreiben mit Audit `CREATE` (`entityType: 'ComplianceAcknowledgement'`). `revoke`: Sperre auf der Quittung, `revokedAt = now()` (SQL), `revokedByUserId`, Audit.
|
||||
@@ -1017,7 +1041,7 @@ it('hides subject labels of the medicine area without veterinary-medicines:read
|
||||
it('lists status, areas, items with severity word, legal basis and acknowledgement note, user and date', ...);
|
||||
it('prints FEHLT for an empty holder', ...);
|
||||
// e2e
|
||||
it('complete data in every area makes GET /compliance/readiness free of open items with the status READY', ...); // Messkriterium 4
|
||||
it('complete data in every area leaves no open item for the subjects of this test and no open item of the Stammdaten, Behörden and Etiketten rules', ...); // Messkriterium 4; die ganze Test-DB kann fremde Lücken enthalten, darum Prüfung je key + subjectId; der Gesamtstatus READY wird als Unit-Test von ReadinessService mit leerem Kontext belegt (Task 12)
|
||||
it('a treatment from a complete lot gives a Bestandsbuch row without FEHLT and no treatment-log-gaps item', ...); // Messkriterium 1
|
||||
it('GET /compliance/readiness/report.pdf answers application/pdf and no-store', ...);
|
||||
it('a negative rest after a treatment from a too small lot opens lot-negative', ...);
|
||||
@@ -1025,6 +1049,7 @@ it('a negative rest after a treatment from a too small lot opens lot-negative',
|
||||
- [ ] **Schritt 2:** `npx jest src/compliance/readiness` und die e2e-Dateien → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- `treatment-log-gaps`: dieselbe Funktionskette wie das PDF (`resolveSourceValues` → `groupTreatmentLog` → `treatmentLogGaps`); je Quelle ohne Packung Subjekt `treatment` (`parentId` = `colonyId`), mit Packung und nur Packungsfeldern Subjekt `lot`. `missing` nennt deutsche Feldnamen („Lieferant“, „Beleg“, „Menge“, „Wartezeit“, „Tierarzt“).
|
||||
- `harvest-withdrawal` baut **keine** zweite Fensterlogik: Fenster über `withdrawalWindowOf(treatment)` / `isInWindow(window, berlinDayOf(harvest.date))`, Konflikte der Ernten über `WithdrawalCheckService.conflictsForHarvests` (`src/withdrawal/`), Text über `withdrawalRangeText`/`lastBlockedDay` (`src/withdrawal/withdrawal-text.ts`); der Regel-Test vergleicht gegen genau diese Helfer. Berührt `ReadinessModule` den Dienst, importiert es `WithdrawalModule`.
|
||||
- Lader: Behandlungen mit `select` der benötigten Spalten und `date >= 1. Januar (Jahr − 5)`; Packungen mit einer Gruppen-Abfrage der Bewegungssummen; Ernten ab `heute − 2 Jahre`; Vorlagen mit der Zielabfüllung (jüngste mit Restbestand über `summarizeStock` der `BatchMovement`, sonst jüngste, sonst `SAMPLE_BOTTLING`).
|
||||
- `label-template-*`: `checkLabel` mit den Textelementen der gespeicherten Vorlage (Bild-, Linien- und QR-Elemente bleiben außen vor).
|
||||
- Bericht: A4 hoch, Kopf mit Halter und Datum, Gesamtstatus, je Bereich eine Tabelle (Status „Offen“/„Erledigt“/„Quittiert“, Schwere, Titel, Norm, bei Quittungen Notiz, Nutzer, Datum).
|
||||
@@ -1044,7 +1069,8 @@ Gemeinsam für alle Frontend-Tasks: Vor Next.js-Code `node_modules/next/dist/doc
|
||||
|
||||
**Files**
|
||||
- Create: `src/lib/compliance/types.ts`, `src/lib/compliance/api.ts` (+ Spec), `src/lib/compliance/fix-links.ts` (+ Spec), `src/lib/compliance/use-readiness.ts`, `src/app/(app)/compliance/page.tsx` (+ `page.spec.tsx`), `src/app/(app)/compliance/readiness-item-card.tsx` (+ Spec), `src/app/(app)/compliance/acknowledge-dialog.tsx` (+ Spec), `src/components/today/compliance-line.tsx` (+ Spec)
|
||||
- Modify: `src/app/(app)/nav-config.ts` (+ `nav-config.spec.ts`), `src/app/(app)/dashboard/page.tsx` (die Zeile)
|
||||
- Create (zusätzlich): `src/lib/compliance/errors.ts` (+ Spec; liest die 409-Körper `DRAFT_EXISTS` → `existingId`, `RESTRICTED_ZONE` → `restrictions`, veralteter Fingerabdruck; baut auf `describeSaveError`/`saveErrorText` aus `src/lib/honey-batches/errors.ts` auf, siehe `confirmationRequired` als Vorbild, keine zweite Fehlerlogik)
|
||||
- Modify: `src/app/(app)/nav-config.ts` (+ `nav-config.spec.ts`), `src/app/(app)/dashboard/page.tsx` (die Zeile), `src/lib/system/labels.ts` (+ `labels.spec.ts`: `DOMAIN_EVENT_LABELS` += `ColonyCreated` „Volk angelegt“, `ColonyMoved` „Volk umgesetzt“, `ApiaryChanged` „Stand geändert“, `TskReferenceDayReached` „TSK-Stichtag erreicht“), `public/sw-routes.js` + `public/sw.js` + `src/lib/offline/sw-routes.spec.ts` (siehe unten)
|
||||
|
||||
**Interfaces**
|
||||
- Consumes: Task 12/13 (`ReadinessReport`, `ReadinessItem`, `ReadinessSubject`), `DetailHeader`, `FormSection`, `ConfirmDialog`.
|
||||
@@ -1059,6 +1085,8 @@ export function fixLinkFor(itemKey: string, subject: { type: string; id: string;
|
||||
// vet-office -> /compliance/settings; registration, herd-change, tsk-report -> /compliance/notices; medicine-withdrawal-missing -> /medicines
|
||||
```
|
||||
- Nav: Eintrag „Prüfbereitschaft“ (Symbol `ShieldCheck`, Gruppe „Betrieb“, Recht `compliance:read`).
|
||||
- **Service Worker (nie zwischenspeichern).** Belege, Anschriften, Koordinaten und Aktenzeichen dürfen nicht im Cache des Geräts landen (gleiche Begründung wie `/apiaries`, `/export/`, `/backup`). `NEVER_CACHED_API_PATHS` in `public/sw-routes.js` bekommt (Tests in `sw-routes.spec.ts` zuerst): `/api/v1/compliance(/|$)`, `/api/v1/authority-notices(/|$)`, `/api/v1/apiary-restrictions(/|$)`, `/api/v1/apiaries/[^/]+/restrictions/?$`, `/api/v1/veterinary-medicines(/|$)`, `/api/v1/veterinary-medicine-lots(/|$)`, `/api/v1/documents(/|$)` (Anhänge zu Packung, Anzeige und Verfügung; hat der Datei-Download eine andere Wurzel, die prüfen) und `/api/v1/label-templates/[^/]+/check/` (enthält Anschrift). Danach `SW_ROUTES_VERSION` in `public/sw.js` auf den neuen Inhalts-Hash heben (der Spec `sw-routes.spec.ts` prüft, dass die Version zum Inhalt passt). Die Formulare der Tasks 15 bis 19 laufen online; die Offline-Warteschlange der Behandlung (Task 16) bleibt unberührt.
|
||||
- Wiederverwenden statt neu bauen (gilt für 14 bis 19): `describeSaveError`/`saveErrorText` (Fehlerzeile), `PrefillStatus` (Task 16, Vorbelegung aus dem Katalog; Speichern bleibt erlaubt, Annahme 53), `ConfirmDialog` mit `triggerDescribedBy`/`triggerDisabled` (Rückfragen, Task 19), `ToggleChip` mit `disabled` (Mehrfachauswahl der gezählten Völkerarten in Task 18), `DetailHeader`, `FormSection`, `ChoiceGroup`, `NumberStepper`, `RecordSelect`, `DocumentUploadCard`. `StepUpDialog` und `refreshOn401: false` sind für Nachweis-Aufrufe (Passwort, Backup-Import, Telegram) da; die Spec verlangt für die T6 keinen Nachweis (siehe Frage 5), also hier nicht einbauen.
|
||||
|
||||
- [ ] **Schritt 1: Failing Tests**
|
||||
```ts
|
||||
@@ -1080,15 +1108,23 @@ it.each([...])('maps %s to %s', ...); // jede
|
||||
it('renders only when openCount > 0 and the user holds compliance:read', ...);
|
||||
// nav-config.spec.ts
|
||||
it('shows Prüfbereitschaft only with compliance:read', ...);
|
||||
// sw-routes.spec.ts
|
||||
it.each([...die neuen Pfade])('never caches %s', ...); // inkl. Großschreibung, Schrägstrich am Ende, Query
|
||||
it('keeps caching the unrelated routes it cached before', ...);
|
||||
it('SW_ROUTES_VERSION in sw.js matches the content of sw-routes.js', ...); // bestehender Test, bleibt grün
|
||||
// labels.spec.ts
|
||||
it('DOMAIN_EVENT_LABELS names ColonyCreated, ColonyMoved, ApiaryChanged and TskReferenceDayReached', ...);
|
||||
// errors.spec.ts
|
||||
it('reads DRAFT_EXISTS with existingId, RESTRICTED_ZONE with its restrictions and a stale fingerprint', ...);
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx vitest run src/lib/compliance "src/app/(app)/compliance" src/components/today "src/app/(app)/nav-config.spec.ts"` → FAIL.
|
||||
- [ ] **Schritt 2:** `npx vitest run src/lib/compliance src/lib/offline src/lib/system "src/app/(app)/compliance" src/components/today "src/app/(app)/nav-config.spec.ts"` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- Seite `/compliance` nach Spec 9: Status oben, Punkte nach Bereich (fünf Abschnitte), offene Punkte rot mit Symbol + „Offen“ + Schwere-Chip + Titel + eine Zeile Detail + Norm klein; „Mehr“-Menü mit Links auf Bericht (PDF, authentifizierter Download über den vorhandenen Export-Helfer), Bestandsbuch, `/compliance/notices`, `/compliance/restrictions`, `/compliance/settings`.
|
||||
- Hauptaktion „Nächsten Punkt lösen“ springt auf das `fixLinkFor` des ersten offenen `PFLICHT`-Punkts; alles grün: „Bestandsbuch {Jahr} drucken“.
|
||||
- Links auf Seiten, die spätere Tasks liefern, sind einfache `Link`s; die Seiten entstehen in den Tasks 15, 18, 19.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
- [ ] **Schritt 5: Verifikation**
|
||||
- `npm run lint && npm run typecheck && npx vitest run src/lib/compliance "src/app/(app)/compliance" src/components/today "src/app/(app)/nav-config.spec.ts"` → PASS
|
||||
- `npm run lint && npm run typecheck && npx vitest run src/lib/compliance src/lib/offline src/lib/system "src/app/(app)/compliance" src/components/today "src/app/(app)/nav-config.spec.ts"` → PASS
|
||||
- Browser: Desktop und 390 px, Leerzustand „Prüfbereit“, Tastatur (Tab-Reihenfolge, sichtbarer Fokus), `prefers-reduced-motion`
|
||||
- kein Commit
|
||||
|
||||
@@ -1098,7 +1134,8 @@ it('shows Prüfbereitschaft only with compliance:read', ...);
|
||||
|
||||
**Files**
|
||||
- Create: `src/lib/medicines/types.ts`, `src/lib/medicines/api.ts` (+ Spec), `src/app/(app)/medicines/page.tsx` (+ Spec), `src/app/(app)/medicines/medicine-form-dialog.tsx` (+ Spec), `src/app/(app)/medicines/[id]/page.tsx`, `src/app/(app)/medicines/[id]/lot-form-dialog.tsx`, `src/app/(app)/medicines/lots/[id]/page.tsx`, `src/app/(app)/medicines/lots/[id]/inventory-dialog.tsx`, `src/app/(app)/medicines/lots/[id]/disposal-dialog.tsx` (je + Spec)
|
||||
- Modify: `src/app/(app)/storage/page.tsx` (Karte „Tierarzneimittel“ mit Link auf `/medicines`)
|
||||
- Modify: `src/app/(app)/storage/page.tsx` (Karte „Tierarzneimittel“ mit Link auf `/medicines`), `src/lib/documents/types.ts` (`DocumentEntityType` += `MEDICINE_LOT` | `AUTHORITY_NOTICE` | `APIARY_RESTRICTION`, einmal für 15, 18, 19; Spec in `src/lib/documents`)
|
||||
- Fehlerzeilen über `saveErrorText`/`describeSaveError` (`src/lib/honey-batches/errors.ts`), nicht neu schreiben.
|
||||
|
||||
**Interfaces**
|
||||
- Consumes: Task 2 (`MedicineView`, `LotView`, `LotDetailView`, `MedicineSuggestion`), `DocumentUploadCard` (Dokumentart `MEDICINE_LOT`), `ChoiceGroup`, `NumberStepper`.
|
||||
@@ -1172,6 +1209,7 @@ it('the colony page opens the edit dialog for ?editTreatment=<id>', ...);
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx vitest run src/components/treatments src/lib/treatments "src/app/(app)/treatments" "src/app/(app)/colonies"` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- Ladezustand und Fehler der Katalog-Vorbelegung zeigt `PrefillStatus` (`src/components/warnings/prefill-status.tsx`) wie bei der Behandlungs-Vorbelegung aus T3; Speichern bleibt erlaubt (Annahme 53). Das Muster `PrefillNote`/`useCaptureMutation`/`applyPrefill` aus T3 gilt unverändert, die Katalog-Vorbelegung kommt als weitere Quelle dazu.
|
||||
- Die Vorbelegung bleibt sichtbar im Formular (Server füllt nie still nach). Das Wetter für den Temperaturhinweis kommt aus `GET /apiaries/:id/weather` (`apiaries:read`); fehlt es, entfällt der Hinweis ohne Fehler.
|
||||
- Dosis im `NumberStepper` mit Einheit des Mittels; Offline wie T3 (Warteschlange); Idempotency-Key unverändert.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
@@ -1226,6 +1264,7 @@ it('Vom Bienenhalter übernehmen fills the fields visibly and saves only on Spei
|
||||
- [ ] **Schritt 2:** `npx vitest run src/lib/label-templates src/components/labels "src/app/(app)/settings" "src/app/(app)/bottling" "src/app/(app)/products"` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- `LabelCompliancePanel` ist eine eigene Datei (der Designer hat schon 695 Zeilen) und bekommt nur das Ergebnis; der Designer ruft den Hook mit den aktuellen Textelementen.
|
||||
- Fehlerzeilen und Feldfehler über `describeSaveError`/`saveErrorText`; das 409 `LABEL_PARTICULARS_MISSING` liest `check.ts` (Muster `confirmationRequired`). Das Ergebnis von `checkTemplateForBottling` enthält die Anschrift: nie in Query-Keys mit Persistenz oder im Service-Worker-Cache ablegen (Pfad `/label-templates/:id/check/…` steht in Task 14 in `NEVER_CACHED_API_PATHS`).
|
||||
- Druckkarte: ruft vor dem Anzeigen `checkTemplateForBottling`; bei Lücken rote Liste mit Links (Stammdaten → Verwaltung > Automatisierung, Produkt → `/products/{id}`, Designer → `/settings/label-templates/{id}`); Hauptknopf „Etiketten als PDF“ mit `aria-disabled`, daneben „Probedruck (MUSTER)“.
|
||||
- Stammdaten-Tab: Felder `foodBusinessName` (200) und `foodBusinessAddress` (300), Knopf übernimmt `holderName`/`holderAddress` in die Formularwerte, ohne zu speichern.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
@@ -1267,6 +1306,7 @@ it('STAFF sees the pages read-only without action buttons', ...);
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx vitest run src/lib/compliance "src/app/(app)/compliance"` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- Fehler über `saveErrorText` und `lib/compliance/errors.ts` (Task 14); gezählte Völkerarten in „Erweitert“ als `ToggleChip`-Gruppe; Rückfragen („Anzeige stornieren“) mit `ConfirmDialog`. Kein Nachweis-Dialog (Frage 5).
|
||||
- Detailseite nach Spec 9 mit Schritt-Anzeige (Symbol + Wort je Schritt); Formulare mit `FormSection`; „Annahme – bitte prüfen“ als sichtbarer Vermerk am Feld, nicht nur als Tooltip. Anlege-Anfragen mit `Idempotency-Key`. Dokument-Upload (Kopie, Eingangsbestätigung) über `DocumentUploadCard` mit `AUTHORITY_NOTICE`.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
- [ ] **Schritt 5: Verifikation**
|
||||
@@ -1311,6 +1351,7 @@ it('the Verfügung is uploaded via DocumentUploadCard for APIARY_RESTRICTION', .
|
||||
```
|
||||
- [ ] **Schritt 2:** `npx vitest run src/lib/compliance src/components/compliance "src/app/(app)/compliance" "src/app/(app)/colonies" "src/app/(app)/apiaries" "src/app/(app)/harvests"` → FAIL.
|
||||
- [ ] **Schritt 3: Implementieren**
|
||||
- Die Rückfrage der Ausnahme ist ein `ConfirmDialog`; ist der Umsetzen-Knopf gesperrt, nutzt er `triggerDisabled` plus `triggerDescribedBy` (Verweis auf die rote Meldung). `RESTRICTED_ZONE` und die Mitteilung „Volk wurde inzwischen umgesetzt“ (409 aus Task 8) liest `lib/compliance/errors.ts`. Der Mutations-Aufruf mit `restrictionOverrideReason` nutzt den bestehenden `apiFetch`; kein `refreshOn401: false` (kein Nachweis-Aufruf). Die Stand-Antworten mit Anschrift stehen im Service-Worker-Pfad der Stand-Seite bereits unter `NEVER_CACHED_API_PATHS`, `/apiaries/:id/restrictions` neu (Task 14).
|
||||
- Aktive Sperren einmal je Seite laden (`useActiveRestrictions`, gemeinsamer Query-Key). Der Server entscheidet endgültig; die Client-Prüfung blendet nur früh aus. Die Berechtigung für die Ausnahme kommt aus dem vorhandenen Auth-Kontext.
|
||||
- Roter Text mit Token `destructive`, Symbol + Wort; Fokus springt bei einer 409-Antwort auf die Meldung.
|
||||
- [ ] **Schritt 4:** → PASS.
|
||||
@@ -1323,9 +1364,9 @@ it('the Verfügung is uploaded via DocumentUploadCard for APIARY_RESTRICTION', .
|
||||
|
||||
## Abschluss und Rollout (Orchestrator)
|
||||
|
||||
- [ ] Gesamtprüfung Backend: `npm run typecheck && npx eslint "{src,test}/**/*.ts" && npm test -- --ci`, `npm run test:e2e`, `npm run build`; Frontend: `npm run lint && npm run typecheck && npm run test && npm run build`. Drift-Check `npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code` → 0.
|
||||
- [ ] Gesamtprüfung Backend: `npm run typecheck && npx eslint "{src,test}/**/*.ts" && npm test -- --ci`, `npm run test:e2e -- --runInBand` (gegen die Test-DB 17 auf Port 5433, 20 s je Test), `npm run build`; Frontend: `npm run lint && npm run typecheck && npm run test && npm run build`. Drift-Check `npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code` → 0.
|
||||
- [ ] Wächter prüfen: Backup-Modellabdeckung, `handler-contract.spec.ts`, `after-commit-usage.spec.ts`, Null-Vertrags-Specs der Tasks 2, 3, 5, 8, 9, Platzhalter-Fixture (Task 5 und 17) und der Seed-Wächter `src/seed-no-medicines.spec.ts` (Task 2 legt ihn an: `prisma/seed.ts` enthält kein `veterinaryMedicine`).
|
||||
- [ ] Docs aus den Berichten: `ARCHITECTURE.md` vier neue Annahmen mit je der nächsten freien Nummer (Katalog und Packungen; Etikett-Pflichtangaben; Behördenpaket und Sperrbezirk; Prüfbereitschaft), Annahme 15 ergänzen, §10 um die vier Ereignisse und die beiden Handler, Dokumente-Rechte je Art; `docs/pruefung-2026-09.md` Abschnitt „Prüfbereitschaft“; `TASKS.md` (T6).
|
||||
- [ ] Docs aus den Berichten: `ARCHITECTURE.md` vier neue Annahmen mit je der nächsten freien Nummer (Stand 2026-10-15 ist 58 die letzte, also ab 59: Katalog und Packungen; Etikett-Pflichtangaben; Behördenpaket und Sperrbezirk; Prüfbereitschaft; dazu die Sperr-Reihenfolge Stand → Volk in §10/Annahme zu Zeilensperren und die Ausnahme „Sperrbezirk-Tag = UTC“, falls bestätigt), §2 Seitenstruktur (`compliance/`, `medicines/`) und Service-Worker-Absatz (neue `NEVER_CACHED_API_PATHS`), Annahme 15 ergänzen, §10 um die vier Ereignisse und die beiden Handler, Dokumente-Rechte je Art; `docs/pruefung-2026-09.md` Abschnitt „Prüfbereitschaft“; `TASKS.md` (T6).
|
||||
- [ ] Rollout nach Spec 13 (jeder Schritt mit Freigabe des Inhabers): Voraussetzung Rückgrat-Runbook bis Schritt 3 und T4 live; Image mit den drei Migrationen (alles additiv, Rückweg ohne Verlust); danach Checkliste für den Inhaber: 1. Stammdaten Lebensmittelunternehmer, 2. Produkte Bezeichnung, 3. Etikettenvorlagen im Designer („Pflichtangaben-Block einfügen“), 4. Einstellungen Veterinäramt, Registriernummer, Stichtag und Frist der Tierseuchenkasse (Annahme), 5. Katalog aus den bisherigen Behandlungen übernehmen und aktuelle Packungen erfassen, 6. Erstanzeige „bereits angezeigt am …“, 7. Prüfbereitschaft durchgehen, bis sie grün ist. Vor dem Rollout einmal die Paragraphen-Nummern der Spec gegen gesetze-im-internet.de bzw. EUR-Lex abgleichen.
|
||||
- [ ] Ein zentraler Commit, Nachricht endet mit `Co-Authored-By: Claude Sonnet 5.5 <[email protected]>`; Push gebündelt.
|
||||
|
||||
@@ -1400,3 +1441,68 @@ it('the Verfügung is uploaded via DocumentUploadCard for APIARY_RESTRICTION', .
|
||||
- Jeder Task schließt mit Verifikation und „kein Commit“; Doku-Texte gehen über den Bericht an den Orchestrator.
|
||||
- Geteilte Dateien je Welle höchstens einmal (Liste unter „Abhängigkeiten und Parallelisierung“). Paare, die nur durch Wellen getrennt sind: 2/9/12 (`app.module.ts`), 7/8 (`colonies.service.ts`), 9/11 (`compliance.module.ts`), 9/10 (`authority-notices.controller.ts`), 12/13 (`readiness-context.ts`, `rules/index.ts`), 5/6 (`label-templates.service.ts`).
|
||||
- Proportion: Code steht nur als Signaturen, Testnamen mit Kern-Assertions und gepinnte Formeln und Texte; die Spec bleibt die Quelle der Einzelheiten.
|
||||
|
||||
---
|
||||
|
||||
## Aktualisierung 2026-10-15 (nach T4)
|
||||
|
||||
Stand: HEAD 9760ce1 (T4 komplett). Der Plan wurde Punkt für Punkt gegen den Code geprüft; nur diese Datei wurde geändert, kein Code, kein Commit. Neben dem Plan läuft eine Nacharbeit, die die Durchsicht-Intervall-Funktion in `src/season/inspection-due.ts` teilt und `intervalDays` zu `GET /phenology/state` ergänzt; beides wird als vorhanden behandelt, T6 berührt es nicht.
|
||||
|
||||
**Änderungen (je eine Zeile)**
|
||||
1. Tech Stack: Postgres der Test-DB ist 17 (Port 5433), Betrieb 18.6; keine 18-only-Funktionen.
|
||||
2. Voraussetzungen T3: `treatmentRow` ist eine freie Modulfunktion (spreizt `...dto`), `recordIn(tx, data, actingUserId, bulkId?)` private Methode; `create` sperrt mit `lockRowForUpdate`; `update` kennt `apiaryId` noch nicht.
|
||||
3. Voraussetzungen T4 vollständig und mit echten Namen: `NotificationCategory` als Prisma-Enum, `recipients` ist privat, `planIn` plant Telegram in der Schreib-Transaktion, `notificationDedupeKey`, `DbClient`, `WithdrawalWindow`-Felder, `withdrawal-text.ts`, `WithdrawalCheckService`, `TimeEventsStep`, Berliner Tage (Annahme 41).
|
||||
4. Tagesregel: `DailyRule.occurrences(db: PrismaService, today, { seasons, day })` statt erfundener `evaluate`; Union `DailyEventType` erweitern; `ctx.day` ist ein Berliner Tag; `CATCH_UP_DAYS` exportiert.
|
||||
5. Migrationen: Stempel geprüft (alle hinter `20261015110000_t4_telegram`); kein Konflikt mit T4 (`Product.shelfLifeMonths` existiert, `AuditAction.PRINT`, `DocumentEntityType`-Werte und Rechte noch nicht).
|
||||
6. Rechte: `sortOrder` 3700 und 3800 (höchster Wert ist 3600).
|
||||
7. Lock-Reihenfolge: `Apiary` an die erste Stelle von `LOCKABLE_TABLES` (Task 1); `Product` steht schon in der Map und wird nicht doppelt eingetragen; acht neue Tabellen (`LabelTemplate`, `SiteSettings`, `VeterinaryMedicine`, `VeterinaryMedicineLot`, `ComplianceSettings`, `AuthorityNotice`, `ApiaryRestriction`, `ComplianceAcknowledgement`).
|
||||
8. Lock-Reihenfolge, Aufrufer (vollständig): heute sperrt nur `AutopilotService.syncApiaryIn` Stand und Volk zusammen (schon Stand zuerst, keine Codeänderung); zu ändern sind `ColoniesService.create/update/move/merge`, `ApiariesService.update/deactivate/reactivate` und `ApiaryRestrictionsService.create/update`; `InspectionsService` ruft `updateIn` unter Volkssperre weiter, `updateIn` sperrt keinen Stand.
|
||||
9. Task 1 Tests: `row-lock.spec.ts` Reihenfolge-Test ersetzt, Einmaligkeit jeder Tabelle, `it.each` um T6-Tabellen; Autopilot-Spec als Wächter.
|
||||
10. Task 8: Sperrmuster Stand (FOR SHARE, aufsteigend) vor Volk, Neuvergleich des Standes unter der Sperre (409), `assertApiaryActive` in die Transaktion, `ApiaryRestrictionsService` sperrt Stände FOR UPDATE; vier neue Tests.
|
||||
11. Task 7: `AggregateType` kennt `'Apiary'` schon (nur `'Compliance'` ergänzen); `ApiariesService` hat heute keine Transaktion, alle vier Pfade werden umgebaut; Handler-Registrierung in einem Modul, das App und Worker importieren, festgehalten.
|
||||
12. Task 14: `DOMAIN_EVENT_LABELS` (`src/lib/system/labels.ts`) bekommt die vier Ereignisse mit deutschen Namen; Test dazu.
|
||||
13. Task 14: Service Worker darf Compliance-Daten nie zwischenspeichern; acht neue Pfade in `NEVER_CACHED_API_PATHS`, `SW_ROUTES_VERSION` heben, Tests in `sw-routes.spec.ts`.
|
||||
14. Task 14: neue `src/lib/compliance/errors.ts` auf Basis von `describeSaveError`/`saveErrorText` (Muster `confirmationRequired`) statt eigener Fehlerlogik; 18 und 19 lesen nur.
|
||||
15. Tasks 14 bis 19: Wiederverwendungsliste (`PrefillStatus`, `ConfirmDialog` `triggerDisabled`/`triggerDescribedBy`, `ToggleChip` `disabled`, `describeSaveError`, `DocumentUploadCard`); `StepUpDialog` und `refreshOn401: false` bewusst nicht (Frage 5).
|
||||
16. Task 15: `src/lib/documents/types.ts` (`DocumentEntityType` plus drei Arten) fehlte im Plan; gehört Task 15, 18 und 19 lesen.
|
||||
17. Task 16: Katalog-Vorbelegung zeigt Lade-/Fehlerzustand mit `PrefillStatus` (Annahme 53).
|
||||
18. Task 17: Fehler über `describeSaveError`, 409 `LABEL_PARTICULARS_MISSING` über Leser in `check.ts`; Prüfantwort enthält die Anschrift und kommt nie in Persistenz oder Cache.
|
||||
19. Task 18/19: Fehler- und Dialogmuster der Wiederverwendungsliste eingetragen.
|
||||
20. Task 2: `roundKg` gibt es seit T4 exportiert in `honey-plausibility.ts`; die Kopie delegiert an `roundToThousandths`, keine dritte Kopie; `test/extractions.e2e-spec.ts` in die Verifikation.
|
||||
21. Task 3: Beschreibung von `treatmentRow`/`update` an den Code angepasst; Reihenfolge Volk vor Packung.
|
||||
22. Task 4: `create`/`update`/`delete` der Dokumente haben `actingUserId` schon; neu nur `findAll` und `readFile`.
|
||||
23. Task 6: Signaturwechsel von `renderLabelsPdf(templateId, bottlingBatchId, startPosition = 1)` samt Aufrufern (Controller, Spec) benannt.
|
||||
24. Task 9: Standardtage `referenceDate`/`sentOn` und `today` der Prüfbereitschaft sind Berliner Tage (`berlinToday()`), nicht UTC der DB-Uhr (Annahme 41).
|
||||
25. Sperrbezirk-Tag bleibt UTC (Spec 5.6 sagt es ausdrücklich), im Plan markiert, Wechsel ist eine Zeile in `dbUtcDay`; Frage 1.
|
||||
26. Task 11: `recipientsFor` gestrichen (die private `recipients` existiert); `recordForPermissionOnce` nutzt sie; Test für Telegram-Planung mit/ohne Kategorie `SYSTEM`.
|
||||
27. Task 11: Kategorie `SYSTEM` bestätigt; Texte (unter 600 Zeichen, ohne Adresse, Link im Feld `link`) passen ins Telegram-Format; Zustellung nur bei gewählter Kategorie (Frage 2).
|
||||
28. Task 11: Worker = API: `ComplianceModule` in beiden Wurzeln, kein `PermissionsModule` in der Kette, Tests in `worker-runtime.e2e-spec.ts` und `domain-event-registry.spec.ts`.
|
||||
29. Task 13: `harvest-withdrawal` nutzt `withdrawalWindowOf`/`isInWindow`/`WithdrawalCheckService.conflictsForHarvests`/`withdrawalRangeText` statt eigener Fensterlogik.
|
||||
30. Geteilte Dateien je Welle: `labels.ts`, `sw-routes.js`/`sw.js`/`sw-routes.spec.ts`, `compliance/errors.ts` nur Task 14; `documents/types.ts` nur Task 15; `honey-plausibility.ts` nur Task 2; `inspections.service.ts` und `autopilot.service.ts` nur gelesen.
|
||||
31. Gates: `npm run lint` hat `--fix`, der Plan nutzt `npx eslint`; e2e `testTimeout` 20 s, `maxWorkers: 4`, Gesamtlauf mit `--runInBand`; Frontend `npm run test` ist `vitest run`.
|
||||
32. Test-DB ist nicht leer: neue Projektregel mit Aufräumen der T6-Tabellen, Differenzen statt absoluten Zahlen, Prüfung je `key` + `subjectId`; Tests in Tasks 9, 11, 12, 13 umformuliert („leeres System“, „READY“).
|
||||
33. Abschluss: Annahmen ab Nummer 59, zusätzlich §2 (Seiten) und Service-Worker-Absatz, Sperr-Reihenfolge als Annahme.
|
||||
|
||||
**Geprüft und unverändert**: `DomainEventsService`, `@OnDomainEvent`, `dailyDedupeKey`, `utcDay`, `inCatchUpWindow`, `AuditLogService.record(entry, tx)`, `lockRowForUpdate`/`lockRowsForShare`/`lockRowsForUpdate`, `runBulk` (Callback `(tx, entry, colony, bulkId)`), `lockColony`/`colonyTransitionEvents`, `LIVING_COLONY_WHERE`, `loggableError`, `drainDomainEvents`, `@Idempotent`, `IsOptionalNotNull`, `optionalDate`, `fifoOrder`, `pdf-table.ts`-Helfer, `JobPermissionsModule`, `groupTreatmentLog`, `summarizeStock`, `ColonySplit`, `GET /treatments/last-by-product`, `PrefillNote`; Annahmen 10, 11, 15, 20 stimmen mit ARCHITECTURE überein. Spec-Abdeckung: jeder Abschnitt hat weiter einen Task; nichts, was T4 geliefert hat, wird doppelt gebaut (Haltbarkeit, Meldungs-Fan-out, Fenster-Helfer, `roundKg` wurden zusammengeführt oder wiederverwendet).
|
||||
|
||||
**Offene Fragen an den Inhaber**
|
||||
1. Sperrbezirk-Tag: Spec 5.6 sagt UTC-Tag; die Tagesauswertung nutzt seit T4 Berliner Tage. Kurz nach Mitternacht Berliner Zeit gilt eine Sperre, die heute beginnt, dann noch nicht, und eine, die gestern endete, noch. Vorschlag: Berliner Tag (Spec 5.6 anpassen). Bis zur Antwort bleibt UTC.
|
||||
2. Die Meldungen „Bestand geändert“ und „TSK-Stichtag“ tragen die Kategorie `SYSTEM`; Telegram bekommt sie nur, wenn der Nutzer `SYSTEM` gewählt hat (Vorgabe nur `TASK_DUE` und `DIGEST`). Reicht das, oder soll es eine eigene Kategorie `COMPLIANCE` geben (Migration, Profil-UI, Telegram-Auswahl)?
|
||||
3. Service Worker: Der Plan schließt alle Compliance-, Packungs- und Dokument-Pfade vom Cache aus (dort kein Offline-Lesen). Gewollt? Das Präfix `/api/v1/documents` trifft damit auch Fotos der Durchsicht.
|
||||
4. Lock-Reihenfolge Stand vor Volk ist wie beschlossen umgesetzt; bitte bestätigen, dass ein Umsetzen, das zwischen Lesen und Sperren den Stand wechselt, mit 409 „bitte neu laden“ enden darf (statt automatisch zu wiederholen).
|
||||
5. Soll die Ausnahme vom Sperrbezirk (`restrictionOverrideReason`) einen Nachweis (Step-up wie beim Backup-Import) verlangen? Die Spec sieht keinen vor; der Plan baut keinen ein.
|
||||
6. Weiterhin offen aus der Spec: Paragraphen-Nummern, Fristen und Landesrecht Bayern (Annahmen als Einstellungen, vor dem Rollout gegen gesetze-im-internet.de/EUR-Lex abgleichen); es wurden keine Rechtswerte in den Plan oder Code geschrieben.
|
||||
|
||||
### Inhaber-Entscheidungen 2026-10-15 (bindend, gehen Spec und Plantext oben vor)
|
||||
|
||||
1. **Sperrbezirk-Tag = Berliner Kalendertag** (`berlinToday()`), nicht UTC (Spec 5.6 wird angepasst). Aktiv bei `startsOn` ≤ Berliner Tag ≤ `endsOn`. Gilt für Task 8 (Wächter-Helfer), Task 12/13 (Prüfbereitschaft) und Task 19 (Stand-Band). Die Zeile `dbUtcDay` entfällt; Vergleich über den Berliner Tag wie in der Tagesauswertung.
|
||||
2. **Eigene Meldungs-Kategorie `COMPLIANCE`** („Behörden & Prüfung“) für die Hinweise „Bestand geändert“ und „TSK-Stichtag“.
|
||||
- Task 1: `NotificationCategory` + `COMPLIANCE` (eigene Migration, Wert nicht in derselben Migration benutzt). Standardmäßig für neue und bestehende Nutzer in der App und für Telegram ausgewählt: Vorgabe-Liste erweitern; bestehende `NotificationPreference`-Zeilen bekommen den Wert per Daten-Migration dazu.
|
||||
- Task 11 nutzt `COMPLIANCE`.
|
||||
- Frontend (Task 18): Label „Behörden & Prüfung“ in `lib/notifications/labels.ts` und in der Kategorienauswahl der Telegram-Karte.
|
||||
3. **Ausnahme vom Sperrbezirk ohne Passwort-Abfrage:** Begründung 10–500 Zeichen, nur `compliance:manage`, Audit + Standhistorie (wie Spec).
|
||||
|
||||
### Orchestrator-Rulings 2026-10-15
|
||||
|
||||
4. **Service Worker:** Nicht `/api/v1/documents` als ganzes Präfix sperren, sonst sind Durchsichtsfotos offline weg. Nie gecacht werden die Compliance-, Arzneimittel- und Sperrbezirk-Pfade. Dokumente nur, wenn der Pfad die neuen Arten (`MEDICINE_LOT`, `AUTHORITY_NOTICE`, `APIARY_RESTRICTION`) eindeutig erkennen lässt. Sonst schützt der Server mit `Cache-Control: no-store` auf diesen Downloads und Listen; der SW respektiert `no-store`. Task 14 prüft das und testet es.
|
||||
5. **Gleichzeitige Standänderung zwischen Lesen und Sperre:** 409 mit „Der Stand wurde gerade geändert. Bitte neu laden.“, kein automatischer Neuversuch (wie bisher in der App).
|
||||
Reference in new issue
Block a user