tenant; $eventType = EventType::factory()->create(); $package = Package::factory()->create(['type' => 'endcustomer', 'max_photos' => 100]); TenantPackage::factory()->create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'active' => true, ]); $purchase = PackagePurchase::factory()->create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'type' => 'endcustomer_event', 'metadata' => [], ]); $response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [ 'name' => 'Test Event', 'slug' => 'test-event', 'event_date' => Carbon::now()->addDays(10)->toDateString(), 'event_type_id' => $eventType->id, 'package_id' => $package->id, 'accepted_waiver' => true, ]); $response->assertStatus(201); $this->assertDatabaseHas('events', [ 'tenant_id' => $tenant->id, 'name' => json_encode('Test Event'), 'slug' => 'test-event', 'event_type_id' => $eventType->id, ]); $event = Event::latest()->first(); $this->assertDatabaseHas('event_packages', [ 'event_id' => $event->id, 'package_id' => $package->id, ]); $this->assertDatabaseHas('event_join_tokens', [ 'event_id' => $event->id, ]); $purchase->refresh(); $this->assertNotNull(data_get($purchase->metadata, 'consents.digital_content_waiver_at')); } public function test_create_event_without_package_fails(): void { $response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [ 'name' => 'Test Event', 'slug' => 'test-event', 'event_date' => Carbon::now()->addDays(10)->toDateString(), ]); $response->assertStatus(402) ->assertJsonPath('error.code', 'event_limit_missing'); } public function test_superadmin_can_create_event_without_tenant_package(): void { $tenant = $this->tenant; $eventType = EventType::factory()->create(); $package = Package::factory()->create([ 'type' => 'endcustomer', 'slug' => 'pro', 'max_photos' => 100, ]); $superadmin = \App\Models\User::factory()->create([ 'tenant_id' => $tenant->id, 'role' => 'superadmin', 'password' => Hash::make('password'), 'email_verified_at' => now(), ]); $login = $this->postJson('/api/v1/tenant-auth/login', [ 'login' => $superadmin->email, 'password' => 'password', ]); $login->assertOk(); $token = (string) $login->json('token'); $response = $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/tenant/events', [ 'name' => 'Owner Event', 'slug' => 'owner-event', 'event_date' => Carbon::now()->addDays(10)->toDateString(), 'event_type_id' => $eventType->id, 'package_id' => $package->id, ]); $response->assertStatus(201); $event = Event::latest()->first(); $this->assertDatabaseHas('events', [ 'tenant_id' => $tenant->id, 'slug' => 'owner-event', ]); $this->assertDatabaseHas('event_packages', [ 'event_id' => $event->id, 'package_id' => $package->id, ]); } public function test_create_event_requires_waiver_for_endcustomer_package(): void { $tenant = $this->tenant; $eventType = EventType::factory()->create(); $package = Package::factory()->create(['type' => 'endcustomer', 'max_photos' => 100]); TenantPackage::factory()->create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'active' => true, ]); $response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [ 'name' => 'Test Event', 'slug' => 'test-event', 'event_date' => Carbon::now()->addDays(10)->toDateString(), 'event_type_id' => $eventType->id, 'package_id' => $package->id, 'accepted_waiver' => false, ]); $response->assertStatus(422) ->assertJsonValidationErrors(['accepted_waiver']); } public function test_create_event_with_reseller_package_limits_events(): void { $tenant = $this->tenant; $eventType = EventType::factory()->create(); $includedPackage = Package::factory()->endcustomer()->create([ 'slug' => 'standard', 'gallery_days' => 30, ]); $package = Package::factory()->create(['type' => 'reseller', 'max_events_per_year' => 1]); TenantPackage::factory()->create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'used_events' => 0, 'active' => true, 'expires_at' => null, ]); // First event succeeds $response1 = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [ 'name' => 'First Event', 'slug' => 'first-event', 'event_date' => Carbon::now()->addDays(10)->toDateString(), 'event_type_id' => $eventType->id, 'package_id' => $package->id, // Use reseller package for event? Adjust if needed ]); $response1->assertStatus(201); $event = Event::where('tenant_id', $tenant->id)->where('slug', 'first-event')->firstOrFail(); $this->assertDatabaseHas('event_packages', [ 'event_id' => $event->id, 'package_id' => $includedPackage->id, 'purchased_price' => 0.00, ]); // Second event fails due to limit $response2 = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [ 'name' => 'Second Event', 'slug' => 'second-event', 'event_date' => Carbon::now()->addDays(11)->toDateString(), 'event_type_id' => $eventType->id, 'package_id' => $package->id, ]); $response2->assertStatus(402) ->assertJsonPath('error.code', 'event_limit_exceeded'); } public function test_update_event_settings_without_required_fields_succeeds(): void { $tenant = $this->tenant; $eventType = EventType::factory()->create(); $event = Event::factory()->create([ 'tenant_id' => $tenant->id, 'event_type_id' => $eventType->id, 'date' => Carbon::now()->subDays(2), 'name' => ['de' => 'Test Event', 'en' => 'Test Event'], 'settings' => [], ]); $response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [ 'settings' => [ 'engagement_mode' => 'photo_only', 'guest_downloads_enabled' => false, 'guest_sharing_enabled' => true, ], ]); $response->assertOk(); $event->refresh(); $this->assertSame('photo_only', data_get($event->settings, 'engagement_mode')); $this->assertFalse((bool) data_get($event->settings, 'guest_downloads_enabled')); $this->assertTrue((bool) data_get($event->settings, 'guest_sharing_enabled')); } public function test_create_event_rejects_unavailable_service_tier_for_partner_kontingent(): void { $tenant = $this->tenant; $eventType = EventType::factory()->create(); Package::factory()->endcustomer()->create(['slug' => 'standard', 'gallery_days' => 30]); Package::factory()->endcustomer()->create(['slug' => 'pro', 'gallery_days' => 30]); $partnerPackage = Package::factory()->reseller()->create([ 'max_events_per_year' => 5, 'included_package_slug' => 'standard', ]); TenantPackage::factory()->create([ 'tenant_id' => $tenant->id, 'package_id' => $partnerPackage->id, 'used_events' => 0, 'active' => true, 'expires_at' => null, ]); $response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', [ 'name' => 'Premium Event', 'slug' => 'premium-event', 'event_date' => Carbon::now()->addDays(10)->toDateString(), 'event_type_id' => $eventType->id, 'service_package_slug' => 'pro', ]); $response->assertStatus(402) ->assertJsonPath('error.code', 'event_tier_unavailable'); } public function test_update_event_accepts_live_show_settings(): void { $eventType = EventType::factory()->create(); $event = Event::factory()->for($this->tenant)->create([ 'event_type_id' => $eventType->id, 'name' => 'Live Show Event', 'slug' => 'live-show-settings', 'date' => now()->addDays(5), ]); $response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [ 'name' => 'Live Show Event', 'event_date' => now()->addDays(5)->toDateString(), 'event_type_id' => $eventType->id, 'settings' => [ 'live_show' => [ 'moderation_mode' => 'manual', 'retention_window_hours' => 12, 'playback_mode' => 'balanced', 'pace_mode' => 'fixed', 'fixed_interval_seconds' => 9, 'layout_mode' => 'single', 'effect_preset' => 'film_cut', 'effect_intensity' => 60, 'background_mode' => 'blur_last', ], ], ]); $response->assertOk(); $event->refresh(); $settings = $event->settings; $this->assertSame('manual', data_get($settings, 'live_show.moderation_mode')); $this->assertSame(12, data_get($settings, 'live_show.retention_window_hours')); $this->assertSame('balanced', data_get($settings, 'live_show.playback_mode')); $this->assertSame('fixed', data_get($settings, 'live_show.pace_mode')); $this->assertSame(9, data_get($settings, 'live_show.fixed_interval_seconds')); $this->assertSame('single', data_get($settings, 'live_show.layout_mode')); $this->assertSame('film_cut', data_get($settings, 'live_show.effect_preset')); $this->assertSame(60, data_get($settings, 'live_show.effect_intensity')); $this->assertSame('blur_last', data_get($settings, 'live_show.background_mode')); } public function test_update_event_accepts_svg_watermark_data_url(): void { Storage::fake('public'); $eventType = EventType::factory()->create(); $event = Event::factory()->for($this->tenant)->create([ 'event_type_id' => $eventType->id, 'name' => 'SVG Watermark Event', 'slug' => 'svg-watermark', 'date' => now()->addDays(2), ]); $svg = ''; $dataUrl = 'data:image/svg+xml;base64,'.base64_encode($svg); $response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [ 'settings' => [ 'watermark' => [ 'mode' => 'custom', 'asset_data_url' => $dataUrl, 'position' => 'bottom-right', ], ], ]); $response->assertOk(); $event->refresh(); $path = data_get($event->settings, 'watermark.asset'); $this->assertSame("branding/watermarks/event-{$event->id}.svg", $path); Storage::disk('public')->assertExists($path); } public function test_show_event_includes_signed_watermark_asset_url(): void { Storage::fake('public'); $eventType = EventType::factory()->create(); $event = Event::factory()->for($this->tenant)->create([ 'event_type_id' => $eventType->id, 'name' => 'Watermark Preview Event', 'slug' => 'watermark-preview', 'date' => now()->addDays(2), 'settings' => [ 'watermark' => [ 'mode' => 'custom', 'asset' => 'branding/watermarks/event-123.png', ], ], ]); Storage::disk('public')->put('branding/watermarks/event-123.png', 'asset'); $response = $this->authenticatedRequest('GET', "/api/v1/tenant/events/{$event->slug}"); $response->assertOk(); $url = (string) $response->json('data.settings.watermark.asset_url'); $this->assertNotSame('', $url); $this->assertStringContainsString('/api/v1/branding/asset/branding/watermarks/event-123.png', $url); $this->assertStringContainsString('signature=', $url); } public function test_show_event_includes_base_watermark_asset_url_when_missing_settings(): void { Storage::fake('public'); config(['watermark.base.asset' => 'branding/watermarks/base-watermark.png']); Storage::disk('public')->put('branding/watermarks/base-watermark.png', 'asset'); $eventType = EventType::factory()->create(); $event = Event::factory()->for($this->tenant)->create([ 'event_type_id' => $eventType->id, 'name' => 'Base Watermark Preview', 'slug' => 'base-watermark-preview', 'date' => now()->addDays(2), 'settings' => [], ]); $response = $this->authenticatedRequest('GET', "/api/v1/tenant/events/{$event->slug}"); $response->assertOk(); $this->assertSame('base', $response->json('data.settings.watermark.mode')); $url = (string) $response->json('data.settings.watermark.asset_url'); $this->assertNotSame('', $url); $this->assertStringContainsString('/api/v1/branding/asset/branding/watermarks/base-watermark.png', $url); $this->assertStringContainsString('signature=', $url); } public function test_update_event_uploads_branding_logo_data_url(): void { Storage::fake('public'); $eventType = EventType::factory()->create(); $event = Event::factory()->for($this->tenant)->create([ 'event_type_id' => $eventType->id, 'name' => 'Branding Event', 'slug' => 'branding-event', 'date' => now()->addDays(5), ]); $logoFile = UploadedFile::fake()->image('logo.png', 64, 64); $logoContents = file_get_contents($logoFile->getRealPath()); $this->assertIsString($logoContents); $logoDataUrl = 'data:image/png;base64,'.base64_encode($logoContents); $response = $this->authenticatedRequest('PUT', "/api/v1/tenant/events/{$event->slug}", [ 'name' => 'Branding Event', 'event_date' => now()->addDays(5)->toDateString(), 'event_type_id' => $eventType->id, 'settings' => [ 'branding' => [ 'logo_data_url' => $logoDataUrl, 'logo' => [ 'mode' => 'upload', 'value' => $logoDataUrl, ], ], ], ]); $response->assertOk(); $event->refresh(); $logoPath = (string) data_get($event->settings, 'branding.logo_url'); $this->assertNotEmpty($logoPath); Storage::disk('public')->assertExists($logoPath); $this->assertSame($logoPath, data_get($event->settings, 'branding.logo.value')); $this->assertNull(data_get($event->settings, 'branding.logo_data_url')); } public function test_upload_exceeds_package_limit_fails(): void { $tenant = $this->tenant; $event = Event::factory()->create(['tenant_id' => $tenant->id, 'status' => 'published']); $package = Package::factory()->create(['type' => 'endcustomer', 'max_photos' => 0]); // Limit 0 EventPackage::create([ 'event_id' => $event->id, 'package_id' => $package->id, 'purchased_price' => $package->price, 'used_photos' => 0, ]); Storage::fake('public'); $token = app(EventJoinTokenService::class)->createToken($event); $response = $this->withHeader('X-Device-Id', 'limit-test') ->post("/api/v1/events/{$token->token}/upload", [ 'photo' => UploadedFile::fake()->image('limit.jpg'), ]); $response->assertStatus(402) ->assertJsonPath('error.code', 'photo_limit_exceeded'); } }