presseportale/dev/migration 2026/02-TARGET-ARCHITECTURE.md
Kevin Adametz 0a3e52d603 19-05-2026 Rebrand Pressekonto, Hub-Flux UI und Legacy-Media-Migration
Umbenennung presseportale → pressekonto in Domains, Themes und Dokumentation.
Design-Tokens, Portal-Shell, Customer-Dashboard, Auth- und Admin-PM-Views.
Artisan-Befehl migrate:legacy-media mit Tests und Hub-Flux-Entwicklungsdocs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 16:36:13 +00:00

18 KiB
Raw Blame History

02 Ziel-Architektur (Laravel 12)

1. Hohes Level

┌─────────────────────────────────────────────────────────────────────────┐
│  Pressekonto Backend (pressekonto.test)                             │
│  Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2                   │
│                                                                         │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐  ┌─────────┐ │
│  │ Admin-UI      │  │ Customer-     │  │ Public API    │  │ Console │ │
│  │ Livewire/Volt │  │ Portal        │  │ v1 (Sanctum)  │  │ Sched.  │ │
│  │ (admin.*)     │  │ (customer.*)  │  │               │  │ + Jobs  │ │
│  └──────┬────────┘  └──────┬────────┘  └──────┬────────┘  └────┬────┘ │
│         │                  │                  │                  │    │
│         └──────────────────┴──────┬───────────┴──────────────────┘    │
│                                   ▼                                   │
│  ┌─────────────────────────────────────────────────────────────────┐  │
│  │            Application Services (app/Services/*)                │  │
│  │  PressReleaseService · CompanyService · InvoiceService …        │   │
│  └───────────────────────────────┬─────────────────────────────────┘   │
│                                  │                                     │
│         ┌────────────────────────┼─────────────────────────┐           │
│         ▼                        ▼                         ▼           │
│  ┌──────────────┐       ┌────────────────┐       ┌──────────────┐      │
│  │  Models      │       │  Actions       │       │  Integrations│      │
│  │  (Eloquent)  │       │  (Invokable)   │       │  Stripe, Mail│      │
│  └──────┬───────┘       └────────┬───────┘       └──────────────┘      │
│         │                        │                                     │
│         ▼                        ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  MySQL 8 (einheitliche DB, `portal` disambiguiert Tenants)      │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  Frontend-Apps (presseecho.test, businessportal24.test) nutzen entweder │
│  dieselbe DB (direkte Reads) oder /api/v1.                             │
└─────────────────────────────────────────────────────────────────────────┘

2. Verzeichnis-Layout (Ziel)

app/
├── Actions/                              ← Invokable Actions (1 Klasse = 1 Use-Case)
│   ├── PressRelease/
│   │   ├── CreatePressRelease.php
│   │   ├── PublishPressRelease.php
│   │   ├── RejectPressRelease.php
│   │   └── CheckForBlacklistedWords.php
│   ├── Company/
│   ├── Invoice/
│   │   ├── GenerateInvoicesForPeriod.php
│   │   └── SendInvoiceReminder.php
│   ├── Newsletter/
│   └── Fortify/                          ← bereits von Fortify angelegt
│
├── Console/
│   └── Commands/
│       ├── Migration/                    ← einmalige Daten-Migrations-Commands
│       │   ├── ImportLegacyUsers.php
│       │   ├── ImportLegacyCompanies.php
│       │   └── ImportLegacyPressReleases.php
│       └── Maintenance/
│           └── PurgeExpiredTokens.php
│
├── Enums/
│   ├── Portal.php                        ← Presseecho | Businessportal24
│   ├── PressReleaseStatus.php
│   ├── InvoiceStatus.php
│   ├── PaymentMethod.php
│   ├── PaymentStatus.php
│   ├── UserPaymentOptionStatus.php
│   ├── PaymentOptionType.php
│   ├── CompanyType.php
│   └── RegistrationType.php
│
├── Events/
│   ├── PressRelease/
│   │   ├── PressReleasePublished.php
│   │   └── PressReleaseRejected.php
│   └── Invoice/
│       ├── InvoiceCreated.php
│       └── InvoicePaid.php
│
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       └── V1/
│   │           ├── PressReleaseController.php
│   │           ├── PressReleaseImageController.php
│   │           ├── CompanyController.php
│   │           └── NewsletterController.php
│   ├── Middleware/
│   │   ├── SetCurrentPortal.php          ← setzt default-Portal aus Domain
│   │   └── EnsureCustomerRole.php        ← für customer.*-Routen
│   ├── Requests/                         ← FormRequests
│   │   ├── PressRelease/
│   │   ├── Company/
│   │   └── Api/V1/
│   └── Resources/V1/
│       ├── PressReleaseResource.php
│       ├── CompanyResource.php
│       └── ...
│
├── Jobs/
│   ├── GenerateInvoices.php
│   ├── SendInvoiceReminders.php
│   ├── CreateRecurringPayments.php
│   └── ExpirePaymentOptions.php
│
├── Livewire/
│   ├── Actions/                          ← existiert
│   ├── Admin/                            ← existiert (users)
│   ├── Customer/                         ← NEU: Self-Service-Komponenten
│   │   ├── Dashboard.php
│   │   ├── PressReleases/Index.php
│   │   ├── PressReleases/Create.php
│   │   ├── Invoices/Index.php
│   │   ├── Tokens/Index.php
│   │   └── Profile.php
│   └── Auth/
│       ├── MagicLinkRequest.php          ← NEU: "Login per Mail-Link"
│       └── MagicLinkConsume.php          ← NEU: prüft signed URL
│
├── Mail/
│   ├── PressReleasePublished.php
│   ├── PressReleaseRejected.php
│   ├── InvoiceCreated.php
│   ├── InvoiceReminder.php
│   ├── MagicLoginLink.php                ← NEU
│   ├── GoLivePasswordReset.php           ← NEU: Kampagnen-Mail beim Go-Live
│   └── NewsletterConfirmation.php
│
├── Models/
│   ├── User.php                          ← existiert (erweitern)
│   ├── Profile.php                       ← entspr. sfGuardUserProfile
│   ├── Company.php
│   ├── Contact.php
│   ├── Category.php
│   ├── CategoryTranslation.php           ← optional
│   ├── PressRelease.php
│   ├── PressReleaseImage.php
│   ├── Newsletter/
│   │   └── Subscription.php
│   ├── Invoice.php                       ← NEU-Billing (Stripe-getrieben)
│   ├── InvoiceBillingAddress.php
│   ├── LegacyInvoice.php                 ← ARCHIV (read-only)
│   ├── BillingAddress.php                ← am User
│   ├── PaymentOption.php                 ← neue Produkte, Stripe-Price-Mapping
│   ├── PaymentOptionTranslation.php      ← optional
│   ├── UserPaymentOption.php             ← inkl. `grandfathered_until`
│   ├── UserPayment.php
│   ├── FooterCode.php                    ← bleibt (Q-08b)
│   ├── MagicLink.php                     ← NEU: Magic-Link-Token (siehe §5.2)
│   ├── Blacklist.php
│   └── LegacyImport/
│       └── ImportMap.php                 ← persistente ID-Mapping-Tabelle
│
│   (entfallen: Coupon, PromotionLink  D-14/D-16)
│
├── Policies/
│   ├── PressReleasePolicy.php
│   ├── CompanyPolicy.php
│   └── ...
│
├── Providers/
│   ├── AppServiceProvider.php
│   ├── AuthServiceProvider.php
│   ├── FortifyServiceProvider.php
│   └── ThemeServiceProvider.php          ← existiert
│
├── Scopes/
│   ├── CurrentPortalScope.php            ← optional: Default-Tenant-Filter
│   └── ActiveScope.php
│
├── Services/                             ← domänenlogische Services
│   ├── PressReleaseService.php
│   ├── CompanyService.php
│   ├── InvoiceService.php
│   │   └── InvoiceNumberGenerator.php
│   ├── NewsletterService.php
│   ├── StripeService.php                 ← Wrapper um Laravel Cashier (optional) oder Stripe-PHP
│   ├── ImageService.php                  ← Intervention-Image + Storage
│   └── BlacklistService.php
│
└── Support/
    ├── Auth/
    │   └── MagicLinkGenerator.php        ← signed URLs mit kurzer TTL
    └── Slug/
        └── SluggableTrait.php

3. Konventionen

  • Naming: snake_case für DB-Spalten, camelCase/StudlyCase für PHP, englisch für Code, deutsch für User-Facing-Strings.
  • Primary Keys: id BIGINT unsigned, ulid optional für extern-sichtbare Ressourcen.
  • Timestamps überall ($timestamps = true), Soft Deletes dort, wo Datenhistorie wichtig ist (User, Company, PressRelease, Invoice).
  • Portal-Scoping: Spalte portal (enum: presseecho | businessportal24 | both) auf allen mandantenfähigen Entitäten. CurrentPortalScope filtert im Frontend; Admin-Panel zeigt beides (mit Filter-Dropdown).
  • Legacy-IDs: Jede importierte Entität hat legacy_portal + legacy_id + Unique-Index. So bleiben API-Clients kompatibel (Mapping-Layer).
  • Enums: Backed Enums mit Label-Methode:
    enum PressReleaseStatus: string
    {
        case Draft = 'draft';
        case Review = 'review';
        case Published = 'published';
        case Rejected = 'rejected';
        case Archived = 'archived';
    
        public function label(): string { /* … */ }
    }
    
  • Action-Pattern über Controller hinaus: Controller/Livewire rufen Actions; Actions nutzen Services; Services kapseln Eloquent.
  • FormRequests für Validierung; in Livewire über rules()-Methode + ValidatesWhenResolvedTrait.

