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

14 KiB
Raw Permalink Blame History

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: Packages/Add-ons ersetzen das frühere Credits-Modell (Credits sind legacy/outdated). 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 Alt-Modells (Legacy Credits)

Früheres Credits-Modell ist deprecated. Alle neuen Flows basieren auf Packages/Add-ons.

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

  • Credits-Migration wird nicht mehr genutzt; Bestandsdaten auf Packages/Add-ons umstellen (event_credits_* Felder/Tables droppen).
    • 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).

  • 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)

  • 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.