test: full E2E audit + fix CsvImportExport vehicle.brand → make
Audit pass (33 new tests in tests/Feature/Audit/): - CrmFunnelE2ETest: full Lead→Deal→Appointment→WO→Payment journey, covering 5 previously-untested models. Verifies WO.balanceDue updates correctly after payments, including refunds (negative amount → balance increases). - WorkOrderTotalsTest: works+parts+subcontract+discount sum correctly, cancelled subcontract excluded, deleting lines triggers recalc, status=done consumes part reservations into issues, cancelled releases reservations. - ShopJourneyE2ETest: register→cart→checkout→email confirmation→tracking page reachable→admin fulfills→stock drops→warehouse event recorded. Also guest checkout still works without account. - CsvImportExportTest: round-trip, dedup-by-phone, **caught real bug** — Vehicle export wrote $row->brand (no such property) and import set 'brand' => row['brand'] in Vehicle::create (column is `make`). Fix applied to both paths. - TenantBackupServiceTest: zip contains valid manifest with counts + data/*.json per model + works embedded with WorkOrder. - WorkOrderPdfServiceTest: generated PDF starts with %PDF, includes WO data, non-trivial size, handles empty WO. - PayrollCalculatorTest: base + works_pct + parts_pct + bonus - fine - advance, scoped to user + period. - NotificationFallbackTest: Telegram wins when chat_id present, falls back to email when not, returns false when neither, tenant disable flag stops both. - AiProvidersCrossCheckTest: OpenAI request shape, Gemini URL with model, no-key friendly message, tenant model override propagates into HTTP body. - SettingsPersistenceTest: 25-key settings JSON round-trips, partial update via array_replace_recursive preserves other keys. - CompanyProvisionerTest: suspend / reactivate / archive behavior. Bug fixed: CsvImportExport used `brand` on Vehicle which has column `make`. The export silently emitted empty values, the import silently dropped the brand. Now both paths use `make`. Full suite: 173 passed (468 assertions). 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Audit;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Central\Plan;
|
||||
use App\Services\Ai\AiAssistantService;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The 3 AI providers share infrastructure but each has a different request
|
||||
* shape. After the model selector refactor, verify each provider still uses
|
||||
* the configured model + key in the actual HTTP call.
|
||||
*/
|
||||
class AiProvidersCrossCheckTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_openai_singleshot_uses_configured_model_and_sends_messages(): void
|
||||
{
|
||||
$this->bootCompany('gpt', 'gpt-fake');
|
||||
|
||||
Http::fake(['api.openai.com/*' => Http::response([
|
||||
'choices' => [['message' => ['content' => 'OpenAI response']]],
|
||||
'usage' => ['prompt_tokens' => 50, 'completion_tokens' => 12],
|
||||
'model' => 'gpt-4o-mini',
|
||||
])]);
|
||||
|
||||
[$reply, $meta] = app(AiAssistantService::class)->singleShot('system here', 'user prompt', 'gpt');
|
||||
|
||||
$this->assertEquals('OpenAI response', $reply);
|
||||
$this->assertEquals('gpt', $meta['provider']);
|
||||
Http::assertSent(function ($req) {
|
||||
if (! str_contains($req->url(), 'openai.com')) return false;
|
||||
$body = json_decode($req->body(), true);
|
||||
return $body['model'] === 'gpt-4o-mini'
|
||||
&& $body['messages'][0]['role'] === 'system'
|
||||
&& str_contains($body['messages'][0]['content'], 'system here');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_gemini_singleshot_hits_url_with_configured_model(): void
|
||||
{
|
||||
$this->bootCompany('gemini', 'gem-fake');
|
||||
|
||||
Http::fake(['generativelanguage.googleapis.com/*' => Http::response([
|
||||
'candidates' => [['content' => ['parts' => [['text' => 'Gemini response']]]]],
|
||||
'usageMetadata' => ['promptTokenCount' => 30],
|
||||
])]);
|
||||
|
||||
[$reply, $meta] = app(AiAssistantService::class)->singleShot('s', 'u', 'gemini');
|
||||
|
||||
$this->assertEquals('Gemini response', $reply);
|
||||
Http::assertSent(fn ($req) => str_contains($req->url(), 'gemini-1.5-flash:generateContent'));
|
||||
}
|
||||
|
||||
public function test_no_api_key_returns_friendly_message_without_http(): void
|
||||
{
|
||||
$this->bootCompany('claude', null);
|
||||
|
||||
Http::fake();
|
||||
[$reply, $meta] = app(AiAssistantService::class)->singleShot('s', 'u', 'claude');
|
||||
|
||||
$this->assertStringContainsString('API key', $reply);
|
||||
$this->assertEquals('no_api_key', $meta['error']);
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_tenant_model_override_propagates_into_request(): void
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'override-' . uniqid(),
|
||||
'name' => 'O', 'status' => 'active',
|
||||
'settings' => [
|
||||
'ai' => [
|
||||
'claude_key' => 'sk-fake',
|
||||
'models' => ['claude' => 'claude-opus-4-7'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
app(TenantManager::class)->setCurrent($company);
|
||||
|
||||
Http::fake(['api.anthropic.com/*' => Http::response([
|
||||
'content' => [['type' => 'text', 'text' => 'ok']],
|
||||
'usage' => ['input_tokens' => 1, 'output_tokens' => 1],
|
||||
])]);
|
||||
|
||||
app(AiAssistantService::class)->singleShot('s', 'u', 'claude');
|
||||
|
||||
Http::assertSent(function ($req) {
|
||||
$body = json_decode($req->body(), true);
|
||||
return $body['model'] === 'claude-opus-4-7';
|
||||
});
|
||||
}
|
||||
|
||||
private function bootCompany(string $provider, ?string $key): Company
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'ai-' . uniqid(),
|
||||
'name' => 'AI Co', 'status' => 'active',
|
||||
'settings' => ['ai' => [
|
||||
'default_provider' => $provider,
|
||||
"{$provider}_key" => $key,
|
||||
]],
|
||||
]);
|
||||
app(TenantManager::class)->setCurrent($company);
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user