4. Datenbank-Setup

  • Connection mysql → Ziel-DB des neuen Backends.
  • Optionale Legacy-Connections: legacy_presseecho + legacy_businessportal (read-only) nur während der Import-Phase. Definition in config/database.php:
    'connections' => [
        'mysql' => [/* Primary */],
        'legacy_presseecho' => [/* read-only */],
        'legacy_businessportal' => [/* read-only */],
    ],
    

5. Auth-Konzept

5.1 Rollen (Spatie Permission)

Rolle Wer Zugang
admin Interne Mitarbeiter admin.*-Routen, alle Aktionen
editor Interne Redaktion admin.*-Routen mit eingeschränkten Permissions
customer Company-Ansprechpartner customer.*-Routen (Self-Service)
api-only Automatisierte API-Clients nur /api/v1/* via Sanctum, kein Web-Login

5.2 Login-Wege

  • Passwort-Login (Admin + Customer): Fortify (bereits konfiguriert).
  • 2FA: Fortify-Standard (Spalten bereits in users). Pflicht für Admins, optional für Customer.
  • Magic-Link (Customer + Initial-Admin-Access) NEU (D-10):
    • Nutzer gibt E-Mail ein → System versendet signed URL (TTL 15 min, Single-Use).
    • Tabelle magic_links: id, user_id, token_hash, purpose (login|press_release_access|invoice_access), expires_at, consumed_at, ip_address.
    • Insbesondere für Companies ohne bestehenden Login (Legacy: viele Companies haben API-Key aber kein Passwort).
    • Nach Verbrauch optional sofortiger Redirect zur Zielseite (z. B. bestimmte Pressemitteilung).
  • Passwort-Reset: Fortify-Standard; beim Go-Live Massen-Mailing via GoLivePasswordReset-Mailable.
  • Legacy-Passwörter (sha1+salt): werden NICHT übernommen (D-09). Beim ersten Login-Versuch mit Legacy-Account → Hinweis "Bitte Passwort zurücksetzen oder Magic-Link anfordern".

5.3 API-Auth

  • Sanctum Personal Access Tokens only. Scopes: press-releases:write, press-releases:read, companies:read, etc.
  • Cut-over (D-05): Keine Kompatibilitätsschicht für Legacy api_key. Alte Clients bekommen Fehlermeldung mit Hinweis auf Migrations-URL.
  • Tokens erstellbar / löschbar unter:
    • Admin: admin/users/{user}/tokens
    • Customer: account/tokens

6. Multi-Domain & Portal-Scope

  • Pro Domain ein Frontend, aber alle Admin-Screens auf pressekonto.test.
  • Middleware SetCurrentPortal:
    • Web (presseecho.test / businessportal24.test) → app()->instance('current_portal', Portal::Presseecho|Businessportal24)
    • Admin → Portal-Auswahl via Dropdown, gespeichert in Session (current_portal)
  • Global Scope CurrentPortalScope auf PressRelease, Company, Newsletter etc. abschaltbar via ->withoutGlobalScope(CurrentPortalScope::class).

7. Payment (Stripe only) komplett neu (D-03/D-12/D-13)

  • Paket: laravel/cashier (Stripe) für Subscription-Lifecycle (entscheiden in Phase 8).
  • Keine Rechnungs-Zahlungsweise mehr ausschließlich Stripe (Card, SEPA, optional Klarna).
  • Keine Legacy-PaymentOption-Migration neue Produkte/Stripe-Prices werden vom Auftraggeber definiert.
  • PaymentOption-Tabelle: interne Produkt-Definition mit stripe_product_id + stripe_price_id.
  • Grandfathering: Spalte grandfathered_until auf UserPaymentOption. Aktive Alt-User behalten Konditionen bis zu diesem Datum (aus Legacy-contract_date + Laufzeit abgeleitet). Scheduler-Job ExpireGrandfatheredSubscriptions stößt automatisch Umstellung an.
  • Webhook-Endpoint /webhooks/stripe → Event-Handler aktualisieren UserPaymentOption + UserPayment + Invoice.

8. Invoicing (neu) + Legacy-Archiv

Neuer Rechnungslauf

  • PDF: barryvdh/laravel-dompdf, Template: resources/views/pdf/invoice.blade.php.
  • Trigger: Stripe-Webhook invoice.payment_succeeded → eigene Invoice generieren, PDF erstellen, Mail senden.
  • InvoiceNumberGenerator: Format {yyyy}{mm}-{seq} (modern, getrennt von Legacy-Nummernkreis).
  • Mahnwesen: stark vereinfacht, nur für Stripe-Failed-Payments (automatische Stripe-Retry-Logik + zusätzliches Reminder-Mailing).

Legacy-Archiv D-12

  • Separate Tabelle legacy_invoices (read-only).
  • Vollständiger DB-Import aller Legacy-Rechnungen inkl. Status, Beträge, Datum, Zahlart, Raw-Snapshot und User-Zuordnung.
  • PDFs werden bei Abruf/on demand aus den archivierten Legacy-Daten generiert. Ein abgelegter pdf_path ist nur Cache/Export, nicht die führende Quelle.
  • Zugriff:
    • Admin: volle Liste aller Legacy-Rechnungen
    • Customer: nur eigene (Filter über user_id/company_id-Mapping aus Legacy-Import)
  • Keine Neuausstellung, kein Mahnwesen auf Legacy-Rechnungen.

9. Images / Media

  • Storage-Disk public (lokal) für Dev, s3 für Prod.
  • ImageService kapselt Upload, Thumbnail-Generierung (Intervention Image), Cleanup.
  • URL-Schema neu: /storage/images/press-releases/{id}/{variant}.{ext}.
  • Alte URLs: Redirect-Regel in routes/web.php oder dedizierte Middleware.

10. i18n-Strategie (D-07: DE + EN initial)

Legacy nutzt Doctrine-I18n-Behavior → eigene *_translation-Tabellen (Salutation_Translation, Category_Translation, PaymentOption_Translation).

Ziel:

  • UI-Locales: de (primär), en (initial aktiviert).
  • Content-Locales: de, en mindestens für Category, optional für PressRelease.
Option Für / Gegen
spatie/laravel-translatable (JSON-Spalten) + schlank, keine FTS/Sortierung per Sprache
Klassische Translation-Tables (Modell CategoryTranslation) + SQL-fähig, mehr Code

Empfehlung: Klassische Translation-Tables für Category (weil Slug/Name pro Sprache gebraucht werden); JSON für Static-Master wie Salutation.

Beim Seeding werden DE + EN direkt für Kategorien und Salutations angelegt.


11. Testing

  • Pest 3; Factories, Seeders, RefreshDatabase-Trait.
  • Feature-Tests für Livewire-Screens, Actions, API-Endpoints.
  • Architektur-Tests (Pest arch()) für Namenskonventionen.
  • Datenimport-Tests: Import-Command gegen Sample-SQL-Dumps in database/legacy-fixtures/.

12. Code-Qualität

  • Laravel Pint (bereits konfiguriert) für Code-Style.
  • PHPStan/Larastan Level 56 auf app/ (empfohlen, kein Muss in Phase 1).
  • PHP-CS-Fixer nicht nötig (Pint deckt ab).
  • Pre-commit-Hook: pint --dirty + Tests.