Files
fotospiel-app/docs/packages-business-model-plan.md

241 lines
17 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.
# 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` (in `tenants`, default 1), `subscription_tier`/`subscription_expires_at` (in `tenants`).
- Tabellen: `event_purchases` (Käufe), `event_credits_ledger` (Transaktionen), `purchase_history` (IAP-Historie).
- **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).
- **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):**
```php
$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):**
```php
$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):**
```php
$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):**
```php
$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:**
1. Deploy Migration/Seeder (php artisan migrate, db:seed --class=PackageSeeder).
2. Run packages:migrate (Command: Transfer Daten, log Errors).
3. Update Code (Controllers/Middleware/Resources, API-Routes).
4. Frontend-Build (npm run build for PWAs).
5. Smoke-Tests (Kauf-Flow, Limits, Webhooks mit Sandbox).
6. 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)
- [x] Analyse.
- [x] Design (15-packages-design.md).
- [x] PRP-Updates.
- [x] Marketing/Legal (Blades mit Checkout).
- [x] 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.