17 KiB
Fotospiel: Umstellung auf Package-basiertes Business Model – Detaillierter Plan
Datum: 2025-09-26
Version: 1.0
Autor: Kilo Code (Architect Mode)
Status: Finaler Plan für Review und Implementation in Code-Mode.
Ziel: Ersetze das aktuelle Credits-basierte Freemium-Modell (One-off-Käufe via Stripe/RevenueCat, Balance-Checks) durch ein package-basiertes Modell mit vordefinierten Bündeln (Einmalkäufe pro Event für Endkunden, jährliche Subscriptions für Reseller/Agenturen). Der Plan deckt Analyse, Design, Änderungen in DB/Code/UI/Billing, Lücken und Rollout ab. Alle Details basieren auf User-Feedback und Best Practices für Laravel 12, Filament 4, React/Vite PWA.
1. Analyse des Aktuellen Modells
Das bestehende Modell ist Credits-basiert (Freemium mit 1 Free-Credit, One-off-Käufen für Events). Subscriptions sind deferred (nicht implementiert).
Betroffene Komponenten:
- DB:
- Felder:
event_credits_balance(intenants, default 1),subscription_tier/subscription_expires_at(intenants). - Tabellen:
event_purchases(Käufe),event_credits_ledger(Transaktionen),purchase_history(IAP-Historie).
- Felder:
- Code (Backend):
- Models:
Tenant::decrementCredits()/incrementCredits(). - Controllers:
EventController(Credit-Check bei Create),CreditController(Balance/Purchase). - Middleware:
CreditMiddleware(prüft Balance >=1 für Events). - Filament:
TenantResource(credits-Column, add_credits-Action),PurchaseHistoryResource(CRUD/Refund).
- Models:
- API: Endpunkte
/api/v1/tenant/credits/balance,/credits/ledger,/credits/purchase,/credits/sync,/purchases/intent. - Frontend (Admin PWA): Dashboard-Cards für Balance, Kauf-Integration (RevenueCat).
- Guest PWA: Keine direkten Checks (Backend-handhabt).
- Billing: Stripe (Checkout/Webhooks), RevenueCat (IAP), PaddleWebhookController (teilweise).
- Tests:
RevenueCatWebhookTest, Credit-Unit-Tests. - Docs: PRP 08-billing.md (Credits-MVP), 14-freemium-business-model.md (IAP-Struktur), API-Specs (credits-Endpunkte).
- Lücken im Aktuellen: Keine Package-Limits (nur Balance), Subscriptions nicht live, Paddle untergenutzt.
Auswirkungen: Vollständige Ersetzung, um Flexibilität (Limits/Features pro Package) zu ermöglichen.
2. Neues Package-basiertes Modell
Packages ersetzen Credits: Vordefinierte Bündel mit Limits/Features. Kauf bei Event-Create (Endkunden) oder Tenant-Upgrade (Reseller). Freemium: Free/Test-Paket für Einstieg.
Endkunden-Pakete (Einmalkäufe pro Event)
| Paket | Preis | max_photos | max_guests | gallery_days | max_tasks | watermark | branding | Features |
|---|---|---|---|---|---|---|---|---|
| Free/Test | 0 € | 30 | 10 | 3 | 1 | Standard | Nein | - |
| Starter | 19 € | 300 | 50 | 14 | 5 | Standard | Nein | - |
| Standard | 39 € | 1000 | 150 | 30 | 10 | Custom | Ja | Logo |
| Premium | 79 € | 3000 | 500 | 180 | 20 | Kein | Ja | Live-Slideshow, Analytics |
Reseller/Agentur-Pakete (Jährliche Subscriptions)
| Paket | Preis/Jahr | max_events/year | Per-Event Limits | Branding | Extra |
|---|---|---|---|---|---|
| Reseller S | 149 € | 5 | Standard | Eingeschränkt | - |
| Reseller M | 299 € | 15 | Standard | Eigene Logos | 3 Monate Galerie |
| Reseller L | 599 € | 40 | Premium | White-Label | - |
| Enterprise | ab 999 € | Unlimited | Premium | Voll | Custom Domain, Support |
Flow: Event-Create: Package wählen → Kauf (Free: direkt; Paid: Checkout) → Limits für Event setzen. Reseller: Tenant-Package limitiert Events/Features global.
3. DB-Schema & Migrationen
Neue Tabellen (Migration: create_packages_tables.php)
- packages (global):
$table->id(); $table->string('name'); $table->enum('type', ['endcustomer', 'reseller']); $table->decimal('price', 8, 2); $table->integer('max_photos')->nullable(); $table->integer('max_guests')->nullable(); $table->integer('gallery_days')->nullable(); $table->integer('max_tasks')->nullable(); $table->boolean('watermark_allowed')->default(true); $table->boolean('branding_allowed')->default(false); $table->integer('max_events_per_year')->nullable(); $table->timestamp('expires_after')->nullable(); // Für Subscriptions $table->json('features')->nullable(); // ['live_slideshow', 'analytics'] $table->timestamps(); $table->index(['type', 'price']); // Für Queries - event_packages (pro Event):
$table->id(); $table->foreignId('event_id')->constrained()->cascadeOnDelete(); $table->foreignId('package_id')->constrained()->cascadeOnDelete(); $table->decimal('purchased_price', 8, 2); $table->timestamp('purchased_at'); $table->integer('used_photos')->default(0); // Counter $table->timestamps(); $table->index('event_id'); - tenant_packages (Reseller):
$table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); $table->foreignId('package_id')->constrained()->cascadeOnDelete(); $table->decimal('price', 8, 2); $table->timestamp('purchased_at'); $table->timestamp('expires_at'); $table->integer('used_events')->default(0); $table->boolean('active')->default(true); $table->timestamps(); $table->index(['tenant_id', 'active']); - package_purchases (Ledger):
$table->id(); $table->foreignId('tenant_id')->nullable()->constrained(); $table->foreignId('event_id')->nullable()->constrained(); $table->foreignId('package_id')->constrained(); $table->string('provider_id'); // Paddle ID $table->decimal('price', 8, 2); $table->enum('type', ['endcustomer_event', 'reseller_subscription']); $table->json('metadata'); // {event_id, ip_address} $table->string('ip_address')->nullable(); $table->string('user_agent')->nullable(); $table->boolean('refunded')->default(false); $table->timestamps(); $table->index(['tenant_id', 'purchased_at']);
Migration-Strategie (php artisan make:migration migrate_to_packages)
- Schritt 1: Neue Tabellen erstellen + Seeder für Standard-Packages (php artisan make:seeder PackageSeeder).
- Schritt 2: Daten-Transfer (Artisan-Command packages:migrate):
- Tenants: if event_credits_balance > 0 → Zuweisen zu Free-Paket (insert tenant_packages mit expires_at = now() + 30 days); alte Balance zu used_events konvertieren (z.B. balance / 100 = initial events).
- Events: Bestehende Events zu Test-Paket migrieren (insert event_packages).
- Ledger: Transfer event_purchases zu package_purchases (map credits_added zu package_id = 'free').
- Schritt 3: Alte Felder/Tabellen droppen (in separater Migration, nach Backup).
- Rollback: php artisan migrate:rollback --step=3; Restore aus Backup.
- Performance: Transactions für Migration; Cache::flush() nach.
4. Filament 4 Resources (Backend-Logik, Todo 6)
-
PackageResource (app/Filament/Resources/PackageResource.php, SuperAdmin):
- Form: TextInput('name'), Select('type'), MoneyInput('price'), NumericInputs für Limits, Toggles für watermark/branding, Repeater('features'), Numeric('max_events_per_year').
- Table: TextColumn('name'), BadgeColumn('type'), MoneyColumn('price'), IconColumn('limits' – z.B. CameraIcon für max_photos), Actions (Edit/Delete/Duplicate).
- Pages: ListPackages, CreatePackage, EditPackage.
- Policy: SuperAdmin only.
-
TenantPackageResource (SuperAdmin/TenantAdmin):
- Form: Select('tenant_id'), Select('package_id'), DateTimePicker('purchased_at'), DateTimePicker('expires_at'), TextInput('used_events', readOnly), Toggle('active').
- Table: TextColumn('tenant.name'), BadgeColumn('package.name'), DateColumn('expires_at', color: expired → danger), ProgressColumn('used_events' / max_events), Actions (Renew: set expires_at +1 year, Cancel: active=false + Paddle cancel).
- Relations: BelongsTo Tenant/Package, HasMany Events (RelationManager mit Event-List).
- Bulk-Actions: Renew Selected.
-
PurchaseResource (SuperAdmin/TenantAdmin):
- Form: Select('tenant_id/event_id'), Select('package_id'), TextInput('provider_id'), MoneyInput('price'), Select('type'), JSONEditor('metadata'), Toggle('refunded').
- Table: BadgeColumn('type'), LinkColumn('tenant' or 'event'), TextColumn('package.name/price'), DateColumn('purchased_at'), BadgeColumn('status' – paid/refunded), Actions (View, Refund: Call Paddle API, decrement counters, log).
- Filters: SelectFilter('type'), DateRangeFilter('purchased_at'), TenantFilter.
- Widgets: StatsOverview (Total Revenue, Monthly Purchases, Top Package), ChartWidget (Revenue over Time via Laravel Charts).
- Export: CSV (für Buchhaltung: tenant, package, price, date).
Integration: Ersetze add_credits in TenantResource durch 'Assign Package'-Action (modal mit Select + Intent-Call). Policies: Role-based (superadmin full, tenant_admin own).
5. Marketing- und Legal-Anpassungen (Todo 4)
-
Webfrontend (Blade, resources/views/marketing/):
- packages.blade.php (neu, Route /packages): Hero ("Entdecken Sie unsere Packages"), Tabs (Endkunden/Reseller), Tabelle/Accordion mit Details (Preis, Limits als Icons, Features-Bullets, i18n-Übersetzungen). CTA: "Kaufen" → /checkout/{id}. Dynamisch: @foreach(Package::where('type', 'endcustomer')->get() as $package).
- checkout.blade.php (neu, Route /checkout/{package_id}): Summary-Box (Package-Details), Form (Name, E-Mail, Adresse für Reseller), Zahlungsoptionen (Radio: Paddle), Stripe-Element/Paddle-Button. Submit: POST /purchases/intent → Redirect. Tailwind: Secure-Design mit Badges.
- success.blade.php: "Vielen Dank! Package {name} gekauft." Details (Limits, Event-Link), Upsell ("Upgrade zu Reseller?"), Rechnung-Download (PDF via Dompdf), Onboarding-Tour-Link.
- marketing.blade.php: Teaser-Section mit Package-Icons/Preisen, Link zu /packages.
- occasions.blade.php/blog.blade.php:* Kontextuelle Erwähnungen (z.B. "Ideal für Partys: Starter-Paket"), Blog-Post "Neues Package-Modell" mit FAQ.
-
Legal (resources/views/legal/):
- datenschutz.blade.php: Abschnitt "Zahlungen" (Paddle: Keine Karten-Speicherung, GDPR: Löschung nach 10 Jahren; Consent für E-Mails). "Package-Daten (Limits) sind anonymisiert."
- impressum.blade.php: "Monetarisierung: Packages via Paddle; USt-ID: ...; Support: support@fotospiel.de".
- Allgemein: Datum "Aktualisiert: 2025-09-26 – Package-Modell"; Links zu Provider-Datenschutz.
i18n: Translations in lang/de/en (z.B. 'package.starter' → 'Starter-Paket').
6. Backend-Logik & API (Todo 6/7)
- Controllers:
PackagesController(index: Liste mit Cache, show: Details, store: Intent für Kauf).PurchasesController(intent: Erstelle Stripe-Session oder Paddle-Order basierend auf method; store: Nach Webhook).
- Middleware:
PackageMiddleware(für Events: Check event_packages.used_photos < max_photos; für Tenant: used_events < max_events_per_year). - Models:
Package(Relationships: hasMany EventPackage/TenantPackage),EventPackage(incrementUsedPhotos-Method),TenantPackage(isActive-Scope, Observer für Expiry: E-Mail + active=false). - API-Endpunkte (routes/api.php, tenant-group):
- GET /packages (Liste, filter by type).
- GET /packages/{id} (Details).
- POST /packages/purchase (Body: package_id, type, event_id?; Response: {checkout_url, provider}).
- GET /tenant/packages (Active Package, Purchases-List).
- POST /tenant/packages/assign (Free-Zuweisung).
- DELETE /credits/* (entfernen, 404-redirect).
- Tokens: Füge 'package_info' (JSON: active_package_id) zu JWT-Claims hinzu (via Sanctum).
- Jobs:
ProcessPackagePurchase(nach Webhook: Zuweisen, E-Mail, Analytics-Event).
7. Frontend-Anpassungen (Todo 8/9)
- Admin PWA (resources/js/admin/):
- EventFormPage.tsx: Select('package_id') mit Details-Modal (Limits/Preis), Button 'Kaufen' → Paddle-Integration (stripe.elements oder Paddle-Button).
- Dashboard: Card 'Aktuelles Package' (Limits, Expiry, Upgrade-Button).
- SettingsPage.tsx: Reseller-Übersicht (used_events/Progress, Renew-Button).
- Hooks: usePackageLimits (fetch /packages, check used_photos).
- Guest PWA (resources/js/guest/):
- EventDetailPage.tsx: Header "Package: Premium – {used_photos}/{max_photos} Fotos, Galerie bis {date}".
- Upload-Component: If used_photos >= max_photos → Disable + Message "Limit erreicht – Upgrade via Admin".
- Features: Watermark-Overlay if watermark_allowed; Branding-Logo if branding_allowed.
- Router: Guard für Limits (z.B. /upload → Check API).
Tech: React Query für API-Calls, Stripe.js/Paddle-SDK in Components, i18n mit react-i18next.
8. Billing-Integration (Todo 10)
- Provider: Stripe (Primär: Einmalkäufe/Subscriptions) + Paddle (Alternative: PHP SDK für Orders/Subscriptions).
- Flow: Auswahl → Intent (Controller: if 'stripe' → Stripe::checkout()->sessions->create([...]); if 'paypal' → Paddle::orders()->create([...]) ) → Redirect → Webhook (verifiziert, insert package_purchases, assign Package, E-Mail).
- Webhooks: StripeWebhookController (neue Events: checkout.session.completed → ProcessPurchase), PaddleWebhookController (erweitert: PAYMENT.CAPTURE.COMPLETED → ProcessPurchase).
- SDKs: composer require stripe/stripe-php ^10.0, paypal/rest-api-sdk-php ^1.14; NPM: @stripe/stripe-js, @paypal/react-paypal-js.
- Free: Kein Provider – direkt assign via API.
- Refunds: Action in PurchaseResource: Call Stripe::refunds->create oder Paddle::refunds, decrement Counters.
- Env: STRIPE_KEY/SECRET, PAYPAL_CLIENT_ID/SECRET, SANDBOX-Flags.
9. Tests (Todo 11)
- Unit/Feature: Pest/PHPUnit: Test PackageSeeder, Migration (assert Tables exist), Controllers (mock Paddle SDKs mit Stripe::mock(), test Intent/Webhook), Models (Package::find(1)->limits, TenantPackage::isActive), Middleware (assert denies if limit exceeded).
- E2E (Playwright): Test Kauf-Flow (navigate /packages, select Starter, choose Paddle, complete sandbox, assert success.blade.php), Limits (upload photo, assert counter +1, deny at max).
- Anpassungen: RevenueCatWebhookTest → PaddleWebhookTest; Add PackageValidationTest (e.g. EventCreate without Package → 422).
- Coverage: 80% für Billing/DB; Mock Providers für Isolation.
10. Deployment & Rollout (Todo 12)
- Vorbereitung: Backup DB (php artisan db:backup), Staging-Env (duplicate prod, test Migration).
- Schritte:
- Deploy Migration/Seeder (php artisan migrate, db:seed --class=PackageSeeder).
- Run packages:migrate (Command: Transfer Daten, log Errors).
- Update Code (Controllers/Middleware/Resources, API-Routes).
- Frontend-Build (npm run build for PWAs).
- Smoke-Tests (Kauf-Flow, Limits, Webhooks mit Sandbox).
- Go-Live: Feature-Flag (config/packages.enabled = true), Monitor mit Telescope/Sentry.
- Rollback: migrate:rollback, restore Backup.
- Post-Deployment: Update TODO.md (neue Tasks: Monitor Conversions), Gogs-Issues (z.B. "Implement Package Analytics"), E-Mail an Users ("Neues Package-Modell – Ihr Free-Paket ist aktiv").
- Monitoring: Scheduled Job (daily: Check expired Packages, notify), Revenue-Dashboard in Filament.
11. Identifizierte Lücken & Best Practices
- Sicherheit: PCI-Compliance (Provider-handhabt), Audit-Logs (payments-channel), Rate-Limiting (/checkout: 5/min), GDPR (Lösch-Job, Consent in Checkout).
- i18n: Package-Features als translatable JSON, Locale in Checkout (Stripe metadata).
- Analytics: GA-Events in Frontend, Telescope für Backend-Käufe, ARPU-Tracking in Widgets.
- Support: E-Mail-Templates (PurchaseMailable), FAQ in /support/packages, Onboarding-Tour post-Kauf.
- Performance: Caching (Packages-Liste), Indexing (purchased_at), Queues für Webhooks (ProcessPurchaseJob).
- Edge-Cases: Upgrade (prorate Preis, transfer Limits), Expiry (Observer + E-Mail), Offline-PWA (queued Käufe sync).
- Dependencies: Paddle SDKs, Dompdf (Rechnungen), Laravel Cashier (optional für Stripe).
- Kosten: Env für Sandbox/Prod-Keys; Test mit Paddle Test-Accounts.
12. Todo-List (Status: Alle Planung completed)
- Analyse.
- Design (15-packages-design.md).
- PRP-Updates.
- Marketing/Legal (Blades mit Checkout).
- DB-Migrationen.
- Backend (Resources/Controllers).
- API.
- PWAs.
- Billing (SDKs/Webhooks).
- Tests.
- Deployment.
Nächster Schritt: Wechsel zu Code-Mode für Implementation (start with DB-Migrationen). Kontaktieren Sie für Änderungen.