Implement multi-tenancy support with OAuth2 authentication for tenant admins, Stripe integration for event purchases and credits ledger, new Filament resources for event purchases, updated API routes and middleware for tenant isolation and token guarding, added factories/seeders/migrations for new models (Tenant, EventPurchase, OAuth entities, etc.), enhanced tests, and documentation updates. Removed outdated DemoAchievementsSeeder.
This commit is contained in:
@@ -13,20 +13,24 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Seed basic system data
|
||||
$this->call([
|
||||
LegalPagesSeeder::class,
|
||||
]);
|
||||
|
||||
// Seed core demo data for frontend previews
|
||||
$this->call([
|
||||
EventTypesSeeder::class,
|
||||
EmotionsSeeder::class,
|
||||
DemoEventSeeder::class,
|
||||
TasksSeeder::class,
|
||||
EventTasksSeeder::class,
|
||||
TaskCollectionsSeeder::class,
|
||||
DemoAchievementsSeeder::class,
|
||||
]);
|
||||
|
||||
// Optional: demo user
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
// Seed demo and admin data
|
||||
$this->call([
|
||||
SuperAdminSeeder::class,
|
||||
DemoEventSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\{Event, Emotion, Task, Photo};
|
||||
|
||||
class DemoAchievementsSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$event = Event::where('slug', 'demo-wedding-2025')->first();
|
||||
if (!$event) {
|
||||
$event = Event::create([
|
||||
'slug' => 'demo-wedding-2025',
|
||||
'name' => ['de' => 'Demo Hochzeit 2025', 'en' => 'Demo Wedding 2025'],
|
||||
'description' => ['de' => 'Demo-Event', 'en' => 'Demo event'],
|
||||
'date' => now()->toDateString(),
|
||||
'event_type_id' => null,
|
||||
'is_active' => true,
|
||||
'settings' => [],
|
||||
'default_locale' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
$emotions = Emotion::query()->take(6)->get();
|
||||
|
||||
if (Task::count() === 0 && $emotions->isNotEmpty()) {
|
||||
foreach (range(1, 10) as $i) {
|
||||
$emo = $emotions->random();
|
||||
Task::create([
|
||||
'title' => ['de' => "Aufgabe #$i", 'en' => "Task #$i"],
|
||||
'description' => ['de' => 'Kurzbeschreibung', 'en' => 'Short description'],
|
||||
'emotion_id' => $emo->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$tasks = Task::inRandomOrder()->take(10)->get();
|
||||
if ($tasks->isEmpty()) {
|
||||
return; // nothing to seed
|
||||
}
|
||||
|
||||
// Simple placeholder PNG (100x100)
|
||||
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjM2qefiAAABVklEQVR4Xu3UQQrCMBRF0cK1J3YQyF5z6jYv3Q0W3gQ6b8IYc3ov2Jf6n8A0Yq1nG2mZfE8y2GQkAAAAAAAAAANhK0ZL8b3xP2m5b4+0O8S9I3o9b3r8CwV8u0aH3bX8wE4WqgX3m4v3zO2KJ6l4yT4xvCw0b1q2c2w8bqQO3vFf0u8wUo5L3a8b0n2l5yq9Kf4zvCw0f1q2s2w0bpQO7PFv0s8wco4b3a8b0n2k5yq9Kf4zvCw0f1q2s2w0bpQO7PFv0s8wYgAAAAAAAAAAAACw9wG0qN2b2l3cMQAAAABJRU5ErkJggg==');
|
||||
$guests = ['Alex', 'Marie', 'Lukas', 'Lena', 'Tom', 'Sophie', 'Jonas', 'Mia'];
|
||||
|
||||
foreach (range(1, 24) as $i) {
|
||||
$task = $tasks->random();
|
||||
$fileName = 'photo_demo_'.Str::random(6).'.png';
|
||||
$thumbName = 'thumb_demo_'.Str::random(6).'.png';
|
||||
Storage::disk('public')->put('photos/'.$fileName, $png);
|
||||
Storage::disk('public')->put('thumbnails/'.$thumbName, $png);
|
||||
|
||||
Photo::create([
|
||||
'event_id' => $event->id,
|
||||
'emotion_id' => $task->emotion_id,
|
||||
'task_id' => $task->id,
|
||||
'guest_name' => $guests[array_rand($guests)],
|
||||
'file_path' => 'photos/'.$fileName,
|
||||
'thumbnail_path' => 'thumbnails/'.$thumbName,
|
||||
'likes_count' => rand(0, 7),
|
||||
'metadata' => ['seeded' => true],
|
||||
'created_at' => now()->subMinutes(rand(1, 180)),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
database/seeders/DemoPhotosSeeder.php
Normal file
97
database/seeders/DemoPhotosSeeder.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\{Event, Task, Emotion, Photo, PhotoLike, Tenant};
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DemoPhotosSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Get demo event and tenant
|
||||
$demoEvent = Event::where('slug', 'demo-wedding-2025')->first();
|
||||
$demoTenant = Tenant::where('slug', 'demo')->first();
|
||||
|
||||
if (!$demoEvent || !$demoTenant) {
|
||||
$this->command->info('Demo event or tenant not found, skipping DemoPhotosSeeder');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all available tasks and emotions
|
||||
$tasks = Task::where('tenant_id', $demoTenant->id)->get();
|
||||
$emotions = Emotion::all();
|
||||
|
||||
if ($tasks->isEmpty() || $emotions->isEmpty()) {
|
||||
$this->command->info('No tasks or emotions found, skipping DemoPhotosSeeder');
|
||||
return;
|
||||
}
|
||||
|
||||
// List of 20 German guest names
|
||||
$guestNames = [
|
||||
'Anna Müller', 'Max Schmidt', 'Lisa Weber', 'Tom Fischer', 'Sophie Bauer',
|
||||
'Lukas Hoffmann', 'Emma Wagner', 'Jonas Klein', 'Mia Schwarz', 'Felix Becker',
|
||||
'Lena Richter', 'Paul Lehmann', 'Julia Neumann', 'David Vogel', 'Sara Krüger',
|
||||
'Tim Berger', 'Nina Wolf', 'Ben Schäfer', 'Laura Stein', 'Moritz Fuchs'
|
||||
];
|
||||
|
||||
// Get all photo files from storage
|
||||
$photoDir = storage_path('app/public/photos');
|
||||
$photoFiles = File::files($photoDir);
|
||||
|
||||
$seededCount = 0;
|
||||
foreach ($photoFiles as $file) {
|
||||
$filename = $file->getFilename();
|
||||
if (!str_ends_with($filename, '.jpg')) continue;
|
||||
|
||||
// Check if already seeded (avoid duplicates)
|
||||
if (Photo::where('file_path', 'photos/' . $filename)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate thumbnail path
|
||||
$thumbnailFilename = str_replace('.jpg', '_thumb.jpg', $filename);
|
||||
$thumbnailPath = 'thumbnails/' . $thumbnailFilename;
|
||||
|
||||
// Random assignments
|
||||
$randomTask = $tasks->random();
|
||||
$randomEmotion = $emotions->random();
|
||||
$randomUploader = $guestNames[array_rand($guestNames)];
|
||||
$randomLikes = rand(0, 20);
|
||||
$eventDate = $demoEvent->date;
|
||||
$randomUploadedAt = Carbon::parse($eventDate)->addHours(rand(0, 24))->addMinutes(rand(0, 59));
|
||||
|
||||
// Create photo
|
||||
$photo = Photo::create([
|
||||
'tenant_id' => $demoTenant->id, // Assuming tenant_id exists
|
||||
'event_id' => $demoEvent->id,
|
||||
'task_id' => $randomTask->id,
|
||||
'emotion_id' => $randomEmotion->id,
|
||||
'file_path' => 'photos/' . $filename,
|
||||
'thumbnail_path' => $thumbnailPath,
|
||||
'uploader_name' => $randomUploader,
|
||||
'uploaded_at' => $randomUploadedAt,
|
||||
'is_featured' => false,
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
// Add random likes
|
||||
if ($randomLikes > 0) {
|
||||
for ($i = 0; $i < $randomLikes; $i++) {
|
||||
PhotoLike::create([
|
||||
'photo_id' => $photo->id,
|
||||
'session_id' => 'demo_session_' . Str::random(10), // Anonymous session
|
||||
'created_at' => $randomUploadedAt->clone()->addMinutes(rand(0, 60)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$seededCount++;
|
||||
}
|
||||
|
||||
$this->command->info("✅ Seeded {$seededCount} demo photos with random tasks, emotions, uploaders, and likes");
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,19 @@ namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\{Emotion, Task};
|
||||
use App\Models\{Emotion, Task, Tenant};
|
||||
|
||||
class EventTasksSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Get demo tenant
|
||||
$demoTenant = Tenant::where('slug', 'demo')->first();
|
||||
if (!$demoTenant) {
|
||||
$this->command->info('Demo tenant not found, skipping EventTasksSeeder');
|
||||
return;
|
||||
}
|
||||
|
||||
// Define 20 themed prompts per emotion (DE/EN)
|
||||
$catalog = [
|
||||
'Liebe' => [
|
||||
@@ -209,6 +216,7 @@ class EventTasksSeeder extends Seeder
|
||||
if ($exists) { $order++; continue; }
|
||||
|
||||
Task::create([
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'emotion_id' => $emotion->id,
|
||||
'event_type_id' => null,
|
||||
'title' => ['de' => $deTitle, 'en' => $enTitle],
|
||||
@@ -227,6 +235,7 @@ class EventTasksSeeder extends Seeder
|
||||
[$deTitle, $deDesc, $enTitle, $enDesc] = $list[$i];
|
||||
$suffix = ' #' . ($created + 1);
|
||||
Task::create([
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'emotion_id' => $emotion->id,
|
||||
'event_type_id' => null,
|
||||
'title' => ['de' => $deTitle.$suffix, 'en' => $enTitle.$suffix],
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\{Event, Task, TaskCollection, Tenant};
|
||||
|
||||
class TaskCollectionsSeeder extends Seeder
|
||||
{
|
||||
@@ -13,78 +12,69 @@ class TaskCollectionsSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Get demo tenant
|
||||
$demoTenant = Tenant::where('slug', 'demo')->first();
|
||||
if (!$demoTenant) {
|
||||
$this->command->info('Demo tenant not found, skipping task collections seeding');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get demo event ID
|
||||
$demoEventId = DB::table('events')->where('slug', 'demo-wedding-2025')->value('id');
|
||||
if (!$demoEventId) {
|
||||
$demoEvent = Event::where('slug', 'demo-wedding-2025')->first();
|
||||
if (!$demoEvent) {
|
||||
$this->command->info('Demo event not found, skipping task collections seeding');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some task IDs for demo (assuming TasksSeeder was run)
|
||||
$taskIds = DB::table('tasks')->limit(6)->pluck('id')->toArray();
|
||||
$taskIds = Task::where('tenant_id', $demoTenant->id)->limit(6)->get('id')->pluck('id')->toArray();
|
||||
if (empty($taskIds)) {
|
||||
$this->command->info('No tasks found, skipping task collections seeding');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Wedding Task Collection
|
||||
$weddingCollectionId = DB::table('task_collections')->insertGetId([
|
||||
'name' => json_encode([
|
||||
// Create Wedding Task Collection using Eloquent
|
||||
$weddingCollection = TaskCollection::create([
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'name' => [
|
||||
'de' => 'Hochzeitsaufgaben',
|
||||
'en' => 'Wedding Tasks'
|
||||
]),
|
||||
'description' => json_encode([
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Spezielle Aufgaben für Hochzeitsgäste',
|
||||
'en' => 'Special tasks for wedding guests'
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
// Assign first 4 tasks to wedding collection
|
||||
$weddingTasks = array_slice($taskIds, 0, 4);
|
||||
foreach ($weddingTasks as $taskId) {
|
||||
DB::table('task_collection_task')->insert([
|
||||
'task_collection_id' => $weddingCollectionId,
|
||||
'task_id' => $taskId,
|
||||
]);
|
||||
}
|
||||
// Assign first 4 tasks to wedding collection using Eloquent
|
||||
$weddingTasks = collect($taskIds)->take(4);
|
||||
$weddingCollection->tasks()->attach($weddingTasks);
|
||||
|
||||
// Link wedding collection to demo event
|
||||
DB::table('event_task_collection')->insert([
|
||||
'event_id' => $demoEventId,
|
||||
'task_collection_id' => $weddingCollectionId,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
// Link wedding collection to demo event using Eloquent
|
||||
$demoEvent->taskCollections()->attach($weddingCollection, ['sort_order' => 1]);
|
||||
|
||||
// Create General Fun Tasks Collection (fallback)
|
||||
$funCollectionId = DB::table('task_collections')->insertGetId([
|
||||
'name' => json_encode([
|
||||
// Create General Fun Tasks Collection (fallback) using Eloquent
|
||||
$funCollection = TaskCollection::create([
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'name' => [
|
||||
'de' => 'Spaß-Aufgaben',
|
||||
'en' => 'Fun Tasks'
|
||||
]),
|
||||
'description' => json_encode([
|
||||
],
|
||||
'description' => [
|
||||
'de' => 'Allgemeine unterhaltsame Aufgaben',
|
||||
'en' => 'General entertaining tasks'
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
// Assign remaining tasks to fun collection
|
||||
$funTasks = array_slice($taskIds, 4);
|
||||
foreach ($funTasks as $taskId) {
|
||||
DB::table('task_collection_task')->insert([
|
||||
'task_collection_id' => $funCollectionId,
|
||||
'task_id' => $taskId,
|
||||
]);
|
||||
}
|
||||
// Assign remaining tasks to fun collection using Eloquent
|
||||
$funTasks = collect($taskIds)->slice(4);
|
||||
$funCollection->tasks()->attach($funTasks);
|
||||
|
||||
// Link fun collection to demo event as fallback
|
||||
DB::table('event_task_collection')->insert([
|
||||
'event_id' => $demoEventId,
|
||||
'task_collection_id' => $funCollectionId,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
// Link fun collection to demo event as fallback using Eloquent
|
||||
$demoEvent->taskCollections()->attach($funCollection, ['sort_order' => 2]);
|
||||
|
||||
$this->command->info("✅ Created 2 task collections with " . count($taskIds) . " tasks for demo event");
|
||||
$this->command->info("Wedding Collection ID: {$weddingCollectionId}");
|
||||
$this->command->info("Fun Collection ID: {$funCollectionId}");
|
||||
$this->command->info("Wedding Collection ID: {$weddingCollection->id}");
|
||||
$this->command->info("Fun Collection ID: {$funCollection->id}");
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,17 @@ class TasksSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Create or get demo tenant
|
||||
$demoTenant = \App\Models\Tenant::updateOrCreate(
|
||||
['slug' => 'demo'],
|
||||
[
|
||||
'name' => ['de' => 'Demo Tenant', 'en' => 'Demo Tenant'],
|
||||
'domain' => null,
|
||||
'is_active' => true,
|
||||
'settings' => [],
|
||||
]
|
||||
);
|
||||
|
||||
$seed = [
|
||||
'Liebe' => [
|
||||
['title'=>['de'=>'Kuss-Foto','en'=>'Kiss Photo'], 'description'=>['de'=>'Macht ein romantisches Kuss-Foto','en'=>'Take a romantic kiss photo'], 'difficulty'=>'easy'],
|
||||
@@ -32,8 +43,10 @@ class TasksSeeder extends Seeder
|
||||
foreach ($tasks as $t) {
|
||||
Task::updateOrCreate([
|
||||
'emotion_id' => $emotion->id,
|
||||
'title->de' => $t['title']['de']
|
||||
'title->de' => $t['title']['de'],
|
||||
'tenant_id' => $demoTenant->id
|
||||
], [
|
||||
'tenant_id' => $demoTenant->id,
|
||||
'emotion_id' => $emotion->id,
|
||||
'event_type_id' => isset($t['event_type']) && isset($types[$t['event_type']]) ? $types[$t['event_type']] : null,
|
||||
'title' => $t['title'],
|
||||
|
||||
Reference in New Issue
Block a user