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:
2026-06-03 07:05:46 +00:00
parent 439ef605a1
commit 0620635abb
12 changed files with 1353 additions and 2 deletions
@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Audit;
use App\Models\Central\Company;
use App\Models\Central\Plan;
use App\Services\CompanyProvisioner;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CompanyProvisionerTest extends TestCase
{
use RefreshDatabase;
public function test_suspend_changes_status_to_suspended(): void
{
$company = $this->makeCompany('active');
app(CompanyProvisioner::class)->suspend($company);
$this->assertEquals('suspended', $company->fresh()->status);
}
public function test_reactivate_restores_active_status(): void
{
$company = $this->makeCompany('suspended');
app(CompanyProvisioner::class)->reactivate($company);
$this->assertEquals('active', $company->fresh()->status);
}
public function test_archive_marks_status_and_soft_deletes(): void
{
$company = $this->makeCompany('active');
$id = $company->id;
app(CompanyProvisioner::class)->archive($company);
$row = Company::withTrashed()->find($id);
$this->assertEquals('archived', $row->status);
$this->assertNotNull($row->deleted_at, 'soft-deleted');
// Default (with SoftDeletes scope) no longer returns it.
$this->assertNull(Company::find($id));
}
private function makeCompany(string $status): Company
{
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
return Company::create([
'plan_id' => $plan->id, 'slug' => 'cp-' . uniqid(),
'name' => 'CP Co', 'status' => $status,
]);
}
}