# Entwicklungsplan: System-Optimierung Pressekonto > Konsolidiert aus `system-optimierung-analyse-codex.md` und `system-optimierungen-analyse-claude.md` (Stand 18.06.2026). > Reihenfolge nach **Risiko/Nutzen**: erst Korrektheit & Security, dann risikoarme Deduplikation, dann größere Extraktionen. ## Arbeitsregeln (für jeden Schritt) - **Test-First bei Korrektheits-/Verhaltensänderungen** (Phase 0): erst einen Pest-Test schreiben, der das Soll-Verhalten festschreibt, dann ändern. - **Test-After bei reiner Extraktion** (Phasen 1–5): bestehende Feature-Tests vor und nach dem Schritt laufen lassen — sie müssen grün bleiben. - Tests gezielt laufen: `vendor/bin/sail artisan test --compact --filter=`. - Nach PHP-Änderungen: `vendor/bin/sail bin pint --dirty --format agent`. - **Web-Frontend** (`resources/views/web/**`, `components/web/**`) gehört dem User → Phase 6, separat committen, vor jedem Löschen bestätigen lassen. - Jeder Schritt = ein eigener, fokussierter Commit. --- ## Phase 0 — Korrektheit & Security (höchste Dringlichkeit, Test-First) Latente Fehlerquellen und Sicherheitslücken. Klein, aber wichtig. - [x] **0.1 BasicAuth absichern** — `!=` → `hash_equals()` (timing-sicher, beide Felder ohne Short-Circuit). Enabled-Check von `env()` (brach unter `config:cache`/im Bootstrap) in die Middleware zur Laufzeit via `config('auth.basic.*')` verlagert; Config-Block in `config/auth.php` + `.env.example` ergänzt. **Fund:** `auth.basic.user/password` waren nie als Config-Key definiert → Gate war faktisch nicht konfigurierbar; jetzt nur erzwungen, wenn aktiviert UND Zugangsdaten gesetzt. Test: `tests/Feature/BasicAuthMiddlewareTest.php`. - [x] **0.2 Legacy-Invoice-„latest"-Abfrage zentralisiert** — `LegacyInvoice::scopeLatestFirst()` als Single Source of Truth + `User::latestLegacyInvoice()`; `ApiAccessEligibilityService` und `LegacyApiCustomerReporter` (N+1-frei via Eager-Load-Scope) nutzen sie. **Bewusst erhalten:** unterschiedliches `null`-Verhalten (Gate=deny vs. Report=needs_review) ist kontextuell korrekt, keine Vereinheitlichung. Test: `tests/Feature/ApiAccessEligibilityServiceTest.php`. - [x] **0.3 Zahlungsoption-Eligibility vereinheitlicht (period-aware, strikt)** — Entscheidung des Users: überall period-aware. Neue Single Source of Truth: `UserPaymentOption::scopeActiveNow()` (Active in laufender Periode ODER laufender Bestandsschutz) + `scopeGrandfatheredNow()`; `User::hasActivePaymentOption()`. Umgestellt: `User::hasUnlimitedPressReleaseQuota`, `User::hasActiveBooking`, `ApiAccessEligibilityService`. Mislabel behoben: `CustomerStatusSummary::$hasGrandfathered` nutzt jetzt `grandfatheredNow()` (spezifisch Bestandsschutz statt „Active ODER Grandfathered"). Test: `tests/Feature/ApiAccessEligibilityServiceTest.php`; 5 bestehende Tests auf gültiges `grandfathered_until` angepasst (waren via Factory-`optional()` nicht-deterministisch). **Folge der strikten Wahl (geklärt 19.06.):** Ein Grandfathered-User **ohne** `grandfathered_until` zählt nicht mehr als aktiv — User-Entscheidung: `NULL = abgelaufen/ungültig`, Verhalten beibehalten (konsistent mit altem API-Gate), kein weiterer Code-Change. - [x] **0.4 Theme→Portal-Vokabular vereinheitlicht** — **Fund:** `ThemeMiddleware` war **nirgends registriert** und referenzierte projektfremde Themes (`b2in/b2a/stileigentum/style2own` aus einem anderen Projekt) → ersatzlos gelöscht. Theme→Portal-Mapping zentralisiert in `Portal::fromTheme()` (genutzt von `SetCurrentPortal`). Fremden `$defaults`-Fallback im `ThemeServiceProvider` (`b2in`/`build/b2in`) auf echten Portal-Default (`portal`/`build/portal`) korrigiert. Test: `tests/Unit/PortalFromThemeTest.php`. - [x] **0.5 Race-Conditions abgesichert** — **Fund:** Die empfohlenen Unique-Indizes existieren bereits (`credit_wallets.user_id`, `invoice_number_sequences.circle`) → keine Migration nötig. `CreditWalletService::post` ist damit bereits race-sicher (Unique-Index + `firstOrCreate`/`createOrFirst` fängt Unique-Violation; Saldo-Update unter `lockForUpdate` in Transaktion) → kein Change. `InvoiceNumberGenerator::next()` gehärtet: Erst-Insert-Pfad (kein Seeder → produktiv getroffen) crashte bei parallelem Erstaufruf an der Unique-Violation; jetzt `insertOrIgnore` (race-sicher, null-Branch eliminiert). Test ergänzt: `tests/Feature/Billing/InvoiceNumberGeneratorTest.php`. --- ## Phase 1 — Aufräumen / Toter Code (risikoarm, Paket A) Keine Fachlogik-Änderung. Reduziert erreichbare Demo-Komponenten und Altlasten. - [x] **1.1 Prototyp `admin/users/table` entfernt** — Route aus `routes/admin.php` (war ohnehin durch `admin/users/{id}` unerreichbar) + `resources/views/livewire/admin/users/table.blade.php` gelöscht; Eintrag aus `routes/ADMIN_ROUTES.md` entfernt. Regressions-Test in `PanelConsolidationTest` (`Route::has('admin.users.table')` ist false). - [x] **1.2 Tote Admin-Wrapper-Views entfernt** — verifiziert: kein `view()`/`@include`/`@extends`/`@livewire` rendert `resources/views/admin/*` außer `admin.dashboard` (via `DashboardController`); Wrapper nutzten alte Route-Parameter (`request()->route('company')`). 16 tote Wrapper-Blades gelöscht; `dashboard.blade.php` + die `.md`-Doku behalten. - [x] **1.3 Tote Helfer entfernt** — `EnsureUserIsAdmin` redundantes `loadMissing('roles')` (Spatie `hasAnyRole()` lädt ohnehin); `statusColor`-Key aus `with()` in admin `edit`/`show` (Blade nutzt nur lokales `$statusClass`); `LegacyInvoicePdfController` Laufzeit-`Schema::hasColumn` (Spalte existiert seit Migration 2026_05_04) + unbenutzter `Schema`-Import. --- ## Phase 2 — Press-Release-Editoren entdoppeln (größter Einzel-Hebel, Paket B) 4 Dateien, ~5.500 Zeilen, ~80–95 % Überschneidung. In kleinen, risikoarmen Schritten. - [ ] **2.1 Admin-Editoren nutzen `InteractsWithPressReleaseTags`** (H5) — Customer nutzt den Trait bereits, Admin inlined `addTag/removeTag/tagsArray`+`tags`-Computed. → `use`-Statement + ~35 Zeilen je Datei löschen. - [ ] **2.2 `InteractsWithPressReleaseScheduling`-Trait** (H1) — `updatedPublishMode/ScheduledDate/ScheduledTime`, `syncScheduledAt`, `resolveScheduledAt`, `validateScheduledAtWhenReady`, `notifyValidationError` + `scheduled*`-Block sind 4× wortgleich. Spiegelt bestehenden Tags-Trait. - [ ] **2.3 `formRules()` zentralisieren** (H2) — ~95 % identisch; einziger Unterschied admin `portal`+`companyId exists` vs. customer `companyId integer`. → `baseRules()` im Trait, Komponenten überschreiben nur abweichende Keys. - [ ] **2.4 `presubmitChecks` kanonisieren** (H3) — 4× dupliziert mit Copy-Paste-Drift (`trans_choice` vs. `__`). → eine Version im Trait. - [ ] **2.5 Kontakt-Helfer + `tagSuggestionsFor()` zentralisieren** (H4) — `defaultContactIdFor/companyContacts/companyContact` in allen vier, divergente Query-Styles. → Trait/Scope. - [ ] **2.6 Persistierung in `PressReleaseService`** (M1) — Customer hat bereits `persistDraft/persistEdit/pressReleaseAttributes`, Admin inlined volle `create/update`. Slug + `HtmlSanitizer::clean()` + `contacts()->sync()` + `contentChanged`→reclassify/rescore überall wiederholt. → `PressReleaseService::createDraft()` / `updateContent()`. - [ ] **2.7 Firmen-Combobox-Suche** (M2) — Admin baut Query inline, Customer delegiert an `CustomerCompanyContext::searchCompaniesFor()`. `supportsFullTextSearch()` 3× kopiert. → `Company::scopeSearchByTerm()` (siehe auch 3.x). - [ ] **2.8 Formular-Markup als Blade-Components** (M3, größte Roh-Zeilenersparnis) — `pr-char-meter`, `pr-tag-input`, `pr-presubmit-check`, `pr-publish-schedule`, `pr-contact-select`, `pr-boilerplate-panel`. - [ ] **2.9 Listen-/Show-Components + Admin-Aktionen** (M4–M6) — `pr-table-row`, `filter-chip`, `pr-status-timeline`, `pr-rejection-banner`; `HandlesAdminPressReleaseActions`-Trait (`findAdminPR()` ersetzt ~10× `withoutGlobalScopes()->findOrFail($id)`); Status-`match` auf `PressReleaseStatus::badgeClass()/color()`. --- ## Phase 3 — Backend-Querschnittslogik zentralisieren (Paket D + Domain) - [ ] **3.1 `Company::scopeAccessibleBy()` / `CompanyPolicy`** — identische `owner_user_id == me OR whereHas('users', me)`-Closure in 4 Controllern + Überlappung mit `User::accessibleCompanyIds()` und `CustomerCompanyContext::accessibleCompanyQuery()`. Policy wird in `ContactAccessService:229` bereits referenziert. → konsequent über Gate/Policy. - [ ] **3.2 API-PressRelease-Zugriff + Eager-Loads zentralisieren** — `findOwnedPressRelease` doppelt (PressRelease/Image-Controller); Relation-Liste (`company:…`, `category.translations`, `images`) mehrfach. → expliziter Scope (`ownedByApiUser`) + `withApiResourceRelations()`. Unique-Slug-Reimplementierung (`PressReleaseController:201-220`) durch bestehendes `HasUniqueSlug` ersetzen. - [ ] **3.3 `DomainContext`-Service** — Host→Config-Auflösung 4× (`ThemeServiceProvider::boot`, `ThemeMiddleware`, `SetDomainUrl`, `SetCurrentPortal`). **Vorher prüfen, ob `SetDomainUrl` toter Code ist** (scheint nicht in `bootstrap/app.php` registriert). `CurrentPortalContext` mutable static → request-scoped Binding. - [ ] **3.4 Performance** — `LogApiUsage` synchroner INSERT/Request → terminable/queued. `CustomerStatusSummary:35-66` Query-Sturm (always-on im Header) memoisieren; offene Single-Purchase 3× → einmal berechnen. `CustomerCompanyContext::selectedCompany()` doppelte Query → einmal `first()`. `LegacySubscriptionService::cancelBundleFor` Update-pro-Iteration → Bulk/Transaction. - [ ] **3.5 Kleinere Zentralisierungen** — `VatResolver::normalize/isEuCountry` (3×), `User::findByEmail()` statt `LOWER()`-Queries (defeated Index, 4×), `config('billing.default_currency')` statt `'EUR'`/`'eur'`-Literale, gemeinsame Account-Provisioning-Methode (`ContactAccessService` ≈ `SocialAuthService`). - [ ] **3.6 Auth-Gates** — `EnsureUserIsAdmin/Customer`, `EnsureApiUserIsActive` von Hand-Rolling → `Gate::define('access-admin'|'access-customer')` + `can:`-Middleware. Impersonation-Redirect aus dem Admin-Gate in eigene Middleware. --- ## Phase 4 — Web-Routen entflechten (Paket C) `routes/web.php` > 1100 Zeilen mit kompletter Datenaufbereitung. Schrittweise, gut absicherbar. - [ ] **4.1 Portal-/Domain-Kontext extrahieren** (baut auf 3.3 `DomainContext` auf). - [ ] **4.2 Published-Release-Query zentralisieren** — gleiche Bedingung (Portal `both`, `status=published`, Sprache, `published_at <= now()`) mehrfach, teils mit/ohne `withoutGlobalScope(PortalScope)`. → Scopes `scopePublished`, `scopeForPortalValues`, `scopeForLanguage`, `scopeVisibleInArchive`. - [ ] **4.3 Page-Data in Services** — `HomePageData`/`CategoryPageData`/`NewsroomPageData` für Home/Kategorie/Newsroom/Detail/Suche/Preise. - [ ] **4.4 Routen auf schlanke (Invokable) Controller umstellen** — schrittweise je Seite. --- ## Phase 5 — Admin-CRUD & Auth: Concerns + Blade-Components (risikoarm) - [ ] **5.1 Tabellen-Concerns** — `HasSortableTable` (`sort()` 3× wortgleich), `HasFullTextSearch` (`supportsFullTextSearch()` 6×+ + `applyFuzzySearch`), `HasResettableFilters` (`updatedXxxFilter()`→`resetPage`). `stats()` mit `SUM(CASE WHEN is_active)` → `AdminPerformanceCache::activeInactiveStats()`; Count-Hydration → `hydrateRelationCounts()`. - [ ] **5.2 User create/edit entdoppeln** — `UserFormRules`-Provider + `UserUpsertService`/`SaveUserAction` (`save()` trägt ~175 Zeilen). Markup → ``, ``, ``. Kontakt → `ValidatesContactData`-Concern + ``. - [ ] **5.3 Formatierungs-Components/Enum** — `Portal::badgeColor()` + ``; ``; `PressReleaseStatus`-Enum-Methoden; ``, ``, `Category::localizedName()`, `Contact::displayName()`. - [ ] **5.4 Seiten-Struktur-Components** — ``, ``, ``, ``, ``, ``. (Muster: bestehende ``/`components.portal.pagination`.) - [ ] **5.5 Flash/Auth-Components** — `` + `` app-weit; ``, ``/``; `ensureNotRateLimited()/throttleKey()`-Trait (login/magic-link divergieren); ``. --- ## Phase 6 — Web-Frontend (separat, mit User abstimmen) > `resources/views/web/**` + `components/web/**` gehören dem User. Beratend; eigener Commit. „unused"-Zählungen stammen aus `grep` — dynamische Auflösung vor Löschen prüfen. - [ ] **6.1 Pricing-Cards** — `web/pressekonto.blade.php` codet 3× ~67-Zeilen-`
`s, obwohl datengetriebenes `` (iteriert `Plan::active()`) existiert. → einsetzen (~200 Zeilen → eine Schleife). - [ ] **6.2 Release-Cards vereinheitlichen** — `press-release-card` ≈ `presseecho-release-card` (~95 %, letztere 0 Verwendungen). → eine Komponente mit `variant`-Prop oder löschen. Industrie-Icon-Map (3×) → `CategoryService`/Config; Release-Normalisierung (3×) → `ReleaseCardData::from()`. - [ ] **6.3 Footer/Header/Toter Code** — `site-footer` ≈ `hub/site-footer`, Top-Utility-Bar; tote Legacy-Cards in `web/examples/` nach Bestätigung entfernen; Inline-SVGs → `components/web/icons/`-Set; `` für `max-w-layout`-Shell. --- ## Reihenfolge-Empfehlung (TL;DR) 1. **Phase 0** zuerst — Korrektheit/Security, Test-First. 2. **Phase 1** — schnelles Aufräumen, kein Risiko. 3. **Phase 2** — größter Dedup-Gewinn (PR-Editoren), inkrementell. 4. **Phase 3 + 4** — Backend/Web-Architektur (mittlerer Aufwand, hoher struktureller Gewinn). 5. **Phase 5** — Admin/Auth-Components. 6. **Phase 6** — Web-Frontend separat mit dem User. **Beibehalten (gute Muster):** `PressRelease::accessibleBy`, `PortalScope`, `HasUniqueSlug`, Form Requests + Eloquent Resources, `AdminPerformanceCache`, `InteractsWithPressReleaseTags`, ``/`components.portal.pagination`.