7.9 KiB
7.9 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;
// Event types (global)
Schema::create('event_types', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->string('slug', 100)->unique();
$table->string('icon', 64)->nullable();
$table->json('settings')->nullable();
$table->timestamps();
});
// Events (tenant-scoped)
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->json('name');
$table->date('date');
$table->string('slug');
$table->json('description')->nullable();
$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');
$table->timestamps();
$table->unique(['tenant_id', 'slug']);
});
// Emotions (global library)
Schema::create('emotions', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->string('icon', 50);
$table->string('color', 7);
$table->json('description')->nullable();
$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']);
});
// Tasks (with optional tenant/event scoping)
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');
$table->json('description');
$table->string('difficulty', 16)->default('easy'); // app enum
$table->json('example_text')->nullable();
$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']);
});
// 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']);
});
Legal Pages
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');
$table->json('body_markdown');
$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; otherwisenullOnDelete(). - Every tenant-owned table should include
tenant_idand appropriate composite indexes.