create([ 'key' => 'local', 'name' => 'Local', 'driver' => 'local', 'config' => [], 'is_hot' => true, 'is_default' => true, 'is_active' => true, 'priority' => 1, ]); } public function test_guest_upload_blocked_when_photo_limit_reached(): void { Bus::fake(); $tenant = Tenant::factory()->create(); $event = Event::factory()->for($tenant)->create([ 'status' => 'published', ]); $package = Package::factory()->endcustomer()->create([ 'max_photos' => 1, 'max_guests' => null, ]); EventPackage::create([ 'event_id' => $event->id, 'package_id' => $package->id, 'purchased_price' => $package->price, 'purchased_at' => now(), 'used_photos' => 1, 'used_guests' => 0, 'gallery_expires_at' => now()->addDays(7), ]); $emotion = Emotion::factory()->create(); $emotion->eventTypes()->attach($event->event_type_id); /** @var EventJoinTokenService $tokenService */ $tokenService = $this->app->make(EventJoinTokenService::class); $joinToken = $tokenService->createToken($event, ['label' => 'Test']); $token = $joinToken->plain_token; $response = $this->post("/api/v1/events/{$token}/upload", [ 'photo' => UploadedFile::fake()->image('limit.jpg', 800, 600), ], [ 'X-Device-Id' => 'device-123', ]); $response->assertStatus(402); $response->assertJsonPath('error.code', 'photo_limit_exceeded'); Bus::assertNothingDispatched(); } public function test_guest_upload_increments_usage_and_succeeds(): void { Bus::fake(); $tenant = Tenant::factory()->create(); $event = Event::factory()->for($tenant)->create([ 'status' => 'published', ]); $package = Package::factory()->endcustomer()->create([ 'max_photos' => 2, 'max_guests' => null, ]); $eventPackage = EventPackage::create([ 'event_id' => $event->id, 'package_id' => $package->id, 'purchased_price' => $package->price, 'purchased_at' => now(), 'used_photos' => 1, 'used_guests' => 0, 'gallery_expires_at' => now()->addDays(7), ]); $emotion = Emotion::factory()->create(); $emotion->eventTypes()->attach($event->event_type_id); /** @var EventJoinTokenService $tokenService */ $tokenService = $this->app->make(EventJoinTokenService::class); $token = $tokenService->createToken($event, ['label' => 'Test'])->plain_token; $response = $this->post("/api/v1/events/{$token}/upload", [ 'photo' => UploadedFile::fake()->image('success.jpg', 1024, 768), ], [ 'X-Device-Id' => 'device-456', ]); $response->assertCreated(); $this->assertEquals( 2, $eventPackage->refresh()->used_photos ); $thresholdJobs = Bus::dispatched(SendEventPackagePhotoThresholdWarning::class); $this->assertGreaterThanOrEqual(2, $thresholdJobs->count()); Bus::assertDispatched(SendEventPackagePhotoLimitNotification::class); } public function test_guest_package_endpoint_returns_limits_summary(): void { $tenant = Tenant::factory()->create(); $event = Event::factory()->for($tenant)->create([ 'status' => 'published', ]); $package = Package::factory()->endcustomer()->create([ 'max_photos' => 10, 'max_guests' => 20, 'gallery_days' => 7, ]); $eventPackage = EventPackage::create([ 'event_id' => $event->id, 'package_id' => $package->id, 'purchased_price' => $package->price, 'purchased_at' => now()->subDay(), 'used_photos' => 8, 'used_guests' => 5, 'gallery_expires_at' => now()->addDays(3), ]); $token = app(EventJoinTokenService::class)->createToken($event)->plain_token; $response = $this->getJson("/api/v1/events/{$token}/package"); $response->assertOk(); $response->assertJsonPath('id', $eventPackage->id); $response->assertJsonPath('limits.photos.limit', 10); $response->assertJsonPath('limits.photos.used', 8); $response->assertJsonPath('limits.photos.state', 'warning'); $response->assertJsonPath('limits.gallery.state', 'warning'); $response->assertJsonPath('limits.can_upload_photos', true); } }