feat(packages): implement package-based business model
This commit is contained in:
241
docs/packages-business-model-plan.md
Normal file
241
docs/packages-business-model-plan.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 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), PayPalWebhookController (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, PayPal 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'); // Stripe/PayPal 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 + Stripe/PayPal 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 Stripe/PayPal 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: Stripe/PayPal), Stripe-Element/PayPal-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" (Stripe/PayPal: 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 Stripe/PayPal; 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 PayPal-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' → Stripe/PayPal-Integration (stripe.elements oder PayPal-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/PayPal-SDK in Components, i18n mit react-i18next.
|
||||
|
||||
## 8. Billing-Integration (Todo 10)
|
||||
- **Provider:** Stripe (Primär: Einmalkäufe/Subscriptions) + PayPal (Alternative: PHP SDK für Orders/Subscriptions).
|
||||
- **Flow:** Auswahl → Intent (Controller: if 'stripe' → Stripe::checkout()->sessions->create([...]); if 'paypal' → PayPal::orders()->create([...]) ) → Redirect → Webhook (verifiziert, insert package_purchases, assign Package, E-Mail).
|
||||
- **Webhooks:** StripeWebhookController (neue Events: checkout.session.completed → ProcessPurchase), PayPalWebhookController (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 PayPal::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 Stripe/PayPal 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 PayPal, complete sandbox, assert success.blade.php), Limits (upload photo, assert counter +1, deny at max).
|
||||
- **Anpassungen:** RevenueCatWebhookTest → Stripe/PayPalWebhookTest; 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:** Stripe/PayPal SDKs, Dompdf (Rechnungen), Laravel Cashier (optional für Stripe).
|
||||
- **Kosten:** Env für Sandbox/Prod-Keys; Test mit Stripe/PayPal 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.
|
||||
@@ -1,7 +1,7 @@
|
||||
# 08 — Billing (MVP: Event Credits)
|
||||
# 08 — Billing (Packages)
|
||||
|
||||
- Model: one-off purchases that grant event credits; no subscriptions in MVP.
|
||||
- Tables: `event_purchases`, `event_credits_ledger` (see 04-data-model-migrations.md).
|
||||
- Providers: Stripe (server-side checkout + webhooks); store receipts deferred.
|
||||
- Idempotency: purchase intents keyed; ledger writes idempotent; retries safe.
|
||||
- Limits: enforce `event_credits_balance >= 1` to create an event; ledger decrements on event creation.
|
||||
- Model: one-off purchases of event packages (Endkunden) or annual subscriptions (Reseller); see 15-packages-design.md for details.
|
||||
- Tables: `packages`, `event_packages`, `tenant_packages`, `package_purchases` (see 04-data-model-migrations.md and 15-packages-design.md).
|
||||
- Providers: Stripe (server-side checkout + webhooks for Einmalkäufe/Subscriptions); store receipts.
|
||||
- Idempotency: purchase intents keyed; purchase writes idempotent; retries safe.
|
||||
- Limits: Enforce package selection at event creation; check event-specific limits (e.g. max_photos) during usage; tenant limits for reseller event count.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document details the Freemium business model for the Fotospiel tenant app, combining free access with in-app purchases for event credits. The model prioritizes user acquisition through a free app download while monetizing through value-driven upgrades. Key metrics: 5-10% conversion rate, ARPU €10-15, scalable to 100k+ users.
|
||||
This document details the Package-based business model for the Fotospiel tenant app, combining free access with purchases of predefined packages. The model prioritizes user acquisition through a free test package while monetizing through value-driven upgrades. Key metrics: 5-10% conversion rate, ARPU €10-15, scalable to 100k+ users. See 15-packages-design.md for package details.
|
||||
|
||||
## Model Analysis
|
||||
|
||||
@@ -30,18 +30,13 @@ This document details the Freemium business model for the Fotospiel tenant app,
|
||||
- Complex IAP setup and testing
|
||||
- Requires strong onboarding to drive conversions
|
||||
|
||||
### Hybrid Freemium Recommendation
|
||||
**Core Strategy:** Free app with limited first event (50 photos, basic features), unlimited upgrades via IAP credits/subscriptions.
|
||||
### Package-based Recommendation
|
||||
**Core Strategy:** Free app with limited test package for first event, upgrades via package purchases (Einmalkäufe for Endkunden, Subscriptions for Reseller).
|
||||
|
||||
**Pricing Structure:**
|
||||
- **Free Tier:** 1 basic event (50 photos, standard tasks, no custom branding)
|
||||
- **Consumable Credits:**
|
||||
- Starter Pack: €4.99 for 5 events (100 photos each)
|
||||
- Pro Pack: €14.99 for 20 events (unlimited photos)
|
||||
- **Subscriptions:**
|
||||
- Pro Unlimited: €4.99/month (all features, unlimited events)
|
||||
- Agency: €19.99/month (multi-tenant, analytics, white-label)
|
||||
- **Non-Consumables:** Lifetime Unlimited: €49.99 (one-time purchase)
|
||||
**Pricing Structure:** See 15-packages-design.md for Endkunden (pro Event: Free/Test 0€, Starter 19€, etc.) and Reseller (jährlich: S 149€, etc.).
|
||||
- **Free Tier:** Test package (30 photos, 10 guests, 3 days gallery, 1 task, standard watermark)
|
||||
- **Endkunden Packages:** Einmalkäufe pro Event with increasing limits/features.
|
||||
- **Reseller Packages:** Annual subscriptions with event limits and branding options.
|
||||
|
||||
**Expected Metrics:**
|
||||
- Downloads: 50k/year
|
||||
@@ -91,38 +86,13 @@ This document details the Freemium business model for the Fotospiel tenant app,
|
||||
- **Analytics:** Firebase for funnel tracking, RevenueCat for purchase events
|
||||
|
||||
### Backend API Extensions
|
||||
- **Credit Management:** `/api/v1/tenant/credits` endpoints
|
||||
- **Purchase Validation:** Webhook receiver from RevenueCat
|
||||
- **Package Management:** `/api/v1/packages` and `/api/v1/tenant/packages` endpoints
|
||||
- **Purchase Validation:** Webhook receiver from Stripe
|
||||
- **Event Limiting:** Middleware checking credit balance before creation
|
||||
- **Subscription Sync:** Real-time updates via WebSockets (optional)
|
||||
|
||||
### Database Schema Additions
|
||||
```sql
|
||||
-- tenant_credits table
|
||||
CREATE TABLE tenant_credits (
|
||||
tenant_id VARCHAR(255) PRIMARY KEY,
|
||||
balance INTEGER DEFAULT 1, -- 1 free event
|
||||
total_purchased INTEGER DEFAULT 0,
|
||||
subscription_active BOOLEAN DEFAULT FALSE,
|
||||
subscription_tier VARCHAR(50),
|
||||
last_sync TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- purchase_history table
|
||||
CREATE TABLE purchase_history (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
tenant_id VARCHAR(255) NOT NULL,
|
||||
package_id VARCHAR(255) NOT NULL,
|
||||
credits_added INTEGER,
|
||||
price DECIMAL(10,2),
|
||||
currency VARCHAR(3),
|
||||
platform VARCHAR(50), -- 'ios' or 'android'
|
||||
transaction_id VARCHAR(255),
|
||||
purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
);
|
||||
```
|
||||
See 15-packages-design.md for updated schema: `packages`, `event_packages`, `tenant_packages`, `package_purchases`.
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
|
||||
115
docs/prp/15-packages-design.md
Normal file
115
docs/prp/15-packages-design.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Packages-Design für Fotospiel
|
||||
|
||||
## Überblick
|
||||
Dieses Dokument definiert das neue package-basierte Business Model, das das bestehende Credits-System ersetzt. Packages sind vordefinierte Bündel mit Limits und Features, die als Einmalkäufe pro Event (für Endkunden) oder jährliche Subscriptions (für Reseller/Agenturen) verkauft werden. Das Modell priorisiert Einfachheit: Bei Event-Erstellung wählt der User ein Package, das Limits für diesen Event setzt. Für Reseller limitiert das Tenant-Package die Anzahl Events pro Jahr und globale Features.
|
||||
|
||||
Ziele:
|
||||
- Ersetze Credits vollständig (keine Balance mehr, sondern Event-spezifische Limits).
|
||||
- Unterstütze Freemium: Free-Paket für Einstieg.
|
||||
- Skalierbar: Endkunden pro Event, Reseller jährlich.
|
||||
- Integration: Stripe für Zahlungen (Einmalkäufe/Subscriptions), Ledger für Transaktionen.
|
||||
|
||||
## Endkunden-Pakete (pro Event, Einmalkauf)
|
||||
Diese Pakete werden bei Event-Erstellung ausgewählt und gekauft. Sie definieren Limits für den spezifischen Event (z.B. max_photos, gallery_duration). Preise basierend auf User-Vorschlag.
|
||||
|
||||
| Paket | Preis | max_photos | max_guests | gallery_days | max_tasks | watermark | branding | Extra 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ährlich, Subscription)
|
||||
Diese Pakete werden auf Tenant-Ebene gekauft und limitieren Events pro Jahr, mit erweiterten Features (z.B. White-Label). Preise als Richtwerte.
|
||||
|
||||
| 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 |
|
||||
|
||||
## Datenbank-Schema
|
||||
### Globale Packages-Tabelle (für alle Pakete, geteilt)
|
||||
```php
|
||||
Schema::create('packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // z.B. 'Starter', 'Reseller M'
|
||||
$table->string('type'); // 'endcustomer' oder 'reseller'
|
||||
$table->decimal('price', 8, 2); // Preis in EUR
|
||||
$table->integer('max_photos')->nullable(); // Null für Reseller (vererbt an Events)
|
||||
$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(); // Für Reseller
|
||||
$table->timestamp('expires_after')->nullable(); // Für Subscriptions
|
||||
$table->json('features')->nullable(); // z.B. ['live_slideshow', 'analytics']
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
Seeder: Füge die obigen Pakete ein.
|
||||
|
||||
### Event-Packages (Zuordnung Event zu Endkunden-Paket)
|
||||
```php
|
||||
Schema::create('event_packages', function (Blueprint $table) {
|
||||
$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 für Limits
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
- Bei Event-Create: Package auswählen/kaufen, Eintrag erstellen.
|
||||
- Checks: z.B. if ($event->package->used_photos >= $event->package->max_photos) abort(403);
|
||||
|
||||
### Tenant-Packages (für Reseller)
|
||||
```php
|
||||
Schema::create('tenant_packages', function (Blueprint $table) {
|
||||
$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'); // z.B. +1 Jahr
|
||||
$table->integer('used_events')->default(0); // Counter für max_events_per_year
|
||||
$table->boolean('active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
- Bei Tenant-Registrierung: Free-Reseller oder Upgrade.
|
||||
- Check: if ($tenant->activePackage->used_events >= $tenant->activePackage->max_events_per_year) block new Event.
|
||||
|
||||
### Ledger und Purchases (ersetzt Credits-Ledger)
|
||||
```php
|
||||
Schema::create('package_purchases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->constrained()->nullable(); // Null für Event-spezifisch
|
||||
$table->foreignId('event_id')->constrained()->nullable(); // Für Endkunden
|
||||
$table->foreignId('package_id')->constrained();
|
||||
$table->string('stripe_id'); // Für Webhook/Idempotenz
|
||||
$table->decimal('price', 8, 2);
|
||||
$table->string('type'); // 'endcustomer_event', 'reseller_subscription'
|
||||
$table->json('metadata'); // z.B. {'event_id': 123}
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
- Kein separater Ledger nötig; Purchases tracken alles.
|
||||
|
||||
## Integration und Logik
|
||||
- **Event-Create Flow**: User wählt Package → Stripe-Checkout (Einmalkauf) → Webhook bestätigt → Event mit package_id erstellen.
|
||||
- **Reseller-Upgrade**: Im Admin-Dashboard Package auswählen → Subscription erstellen → expires_at setzen.
|
||||
- **Limits-Checks**: Middleware prüft Event-Package-Limits (Photos/Uploads), Tenant-Package für Event-Anzahl.
|
||||
- **Fallback**: Bestehende Tenants migrieren zu Free-Paket (z.B. if old_credits > 100 → Standard).
|
||||
- **UI/Features**: Wasserzeichen: if (!$package->watermark_allowed) hide; Branding: Custom Logo-Upload if allowed.
|
||||
- **Billing**: Stripe Products für jedes Package; Webhooks updaten Status (z.B. Subscription cancel → active=false).
|
||||
|
||||
## Migration von Altem System
|
||||
- Entferne: event_credits_balance aus tenants, event_purchases, event_credits_ledger.
|
||||
- Migriere: Für Events mit Credits → Zuordnen zu Free-Paket; Tenant-Balance → Initial Reseller S.
|
||||
- Skript: Artisan Command `php artisan migrate:packages` für Daten-Transfer.
|
||||
|
||||
Dieses Design ist final und bereit für Implementierung. Updates via PR.
|
||||
@@ -12,7 +12,8 @@ This directory supersedes the legacy `fotospiel_prp.md`. Content is split into s
|
||||
- 05-admin-superadmin.md — Super Admin web console (Filament)
|
||||
- 06-tenant-admin-pwa.md — Store-ready Tenant Admin PWA
|
||||
- 07-guest-pwa.md — Guest (event attendee) PWA
|
||||
- 08-billing.md — Event credits MVP, ledger, purchases
|
||||
- 08-billing.md — Packages (Einmalkäufe/Subscriptions), ledger, purchases
|
||||
- 15-packages-design.md — Package definitions, schema, integration
|
||||
- 09-security-compliance.md — RBAC, audit, GDPR
|
||||
- 10-storage-media-pipeline.md — Object storage, processing, CDN
|
||||
- 11-ops-ci-cd.md — CI, releases, environments
|
||||
|
||||
@@ -28,8 +28,8 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit
|
||||
### Stats laden
|
||||
- **GET /api/v1/tenant/dashboard**
|
||||
- **Headers**: `Authorization: Bearer {token}`
|
||||
- **Response**: `{ credits, active_events, new_photos, task_progress }`
|
||||
- **Zweck**: Übersicht-Daten für Dashboard-Cards
|
||||
- **Response**: `{ active_package, active_events, new_photos, task_progress }`
|
||||
- **Zweck**: Übersicht-Daten für Dashboard-Cards (active_package: current tenant package info)
|
||||
|
||||
## Events
|
||||
|
||||
@@ -47,9 +47,9 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit
|
||||
### Event erstellen
|
||||
- **POST /api/v1/tenant/events**
|
||||
- **Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`
|
||||
- **Body**: `{ title, date, location, description }`
|
||||
- **Body**: `{ title, date, location, description, package_id }`
|
||||
- **Response**: 201 Created mit erstelltem Event
|
||||
- **Validierung**: Prüft Credit-Balance (1 Credit pro Event)
|
||||
- **Validierung**: Prüft Tenant-Package (Reseller-Limit) und erstellt Event-Package (Einmalkauf oder Free)
|
||||
|
||||
### Event-Details
|
||||
- **GET /api/v1/tenant/events/{slug}**
|
||||
@@ -198,8 +198,8 @@ Alle API-Requests enthalten:
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "INSUFFICIENT_CREDITS",
|
||||
"message": "Nicht genügend Credits verfügbar"
|
||||
"code": "PACKAGE_LIMIT_EXCEEDED",
|
||||
"message": "Package-Limit überschritten (z.B. max_photos)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ Die Tenant Admin App muss folgende Kernfunktionen bereitstellen:
|
||||
- **Member-Management**: Hinzufügen/Entfernen von Event-Mitgliedern; Rollen (Admin, Member).
|
||||
- **Task & Emotion Management**: Zuweisen von Tasks und Emotions zu Events; Overrides für Tenant-spezifische Bibliotheken.
|
||||
- **Settings-Management**: Tenant-spezifische Einstellungen (Theme, Limits, Legal Pages).
|
||||
- **Billing & Purchases**: Kaufen von Event-Credits; Ledger-Übersicht; Integration mit Stripe.
|
||||
- **Billing & Purchases**: Kaufen von Packages (pro Event oder Tenant); Ledger-Übersicht; Integration mit Stripe.
|
||||
- **Notifications**: Push-Benachrichtigungen für neue Photos, Event-Updates, niedrigen Credit-Balance.
|
||||
- **Offline-Support**: Caching von Events und Photos; Queuing von Uploads/Mutations mit Sync bei Online-Wiederkehr.
|
||||
- **Audit & Compliance**: Logging kritischer Aktionen; ETag-basierte Conflict-Resolution; GDPR-konforme Datenlöschung.
|
||||
@@ -27,7 +27,7 @@ Die App ist API-first und interagiert ausschließlich über den Backend-API-Endp
|
||||
|
||||
### Core Features
|
||||
- **Event Lifecycle**:
|
||||
- Erstellen: Erfordert mind. 1 Event-Credit; Slug-Generierung (unique pro Tenant).
|
||||
- Erstellen: Erfordert Package-Auswahl (Free oder Kauf); Slug-Generierung (unique pro Tenant).
|
||||
- Bearbeiten: Update von Datum, Ort, Tasks, Emotions, Join-Link.
|
||||
- Veröffentlichen: Generiert QR-Code und Share-Link; aktiviert Guest-PWA-Zugriff.
|
||||
- Archivieren: Soft-Delete mit Retention-Periode (GDPR); Credit-Rückerstattung optional.
|
||||
@@ -39,8 +39,8 @@ Die App ist API-first und interagiert ausschließlich über den Backend-API-Endp
|
||||
- Bibliothek: Globale + Tenant-spezifische Tasks/Emotions.
|
||||
- Zuweisung: Drag-and-Drop zu Events; Fortschritts-Tracking.
|
||||
- **Billing Integration**:
|
||||
- Credit-Balance: Anzeige und Kauf von Packs (z.B. 5 Events für 29€).
|
||||
- Ledger: Historie von Käufen, Consumptions, Refunds.
|
||||
- Package-Auswahl: Anzeige verfügbarer Packages und Kauf (Einmalkauf/Subscription).
|
||||
- Ledger: Historie von Package-Käufen und Nutzung.
|
||||
- Stripe-Checkout: Server-side Intent-Erstellung; Webhook-Handling für Confirmation.
|
||||
|
||||
### Offline & Sync
|
||||
@@ -62,8 +62,8 @@ Die App konsumiert den API-Contract aus docs/prp/03-api.md. Schlüssel-Endpunkte
|
||||
|
||||
### Events
|
||||
- `GET /tenant/events`: Liste (paginiert, filterbar nach Status/Datum).
|
||||
- `POST /tenant/events`: Erstellen (validiert Credit-Balance).
|
||||
- `GET /tenant/events/{id}`: Details inkl. Tasks, Stats.
|
||||
- `POST /tenant/events`: Erstellen (validiert Tenant-Package und erstellt Event-Package).
|
||||
- `GET /tenant/events/{id}`: Details inkl. Tasks, Stats, package_limits.
|
||||
- `PATCH /tenant/events/{id}`: Update (ETag für Concurrency).
|
||||
- `DELETE /tenant/events/{id}`: Archivieren.
|
||||
|
||||
@@ -83,13 +83,13 @@ Die App konsumiert den API-Contract aus docs/prp/03-api.md. Schlüssel-Endpunkte
|
||||
- `PATCH /tenant/settings`: Update.
|
||||
|
||||
### Billing
|
||||
- `GET /tenant/ledger`: Credit-Historie.
|
||||
- `POST /tenant/purchases/intent`: Stripe-Checkout-Session erstellen.
|
||||
- `GET /tenant/credits/balance`: Aktueller Stand.
|
||||
- `GET /tenant/packages`: Tenant-Packages und Limits.
|
||||
- `POST /tenant/purchases/intent`: Stripe-Checkout-Session für Package erstellen.
|
||||
- `GET /api/v1/packages`: Verfügbare Packages.
|
||||
|
||||
### Pagination & Errors
|
||||
- Standard: `page`, `per_page` (max 50 für Mobile).
|
||||
- Errors: Parsen von `{ error: { code, message } }`; User-freundliche Messages (z.B. "Nicht genug Credits").
|
||||
- Errors: Parsen von `{ error: { code, message } }`; User-freundliche Messages (z.B. "Package-Limit überschritten").
|
||||
|
||||
## Non-Functional Requirements
|
||||
- **Performance**: Ladezeiten < 2s; Lazy-Loading für Galleries.
|
||||
|
||||
Reference in New Issue
Block a user