assertTrue(Schema::hasTable('ai_styles')); $this->assertTrue(Schema::hasTable('ai_edit_requests')); $this->assertTrue(Schema::hasTable('ai_edit_outputs')); $this->assertTrue(Schema::hasTable('ai_provider_runs')); $this->assertTrue(Schema::hasTable('ai_usage_ledgers')); foreach ([ 'key', 'provider', 'provider_model', 'requires_source_image', 'is_premium', ] as $column) { $this->assertTrue(Schema::hasColumn('ai_styles', $column)); } foreach ([ 'tenant_id', 'event_id', 'photo_id', 'style_id', 'provider', 'idempotency_key', 'safety_state', 'status', ] as $column) { $this->assertTrue(Schema::hasColumn('ai_edit_requests', $column)); } foreach ([ 'request_id', 'provider', 'provider_task_id', 'request_payload', 'response_payload', 'cost_usd', ] as $column) { $this->assertTrue(Schema::hasColumn('ai_provider_runs', $column)); } } public function test_ai_edit_flow_records_request_run_output_and_usage_with_relations(): void { $photo = Photo::factory()->create(); $event = $photo->event; $tenant = $event->tenant; $user = User::factory()->create(['tenant_id' => $tenant->id]); $style = AiStyle::query()->create([ 'key' => 'bg-colosseum', 'name' => 'Colosseum Background', 'provider' => 'runware', 'provider_model' => 'runware-default', 'requires_source_image' => true, 'is_active' => true, ]); $request = AiEditRequest::query()->create([ 'tenant_id' => $tenant->id, 'event_id' => $event->id, 'photo_id' => $photo->id, 'style_id' => $style->id, 'requested_by_user_id' => $user->id, 'provider' => 'runware', 'provider_model' => 'runware-default', 'status' => AiEditRequest::STATUS_QUEUED, 'safety_state' => 'pending', 'prompt' => 'Replace background with Colosseum in Rome.', 'idempotency_key' => 'req-'.$photo->id.'-1', 'queued_at' => now(), 'metadata' => ['source' => 'guest_pwa'], ]); $run = AiProviderRun::query()->create([ 'request_id' => $request->id, 'provider' => 'runware', 'attempt' => 1, 'provider_task_id' => 'task-123', 'status' => AiProviderRun::STATUS_RUNNING, 'request_payload' => ['positivePrompt' => 'Rome Colosseum'], 'response_payload' => ['status' => 'processing'], 'started_at' => now(), ]); $output = AiEditOutput::query()->create([ 'request_id' => $request->id, 'storage_disk' => 'public', 'storage_path' => 'ai/outputs/final.jpg', 'mime_type' => 'image/jpeg', 'width' => 1024, 'height' => 1024, 'is_primary' => true, 'generated_at' => now(), 'metadata' => ['variant' => 'v1'], ]); $ledger = AiUsageLedger::query()->create([ 'tenant_id' => $tenant->id, 'event_id' => $event->id, 'request_id' => $request->id, 'entry_type' => AiUsageLedger::TYPE_DEBIT, 'quantity' => 1, 'unit_cost_usd' => 0.015, 'amount_usd' => 0.015, 'recorded_at' => now(), 'metadata' => ['provider' => 'runware'], ]); $request->refresh(); $run->refresh(); $output->refresh(); $ledger->refresh(); $this->assertSame($style->id, $request->style?->id); $this->assertSame($tenant->id, $request->tenant?->id); $this->assertSame($event->id, $request->event?->id); $this->assertSame($photo->id, $request->photo?->id); $this->assertSame(1, $request->providerRuns()->count()); $this->assertSame(1, $request->outputs()->count()); $this->assertSame(1, $request->usageLedgers()->count()); $this->assertIsArray($run->request_payload); $this->assertIsArray($run->response_payload); $this->assertIsArray($ledger->metadata); $this->assertGreaterThan(0, (float) $ledger->amount_usd); } }