presseportale/dev/optimize/entwicklungsplan.md
Kevin Adametz a5f3ddd0f3 Optimierung Phase 1: Aufraeumen / toter Code
1.1 Prototyp admin/users/table entfernt: Route (war durch admin/users/{id}
    ohnehin unerreichbar) + Volt-Komponente table.blade.php geloescht,
    ADMIN_ROUTES.md aktualisiert; Regressions-Test in PanelConsolidationTest.
1.2 16 tote Legacy-Wrapper-Views unter resources/views/admin/* geloescht
    (nutzten alte Route-Parameter, nirgends via view/@include/@livewire
    gerendert); admin.dashboard + .md-Doku behalten.
1.3 Tote Helfer entfernt: EnsureUserIsAdmin redundantes loadMissing('roles'),
    ungenutzter statusColor-Key aus with() in admin PR edit/show,
    LegacyInvoicePdfController Laufzeit-Schema::hasColumn (Spalte existiert
    seit Migration) inkl. Schema-Import.

Volle Suite gruen (794 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:27:12 +00:00

108 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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=<Name>`.
- 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 → `<x-admin.billing-fields>`, `<x-admin.company-link-picker>`, `<x-admin.role-checkboxes>`. Kontakt → `ValidatesContactData`-Concern + `<x-contact-fields>`.
- [ ] **5.3 Formatierungs-Components/Enum** — `Portal::badgeColor()` + `<x-portal.portal-badge>`; `<x-portal.status-badge>`; `PressReleaseStatus`-Enum-Methoden; `<x-portal.money>`, `<x-portal.datetime>`, `Category::localizedName()`, `Contact::displayName()`.
- [ ] **5.4 Seiten-Struktur-Components** — `<x-portal.page-header>`, `<x-portal.stat-cards>`, `<x-portal.table-panel>`, `<x-portal.table-pagination>`, `<x-portal.empty-state>`, `<x-portal.searchable-select>`. (Muster: bestehende `<x-portal.stat-card>`/`components.portal.pagination`.)
- [ ] **5.5 Flash/Auth-Components** — `<x-portal.alert :dismissible>` + `<x-session-status>` app-weit; `<x-password-input>`, `<x-form-field>`/`<x-email-input>`; `ensureNotRateLimited()/throttleKey()`-Trait (login/magic-link divergieren); `<x-auth.divider-actions>`.
---
## 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-`<article>`s, obwohl datengetriebenes `<x-web.pricing-teaser>` (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; `<x-web.container>` 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`, `<x-portal.stat-card>`/`components.portal.pagination`.
</content>
</invoke>