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,121 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Audit;
|
||||
|
||||
use App\Mail\WorkOrderReadyMail;
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Central\Plan;
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\Vehicle;
|
||||
use App\Models\Tenant\WorkOrder;
|
||||
use App\Services\NotificationDispatcher;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The multi-channel fallback is the most subtle integration in the system:
|
||||
* Telegram wins if the client has a chat_id + tenant bot configured; otherwise
|
||||
* fall back to email. If neither, do nothing. Tenant flag must respect both.
|
||||
*/
|
||||
class NotificationFallbackTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_telegram_wins_when_chat_id_present(): void
|
||||
{
|
||||
$ctx = $this->bootTenantWithBot();
|
||||
$client = $this->makeClient(['telegram_chat_id' => '12345', 'email' => 'x@example.com']);
|
||||
$wo = $this->makeReadyWo($client, $ctx);
|
||||
|
||||
Mail::fake();
|
||||
Http::fake(['api.telegram.org/*' => Http::response(['ok' => true])]);
|
||||
|
||||
$ok = app(NotificationDispatcher::class)->workOrderReady($wo);
|
||||
|
||||
$this->assertTrue($ok);
|
||||
Http::assertSent(fn ($r) => str_contains($r->url(), 'sendMessage'));
|
||||
Mail::assertNotSent(WorkOrderReadyMail::class);
|
||||
}
|
||||
|
||||
public function test_falls_back_to_email_when_no_chat_id(): void
|
||||
{
|
||||
$ctx = $this->bootTenantWithBot();
|
||||
$client = $this->makeClient(['email' => 'x@example.com']);
|
||||
$wo = $this->makeReadyWo($client, $ctx);
|
||||
|
||||
Mail::fake();
|
||||
Http::fake();
|
||||
$ok = app(NotificationDispatcher::class)->workOrderReady($wo);
|
||||
|
||||
$this->assertTrue($ok);
|
||||
Mail::assertSent(WorkOrderReadyMail::class);
|
||||
Http::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_returns_false_when_no_channel_available(): void
|
||||
{
|
||||
$ctx = $this->bootTenantWithBot();
|
||||
$client = $this->makeClient([]); // no email, no chat_id
|
||||
$wo = $this->makeReadyWo($client, $ctx);
|
||||
|
||||
Mail::fake();
|
||||
$ok = app(NotificationDispatcher::class)->workOrderReady($wo);
|
||||
|
||||
$this->assertFalse($ok);
|
||||
Mail::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_tenant_disable_flag_overrides_channels(): void
|
||||
{
|
||||
$ctx = $this->bootTenantWithBot();
|
||||
// Disable the wo_ready notification globally for this tenant.
|
||||
$ctx['company']->update([
|
||||
'settings' => array_merge_recursive((array) $ctx['company']->settings, [
|
||||
'notify' => ['wo_ready' => false],
|
||||
]),
|
||||
]);
|
||||
|
||||
$client = $this->makeClient(['email' => 'x@example.com']);
|
||||
$wo = $this->makeReadyWo($client, $ctx);
|
||||
|
||||
Mail::fake();
|
||||
Http::fake();
|
||||
$ok = app(NotificationDispatcher::class)->workOrderReady($wo);
|
||||
|
||||
$this->assertFalse($ok);
|
||||
Mail::assertNothingSent();
|
||||
}
|
||||
|
||||
private function bootTenantWithBot(): array
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'nf-' . uniqid(),
|
||||
'name' => 'Notify', 'status' => 'active',
|
||||
'settings' => ['telegram' => ['bot_token' => 'FAKE:TOKEN']],
|
||||
]);
|
||||
app(TenantManager::class)->setCurrent($company);
|
||||
return compact('company');
|
||||
}
|
||||
|
||||
private function makeClient(array $attrs): Client
|
||||
{
|
||||
return Client::create(array_merge([
|
||||
'name' => 'C', 'phone' => '+3737' . random_int(1000000, 9999999),
|
||||
'type' => 'individual', 'status' => 'active',
|
||||
], $attrs));
|
||||
}
|
||||
|
||||
private function makeReadyWo(Client $client, array $ctx): WorkOrder
|
||||
{
|
||||
$vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'X', 'model' => 'Y', 'plate' => 'NF' . random_int(100, 999)]);
|
||||
return WorkOrder::create([
|
||||
'number' => WorkOrder::generateNumber($ctx['company']->id),
|
||||
'client_id' => $client->id, 'vehicle_id' => $vehicle->id,
|
||||
'opened_at' => today(), 'status' => 'ready', 'total' => 500,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user