Files
fotospiel-app/docs/archive/prp/04-data-model-migrations.md
2025-11-20 12:31:21 +01:00

8.6 KiB

04 — Data Model & Portable Migrations

Use Laravel Schema builder; avoid database-specific ENUM/DDL. Composite unique indexes include tenant_id for tenant-owned data.

Core (Tenants, Users, Events, Settings, Analytics, Purchases, Ledger)

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

// Tenants
Schema::create('tenants', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('domain')->nullable()->unique();
    $table->string('contact_name');
    $table->string('contact_email');
    $table->string('contact_phone')->nullable();
    $table->integer('event_credits_balance')->default(1);
    $table->timestamp('free_event_granted_at')->nullable();
    $table->integer('max_photos_per_event')->default(500);
    $table->integer('max_storage_mb')->default(1024);
    $table->json('features')->nullable();
    $table->timestamp('last_activity_at')->nullable();
    $table->timestamps();
});

// Users: tenancy + role (portable — avoid DB ENUM)
Schema::table('users', function (Blueprint $table) {
    $table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
    $table->string('role', 32)->default('tenant_user')->index();
});

// Events: include tenant_id and composite unique
Schema::table('events', function (Blueprint $table) {
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->unique(['tenant_id', 'slug']);
});

// System settings
Schema::create('system_settings', function (Blueprint $table) {
    $table->id();
    $table->string('key')->unique();
    $table->text('value')->nullable();
    $table->text('description')->nullable();
    $table->boolean('is_public')->default(false);
    $table->timestamps();
});

// Platform analytics
Schema::create('platform_analytics', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
    $table->string('metric_name', 100);
    $table->bigInteger('metric_value');
    $table->date('metric_date');
    $table->json('metadata')->nullable();
    $table->timestamps();
    $table->index(['metric_date', 'metric_name']);
    $table->index(['tenant_id', 'metric_date']);
});

// Event purchases (event credits)
Schema::create('event_purchases', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->unsignedInteger('events_purchased')->default(1);
    $table->decimal('amount', 10, 2);
    $table->string('currency', 3)->default('EUR');
    $table->string('provider', 32); // app enum: app_store|play_store|stripe|paypal
    $table->string('external_receipt_id')->nullable();
    $table->string('status', 16)->default('pending'); // app enum
    $table->timestamp('purchased_at')->nullable();
    $table->timestamps();
    $table->index(['tenant_id', 'purchased_at']);
});

// Credits ledger
Schema::create('event_credits_ledger', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->integer('delta');
    $table->string('reason', 32); // app enum
    $table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
    $table->text('note')->nullable();
    $table->timestamps();
    $table->index(['tenant_id', 'created_at']);
});

Domain (Event Types, Events, Emotions, Tasks, Photos, Likes)

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

Schema::create('event_types', function (Blueprint $table) {
    $table->id();
    $table->json('name'); // Translatable: { "de": "Hochzeit", "en": "Wedding" }
    $table->string('slug', 100)->unique();
    $table->string('icon', 64)->nullable();
    $table->json('settings')->nullable();
    $table->timestamps();
});

Schema::create('events', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->json('name'); // Translatable: { "de": "Event Name", "en": "Event Name" }
    $table->date('date');
    $table->string('slug');
    $table->json('description')->nullable(); // Translatable
    $table->json('settings')->nullable();
    $table->foreignId('event_type_id')->constrained('event_types');
    $table->boolean('is_active')->default(true);
    $table->string('default_locale', 5)->default('de'); // For event-specific i18n fallback
    $table->timestamps();
    $table->unique(['tenant_id', 'slug']);
});

Schema::create('emotions', function (Blueprint $table) {
    $table->id();
    $table->json('name'); // Translatable: { "de": "Freude", "en": "Joy" }
    $table->string('icon', 50);
    $table->string('color', 7);
    $table->json('description')->nullable(); // Translatable
    $table->integer('sort_order')->default(0);
    $table->boolean('is_active')->default(true);
});

// Pivot: emotion x event_type
Schema::create('emotion_event_type', function (Blueprint $table) {
    $table->foreignId('emotion_id')->constrained('emotions')->cascadeOnDelete();
    $table->foreignId('event_type_id')->constrained('event_types')->cascadeOnDelete();
    $table->primary(['emotion_id', 'event_type_id']);
});

Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->foreignId('emotion_id')->constrained('emotions');
    $table->foreignId('event_type_id')->nullable()->constrained('event_types')->nullOnDelete();
    $table->json('title'); // Translatable
    $table->json('description'); // Translatable
    $table->string('difficulty', 16)->default('easy'); // app enum
    $table->json('example_text')->nullable(); // Translatable
    $table->integer('sort_order')->default(0);
    $table->boolean('is_active')->default(true);
    $table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
    $table->string('scope', 16)->default('global'); // global|tenant|event
    $table->foreignId('event_id')->nullable()->constrained('events')->nullOnDelete();
    $table->timestamps();
    $table->unique(['tenant_id', 'emotion_id', 'title->de']); // Example for de fallback; adjust for multi-locale
});

// Photos
Schema::create('photos', function (Blueprint $table) {
    $table->id();
    $table->foreignId('event_id')->constrained('events')->cascadeOnDelete();
    $table->foreignId('emotion_id')->constrained('emotions');
    $table->foreignId('task_id')->nullable()->constrained('tasks')->nullOnDelete();
    $table->string('guest_name');
    $table->string('file_path');
    $table->string('thumbnail_path');
    $table->unsignedInteger('likes_count')->default(0);
    $table->boolean('is_featured')->default(false);
    $table->json('metadata')->nullable();
    $table->timestamps();
    $table->index(['event_id', 'created_at']);
});

// Photo likes
Schema::create('photo_likes', function (Blueprint $table) {
    $table->id();
    $table->foreignId('photo_id')->constrained('photos')->cascadeOnDelete();
    $table->string('guest_name');
    $table->string('ip_address', 45)->nullable();
    $table->timestamp('created_at')->useCurrent();
    $table->unique(['photo_id', 'guest_name', 'ip_address']);
});
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

Schema::create('legal_pages', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
    $table->string('slug', 32); // imprint|privacy|terms|custom
    $table->json('title'); // Translatable
    $table->json('body_markdown'); // Translatable Markdown content per locale
    $table->string('locale_fallback', 5)->default('de');
    $table->unsignedInteger('version')->default(1);
    $table->timestamp('effective_from')->nullable();
    $table->boolean('is_published')->default(false);
    $table->timestamps();
    $table->unique(['slug', 'tenant_id', 'version']);
});

Notes

  • Prefer app-level enums (string columns + validation) over DB ENUM.
  • Use cascadeOnDelete() only where child data must be removed with parent; otherwise nullOnDelete().
  • Every tenant-owned table should include tenant_id and appropriate composite indexes.
  • i18n Integration: JSON fields (e.g., name, description) store locale-specific values as { "de": "Text", "en": "Text" }. Use Laravel's json cast or spatie/laravel-translatable for access. Fallback to default_locale or global fallback ('de'). Update via Filament resources with locale selectors. Ensure indexes on JSON paths if querying (e.g., ->de for German titles).