'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $this->company = Company::create(['plan_id' => $plan->id, 'slug' => 't3-' . uniqid(), 'name' => 'T3', 'status' => 'active']); app(TenantManager::class)->setCurrent($this->company); } private function makeClient(string $prefix = ''): Client { return Client::create(['name' => $prefix . 'C', 'phone' => '+3739900' . random_int(1000, 9999), 'type' => 'individual', 'status' => 'active']); } // ── M12 ── public function test_pricing_engine_matches_body_type_condition(): void { PricingCoefficient::create([ 'name' => 'Pickup +20%', 'multiplier' => 1.20, 'conditions' => ['body_types' => ['pickup']], 'stackable' => true, 'priority' => 100, 'is_active' => true, ]); $part = Part::create(['name' => 'P', 'article' => 'X', 'buy_price' => 100, 'sell_price' => 100]); $pickup = Vehicle::create(['client_id' => $this->makeClient('p')->id, 'make' => 'Ford', 'model' => 'Ranger', 'plate' => 'P1', 'body_type' => 'pickup']); $sedan = Vehicle::create(['client_id' => $this->makeClient('s')->id, 'make' => 'BMW', 'model' => 'X5', 'plate' => 'S1', 'body_type' => 'sedan']); $q1 = app(PricingEngine::class)->quote($part, $pickup); $q2 = app(PricingEngine::class)->quote($part, $sedan); $this->assertCount(1, $q1['applied']); $this->assertEmpty($q2['applied']); } public function test_pricing_engine_matches_transmission_dsg(): void { PricingCoefficient::create([ 'name' => 'DSG +15%', 'multiplier' => 1.15, 'conditions' => ['transmissions' => ['dsg']], 'stackable' => true, 'priority' => 100, 'is_active' => true, ]); $part = Part::create(['name' => 'P', 'article' => 'X', 'buy_price' => 100, 'sell_price' => 100]); $dsg = Vehicle::create(['client_id' => $this->makeClient('d')->id, 'make' => 'VW', 'model' => 'Golf', 'plate' => 'D1', 'transmission_type' => 'dsg']); $q = app(PricingEngine::class)->quote($part, $dsg); $this->assertCount(1, $q['applied']); $this->assertEquals('DSG +15%', $q['applied'][0]['name']); } public function test_pricing_log_persists_breakdown(): void { PricingCoefficient::create([ 'name' => 'SUV +15%', 'multiplier' => 1.15, 'conditions' => ['classes' => ['suv']], 'stackable' => true, 'priority' => 100, 'is_active' => true, ]); $part = Part::create(['name' => 'P', 'article' => 'X', 'buy_price' => 100, 'sell_price' => 150]); $client = $this->makeClient('pl'); $vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'BMW', 'model' => 'X5', 'plate' => 'X1', 'vehicle_class' => 'suv', 'year' => 2020]); $wo = WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 0]); $line = \App\Models\Tenant\WorkOrderPart::create(['work_order_id' => $wo->id, 'name' => 'P', 'qty' => 1, 'sell_price' => 150]); $quote = app(PricingEngine::class)->quote($part, $vehicle, $client); $log = app(PricingEngine::class)->logApplication($quote, $line, $vehicle, $client, $part); $this->assertEqualsWithDelta(150.0, (float) $log->base_price, 0.01); $this->assertEqualsWithDelta(172.5, (float) $log->final_price, 0.01); $this->assertCount(1, $log->applied_coefficients); $this->assertEquals('suv', $log->context['class']); } // ── M13 ── public function test_mechanic_api_board_returns_only_own_wos(): void { $mech = User::create(['name' => 'M', 'email' => 'm@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); $other = User::create(['name' => 'O', 'email' => 'o@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'master_id' => $mech->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 100]); WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'master_id' => $other->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 200]); Sanctum::actingAs($mech); $resp = $this->getJson('/api/v1/mechanic/board'); $resp->assertOk(); $this->assertCount(1, $resp->json('data')); } public function test_mechanic_api_start_task_only_own(): void { $mech = User::create(['name' => 'M', 'email' => 'm@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); $other = User::create(['name' => 'O', 'email' => 'o@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); $foreignWo = WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'master_id' => $other->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 0]); $foreignWork = WorkOrderWork::create(['work_order_id' => $foreignWo->id, 'name' => "Other's", 'hours' => 1, 'price_per_hour' => 100]); Sanctum::actingAs($mech); $resp = $this->postJson("/api/v1/mechanic/tasks/{$foreignWork->id}/start"); $resp->assertForbidden(); } public function test_mechanic_kpi_endpoint_aggregates_period(): void { $mech = User::create(['name' => 'M', 'email' => 'm@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); $wo = WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'master_id' => $mech->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 0]); // Two done works in 2026-06 WorkOrderWork::create(['work_order_id' => $wo->id, 'name' => 'A', 'hours' => 2, 'price_per_hour' => 300, 'mechanic_status' => 'done', 'actual_hours' => 1.5, 'mechanic_done_at' => '2026-06-10 10:00:00']); WorkOrderWork::create(['work_order_id' => $wo->id, 'name' => 'B', 'hours' => 1, 'price_per_hour' => 300, 'mechanic_status' => 'done', 'actual_hours' => 1.0, 'mechanic_done_at' => '2026-06-15 10:00:00']); Sanctum::actingAs($mech); $resp = $this->getJson('/api/v1/mechanic/kpi?period=2026-06'); $resp->assertOk(); $this->assertEquals(2, $resp->json('tasks_done')); $this->assertEqualsWithDelta(3.0, $resp->json('norm_hours'), 0.01); $this->assertEqualsWithDelta(2.5, $resp->json('actual_hours'), 0.01); $this->assertEquals(83, $resp->json('efficiency_pct')); // 2.5/3 = 83% } // ── M14 ── public function test_ocr_job_can_be_queued_and_processed(): void { \Queue::fake(); $jobModel = \App\Models\Tenant\OcrJob::create([ 'company_id' => $this->company->id, 'source_type' => 'pdf', 'file_path' => 'imports/test.pdf', 'status' => 'pending', ]); \App\Jobs\ProcessOcrJob::dispatch($jobModel->id, $this->company->id); \Queue::assertPushed(\App\Jobs\ProcessOcrJob::class, fn ($j) => $j->ocrJobId === $jobModel->id); } // ── M15 ── public function test_tracking_json_endpoint_returns_status_payload(): void { $client = Client::create(['name' => 'C', 'phone' => '+37399000000', 'type' => 'individual', 'status' => 'active']); $vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'BMW', 'model' => 'X5', 'plate' => 'JS-1']); $wo = WorkOrder::create([ 'number' => WorkOrder::generateNumber($this->company->id), 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 500, 'eta_promised' => now()->addHours(3), 'eta_at' => now()->addHours(4), 'eta_change_reason' => 'Aștept piesă', ]); $resp = $this->getJson("/api/track/{$wo->tracking_token}"); $resp->assertOk(); $this->assertEquals($wo->number, $resp->json('number')); $this->assertEquals('in_work', $resp->json('status')); $this->assertEquals('Aștept piesă', $resp->json('eta_change_reason')); $this->assertNotNull($resp->json('eta_promised')); $this->assertNotNull($resp->json('eta_current')); } public function test_tracking_json_returns_pending_approvals_with_signed_urls(): void { $client = Client::create(['name' => 'C', 'phone' => '+37399000000', 'type' => 'individual', 'status' => 'active']); $wo = WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'client_id' => $client->id, 'opened_at' => today(), 'status' => 'in_work', 'total' => 0]); WorkOrderWork::create(['work_order_id' => $wo->id, 'name' => 'Needs OK', 'hours' => 1, 'price_per_hour' => 200, 'requires_approval' => true]); $resp = $this->getJson("/api/track/{$wo->tracking_token}"); $resp->assertOk(); $this->assertCount(1, $resp->json('pending_approvals')); $this->assertEquals('work', $resp->json('pending_approvals.0.kind')); $this->assertStringContainsString('/approve/work/', $resp->json('pending_approvals.0.approve_url')); } public function test_dispatcher_writes_notification_log_entry(): void { $client = Client::create(['name' => 'C', 'phone' => '+37399000000', 'email' => 'c@e.com', 'type' => 'individual', 'status' => 'active']); $wo = WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'client_id' => $client->id, 'opened_at' => today(), 'closed_at' => today(), 'status' => 'ready', 'total' => 500]); \Mail::fake(); app(\App\Services\NotificationDispatcher::class)->workOrderReady($wo); $log = ClientNotificationLog::where('work_order_id', $wo->id)->first(); $this->assertNotNull($log); $this->assertEquals('wo_ready', $log->template_key); $this->assertContains($log->channel, ['email', 'telegram']); $this->assertEquals($wo->id, $log->work_order_id); } public function test_vehicle_body_and_transmission_round_trip(): void { $v = Vehicle::create(['client_id' => $this->makeClient()->id, 'make' => 'VW', 'model' => 'Tiguan', 'plate' => 'VR-1', 'body_type' => 'crossover', 'transmission_type' => 'dsg']); $fresh = Vehicle::find($v->id); $this->assertEquals('crossover', $fresh->body_type); $this->assertEquals('dsg', $fresh->transmission_type); } }