Warenwirtschaft: OPT-1 Quick Wins + Entwicklungsplan V6.0

- N+1-Fix Ausschussliste: user.account eager laden (StockDisposalController@index)
- Composite-Indizes stock_entries(entry_type,status,ingredient_id/packaging_item_id) für InventoryService-Aggregationen
- Ungenutzten Eager-Load producedByUser aus ProductionRepository::listForIndex entfernt
- ExampleTest auf assertRedirect umgestellt (Suite 100% grün: 155 Tests)
- Query-Count-Regressionstest für die Ausschussliste
- Plan V6.0 (Code-Verifikation, OPT-Block, geklärte Kundenfragen)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-06 09:54:24 +00:00
parent e53201f229
commit 65f949dd7a
6 changed files with 240 additions and 5 deletions

View file

@ -24,7 +24,7 @@ class StockDisposalController extends Controller
public function index(Request $request): View
{
$query = StockDisposal::query()
->with(['ingredient', 'packagingItem', 'location', 'stockEntry', 'user'])
->with(['ingredient', 'packagingItem', 'location', 'stockEntry', 'user.account'])
->latest('disposed_at')
->latest('id');

View file

@ -13,7 +13,7 @@ class ProductionRepository
public function listForIndex(): Collection
{
return Production::query()
->with(['product', 'location', 'producedByUser'])
->with(['product', 'location'])
->orderByDesc('produced_at')
->orderByDesc('id')
->get();

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Bestands-Aggregationen (InventoryService) filtern durchgängig auf
* (entry_type, status) und gruppieren nach ingredient_id bzw. packaging_item_id.
*/
public function up(): void
{
Schema::table('stock_entries', function (Blueprint $table) {
$table->index(['entry_type', 'status', 'ingredient_id'], 'stock_entries_type_status_ingredient_index');
$table->index(['entry_type', 'status', 'packaging_item_id'], 'stock_entries_type_status_packaging_index');
});
}
public function down(): void
{
Schema::table('stock_entries', function (Blueprint $table) {
$table->dropIndex('stock_entries_type_status_ingredient_index');
$table->dropIndex('stock_entries_type_status_packaging_index');
});
}
};

View file

@ -0,0 +1,162 @@
# Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand
> **Version:** 6.0 - Stand 06.07.2026
> **Ersetzt:** `entwicklungsplan-aktualisiert-12-06-2026.md` (V5.0) als operative Arbeitsgrundlage
> **Anforderungsquellen:** `docs/nächsten Anforderungen an die WaWi.md` (12.06.2026), `docs/Todos.md`, Screens `screens/2026-12-06-Rezeptur-phase.jpeg` + `screens/2026-12-06-neue-produktion.jpeg`
> **Referenzen:** V5.0 (vollständige AP-Details zu AP-20 … AP-28), V4.0 (AP-00 … AP-19), `briefing-anpassungen-27-04-2026.md`, `konzept-final.md`
> **Methodik:** unverändert — kleine, sequenziell abarbeitbare Arbeitspakete (AP), jedes einzeln deploybar; jedes AP endet mit `vendor/bin/pint --dirty` + Pest-Tests.
---
## 0. Was dieses Dokument neu macht
V6.0 basiert auf einer **vollständigen Code-Verifikation vom 06.07.2026** (alle AP-Status per Grep/Migrations-/Test-Abgleich geprüft, komplette Testsuite ausgeführt) plus einem **Code-Qualitäts-Review** des gesamten WaWi-Moduls. Es gibt zwei Neuerungen gegenüber V5.0:
1. **Verifizierter Stand:** Seit dem 12.06.2026 wurde nicht weiterentwickelt (letzter Commit `e53201f`, Arbeitsverzeichnis sauber). Der V5.0-Status stimmt exakt: **AP-22, AP-25, AP-26 sind fertig**, alles andere offen.
2. **Neuer Optimierungs-Block AP-29 (OPT-1 … OPT-5):** Ergebnisse des Code-Reviews als eigene, kleine Arbeitspakete. Zwei davon (OPT-1, OPT-2) werden **vor** AP-21 eingeschoben, weil AP-21 drei weitere Settings-CRUDs erzeugt und von der Entduplizierung direkt profitiert.
---
## 1. Verifizierter Ist-Stand (06.07.2026)
### 1.1 Testsuite
`php artisan test`: **153 Tests grün, 533 Assertions, Dauer ~2,3 s.** Einziger Fail: `ExampleTest` (erwartet 200 auf `/`, bekommt den Domain-Redirect 302) — kein WaWi-Problem, Test sollte auf `assertRedirect()` angepasst oder entfernt werden (in OPT-1 miterledigt).
### 1.2 AP-Status (per Code verifiziert, nicht nur per Doku)
| AP | Titel | Status | Evidenz |
|---|---|---|---|
| AP-00…AP-12, AP-18, AP-19 | V4.0-Pakete (Bugfixes, Stammdaten, Einkauf, Produktion, Rohstoff-/Produktbestand, Ausschuss, Doku, UI) | ✅ fertig | Migrationen bis `2026_06_03_*`, 20+ Feature-Tests grün |
| AP-22 | Produktbestand-Erweiterungen (Flag, Dringlichkeits-Sortierung, 4 Kacheln, Verbrauch/Monat) | ✅ fertig | `2026_06_12_175615_add_show_in_product_stock_to_products_table`, `ProductStockTest` (13 grün) |
| AP-25 | Lieferbestand: Datum statt Tage, überall kaufbar | ✅ fertig | `out_of_stock_until`-Datepicker in `product/form.blade.php`, `ProductOutOfStockTest` (8 grün) |
| AP-26 | Ausschuss-Gründe konfigurierbar | ✅ fertig | `2026_06_12_170531_create_disposal_reasons_table`, `DisposalReasonController`, `DisposalReasonSettingsTest` (8 grün) |
| AP-21 | INCI-Erweiterungen | ❌ offen | kein `alt_name`, keine Storage-Tabellen, Formular hat nur Multi-Select (Pivot `ingredient_supplier.url` existiert, wird aber nicht bespielt), kein `scopeUsedInActiveRecipes` |
| AP-20 | Rohstoffbestand-Filter + „Kein Rohstoffbestand" | ❌ offen | kein `exclude_from_stock`, `RawMaterialStockController@index` filtert hart `active=true` |
| AP-23 | Rezeptur-Phasen | ❌ offen | keine `recipe_phases`-Tabelle, kein `recipe_phase_id` |
| AP-24 | Chargen-Nr.-Präfix | ❌ offen | kein `batch_prefix`, kein `suggestedBatchNumber()` |
| AP-27 | Wareneingang: Charge zusammenführen | ❌ offen | `StockEntryController::store()` ohne Batch-Check |
| AP-28 | Neue Produktions-Maske + Produktionsergebnis | ❌ offen | keine `production_outputs`, `recordProductionStock()` bucht aus `productions.quantity` |
| AP-13 | Shop-Bestandsabzug bei Versand (inkl. Sets) | ❌ offen (Konzept liegt vor) | nirgends wird `source='sale'` erzeugt; `monthlyConsumptionByProduct()` ist dafür schon vorbereitet |
| AP-14 | Audit-Trail | ❌ offen | kein Package, keine Spur |
| AP-15 | Blockbasierte Rechte | ❌ offen | nur `isAdmin()`-Stufen |
| AP-16 | 2FA (Google Authenticator) | ❌ offen | kein TOTP-Package in composer.json |
| AP-17 | WaWi-Einstellungsseite | ❌ offen | `wawi-ui.blade.php` ist nur das CSS-Partial |
### 1.3 `docs/Todos.md` (03.06.2026) — Abgleich
Alle Punkte dieser älteren Runde sind in V4.0 eingeflossen und umgesetzt (URL-Felder inkl. Parameter-Validierung, Brutto/Netto-Rechner + UST-Dropdown im Einkauf, Einkauf duplizierbar, Lieferzeiten Lieferant/INCI, INCI-Lieferanten-Mehrfachauswahl, Produktions-Maske-Feinschliff, Produktbestand + Historie, Set-/Child-Produkte). **Offen daraus nur noch:** 2FA (= AP-16) und blockweise Rechtevergabe (= AP-15). Die Datei kann als „historisch/eingearbeitet" markiert werden.
### 1.4 Code-Qualitäts-Review (06.07.2026) — Kernaussagen
**Positiv:** Bestands- und Verbrauchsrechnungen laufen über aggregierte SQL-Queries (kein Zeilen-Looping), keine Queries in Blade-Views, Produktionsbuchung korrekt in `DB::transaction()` mit idempotentem Differenz-Abgleich. Die Architektur (Controller → Repository/Service) ist tragfähig.
**Gefundene Schwachstellen (nach Impact):**
| # | Befund | Ort | Impact |
|---|---|---|---|
| 1 | **N+1 in Ausschuss-Übersicht:** eager load `user`, aber `getFullName()` greift auf `user.account` → bis zu 500 Lazy-Loads | `StockDisposalController.php:27` + `stock-disposals/index.blade.php:72` | hoch |
| 2 | **7× Copy-Paste-Settings-CRUD** (TaxRate, DeliveryTime, DisposalReason, MaterialQuality, Location, SupplierCategory, PackagingMaterial — je ~50 nahezu wortgleiche Zeilen) | `app/Http/Controllers/Admin/Inventory/*` | hoch (Wartbarkeit; AP-21 würde 3 weitere Kopien erzeugen) |
| 3 | **Index-Listen ohne Limit/Pagination:** Wareneingänge und Produktionen laden die komplette Tabelle | `StockEntryRepository::listForIndex():128`, `ProductionRepository:13` | mittel-hoch (Skalierung) |
| 4 | **Fehlende Composite-Indizes** für den Aggregations-Hotpath `(entry_type, status, ingredient_id/packaging_item_id)` auf `stock_entries` | `InventoryService.php:33-191` vs. Migration `create_stock_entries_table` | mittel |
| 5 | **Query-Logik im Controller** + In-Memory-Preisermittlung: `lastNetPricePerSupplier()` lädt alle Eingänge eines Rohstoffs | `RawMaterialStockController::show():63-159` | mittel |
| 6 | Kleinigkeiten: Horizont-Filter im Rohstoffbestand wirkt nur clientseitig (Service ist bereits parametrisierbar); `ProductionRepository` lädt `producedByUser` ungenutzt | `RawMaterialStockController:58`, `ProductionRepository:13` | niedrig |
---
## 2. Neuer Block: AP-29 — Code-Optimierung (OPT-1 … OPT-5)
> Kleine, risikoarme Pakete. OPT-1 und OPT-2 kommen **vor** AP-21 (Begründung: OPT-2 liefert die Basis für die drei neuen Lagerort-CRUDs aus AP-21). OPT-3/4/5 laufen als Lückenfüller zwischen den Fach-APs.
### OPT-1 — Quick Wins: N+1-Fix, Indizes, Test-Hygiene (0,5 Tage)
- `StockDisposalController@index`: eager load `user.account` statt `user` (Muster: `ProductStockController::history`).
- Migration: Composite-Indizes `stock_entries(entry_type, status, ingredient_id)` + `stock_entries(entry_type, status, packaging_item_id)`; redundanten Index `production_ingredients(production_id, ingredient_id)` prüfen/entfernen.
- `ExampleTest` reparieren (`assertRedirect` statt 200) — Suite dauerhaft 100 % grün.
- Unnötigen Eager-Load `producedByUser` in `ProductionRepository::listForIndex()` entfernen.
- **Tests:** Regression `StockDisposalTest`, `RawMaterialStockTest`, `ProductionPhase5Test`; Query-Count-Assertion für die Ausschussliste.
### OPT-2 — Settings-CRUD entduplizieren (1 Tag)
- Abstrakte Basisklasse `AbstractSettingCrudController` (oder Trait): Template-Methoden für Model-Klasse, View-Prefix, Redirect-Route, FormRequests; Kind-Controller schrumpfen auf ~10 Zeilen Konfiguration.
- Umstellen: `TaxRateController`, `DeliveryTimeController`, `DisposalReasonController`, `MaterialQualityController`, `LocationController`, `SupplierCategoryController`, `PackagingMaterialController`.
- Gemeinsames Blade-Partial für das Settings-Formular prüfen (Karten auf „Einstellungen → Allgemein" sind bereits ähnlich).
- **Akzeptanz:** Verhalten unverändert (alle bestehenden Settings-Tests grün), neue CRUDs in < 30 Minuten anlegbar.
- **Tests:** bestehende `TaxRateSettingsTest`, `DeliveryTimeSettingsTest`, `DisposalReasonSettingsTest` + je ein Smoke-Test der umgestellten Controller.
### OPT-3 — Pagination der Index-Listen (1–1,5 Tage)
- `StockEntryRepository::listForIndex()` und `ProductionRepository::listForIndex()`: serverseitige Pagination bzw. hartes Limit + „ältere anzeigen"; DataTables-Konfiguration entsprechend anpassen. Ausschuss- und Produktbestands-Historie (`limit(500)`) vereinheitlichen.
- **Akzeptanz:** Seiten bleiben bei 10.000+ Einträgen unter 1 s; UI-Verhalten (Suche/Sortierung) bleibt erhalten.
### OPT-4 — `RawMaterialStockController::show` entschlacken (0,5–1 Tag)
- `entries`-Ladeblock, `openOrders`-Query und `lastNetPricePerSupplier()` nach `InventoryService`/`StockEntryRepository` verschieben.
- Letzten Netto-Preis je Lieferant per gruppierter SQL-Query statt In-Memory-Schleife.
- **Sinnvoll unmittelbar vor AP-27/AP-28**, weil die Chargen-Gruppierung dieselben Stellen anfasst.
### ~~OPT-5 — Horizont-Filter serverseitig~~ (entfällt, Kundenentscheid 06.07.2026)
- Kunde: clientseitige Filterung reicht vorerst. Falls später doch gewünscht: `dailyConsumptionByIngredient($ids, $windowDays)` ist bereits parametrisierbar → nur `?horizon=`-Parameter im `RawMaterialStockController@index` durchreichen.
---
## 3. Priorisierte Roadmap V6.0
| # | AP | Titel | Abhängigkeit | Aufwand |
|---|---|---|---|---|
| 1 | ✅ OPT-1 | Quick Wins (N+1, Indizes, ExampleTest) — **erledigt 06.07.2026** | – | 0,5 Tage |
| 2 | ➡️ OPT-2 | **NÄCHSTER SCHRITT:** Settings-CRUD-Basisklasse | – | 1 Tag |
| 3 | AP-21 | INCI-Erweiterungen (Lieferant+URL-Repeater, „nur aktive", Alternativname, Lagerort + 3 Stammdaten-CRUDs) | OPT-2 | 2–3,5 Tage |
| 4 | AP-20 | Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag | AP-21 | 1–2 Tage |
| 5 | AP-23 | Rezeptur-Phasen (Hersteller-Rezeptur) | – | 3–5 Tage |
| 6 | AP-24 | Chargen-Nr.-Präfix | – | 1 Tag |
| 7 | OPT-3 + OPT-4 | Pagination + show()-Refactoring | – | 1,5–2,5 Tage |
| 8 | AP-27 | Wareneingang: Chargen-Merge | OPT-4 | 1–2 Tage |
| 9 | AP-28 | Neue Produktions-Maske + Produktionsergebnis | AP-21, AP-23, AP-24, AP-27 | 5–8 Tage |
| 10 | AP-13 | Shop-Bestandsabzug bei Versand (inkl. Sets; Konzept aus V4.0) | AP-28 (saubere Buchungslogik) | 3–5 Tage |
| 11 | AP-14 | Audit-Trail | – | 2–3 Tage |
| 12 | AP-16 | 2FA für Admins (Google Authenticator) | – | 2–3 Tage |
| 13 | AP-15 | Blockbasierte Rechte (sehen/bearbeiten je Block) | AP-16 sinnvoll davor | 4–6 Tage |
| 14 | AP-17 | WaWi-Einstellungsseite (Schwellwerte, Fenster, Defaults) | – | 1–2 Tage |
**Begründung der Reihenfolge:**
- OPT-1/OPT-2 zuerst: minimales Risiko, sofortiger Nutzen, und AP-21 baut direkt auf OPT-2 auf (drei neue Lagerort-CRUDs Raum/Regal/Fach).
- Danach unverändert die V5.0-Logik: kleine fachliche Ergänzungen (AP-21/20) → Datenmodell-Grundlagen (AP-23/24) → große Produktions-Maske (AP-28), der OPT-4 und AP-27 vorgelagert sind (gemeinsame Chargen-Gruppierung).
- AP-13 direkt nach AP-28: dann existiert die endgültige Buchungslogik über `production_outputs`, und `monthlyConsumptionByProduct()` (wartet auf `source='sale'`) wird automatisch korrekt.
- Sicherheits-/Governance-Pakete (AP-14/15/16/17) zum Schluss als eigener Block — fachlich unabhängig, gut geschlossen lieferbar.
**Gesamtaufwand offen:** grob 28–42 Personentage.
---
## 4. Arbeitspakete im Detail
Die fachlichen Detailspezifikationen zu **AP-20, AP-21, AP-23, AP-24, AP-27, AP-28** stehen unverändert und weiterhin gültig in `entwicklungsplan-aktualisiert-12-06-2026.md` (§4 + §6) — dort sind DB-Schema, Dateipfade, Akzeptanzkriterien und Tests je AP vollständig beschrieben. Die OPT-Pakete sind in §2 dieses Dokuments spezifiziert. Für **AP-13** gilt das Konzept aus V4.0 (`entwicklungsplan-aktualisiert-02-06-2026.md`), mit einer Änderung: Buchung gegen `production_outputs`-basierte Bestände (nach AP-28).
### Geklärte Punkte mit dem Kunden (beantwortet 06.07.2026)
1. **Lagerort-Modell (AP-21):** ✅ **drei getrennte Stammdaten-Tabellen** (Raum/Regal/Fach) — wie empfohlen.
2. **Chargen-Nr.-Datumsformat (AP-24):** ✅ `TP1` + `ddmmyy` bestätigt (Beispiel TP1150626 = 15.06.26). **Bei AP-24-Umsetzung in der Hinweise-Doku (AP-18) hinterlegen.**
3. **„Verbrauch/Monat" (AP-22):** ✅ rollierendes 6-Monats-Fenster bleibt wie umgesetzt.
4. **Produktionsergebnis (AP-28):** ✅ Output-Produkte dürfen andere Produkte sein als das geplante (z. B. 40×50 ml + 50×5 ml).
5. **Horizont-Filter Rohstoffbestand (OPT-5):** ✅ vorerst nur clientseitige Filterung → **OPT-5 entfällt.**
---
## 5. Pflege dieses Dokuments
- Jedes abgeschlossene AP im Umsetzungsprotokoll (§6) mit Datum, Dateien und Test-Status protokollieren.
- UI-Konventionen unverändert: Design-System `resources/views/admin/inventory/partials/wawi-ui.blade.php` (AP-19), Datumsfelder `datepicker-base` (`dd.mm.yyyy`), Status über `wawi-pill`.
- Vor jedem Commit: `vendor/bin/pint --dirty` + betroffene Tests.
- `docs/Todos.md` ist vollständig eingearbeitet (Rest: AP-15/AP-16) und kann archiviert werden.
---
## 6. Umsetzungsprotokoll V6.0 (laufend)
| Datum | AP | Kurzbeschreibung | Tests |
|---|---|---|---|
| 06.07.2026 | **OPT-1 (Quick Wins)** | **N+1-Fix Ausschussliste:** `StockDisposalController@index` lädt jetzt `user.account` eager (vorher `user` → bis zu 500 Lazy-Loads durch `getFullName()`). **Composite-Indizes:** Migration `2026_07_06_113731_add_aggregation_indexes_to_stock_entries_table` — `stock_entries(entry_type, status, ingredient_id)` + `(entry_type, status, packaging_item_id)` für die Aggregations-Queries im `InventoryService`; auf DB ausgeführt. **Befund-Korrektur:** Der Index `production_ingredients(production_id, ingredient_id)` ist **nicht** redundant — er bedient den FK auf `production_id` mit (kein separater FK-Index vorhanden, per `SHOW INDEX` verifiziert) → bleibt bestehen. **Ungenutzter Eager-Load** `producedByUser` aus `ProductionRepository::listForIndex()` entfernt (wird nur in `show` gebraucht, dort bereits via `producedByUser.account` geladen). **`ExampleTest`** auf `assertRedirect()` umgestellt (Domain-Redirect auf `/`) — Suite dauerhaft 100 % grün. | Neuer Query-Count-Test `ausschuss-liste laedt benutzerkonten ohne n+1` in `StockDisposalTest` (verifiziert: schlägt ohne Fix fehl). Volle Suite: **155 grün, 538 Assertions** |
| _offen_ | OPT-2 | Settings-CRUD-Basisklasse (7 Controller) | – |
| _offen_ | AP-21 | INCI-Erweiterungen | – |
| _offen_ | AP-20 | Rohstoffbestand-Filter + Flag | – |
| _offen_ | AP-23 | Rezeptur-Phasen | – |
| _offen_ | AP-24 | Chargen-Präfix | – |
| _offen_ | OPT-3/OPT-4 | Pagination + show()-Refactoring | – |
| _offen_ | AP-27 | Chargen-Merge Wareneingang | – |
| _offen_ | AP-28 | Neue Produktions-Maske + Produktionsergebnis | – |
| _offen_ | AP-13 | Shop-Bestandsabzug | – |
| _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – |
> **➡️ HIER GEHT ES WEITER: OPT-2 (Settings-CRUD-Basisklasse).** Danach AP-21 gemäß V5.0-Spezifikation — alle Klärungspunkte sind beantwortet (§4): Lagerort = drei getrennte Stammdaten-Tabellen Raum/Regal/Fach.

View file

@ -3,7 +3,6 @@
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
@ -12,10 +11,10 @@ class ExampleTest extends TestCase
*
* @return void
*/
public function testBasicTest()
public function test_basic_test()
{
$response = $this->get('/');
$response->assertStatus(200);
$response->assertRedirect();
}
}

View file

@ -1,11 +1,13 @@
<?php
use App\Models\Country;
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\PackagingItem;
use App\Models\StockDisposal;
use App\Models\StockEntry;
use App\Models\Supplier;
use App\Models\UserAccount;
use App\Services\InventoryService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -207,6 +209,50 @@ test('liste und formular rendern', function () {
->assertSee('AP12-Render');
});
test('ausschuss-liste laedt benutzerkonten ohne n+1', function () {
$admin = ap12MakeUser(7);
$location = Location::factory()->create();
$ing = ap12MakeIngredient('AP12-N1');
$country = Country::query()->firstOrCreate(
['code' => 'DE'],
['phone' => '00', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Germany', 'fr' => 'Germany', 'it' => 'Germany', 'ru' => 'Germany', 'active' => true]
);
$makeDisposal = function () use ($ing, $location, $country) {
$account = UserAccount::query()->create(['first_name' => 'Vera', 'last_name' => 'Sucher', 'country_id' => $country->id]);
$user = ap12MakeUser(1);
$user->forceFill(['account_id' => $account->id])->save();
StockDisposal::query()->create([
'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $location->id,
'quantity' => 10, 'unit' => 'gram', 'reason' => 'Bruch / Beschädigung',
'disposed_at' => '2026-03-01', 'user_id' => $user->id,
]);
};
$this->actingAs($admin, 'user');
$countQueries = function (): int {
DB::flushQueryLog();
DB::enableQueryLog();
$this->get(route('admin.inventory.stock-disposals.index'))->assertSuccessful();
$count = count(DB::getQueryLog());
DB::disableQueryLog();
return $count;
};
$makeDisposal();
$this->get(route('admin.inventory.stock-disposals.index'))->assertSuccessful(); // Warm-up
$baseline = $countQueries();
foreach (range(1, 5) as $i) {
$makeDisposal();
}
expect($countQueries())->toBeLessThanOrEqual($baseline);
});
test('chargen-endpoint liefert eingegangene chargen', function () {
$user = ap12MakeUser(7);
$location = Location::factory()->create();