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,100 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Audit;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Central\Plan;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Settings JSON has grown ~25 keys across 8 sections. Verify the round-trip
|
||||
* shape is correct: every setting our pages write must come back when read.
|
||||
*/
|
||||
class SettingsPersistenceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_settings_round_trip_full_shape(): void
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$payload = [
|
||||
'currency' => 'MDL',
|
||||
'language' => 'ro',
|
||||
'theme_color' => '#ff8800',
|
||||
'labor_rate' => 450.5,
|
||||
'services' => ['Diagnoză', 'Frâne'],
|
||||
'cars' => ['BMW', 'Audi'],
|
||||
'notify' => [
|
||||
'wo_ready' => true,
|
||||
'payment' => false,
|
||||
'appointment' => true,
|
||||
'reminder' => true,
|
||||
],
|
||||
'telegram' => ['bot_token' => 'secret-token'],
|
||||
'reminder' => ['after_days' => 400, 'cooldown_days' => 45],
|
||||
'shop' => [
|
||||
'enabled' => true,
|
||||
'delivery_methods' => ['pickup', 'courier'],
|
||||
'delivery_fee' => 50,
|
||||
'free_delivery_over' => 1000,
|
||||
],
|
||||
'ai' => [
|
||||
'default_provider' => 'claude',
|
||||
'claude_key' => 'sk-ant-xxx',
|
||||
'gpt_key' => null,
|
||||
'gemini_key' => null,
|
||||
'models' => [
|
||||
'claude' => 'claude-opus-4-7',
|
||||
'gpt' => 'gpt-4o',
|
||||
'gemini' => 'gemini-1.5-pro',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'st-' . uniqid(),
|
||||
'name' => 'Settings Co', 'status' => 'active',
|
||||
'settings' => $payload,
|
||||
]);
|
||||
app(TenantManager::class)->setCurrent($company);
|
||||
|
||||
$fresh = Company::withoutGlobalScopes()->find($company->id);
|
||||
$this->assertEquals($payload, $fresh->settings);
|
||||
|
||||
// Spot-check critical values via data_get (the way services read them).
|
||||
$this->assertEquals('MDL', data_get($fresh->settings, 'currency'));
|
||||
$this->assertTrue(data_get($fresh->settings, 'shop.enabled'));
|
||||
$this->assertEquals(450.5, data_get($fresh->settings, 'labor_rate'));
|
||||
$this->assertEquals('claude-opus-4-7', data_get($fresh->settings, 'ai.models.claude'));
|
||||
$this->assertEquals(45, data_get($fresh->settings, 'reminder.cooldown_days'));
|
||||
}
|
||||
|
||||
public function test_partial_update_preserves_other_keys(): void
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'pu-' . uniqid(),
|
||||
'name' => 'Partial', 'status' => 'active',
|
||||
'settings' => [
|
||||
'currency' => 'MDL',
|
||||
'shop' => ['enabled' => true, 'delivery_fee' => 50],
|
||||
'ai' => ['claude_key' => 'sk-xxx'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Update only the shop subsection (simulating an isolated UI save).
|
||||
$company->update([
|
||||
'settings' => array_replace_recursive((array) $company->settings, [
|
||||
'shop' => ['delivery_fee' => 100],
|
||||
]),
|
||||
]);
|
||||
|
||||
$fresh = Company::withoutGlobalScopes()->find($company->id);
|
||||
$this->assertEquals('MDL', data_get($fresh->settings, 'currency'), 'currency preserved');
|
||||
$this->assertTrue(data_get($fresh->settings, 'shop.enabled'), 'shop.enabled preserved');
|
||||
$this->assertEquals(100, data_get($fresh->settings, 'shop.delivery_fee'), 'shop.delivery_fee updated');
|
||||
$this->assertEquals('sk-xxx', data_get($fresh->settings, 'ai.claude_key'), 'ai key untouched');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user