environment(['local', 'development', 'demo'])) { $this->error('Cleanup/Seeding is restricted to local/development/demo environments.'); return self::FAILURE; } if ($this->option('cleanup')) { return $this->cleanup(); } $this->info('Seeding demo tenants for switcher...'); $packages = $this->loadPackages(); $eventTypes = $this->loadEventTypes(); DB::transaction(function () use ($packages, $eventTypes) { $this->seedCustomerStandardEmpty($packages, $eventTypes); $this->seedCustomerStarterWedding($packages, $eventTypes); $this->seedResellerActive($packages, $eventTypes); $this->seedResellerFull($packages, $eventTypes); }); if ($this->option('with-photos')) { $this->seedPhotosFromPexels((int) $this->option('photos-per-event')); } $this->info('Done.'); return self::SUCCESS; } private function cleanup(): int { $slugs = [ 'demo-standard-empty', 'demo-starter-wedding', 'demo-reseller-active', 'demo-reseller-full', ]; $eventsDeleted = 0; $photosDeleted = 0; $photoLikesDeleted = 0; $usersDeleted = 0; foreach ($slugs as $slug) { $tenant = Tenant::where('slug', $slug)->first(); if (! $tenant) { continue; } foreach ($tenant->events as $event) { $eventsDeleted++; $photos = Photo::where('event_id', $event->id)->get(); foreach ($photos as $photo) { $deletedLikes = $photo->likes()->count(); $photo->likes()->delete(); $photoLikesDeleted += $deletedLikes; $this->deleteDemoPhotoAssets($photo, $event); $photo->delete(); $photosDeleted++; } $cleanupDisk = $this->eventStorageManager->getHotDiskForEvent($event); Storage::disk($cleanupDisk)->deleteDirectory("events/{$event->id}/gallery"); Storage::disk($cleanupDisk)->deleteDirectory("events/{$event->id}/gallery/thumbs"); $event->taskCollections()->detach(); $event->tasks()->detach(); $event->eventPackages()->delete(); $event->delete(); } TenantPackage::where('tenant_id', $tenant->id)->delete(); $usersDeleted += User::where('tenant_id', $tenant->id)->count(); User::where('tenant_id', $tenant->id)->delete(); $tenant->delete(); } $this->info( 'Cleanup completed. Tenants deleted: '.count($slugs) .", Users deleted: {$usersDeleted}, Events deleted: {$eventsDeleted}, Photos deleted: {$photosDeleted}, Photo likes deleted: {$photoLikesDeleted}" ); return self::SUCCESS; } private function loadPackages(): array { $slugs = [ 'starter' => 'Starter', 'standard' => 'Standard', 's-small-reseller' => 'Reseller S', ]; $packages = []; foreach ($slugs as $slug => $label) { $package = Package::where('slug', $slug)->first(); if (! $package) { $this->error("Package {$label} ({$slug}) not found. Run PackageSeeder first."); abort(1); } $packages[$slug] = $package; } return $packages; } private function loadEventTypes(): array { $slugs = ['wedding', 'corporate', 'birthday', 'festival']; $types = []; foreach ($slugs as $slug) { $eventType = EventType::where('slug', $slug)->first(); if ($eventType) { $types[$slug] = $eventType; } } return $types; } private function seedCustomerStandardEmpty(array $packages, array $eventTypes): void { $tenant = $this->upsertTenant( slug: 'demo-standard-empty', name: 'Demo Standard (ohne Event)', contactEmail: 'standard-empty@demo.fotospiel', attributes: [ 'subscription_tier' => 'standard', 'subscription_status' => 'active', ], ); $this->upsertAdmin($tenant, 'standard-empty@demo.fotospiel'); TenantPackage::updateOrCreate( ['tenant_id' => $tenant->id, 'package_id' => $packages['standard']->id], [ 'price' => $packages['standard']->price, 'purchased_at' => Carbon::now()->subDays(1), 'expires_at' => Carbon::now()->addMonths(12), 'used_events' => 0, 'active' => true, ] ); $this->comment('Seeded Standard tenant without events.'); } private function seedCustomerStarterWedding(array $packages, array $eventTypes): void { $tenant = $this->upsertTenant( slug: 'demo-starter-wedding', name: 'Demo Starter Wedding', contactEmail: 'starter-wedding@demo.fotospiel', attributes: [ 'subscription_tier' => 'starter', 'subscription_status' => 'active', ], ); $this->upsertAdmin($tenant, 'starter-wedding@demo.fotospiel'); $event = $this->upsertEvent( tenant: $tenant, package: $packages['starter'], eventType: $eventTypes['wedding'] ?? null, attributes: [ 'name' => ['de' => 'Hochzeit Mia & Jonas', 'en' => 'Wedding Mia & Jonas'], 'slug' => 'demo-starter-wedding', 'status' => 'published', 'is_active' => true, 'date' => Carbon::now()->addWeeks(5), ], ); $this->attachDefaultCollections($event); } private function seedResellerActive(array $packages, array $eventTypes): void { $tenant = $this->upsertTenant( slug: 'demo-reseller-active', name: 'Demo Reseller Active', contactEmail: 'reseller-active@demo.fotospiel', attributes: [ 'subscription_tier' => 'reseller', 'subscription_status' => 'active', ], ); $this->upsertAdmin($tenant, 'reseller-active@demo.fotospiel'); TenantPackage::updateOrCreate( ['tenant_id' => $tenant->id, 'package_id' => $packages['s-small-reseller']->id], [ 'price' => $packages['s-small-reseller']->price, 'purchased_at' => Carbon::now()->subMonths(1), 'expires_at' => Carbon::now()->addMonths(11), 'used_events' => 3, 'active' => true, ] ); $events = [ [ 'name' => ['de' => 'Corporate Summit', 'en' => 'Corporate Summit'], 'slug' => 'demo-reseller-corporate', 'type' => $eventTypes['corporate'] ?? null, 'date' => Carbon::now()->addWeeks(3), ], [ 'name' => ['de' => 'Sommerfestival', 'en' => 'Summer Festival'], 'slug' => 'demo-reseller-festival', 'type' => $eventTypes['festival'] ?? ($eventTypes['birthday'] ?? null), 'date' => Carbon::now()->addWeeks(6), ], [ 'name' => ['de' => 'Geburtstag Lisa', 'en' => 'Lisa Birthday'], 'slug' => 'demo-reseller-birthday', 'type' => $eventTypes['birthday'] ?? null, 'date' => Carbon::now()->addWeeks(9), ], ]; foreach ($events as $index => $config) { $event = $this->upsertEvent( tenant: $tenant, package: $packages['standard'], eventType: $config['type'], attributes: [ 'name' => $config['name'], 'slug' => $config['slug'], 'status' => 'published', 'is_active' => true, 'date' => $config['date'], ], ); $this->attachDefaultCollections($event); } } private function seedResellerFull(array $packages, array $eventTypes): void { $tenant = $this->upsertTenant( slug: 'demo-reseller-full', name: 'Demo Reseller Voll', contactEmail: 'reseller-full@demo.fotospiel', attributes: [ 'subscription_tier' => 'reseller', 'subscription_status' => 'active', ], ); $this->upsertAdmin($tenant, 'reseller-full@demo.fotospiel'); TenantPackage::updateOrCreate( ['tenant_id' => $tenant->id, 'package_id' => $packages['s-small-reseller']->id], [ 'price' => $packages['s-small-reseller']->price, 'purchased_at' => Carbon::now()->subMonths(6), 'expires_at' => Carbon::now()->addMonths(6), 'used_events' => 5, 'active' => true, ] ); $eventConfigs = [ ['slug' => 'demo-full-wedding', 'name' => ['de' => 'Hochzeit Clara & Ben', 'en' => 'Wedding Clara & Ben'], 'type' => $eventTypes['wedding'] ?? null], ['slug' => 'demo-full-corporate', 'name' => ['de' => 'Jahrestagung', 'en' => 'Annual Summit'], 'type' => $eventTypes['corporate'] ?? null], ['slug' => 'demo-full-birthday', 'name' => ['de' => 'Geburtstag Jonas', 'en' => 'Birthday Jonas'], 'type' => $eventTypes['birthday'] ?? null], ['slug' => 'demo-full-festival', 'name' => ['de' => 'Stadtfest', 'en' => 'City Festival'], 'type' => $eventTypes['festival'] ?? null], ['slug' => 'demo-full-christmas', 'name' => ['de' => 'Weihnachtsfeier', 'en' => 'Christmas Party'], 'type' => $eventTypes['corporate'] ?? null], ]; foreach ($eventConfigs as $index => $config) { $event = $this->upsertEvent( tenant: $tenant, package: $packages['standard'], eventType: $config['type'], attributes: [ 'name' => $config['name'], 'slug' => $config['slug'], 'status' => 'archived', 'is_active' => false, 'date' => Carbon::now()->subWeeks(5 - $index), ], ); $this->attachDefaultCollections($event); } } private function upsertTenant(string $slug, string $name, string $contactEmail, array $attributes = []): Tenant { $defaults = [ 'name' => $name, 'contact_email' => $contactEmail, 'subscription_expires_at' => Carbon::now()->addMonths(12), 'is_active' => true, 'is_suspended' => false, 'settings_updated_at' => Carbon::now(), 'settings' => [ 'branding' => [ 'logo_url' => null, 'primary_color' => '#1D4ED8', 'secondary_color' => '#0F172A', 'font_family' => 'Inter, sans-serif', ], 'features' => [ 'photo_likes_enabled' => true, 'event_checklist' => true, ], 'contact_email' => $contactEmail, ], ]; return Tenant::updateOrCreate( ['slug' => $slug], array_merge($defaults, $attributes, ['slug' => $slug]) ); } private function upsertAdmin(Tenant $tenant, string $email): User { $password = config('seeding.demo_tenant_password', 'Demo1234!'); $user = User::updateOrCreate( ['email' => $email], [ 'tenant_id' => $tenant->id, 'role' => 'tenant_admin', 'password' => Hash::make($password), 'first_name' => Str::headline(Str::before($tenant->slug, '-')), 'last_name' => 'Demo', ] ); if (! $user->email_verified_at) { $user->forceFill(['email_verified_at' => now()])->save(); } return $user; } private function upsertEvent(Tenant $tenant, Package $package, ?EventType $eventType, array $attributes): Event { $resolvedEventType = $eventType ?? $this->fallbackEventType(); $payload = array_merge([ 'tenant_id' => $tenant->id, 'event_type_id' => $resolvedEventType?->id, 'settings' => [ 'features' => [ 'photo_likes_enabled' => true, 'event_checklist' => true, ], ], ], $attributes); /** @var Event $event */ $event = Event::updateOrCreate( ['slug' => $attributes['slug']], $payload ); EventPackage::updateOrCreate( [ 'event_id' => $event->id, 'package_id' => $package->id, ], [ 'purchased_price' => $package->price, 'purchased_at' => Carbon::now()->subDays(2), 'used_photos' => 0, 'used_guests' => 0, 'gallery_expires_at' => Carbon::now()->addDays($package->gallery_days ?? 30), ] ); return $event; } private function fallbackEventType(): ?EventType { $fallback = EventType::first(); if (! $fallback) { $this->warn('No EventType available, events will miss type. Please run EventTypesSeeder.'); } return $fallback; } private function attachDefaultCollections(Event $event): void { if (! $event->event_type_id) { return; } $collection = TaskCollection::where('event_type_id', $event->event_type_id) ->where('is_default', true) ->orderBy('position') ->first(); if (! $collection) { return; } $event->taskCollections()->syncWithoutDetaching([$collection->id]); $taskIds = $collection->tasks()->pluck('tasks.id')->all(); if ($taskIds !== []) { $event->tasks()->syncWithoutDetaching($taskIds); } } private function seedPhotosFromPexels(int $targetPerEvent): void { $apiKey = config('services.pexels.key') ?? env('PEXELS_API_KEY'); if (! $apiKey) { $this->warn('PEXELS_API_KEY missing, skipping photo download.'); return; } $events = Event::whereIn('slug', [ 'demo-starter-wedding', 'demo-reseller-corporate', 'demo-reseller-festival', 'demo-reseller-birthday', 'demo-full-wedding', 'demo-full-corporate', 'demo-full-birthday', 'demo-full-festival', 'demo-full-christmas', ])->get(); foreach ($events as $event) { $query = $this->guessQueryForEvent($event); $this->info("Downloading photos for {$event->slug} ({$query})..."); $photos = $this->fetchPexels($apiKey, $query, $targetPerEvent); if ($photos === []) { $this->warn('No photos returned from Pexels.'); continue; } $this->storePhotos($event, $photos, $targetPerEvent); } } private function guessQueryForEvent(Event $event): string { $typeSlug = optional($event->eventType)->slug; return match ($typeSlug) { 'wedding' => 'wedding photography couple', 'corporate' => 'corporate event people', 'birthday' => 'birthday party friends', default => 'event celebration crowd', }; } private function fetchPexels(string $apiKey, string $query, int $count): array { $perPage = min(40, max(5, $count)); $response = Http::withHeaders([ 'Authorization' => $apiKey, ])->get('https://api.pexels.com/v1/search', [ 'query' => $query, 'per_page' => $perPage, 'orientation' => 'landscape', ]); if (! $response->ok()) { $this->warn('Pexels request failed: '.$response->status()); return []; } $data = $response->json(); return Arr::get($data, 'photos', []); } private function storePhotos(Event $event, array $photos, int $targetPerEvent): void { $tenantId = $event->tenant_id; $disk = $this->eventStorageManager->getHotDiskForEvent($event); $storage = Storage::disk($disk); $storage->makeDirectory("events/{$event->id}/gallery"); $storage->makeDirectory("events/{$event->id}/gallery/thumbs"); $demoPhotos = Photo::where('event_id', $event->id) ->where('metadata->demo', true) ->get(); foreach ($demoPhotos as $photo) { $this->deleteDemoPhotoAssets($photo, $event); $photo->delete(); } $limit = min(count($photos), $targetPerEvent); for ($i = 0; $i < $limit; $i++) { $photo = $photos[$i]; $src = $photo['src'] ?? []; $originalUrl = $src['large2x'] ?? $src['large'] ?? null; $thumbUrl = $src['medium'] ?? $src['small'] ?? $originalUrl; if (! $originalUrl) { continue; } $filename = sprintf('%s-demo-%02d.jpg', $event->slug, $i + 1); $thumbFilename = sprintf('%s-demo-%02d_thumb.jpg', $event->slug, $i + 1); $filePath = "events/{$event->id}/gallery/{$filename}"; $thumbPath = "events/{$event->id}/gallery/thumbs/{$thumbFilename}"; $originalBody = null; $thumbBody = null; try { $imageResponse = Http::get($originalUrl); if ($imageResponse->ok()) { $originalBody = $imageResponse->body(); $storage->put($filePath, $originalBody); } if ($thumbUrl) { $thumbResponse = Http::get($thumbUrl); if ($thumbResponse->ok()) { $thumbBody = $thumbResponse->body(); $storage->put($thumbPath, $thumbBody); } } } catch (\Throwable $exception) { $this->warn('Failed to download image: '.$exception->getMessage()); continue; } $timestamp = Carbon::parse($event->date ?? Carbon::now())->addHours($i); $photoRecord = Photo::updateOrCreate( [ 'tenant_id' => $tenantId, 'event_id' => $event->id, 'file_path' => $filePath, ], [ 'thumbnail_path' => $thumbPath, 'guest_name' => 'Demo Guest '.($i + 1), 'likes_count' => rand(1, 25), 'is_featured' => $i === 0, 'status' => 'approved', 'mime_type' => 'image/jpeg', 'size' => $originalBody ? strlen($originalBody) : null, 'metadata' => ['demo' => true, 'source' => 'pexels'], 'created_at' => $timestamp, 'updated_at' => $timestamp, ] ); $originalAsset = $this->eventStorageManager->recordAsset($event, $disk, $filePath, [ 'variant' => 'original', 'mime_type' => 'image/jpeg', 'size_bytes' => $originalBody ? strlen($originalBody) : null, 'checksum' => $originalBody ? hash('sha256', $originalBody) : null, 'photo_id' => $photoRecord->id, ]); if ($thumbBody) { $this->eventStorageManager->recordAsset($event, $disk, $thumbPath, [ 'variant' => 'thumbnail', 'mime_type' => 'image/jpeg', 'size_bytes' => strlen($thumbBody), 'photo_id' => $photoRecord->id, 'meta' => [ 'source_variant_id' => $originalAsset->id, ], ]); } $photoRecord->update(['media_asset_id' => $originalAsset->id]); } EventPackage::where('event_id', $event->id)->update([ 'used_photos' => max($limit, 0), 'used_guests' => max(15, $event->eventPackage?->used_guests ?? 0), ]); $this->info("Seeded {$limit} photos for {$event->slug}"); } private function deleteDemoPhotoAssets(Photo $photo, Event $event): void { $fallbackDisk = $this->eventStorageManager->getHotDiskForEvent($event); $assets = EventMediaAsset::where('photo_id', $photo->id)->get(); foreach ($assets as $asset) { $assetDisk = $asset->disk ?: $fallbackDisk; if (! config("filesystems.disks.{$assetDisk}")) { $assetDisk = $fallbackDisk; } if ($asset->path) { Storage::disk($assetDisk)->delete($asset->path); } $asset->delete(); } if ($photo->file_path) { Storage::disk($fallbackDisk)->delete($photo->file_path); } if ($photo->thumbnail_path) { Storage::disk($fallbackDisk)->delete($photo->thumbnail_path); } } }