12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,351 @@
# 02 Ziel-Architektur (Laravel 12)
## 1. Hohes Level
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Presseportale Backend (presseportale.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 `presseportale.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.