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>
351 lines
18 KiB
Markdown
351 lines
18 KiB
Markdown
# 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 5–6 auf `app/` (empfohlen, kein Muss in Phase 1).
|
||
- **PHP-CS-Fixer** nicht nötig (Pint deckt ab).
|
||
- **Pre-commit-Hook**: `pint --dirty` + Tests.
|