e8078f157a
Schema: - subcontractors (specialty, rating, contact) - subcontract_jobs (work_order link, cost, markup_pct, client_price, status workflow, sent_at/eta/returned_at, paid_to_sub) Models: - SubcontractJob: auto number (SC-YY-NNNN), client_price = cost×(1+markup/100) when markup>0 (else manual), margin() helper, recalcs parent WO on save/delete - WorkOrder.recalcTotal now includes non-cancelled subcontract job client_price Filament (new "Subcontractare" nav group): - SubcontractorResource (specialty/rating CRUD) - SubcontractJobResource board with cost/client/margin columns + status filters, nav badge = open jobs - SubcontractJobsRelationManager on WorkOrder Tests (7 new): - client_price from markup; manual price without markup; auto number; WO total includes jobs; cancelled excluded; delete recalcs; tenant isolation Closes roadmap to 16/18 stages (only Stage 10 Bodyshop remains). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.1 KiB
PHP
120 lines
4.1 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\Central\Company;
|
||
use App\Models\Central\Plan;
|
||
use App\Models\Tenant\Client;
|
||
use App\Models\Tenant\Subcontractor;
|
||
use App\Models\Tenant\SubcontractJob;
|
||
use App\Models\Tenant\Vehicle;
|
||
use App\Models\Tenant\WorkOrder;
|
||
use App\Tenancy\TenantManager;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Tests\TestCase;
|
||
|
||
class SubcontractorTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
private Company $company;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
$this->company = $this->makeCompany('subs');
|
||
}
|
||
|
||
public function test_client_price_computed_from_cost_and_markup(): void
|
||
{
|
||
$job = SubcontractJob::create([
|
||
'category' => 'Turbo', 'cost' => 1000, 'markup_pct' => 25, 'status' => 'sent',
|
||
]);
|
||
$this->assertEquals(1250.0, (float) $job->client_price);
|
||
$this->assertEquals(250.0, $job->margin());
|
||
}
|
||
|
||
public function test_manual_client_price_when_no_markup(): void
|
||
{
|
||
$job = SubcontractJob::create([
|
||
'category' => 'Vopsitorie', 'cost' => 800, 'markup_pct' => 0, 'client_price' => 1100, 'status' => 'sent',
|
||
]);
|
||
$this->assertEquals(1100.0, (float) $job->client_price);
|
||
$this->assertEquals(300.0, $job->margin());
|
||
}
|
||
|
||
public function test_number_auto_generated(): void
|
||
{
|
||
$job = SubcontractJob::create(['cost' => 100, 'markup_pct' => 10, 'status' => 'sent']);
|
||
$this->assertStringStartsWith('SC-', $job->number);
|
||
}
|
||
|
||
public function test_wo_total_includes_subcontract_jobs(): void
|
||
{
|
||
$wo = $this->makeWorkOrder();
|
||
SubcontractJob::create([
|
||
'work_order_id' => $wo->id, 'category' => 'Cutie viteze',
|
||
'cost' => 2000, 'markup_pct' => 20, 'status' => 'sent',
|
||
]);
|
||
|
||
$wo->refresh();
|
||
// client_price = 2000 × 1.20 = 2400 → WO total = 2400
|
||
$this->assertEquals(2400.0, (float) $wo->total);
|
||
}
|
||
|
||
public function test_cancelled_job_excluded_from_total(): void
|
||
{
|
||
$wo = $this->makeWorkOrder();
|
||
$job = SubcontractJob::create([
|
||
'work_order_id' => $wo->id, 'cost' => 1000, 'markup_pct' => 50, 'status' => 'sent',
|
||
]);
|
||
$wo->refresh();
|
||
$this->assertEquals(1500.0, (float) $wo->total);
|
||
|
||
$job->update(['status' => 'cancelled']);
|
||
$wo->refresh();
|
||
$this->assertEquals(0.0, (float) $wo->total);
|
||
}
|
||
|
||
public function test_deleting_job_recalcs_wo(): void
|
||
{
|
||
$wo = $this->makeWorkOrder();
|
||
$job = SubcontractJob::create([
|
||
'work_order_id' => $wo->id, 'cost' => 500, 'markup_pct' => 100, 'status' => 'sent',
|
||
]);
|
||
$wo->refresh();
|
||
$this->assertEquals(1000.0, (float) $wo->total);
|
||
|
||
$job->delete();
|
||
$wo->refresh();
|
||
$this->assertEquals(0.0, (float) $wo->total);
|
||
}
|
||
|
||
public function test_subcontractors_isolated_per_tenant(): void
|
||
{
|
||
Subcontractor::create(['name' => 'TurboFix', 'is_active' => true]);
|
||
$other = $this->makeCompany('othersubs');
|
||
app(TenantManager::class)->setCurrent($other);
|
||
$this->assertEquals(0, Subcontractor::count());
|
||
}
|
||
|
||
private function makeCompany(string $slug): Company
|
||
{
|
||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||
$company = Company::create(['plan_id' => $plan->id, 'slug' => $slug, 'name' => ucfirst($slug), 'status' => 'active']);
|
||
app(TenantManager::class)->setCurrent($company);
|
||
return $company;
|
||
}
|
||
|
||
private function makeWorkOrder(): WorkOrder
|
||
{
|
||
$client = Client::create(['name' => 'C', 'phone' => '+3736' . random_int(1000000, 9999999), 'type' => 'individual', 'status' => 'active']);
|
||
$vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'X', 'model' => 'Y', 'plate' => 'P' . random_int(100, 999)]);
|
||
return WorkOrder::create([
|
||
'number' => WorkOrder::generateNumber($this->company->id),
|
||
'client_id' => $client->id, 'vehicle_id' => $vehicle->id,
|
||
'opened_at' => now(), 'status' => 'in_work',
|
||
]);
|
||
}
|
||
}
|