presseportale/dev/optimize/entwicklungsplan.md
Kevin Adametz c7bd940f28 Optimierung Phase 0: Korrektheit & Security
Konsolidierter Entwicklungsplan aus den zwei Analyse-Dokumenten plus
Umsetzung der Phase-0-Massnahmen (Korrektheit/Security, je mit Tests):

0.1 BasicAuth: timing-sicherer Vergleich (hash_equals, beide Felder ohne
    Short-Circuit); Enabled-Check zur Laufzeit via config('auth.basic.*')
    statt env() im Bootstrap-Closure (greift jetzt unter config:cache);
    Config-Block + .env.example. Fund: auth.basic.* war nie als Config
    definiert, Gate war faktisch unkonfigurierbar.
0.2 Legacy-Invoice-"latest"-Abfrage zentralisiert (LegacyInvoice::
    scopeLatestFirst + User::latestLegacyInvoice); beide Consumer N+1-frei
    umgestellt; unterschiedliches null-Verhalten bewusst erhalten.
0.3 Payment-Option-Eligibility vereinheitlicht (period-aware, strikt):
    UserPaymentOption::scopeActiveNow/grandfatheredNow + User::
    hasActivePaymentOption; hasActiveBooking/hasUnlimitedPressReleaseQuota
    und ApiAccessEligibilityService darauf umgestellt; $hasGrandfathered-
    Mislabel behoben.
0.4 Theme->Portal-Vokabular: totes, projektfremdes ThemeMiddleware
    geloescht; Mapping in Portal::fromTheme() zentralisiert; fremden
    b2in-Default im ThemeServiceProvider auf Portal-Default korrigiert.
0.5 InvoiceNumberGenerator: Erst-Insert race-sicher via insertOrIgnore
    (Unique-Index auf circle existiert bereits). CreditWalletService war
    bereits abgesichert (Unique-Index + createOrFirst).

Tests: 4 neue Dateien, 5 bestehende auf gueltiges grandfathered_until
angepasst. Volle Suite gruen (793 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:09:02 +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.
- [ ] **1.1 Prototyp `admin/users/table` entfernen** — Route in `routes/admin.php` + `resources/views/livewire/admin/users/table.blade.php`. Filtert auf nicht existentes `users.status`, liest `$user->role` (statt Spatie), englische Demo-Menüs → kann SQL-Fehler/falsche UI erzeugen. Test für entfernte Route anpassen/ergänzen.
- [ ] **1.2 Tote Admin-Wrapper-Views prüfen & entfernen** — `resources/views/admin/*` werden außer `admin.dashboard` (via `DashboardController`) nicht produktiv gerendert. Wrapper mit alten Parametern (`request()->route('pressRelease')` vs. aktuelle `{id}`-Route) entfernen oder klar als Legacy markieren. `admin.dashboard` behalten.
- [ ] **1.3 Tote Helfer entfernen** — `EnsureUserIsAdmin:16-18` totes `loadMissing('roles')`; `statusColor` aus `with()` (admin/edit:558-564, admin/show:111-117) prüfen/entfernen; `LegacyInvoicePdfController:26` Laufzeit-`Schema::hasColumn` nach Migration entfernen.
---
## 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>