Commit graph

104 commits

Author SHA1 Message Date
e879a38ee4 Optimierung 4.3/4.4 (Release-Detail): Page-Data-Service + Invokable Controller
Detailseite einer Pressemitteilung (inkl. Hit-Zaehler) in ReleaseDetailPageData
+ Web\ReleaseDetailController. Verhalten unveraendert, 92 Web-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:11:35 +00:00
ee74126b89 Optimierung 4.3/4.4 (Kategorie): Page-Data-Service + Invokable Controller
Kategorie-/Branchenseite in CategoryPageData + Web\CategoryController.
Verhalten unveraendert, 92 Web-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:09:58 +00:00
179dd8e004 Optimierung 4.3/4.4 (Newsroom-Detail): Page-Data-Service + Invokable Controller
Newsroom-Markenseite in NewsroomDetailPageData + Web\NewsroomDetailController.
Controller-Signatur fuehrt {edition} vor {slug} (positionsgebundene
Route-Parameter-Aufloesung). Verhalten unveraendert, 92 Web-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:08:21 +00:00
a2179ea47f Optimierung 4.3/4.4 (Newsrooms): Page-Data-Service + Invokable Controller
Newsrooms-Verzeichnis-Closure in NewsroomsPageData + Web\NewsroomsController
verschoben. Verhalten unveraendert, 92 Web-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:04:59 +00:00
fef6bbc9a6 Optimierung 4.3/4.4 (Veroeffentlichen): Page-Data-Service + Invokable Controller
Veroeffentlichen-Closure in VeroeffentlichenPageData + Web\VeroeffentlichenController
verschoben. Verhalten unveraendert, 92 Web-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:03:34 +00:00
418a2bca4c Optimierung 4.3/4.4 (Suche): Page-Data-Service + Invokable Controller
Suche-Closure in SuchePageData + Web\SucheController verschoben. Verhalten
unveraendert, 92 Web-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:01:18 +00:00
7625ec7ed5 Optimierung 4.3/4.4 (Preise): Page-Data-Service + Invokable Controller
Erste Seite des Web-Routen-Umbaus: die Preise-Closure aus routes/web.php in
PreisePageData (Datenaufbereitung) + Web\PreiseController (schlanker Invokable
Controller) verschoben. Route nutzt jetzt die Controller-Klasse. Verhalten
unveraendert, Web-Tests bleiben gruen (92).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:59:59 +00:00
e5a63bcedd Optimierung 4.1: Web-Portal-/Domain-Kontext extrahiert
- WebPortalContext::fromRequest() buendelt die zuvor ~6x je Route-Closure
  wiederholte Host->Portal-Ableitung (portal, language, portalValues,
  domainConfig).
- DomainAssetContext::applyWebDomainConfig() ist die Single Source of Truth fuer
  die Domain-/Theme-Anwendung (config + View::share + Vite); die fruehere
  $applyWebDomainConfig-Closure in routes/web.php ist nur noch ein duenner
  Delegations-Wrapper, damit Bare-Root- und Edition-Home-Route unveraendert
  per Closure aufrufen koennen.
- Die 6 Portal-Seiten-Closures (Kategorie, Detail, Veroeffentlichen, Suche,
  Newsrooms, Newsroom-Detail) sowie Preise nutzen jetzt den zentralen Kontext.

Abgesichert durch die 92 Web/-Tests (jede Seite laeuft ueber den neuen Kontext).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:42:00 +00:00
987a5a2c42 Optimierung 4.2: Published-Release-Query ueber PressRelease-Scopes zentralisiert
Vier komponierbare Scopes auf PressRelease:
- published()         Status freigegeben + published_at gesetzt (ohne Zeitschranke,
                      damit Statistik-Fenster frei einschraenken koennen)
- visibleInArchive()  published() + published_at <= jetzt (oeffentlich sichtbar)
- forPortalValues()   whereIn portal
- forLanguage()       where language

~19 duplizierte Query-Bloecke in routes/web.php (Home, Kategorie, Detail,
Veroeffentlichen, Suche, Newsrooms, Newsroom-Detail, Legacy-Redirect) nutzen
jetzt diese Scopes. Jedes withoutGlobalScope(PortalScope) bleibt an seiner
Stelle erhalten; die Klausel-Reihenfolge ist fuers Ergebnis irrelevant (reine
AND-Verknuepfung), daher verhaltensgleich.

