Files
autocrm/tests/Feature/SubcontractorTest.php
Vasyka e8078f157a Stage 9 — Subcontractor System: outsourced work with cost+markup
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>
2026-05-28 06:43:15 +00:00

120 lines
4.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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',
]);
}
}