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

351 lines
18 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.

# 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:
```php
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`:
```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.