Test: Web/PublishedReleaseScopeTest; die 89 bestehenden Web/-Tests bleiben gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:35:55 +00:00
4c80b934ab Doku: Phase 3 im Entwicklungsplan abgeschlossen markiert (3.1-3.3 nachgezogen)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:21:22 +00:00
31e01e46b6 Optimierung 3.6: Zugriffs-Gates und getrennte Impersonation-Sperre
- Gate::define('access-admin'|'access-customer') als Single Source of Truth im
  AppServiceProvider (auch via @can/Policies nutzbar). EnsureUserIsAdmin und
  EnsureUserIsCustomer pruefen jetzt ueber diese Gates.
- Impersonation-Redirect aus EnsureUserIsAdmin in die eigene Middleware
  BlockAdminDuringImpersonation ausgelagert (Sitzungs-Bedingung statt
  Rollenfrage); laeuft vor der Zugriffspruefung und ist zusaetzlich als
  persistente Livewire-Middleware registriert.
- Routen-Alias 'can:' bewusst NICHT verwendet: die Admin-Middleware ist
  persistente Livewire-Middleware und braucht eine konkrete Klasse; der
  parametrisierte Alias kann dort nicht stehen. EnsureApiUserIsActive bleibt
  eigenstaendig (is_active-Pruefung mit getesteter JSON-Antwort, kein Gate).
- Tests: Auth/AccessGatesTest; UserImpersonationTest bleibt gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:20:28 +00:00
b255d90721 Optimierung 3.5: Kleinere Zentralisierungen (VAT, findByEmail, Waehrung, Provisioning)
- VatResolver::normalizeCountryCode()/isEuCountry() als Single Source of Truth;
  resolve(), VatIdValidationService und StripeCheckoutService nutzen jetzt die
  zentrale EU-Pruefung statt eigener config-Lookups.
- User::findByEmail() buendelt die LOWER(email)-Lookups (ContactAccessService,
  SocialAuthService). Bleibt bewusst case-insensitiv, da E-Mails nicht
  normalisiert gespeichert werden; der indexfreundliche Direktvergleich
  braucht erst eine Schreib-Normalisierung (offen, dokumentiert).
- config('billing.default_currency') ersetzt 8 verstreute 'EUR'/'eur'-Literale
  (Stripe-Calls via strtolower). Vereinheitlicht nebenbei die zuvor uneinheitlich
  gespeicherte SinglePurchase.currency.
- Neuer CustomerAccountProvisioner::provisionVerifiedCustomer() ist die
  gemeinsame Provisioning-Methode (verifiziert + aktiv + customer-Rolle) fuer
  ContactAccessService und SocialAuthService.
- Tests: UserFindByEmailTest, erweiterte VatResolverTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:14:05 +00:00
31d407d3ad Optimierung 3.4: Performance (Logging terminable, Single-Purchase memoisiert)
- LogApiUsage als terminable Middleware: der ApiUsageLog-INSERT laeuft nach
  gesendeter Antwort und verlaengert nicht mehr die Request-Dauer. Startzeit
  wird ueber ein Request-Attribut uebergeben (terminate() laeuft auf frischer
  Instanz).
- User::openSinglePurchaseCount() memoisiert die offenen Einmalkaeufe pro
  Request. Vorher wurde derselbe grantingSubmission-Count im Status-Widget
  mehrfach (Quota-Total, Quota-Remaining, openSinglePurchases) plus ein
  exists() in hasActiveBooking abgesetzt -> jetzt eine Query.
- CustomerCompanyContext::selectedCompany() nutzt eine einzige first()-Query
  statt exists()+first(); das Verwerfen nicht (mehr) erreichbarer Auswahlen
  bleibt erhalten.
- LegacySubscriptionService::cancelBundleFor() ist ein einziges Bulk-Update
  statt Update-pro-Iteration (UserPaymentOption hat keine Observer/Events).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:05:07 +00:00
9a93903cd9 Optimierung 3.3: totes SetDomainUrl entfernt
SetDomainUrl war nirgends registriert und duplizierte die URL-Forcing-
Logik (forceRootUrl/forceScheme/useAssetOrigin/config) aus
ThemeServiceProvider::boot(). Ersatzlos geloescht. Die Host->Config-
Aufloesung ist bereits zentral in DomainAssetContext::resolve gebuendelt
(ThemeMiddleware wurde in 0.4 entfernt, SetCurrentPortal nutzt seit 0.4
Portal::fromTheme).

