makeCtx(provider: 'claude', key: 'sk-fake'); Http::fake([ 'api.anthropic.com/*' => Http::response([ 'content' => [['type' => 'text', 'text' => 'Verifică pompa de apă și termostatul.']], 'usage' => ['input_tokens' => 100, 'output_tokens' => 30], 'model' => 'claude-sonnet-4-5', ]), ]); $wo = WorkOrder::create([ 'number' => WorkOrder::generateNumber($ctx['company']->id), 'client_id' => $ctx['client']->id, 'vehicle_id' => $ctx['vehicle']->id, 'opened_at' => now(), 'status' => 'diagnosis', 'complaint' => 'Motorul se supraîncălzește în trafic.', ]); [$reply, $meta] = app(AiAssistantService::class)->suggestDiagnosis($wo); $this->assertStringContainsString('termostat', $reply); $this->assertEquals('claude', $meta['provider']); $this->assertEquals(100, $meta['tokens_in']); Http::assertSent(function ($req) { $body = json_decode($req->body(), true); return str_contains($req->url(), 'anthropic.com') && str_contains($body['messages'][0]['content'], 'supraîncălzește'); }); } public function test_suggest_price_includes_buy_and_sell(): void { $ctx = $this->makeCtx(provider: 'gpt', key: 'sk-openai'); Http::fake([ 'api.openai.com/*' => Http::response([ 'choices' => [['message' => ['content' => 'Markup 40% → preț 70 MDL.']]], 'usage' => ['prompt_tokens' => 80, 'completion_tokens' => 15], 'model' => 'gpt-4o-mini', ]), ]); $part = Part::create([ 'name' => 'Filtru ulei MANN W811/80', 'brand' => 'MANN', 'category' => 'Filtre', 'buy_price' => 50, 'sell_price' => 65, 'qty' => 5, 'unit' => 'buc', 'is_active' => true, ]); [$reply, $meta] = app(AiAssistantService::class)->suggestPrice($part); $this->assertStringContainsString('Markup', $reply); Http::assertSent(function ($req) { $body = json_decode($req->body(), true); $userMsg = collect($body['messages'])->firstWhere('role', 'user'); return str_contains($userMsg['content'], '50.00') && str_contains($userMsg['content'], 'MANN'); }); } public function test_vin_recommendations_includes_decoded_data(): void { $ctx = $this->makeCtx(provider: 'claude', key: 'sk-fake'); Http::fake([ 'api.anthropic.com/*' => Http::response([ 'content' => [['type' => 'text', 'text' => 'Recomandări mentenanță pentru Honda 2003']], 'usage' => ['input_tokens' => 50, 'output_tokens' => 20], ]), ]); [$reply, $meta] = app(AiAssistantService::class) ->vinRecommendations('1HGCM82633A123456', 150000); $this->assertStringContainsString('Honda', $reply); $this->assertEquals('Honda', $meta['vin_decoded']['manufacturer']); $this->assertEquals(2003, $meta['vin_decoded']['year']); } public function test_no_api_key_returns_friendly_message(): void { $this->makeCtx(provider: 'claude', key: null); $part = Part::create([ 'name' => 'X', 'unit' => 'buc', 'buy_price' => 1, 'sell_price' => 2, 'qty' => 0, 'is_active' => true, ]); [$reply, $meta] = app(AiAssistantService::class)->suggestPrice($part); $this->assertStringContainsString('API key', $reply); $this->assertEquals('no_api_key', $meta['error']); Http::assertNothingSent(); } public function test_monthly_usage_aggregates_by_provider(): void { $ctx = $this->makeCtx(provider: 'claude', key: 'sk-fake'); $user = \App\Models\Tenant\User::create([ 'company_id' => $ctx['company']->id, 'name' => 'AI User', 'email' => 'ai-' . uniqid() . '@example.com', 'password' => bcrypt('x'), 'role' => 'admin', 'status' => 'active', ]); $chat = AiChat::create([ 'company_id' => $ctx['company']->id, 'user_id' => $user->id, 'provider' => 'claude', ]); AiMessage::create([ 'company_id' => $ctx['company']->id, 'ai_chat_id' => $chat->id, 'role' => 'assistant', 'content' => 'X', 'meta' => ['provider' => 'claude', 'tokens_in' => 100, 'tokens_out' => 50], ]); AiMessage::create([ 'company_id' => $ctx['company']->id, 'ai_chat_id' => $chat->id, 'role' => 'assistant', 'content' => 'Y', 'meta' => ['provider' => 'gpt', 'tokens_in' => 200, 'tokens_out' => 80], ]); $usage = app(AiAssistantService::class)->monthlyUsage(); $this->assertEquals(100, $usage['claude']['tokens_in']); $this->assertEquals(50, $usage['claude']['tokens_out']); $this->assertEquals(1, $usage['claude']['calls']); $this->assertEquals(280, $usage['gpt']['tokens_in'] + $usage['gpt']['tokens_out']); } private function makeCtx(string $provider, ?string $key): array { $plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'Test', 'price' => 0, 'features' => []]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'ai-' . uniqid(), 'name' => 'AI Service', 'city' => 'Chișinău', 'status' => 'active', 'settings' => [ 'ai' => [ 'default_provider' => $provider, "{$provider}_key" => $key, ], ], ]); app(TenantManager::class)->setCurrent($company); $client = Client::create([ 'name' => 'C', 'phone' => '+37399' . random_int(100000, 999999), 'type' => 'individual', 'status' => 'active', ]); $vehicle = Vehicle::create([ 'client_id' => $client->id, 'make' => 'VW', 'model' => 'Golf', 'year' => 2015, 'plate' => 'AI' . random_int(100, 999), 'mileage' => 120000, ]); return compact('company', 'client', 'vehicle'); } }