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>
18 KiB
18 KiB
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_casefür DB-Spalten,camelCase/StudlyCasefür PHP, englisch für Code, deutsch für User-Facing-Strings. - Primary Keys:
idBIGINT unsigned,ulidoptional 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.CurrentPortalScopefiltert 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 inconfig/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
- Admin:
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)
- Web (presseecho.test / businessportal24.test) →
- Global Scope
CurrentPortalScopeaufPressRelease,Company,Newsletteretc. – 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 mitstripe_product_id+stripe_price_id.- Grandfathering: Spalte
grandfathered_untilaufUserPaymentOption. Aktive Alt-User behalten Konditionen bis zu diesem Datum (aus Legacy-contract_date+ Laufzeit abgeleitet). Scheduler-JobExpireGrandfatheredSubscriptionsstößt automatisch Umstellung an. - Webhook-Endpoint
/webhooks/stripe→ Event-Handler aktualisierenUserPaymentOption+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→ eigeneInvoicegenerieren, 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_pathist 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,s3für Prod. ImageServicekapselt Upload, Thumbnail-Generierung (Intervention Image), Cleanup.- URL-Schema neu:
/storage/images/press-releases/{id}/{variant}.{ext}. - Alte URLs: Redirect-Regel in
routes/web.phpoder 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ürCategory, optional fürPressRelease.
| 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.