create(['status' => 'published']); $owner = User::factory()->create(); $event->tenant->update(['user_id' => $owner->id]); $photo = Photo::factory()->for($event)->create([ 'tenant_id' => $event->tenant_id, 'status' => 'approved', ]); $request = AiEditRequest::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'status' => AiEditRequest::STATUS_SUCCEEDED, 'safety_state' => 'passed', 'requested_by_device_id' => 'device-ai-1', 'idempotency_key' => 'notify-guest-success-1', 'queued_at' => now()->subMinute(), 'completed_at' => now(), ]); app(AiStatusNotificationService::class)->notifyTerminalOutcome($request); $this->assertDatabaseHas('guest_notifications', [ 'event_id' => $event->id, 'type' => 'upload_alert', 'audience_scope' => 'guest', 'target_identifier' => 'device-ai-1', ]); $this->assertDatabaseHas('tenant_notification_logs', [ 'tenant_id' => $event->tenant_id, 'type' => 'ai_edit_succeeded', 'channel' => 'system', 'status' => 'sent', ]); $this->assertDatabaseHas('tenant_notification_receipts', [ 'tenant_id' => $event->tenant_id, 'user_id' => $owner->id, 'status' => 'delivered', ]); } public function test_it_creates_only_tenant_notification_for_tenant_admin_requests(): void { $event = Event::factory()->create(['status' => 'published']); $owner = User::factory()->create(); $event->tenant->update(['user_id' => $owner->id]); $photo = Photo::factory()->for($event)->create([ 'tenant_id' => $event->tenant_id, 'status' => 'approved', ]); $request = AiEditRequest::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'requested_by_user_id' => $owner->id, 'status' => AiEditRequest::STATUS_FAILED, 'safety_state' => 'pending', 'failure_code' => 'provider_timeout', 'idempotency_key' => 'notify-tenant-failed-1', 'queued_at' => now()->subMinute(), 'completed_at' => now(), ]); app(AiStatusNotificationService::class)->notifyTerminalOutcome($request); $this->assertDatabaseCount('guest_notifications', 0); $this->assertDatabaseHas('tenant_notification_logs', [ 'tenant_id' => $event->tenant_id, 'type' => 'ai_edit_failed', 'channel' => 'system', 'status' => 'sent', ]); } public function test_it_deduplicates_terminal_notifications_per_request_and_status(): void { $event = Event::factory()->create(['status' => 'published']); $owner = User::factory()->create(); $event->tenant->update(['user_id' => $owner->id]); $photo = Photo::factory()->for($event)->create([ 'tenant_id' => $event->tenant_id, 'status' => 'approved', ]); $request = AiEditRequest::query()->create([ 'tenant_id' => $event->tenant_id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'status' => AiEditRequest::STATUS_BLOCKED, 'safety_state' => 'blocked', 'requested_by_device_id' => 'device-ai-dup', 'idempotency_key' => 'notify-dedupe-1', 'queued_at' => now()->subMinute(), 'completed_at' => now(), ]); $service = app(AiStatusNotificationService::class); $service->notifyTerminalOutcome($request); $service->notifyTerminalOutcome($request->fresh()); $this->assertDatabaseCount('guest_notifications', 1); $this->assertDatabaseCount('tenant_notification_logs', 1); } }