Bewusst NICHT angefasst: CurrentPortalContext (mutable static) ->
request-scoped Binding - Tests koppeln an ::clear(), Umbau beruehrt viele
Call-Sites (eigener, riskanter Schritt fuer spaeter).

Tests: volle Suite gruen (802 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:31:23 +00:00
4fc46b02bf Optimierung 3.2: API-PR-Ownership-Scope + zentrale Eager-Loads
- PressRelease::scopeOwnedByApiUser() (bewusst eng: nur user_id-Autorschaft,
  anders als das breitere scopeAccessibleBy) ersetzt die byte-gleiche
  findOwnedPressRelease-Query in PressReleaseController + ImageController.
- apiResourceRelations()-Helfer ersetzt den 5x identischen Eager-Load-Set
  (company/category.translations/images) in Index/Show/Create/Update/Reclassify.

Bewusst NICHT umgesetzt: Slug-Reimplementierung (uniqueSlug) durch
HasUniqueSlug ersetzen - der API-Fallback Str::random(8) weicht vom
Trait-slugFallback() ab (Verhaltensaenderung bei leer-slugifizierten Titeln).

Tests: neuer Scope-Test; volle Suite gruen (802 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:27:59 +00:00
dba76910ca Optimierung 3.1: Company::scopeAccessibleBy() zentralisiert Firmenzugriff
Die 4x identische Zugriffs-Closure (owner_user_id == me OR whereHas('users',
me)) in CompanyController (index/show), PressReleaseController::
findOwnedCompany und CustomerCompanyContext::accessibleCompanyQuery durch
einen Company::scopeAccessibleBy() ersetzt; das Scope-Verhalten
(withoutGlobalScopes) bleibt beim Aufrufer.

Bewusst unangetastet: User::accessibleCompanyIds() (relationenbasiert MIT
globalen Scopes - anderes Scope-Verhalten) und PressReleaseImageController
(prueft PR-Autorschaft user_id, nicht Firmenzugriff).

Tests: neuer Scope-Test; volle Suite gruen (801 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:23:16 +00:00
6a1240bdfb Doku: Phase 2 im Entwicklungsplan als abgeschlossen markiert
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:17:28 +00:00
153bac7a9c Optimierung 2.8: <x-portal.pr-presubmit-check> Blade-Component
Den in allen vier PM-Editoren identischen, rein praesentationalen
Pre-Submit-Checklisten-Block (~34 Zeilen je Datei) in eine
Blade-Component ausgelagert, die $this->presubmitChecks als Prop nimmt.
Render-Test ergaenzt.

Bewusst konservativ: Nur dieser wire:model-freie, presentational Block
wurde extrahiert. Die interaktiven Markup-Panels (Tag-Input, Publish-
Schedule, Kontakt-/Firmen-Select) bleiben in den Editoren, da ihre
wire:model-/Alpine-Bindungen ohne visuelle QA nicht risikofrei in
Components zu ueberfuehren sind und die Tests gerendertes Markup nur
begrenzt abdecken.

Tests: volle Suite gruen (800 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:16:21 +00:00
3a18a2b494 Optimierung 2.9: HandlesAdminPressReleaseActions::findAdminPR()
Den ~15x duplizierten Admin-Lookup PressRelease::withoutGlobalScopes()
->findOrFail($id) in einen findAdminPR()-Helfer (Concern) zentralisiert;
index/edit/show nutzen ihn. Die eager-load-Query in show::with() bleibt
unberuehrt (anderer Zweck).

Bewusst NICHT vereinheitlicht: das Status-Badge-Mapping divergiert real
(index: archived/draft -> muted; edit/show: -> hub via default) -> waere
eine Design-Entscheidung ueber kanonische Badge-Farben, keine mechanische
Dedup.

Tests: volle Suite gruen (799 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:11:48 +00:00
86ba6c1a51 Optimierung 2.7: Company::scopeSearchByTerm fuer die Admin-Combobox
Die in admin/create und admin/edit identische Inline-Firmensuche
(Volltext-oder-LIKE) inkl. des 2x duplizierten supportsFullTextSearch()-
Treiберchecks in einen Company::scopeSearchByTerm() zentralisiert; beide
Editoren nutzen ihn jetzt (Verhalten unveraendert). Ungenutzte DB-Importe
entfernt. Customer nutzt bereits CustomerCompanyContext (eigene
join-/user-gescopte Query, daher nicht zusammengelegt).

Tests: neuer Scope-Test; volle Suite gruen (799 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:06:54 +00:00
83d2ee659a Optimierung 2.6: reprocessAfterContentChange im PressReleaseService
Den 3x identischen contentChanged->fresh->reclassifyIfClassified->
rescoreIfScored-Block (admin/edit 1x, customer/edit 2x) in
PressReleaseService::reprocessAfterContentChange() zentralisiert.

Hinweis: Eine vollstaendige createDraft()/updateContent()-Verlagerung
wurde bewusst NICHT erzwungen — die Attributsaetze und der umgebende
Validierungs-/Fehlerfluss von Admin (no_export, direkter review-Status)
und Customer (placeholder_variant, Titel-Fallback, immer Draft) divergieren
real; eine gemeinsame Persistenzmethode waere fragil bei grosser Testflaeche.

Tests: 2 neue Faelle; volle Suite gruen (796 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:01:29 +00:00
8c53146548 Optimierung 2.5: Kontakt-Helfer + tagSuggestionsFor zentralisiert
Erkenntnis: Die Admin-Inline-Query (Contact::withoutGlobalScopes()->where
->orderBy) ist exakt der Contact::forCompany()-Scope -> alle vier
Kontakt-Helfer waren funktional identisch. companyContacts/companyContact/
defaultContactIdFor in neuen Concern InteractsWithPressReleaseContacts
(ueber forCompany) ausgelagert. tagSuggestionsFor() (4x identisch) in
InteractsWithPressReleaseTags verschoben. Ungenutzte Collection-Importe
in den Customer-Editoren entfernt; selectedCompany() bleibt je Kontext
(Admin: withoutGlobalScopes find; Customer: CustomerCompanyContext).

Tests: volle Suite gruen (794 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:59:38 +00:00
b41d34e39f Optimierung 2.4: Kanonische Pre-Submit-Checkliste
presubmitChecks() der vier Editoren in ProvidesPressReleasePresubmitChecks
zentralisiert (war 4x dupliziert mit Drift). Kanonische Version uebernimmt
die bessere Customer-Variante: Firmenname statt leerer Hinweis in der
company-Zeile. Dafuer erhalten die Admin-Editoren ein selectedCompany()
(Company::withoutGlobalScopes find), das auch in with() genutzt wird;
Customer behaelt seine user-gescopte Aufloesung via CustomerCompanyContext.

Tests: volle Suite gruen (794 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:53:06 +00:00
fa995e30a2 Optimierung 2.3: formRules() via ProvidesPressReleaseFormRules
Die ~95% identischen Validierungsregeln (Inhaltsfelder, publishMode,
terminabhängiger Scheduling-Block mit zeitzonenbewusster Closure, Embargo)
in baseFormRules() zentralisiert. Die vier Editor-formRules() ergänzen nur
noch die abweichenden Schlüssel (portal nur Admin, companyId mit/ohne
exists). Ungenutzten Rule-Import in customer/edit entfernt.

Tests: volle Suite gruen (794 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:46:27 +00:00
64349ce6b4 Optimierung 2.2: InteractsWithPressReleaseScheduling-Trait
Die in allen vier PM-Editoren wortgleiche Scheduling-Maschinerie
(updatedPublishMode/ScheduledDate/ScheduledTime, updated, syncScheduledAt,
resolveScheduledAt, validateScheduledAtWhenReady, notifyValidationError)
in einen gemeinsamen Concern ausgelagert (spiegelt InteractsWith-
PressReleaseTags). ~85 Zeilen je Datei entfernt.

Tests: volle Suite gruen (794 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:41:21 +00:00
0344ac26af Optimierung 2.1: Admin-PM-Editoren nutzen Tags-Trait
Admin create/edit nutzen jetzt InteractsWithPressReleaseTags (wie die
Customer-Editoren) statt die byte-gleiche addTag/removeTag/tagsArray-Logik
lokal zu duplizieren. ~48 Zeilen je Datei entfernt.

Tests: 259 PressRelease-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:32:41 +00:00
554a731a24 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
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
6cb7b79dae Doku: Launch-Vorbereitung 18.06. (Status-Widget, WS-8, i18n, Paket-Vorteile)
- Decision-Update: neuer Abschnitt 7 "Launch-Vorbereitung (Stand 18.06.2026)"
  dokumentiert Status-Widget, WS-8-Optimierungen, Paket-Vorteile in der DB,
  i18n-Readiness (Default-Locale de, Views __()-gewrappt, EN-Inhalt bewusst
  spaeter) sowie den Editor-Umbruch-Fix. End-to-End-QA als naechster Schritt.
- WS-7-Checkliste: add_benefits_to_plans_table in die Migrations-Liste
  aufgenommen (APP_LOCALE=de stand bereits drin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:04:17 +00:00
052b214bd9 i18n: Auth-Views vollstaendig uebersetzbar machen (Readiness)
Die Auth-Strecke (Login, Registrierung, Passwort-Reset/-Bestaetigung,
Magic-Link, E-Mail-Verifizierung, 2FA) war als einziger Backend-Bereich
noch nicht durchgaengig __()-gewrappt. Alle sichtbaren deutschen Strings
in __() gewickelt (Deutsch bleibt Schluessel). Eingebettete Links (AGB/
Datenschutz) und Alpine-x-text sauber segmentiert. Heading/Eyebrow und
topRight-Label/-Link sowie der From-Banner werden jetzt im Auth-Layout am
Ausgabepunkt uebersetzt (im #[Layout]-Attribut sind keine __()-Aufrufe
moeglich). Leeres lang/en.json als Geruest fuer die spaetere EN-Befuellung.

Kunden-, Admin-, Settings- und Component-Views waren bereits vollstaendig
gewrappt (per Audit bestaetigt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:00:20 +00:00
f6a8143a2c Preise-Seite: dynamische Tarif-Vorteile aus dem Plan anzeigen
Die oeffentliche Preise-Seite rendert jetzt die im Admin gepflegte
Vorteilsliste (plans.benefits) pro Tarif; faellt ohne gepflegte Vorteile
auf den statischen pricing.plans.included-Text zurueck. DB-Vorteile sind
admin-redaktionelle Texte und erscheinen auch unter /en in der erfassten
Sprache (Preis-/Struktur-Keys bleiben uebersetzt).

Hinweis: betrifft Web-Frontend (WIP des Users), daher separater Commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:37:01 +00:00
e96a11824b Paket-Vorteile: dynamische, im Admin pflegbare Vorteilsliste je Tarif
Neue JSON-Spalte plans.benefits (Migration + Cast + Plan::benefitsList()).
Im Admin (Tarife & Pakete) pro Tarif zeilenweise pflegbar; die In-App-
Buchungsseite rendert die Liste in den Tarif-Karten statt einer festen
Zeile. Seeder/Factory mit Beispiel-Vorteilen. Single Source of Truth fuer
Buchungsseite und (folgt separat) die oeffentliche Preise-Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:36:09 +00:00
7d03deddb1 i18n: Default-Locale auf Deutsch umstellen (Basis fuer EN-Umschaltung)
APP_LOCALE/APP_FALLBACK_LOCALE auf 'de'. Das Backend nutzt durchgaengig
__('Deutscher Text') als Schluessel; mit Default-Locale 'en' wurden zwar
(mangels en.json) ebenfalls die deutschen Schluessel ausgegeben, aber eine
spaeter ergaenzte en.json haette die App bei Locale 'en' ungewollt teilweise
auf Englisch umgestellt. Mit 'de' rendert Deutsch nativ und 'en' wird zum
bewussten Umschalter. Frontend bleibt unveraendert (URL-Prefix /de|/en via
SetEdition + strukturierte lang/de|en/*.php-Keys).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:31:55 +00:00
1aa43914da WS-8: doppelte Logik konsolidieren (Tags, Kontakt-Query, Billing)
- Themen-Tag-Verwaltung (addTag/removeTag/tagsArray) war in create- und
  edit-Editor byte-identisch dupliziert -> Trait
  App\Livewire\Concerns\InteractsWithPressReleaseTags.
- Kontaktauswahl je Firma (withoutGlobalScopes + where + Sortierung) aus
  create/edit/dashboard in Contact::scopeForCompany() zentralisiert.
- Rechnungsadress-Pflichtfelder als BillingAddress::REQUIRED_FIELDS; isComplete()
  und neues completenessPercent() teilen die Liste, Dashboard nutzt es statt
  eigener Feldliste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:33:59 +00:00
1ff1cc3626 WS-8: N+1 entschaerfen und PM-Zugriffsmuster konsolidieren
- User::currentPlan() und hasUnlimitedPressReleaseQuota() pro Request
  memoisiert. Beide werden je Render mehrfach abgefragt (Status-Widget auf
  jeder Seite, Buchungsseite, Quota-Methoden) und loesten bisher jeweils
  eigene subscription-/Plan-/payment-option-Queries aus. fresh() setzt den
  Cache zurueck.
- show.blade.php: getMyPR() war NICHT memoisiert und wurde pro Request bis
  zu 7x mit voller Eager-Load-Query aufgerufen -> jetzt memoisiert. Firmen-
  Kennzahlen direkt per withCount() eager geladen statt nachtraeglichem
  loadCount().
- Dupliziertes Zugriffsmuster (withoutGlobalScopes + user_id/company_id) aus
  edit/show/index in PressRelease::scopeAccessibleBy(User) zentralisiert
  (Single Source of Truth fuer Kunden-PM-Zugriff), mit Tests abgesichert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:27:39 +00:00
7de0db3547 Status-Widget: kompakte Topbar statt hoher Header-Leiste
Der Flux-Header erzwang min-h-14 und zentrierte das kleine Status-Badge
vertikal — das ergab auf dem Desktop eine fast leere Leiste mit viel
Freiraum zum Inhalt. Header-Hoehe an den Inhalt angepasst (min-h-0,
oben knapper Abstand, unten keiner), sodass das Widget oben sitzt und
der nachfolgende Content direkt anschliesst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:14:24 +00:00
4ab746077c Status-Widget: immer verfuegbarer Buchungsueberblick in der Topbar
Ein Header-Dropdown oben rechts (Desktop) bzw. im Mobile-Header zeigt
fuer alle Kunden-Panel-User den aktuellen Status: aktuelle Buchung/Tarif,
verbleibendes PM-Kontingent und Credit-Guthaben. Ohne aktive Buchung
weist es direkt auf die noetige Buchung hin und verlinkt die Buchungsseite.

CustomerStatusSummary buendelt die Anzeige-Werte ueber die oeffentliche
User-API (Gating bleibt Single Source of Truth im Modell); die anonyme
Blade-Komponente x-customer.status-menu rendert das Dropdown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:03:25 +00:00
bcac326814 Fix: Umbrueche bleiben beim Bearbeiten von Legacy-PMs erhalten
Bestehende (importierte) Pressemitteilungen speichern ihren Text als
Plaintext mit \n-Umbruechen. Beim Laden in den HTML-Editor (flux:editor)
kollabierten diese zu einem Fliesssatz, weil HTML Whitespace faltet.

Die Plaintext->HTML-Konvertierung (Doppelumbruch = Absatz, Einzelumbruch
= <br>) lag bisher nur im Anzeige-Pfad (render()). Sie ist jetzt als
toEditorHtml() wiederverwendbar und wird beim Laden des Editors genutzt;
die Link-Policy (rel/target) bleibt dabei dem Anzeige-Pfad vorbehalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 12:57:45 +00:00
624fca68a8 Zeitzone Europe/Berlin: unnoetige UTC-Konvertierung entfernt
Beim Speichern/Veroeffentlichen waren Termine 2 Stunden versetzt: die
App-Zeitzone stand auf UTC, Termine wurden nach UTC umgerechnet, aber
published_at roh angezeigt.

- config/app.php: timezone UTC -> Europe/Berlin
- PressRelease: DISPLAY_TIMEZONE + setTimezone-Umrechnung entfernt
- Admin-/Customer-Formulare: scheduledAtUtc() -> resolveScheduledAt(),
  parst direkt in App-Zeitzone; Edit-Hydration ohne setTimezone
- ProofPdfRenderer + Boost-Anzeige: setTimezone entfernt
- Tests an verschiebungsfreies Verhalten angepasst

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:40:19 +00:00
326ff9b2a0 Editorial-Frontend: oeffentliche Unterseiten, Preise & Linkpflege
Oeffentliche Strecke durchgaengig auf das Editorial-Design umgestellt und
datengetrieben gemacht:

- Newsrooms-Verzeichnis + Newsroom-Detailseiten (Suche, Pagination)
- Preise-Seite neu (Abos via Plan, Einzel-PM via config/billing, Add-ons via
  config/credits); Texte ausgelagert nach lang/{de,en}/pricing.php
- Pricing-Teaser-Komponente auf den Startseiten + Preise-Link in Header/Footer
- Statische Seiten im Editorial-Design ueber neue Komponente x-web.static-page:
  impressum, datenschutz, agb, cookies, faq (Akkordeon), hilfe, kontakt,
  ueber-uns, api (altes Theme/gradient-hero entfernt)
- Header/Footer-Linkpflege: tote #-Anker raus, FAQ/Hilfe/Cookies verlinkt,
  "Anmelden" fuehrt in den Hub
- Legacy-URL-Redirects (.html, presskit->newsroom, Regionen)
- Datums-Bugfix in Feed-Komponenten

Tests: NewsroomPage, PreisePage, PricingTeaser, StaticPages, NavigationLinks,
LegacyRedirect. Doku in docs/frontend aktualisiert.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:08:46 +00:00
0618080e02 Doku: Umsetzungsstand der Credit-Oekonomie im Decision-Update
Abschnitt 6 'Umsetzungsstand 17.06.2026': Fundament (Wallet/Ledger/Tier/Topup),
Launch-Funktionen (Extra-PM/Boost/Nachweis/UI/Gate), Phase-2-vorbereitet
(Pruefzaehler, Pfade C/D/G), offene Punkte. Nachzug zu dd5a937 (Dateiname
mit NFD-Umlaut wurde beim ersten Commit verfehlt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:02:24 +00:00
6a7483e805 Doku: Credit-Oekonomie in WS-7-Checkliste + Umsetzungsstand im Decision-Update
- WS-7: Credit-Migrationen (wallets/transactions/review_checks/boosts/topups)
  in die Pflichtliste; Hinweis dass Credit-Pakete keine Stripe-Preis-IDs
  brauchen (checkoutCharge, config/credits.php); Webhook erfuellt credit_topup_id
  idempotent; Post-Deploy-Checks fuer Wallet-Topup + Gate-Datenschutz
- Decision-Update: Abschnitt 'Umsetzungsstand 17.06.2026' (Fundament,
  Launch-Funktionen, Phase-2-vorbereitet, offene Punkte)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:01:24 +00:00
78229296ed Fix: kein Datenverlust beim 'Buchung auswaehlen' aus dem Submit-Modal
Bug: Im Submit-Modal navigierte 'Buchung auswaehlen' per wire:navigate direkt
zur Buchungsseite, OHNE vorher zu speichern -> im Erstellen ging die ganze PM
verloren, im Bearbeiten die ungespeicherten Aenderungen.

- Submit-Modal: neuer Prop booking-action; ist er gesetzt, speichert der Button
  ueber eine Livewire-Aktion zuerst und navigiert erst danach
  ('Speichern & Buchung auswaehlen'). Fallback-href nur dort, wo bereits
  gespeichert ist (Detailansicht).
- create/edit: Persistenz in persistDraft()/persistEdit() extrahiert;
  saveDraftAndChooseBooking() speichert den Entwurf bzw. die Aenderungen und
  leitet erst dann zur Buchung (Edit reklassifiziert bei Inhaltsaenderung).
- Beide Submit-Modals mit booking-action verdrahtet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:43:21 +00:00
2c4171106a Veroeffentlichungs-Gate: proaktive Buchungs-Meldung + Schalter test-stabil
- PM-Detailseite (Entwurf/Ueberarbeiten): ist kein Buchungs-Gate erfuellt,
  erscheint statt 'Zur Pruefung einreichen' der Hinweis 'Noch keine Buchung'
  mit Link zur Buchungsseite ('Buchung waehlen'). Entwurf speichern bleibt frei.
- phpunit pinnt BILLING_ENFORCE_BOOKING=false, damit der Gate in Tests
  deterministisch bleibt (Tests, die ihn brauchen, setzen ihn per config)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:32:07 +00:00
dac8fe0eb1 UI-Feinschliff Buchungen: Tarife oben, Credits darunter, Preisliste, USt.-Modal
- Reihenfolge: Tarife -> Einzel-PM -> Credit-Wallet & Add-ons -> Preisliste
- Buchungs-Bestaetigungs-Modal mit USt.-Aufschluesselung (netto/USt./gesamt,
  Reverse-Charge & steuerbefreit) vor Stripe; greift fuer Tarife, Einzel-PM
  und Credit-Pakete (selectBooking)
- Proaktiver Banner oben, wenn die Rechnungsadresse fehlt (Link ins Profil);
  im Modal Adress-Gate statt Stripe-Link
- Extra-PM-Karte nur bei Guthaben > 0 oder aktivem Abo (sonst Preisliste)
- Pruefkontingent ausgeblendet (Phase 2)
- Preisliste: Extra-PM nach Tarif (eigener Tarif markiert), Boost-Laufzeiten,
  Nachweis-PDF

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:26:04 +00:00
31bef7ab49 UI: Credit-Wallet-Hub + Per-PM Add-ons im Backend-Pressekonto
Wallet-Oekonomie im Customer-Bereich sichtbar gemacht:

- Buchungen & Add-ons: Guthaben-Anzeige, Credit-Paket-Topup (Bonus-Staffel),
  Extra-PM-Kauf aus der Wallet mit kontextuellem Mini-Checkout-Hinweis,
  Pruefkontingent-Status (frei/Monat, heute/Tageslimit), Wallet-Verlauf
- PM-Detailseite: Add-ons-Panel fuer veroeffentlichte PMs -- Boost buchen
  (7/14/30 Tage, Gate nur gruen, Verlaengerung), Veroeffentlichungsnachweis
  kaufen + Download. Aktionen ueber BoostService/ProofPdfService

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:02:31 +00:00
bd7022ffaf Wallet-Topup ueber Stripe (Credit-Pakete mit Bonus-Staffel)
End-to-end-Aufladung der Credit-Wallet, spiegelt den Einzel-PM-Fluss:

- Paket-Staffel mit Bonus in config/credits.php (10->10, 25->27, 50->55,
  100->115 Credits; price_cents netto, Stripe Tax ergaenzt USt.)
- credit_topups (Pending->Paid) + CreditTopupService: startTopup legt den
  Pending-Kauf an, fulfill() schreibt die Wallet idempotent gut (credited_at)
- StripeCheckoutService::forCreditTopup via checkoutCharge + credit_topup_id
  Metadata; ProcessStripeWebhook schreibt bei checkout.session.completed gut
- Checkout-Route /admin/me/checkout/credits/{pack} mit Rechnungsadress-Gate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:56:23 +00:00
c89589beb0 Proof-PDF + Extra-PM-Verkauf ueber die Credit-Wallet (Decision-Update 2.1/2.3)
- ProofPdfService: Veroeffentlichungsnachweis 3 Credits pauschal, einmal pro
  PM (Zweitdownload kostenfrei); ProofPdfRenderer erzeugt das PDF on-demand
  aus vorhandenen PM-Daten (kein externer Renderer); GET-Download-Endpoint
  /admin/me/press-releases/{id}/nachweis hinter downloadProof-Policy + Kauf-Gate
- ExtraPmPurchaseService: tier-gestaffelter Nachkauf (19/15/12/10/8) aus der
  Wallet; verbucht als bezahlter SinglePurchase(ExtraPm) und greift damit in
  die bestehende Kontingent-/Slot-Mechanik. InsufficientCreditsException
  liefert das Mini-Checkout-Signal (required/available/shortfall)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:28:08 +00:00
0ff874efc8 Boost-Geschaeftslogik (Decision-Update 2.2)
Bezahlte Platzierung gruener, veroeffentlichter PMs ueber die Credit-Wallet:

- boosts Tabelle (Zeitraum starts_at/ends_at, days, credits_charged)
- BoostService: Gate (nur Published + Green), Preis nach Laufzeit
  (7/14/30 -> 12/20/35 Credits), Mehrfachkauf verlaengert vom laufenden
  Ende, Wallet-Belastung referenziert den Boost im Ledger
- PressRelease::boosts()/isBoosted()/scopeBoosted() als Basis fuer die
  Featured-Platzierung (Frontend-Anbindung bleibt der Web-Strecke ueberlassen)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:22:12 +00:00
460a255df7 Pruefzaehler + Pruefkontingent (Decision-Update Phase-2 vorgezogen)
Eigener Pruef-Zaehler, getrennt von der Credit-Wallet (Paragraph 4.2/4.3):

- review_checks Ledger (eine Zeile je Pruefung, source free|credit,
  charged_credits), aggregiert pro Account/Monat statt pro PM
- ReviewCheckService: Tageslimit (harte Bremse, nicht freikaufbar) ->
  Monats-Freikontingent (tier-gestaffelt 4/12/30/60/120) -> Overflow
  zieht 1 Credit/Pruefung aus der Wallet
- ReviewLimitException fuer das erreichte Tageslimit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:19:37 +00:00