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,134 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Audit;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Central\Plan;
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\EmployeeProfile;
|
||||
use App\Models\Tenant\PayrollAdjustment;
|
||||
use App\Models\Tenant\User;
|
||||
use App\Models\Tenant\Vehicle;
|
||||
use App\Models\Tenant\WorkOrder;
|
||||
use App\Models\Tenant\WorkOrderPart;
|
||||
use App\Models\Tenant\WorkOrderWork;
|
||||
use App\Services\PayrollCalculator;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PayrollCalculatorTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_compute_combines_base_works_parts_and_adjustments(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-06-15');
|
||||
$ctx = $this->bootTenant();
|
||||
$user = $this->makeMaster();
|
||||
EmployeeProfile::create([
|
||||
'user_id' => $user->id,
|
||||
'base_salary' => 5000,
|
||||
'works_pct' => 30, // 30% from done work revenue
|
||||
'parts_pct' => 10, // 10% from parts margin
|
||||
]);
|
||||
|
||||
$client = Client::create(['name' => 'C', 'phone' => '+37399100100', 'type' => 'individual', 'status' => 'active']);
|
||||
$vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'X', 'model' => 'Y', 'plate' => 'PR-1']);
|
||||
$wo = WorkOrder::create([
|
||||
'number' => WorkOrder::generateNumber($ctx['company']->id),
|
||||
'client_id' => $client->id, 'vehicle_id' => $vehicle->id,
|
||||
'opened_at' => today()->subDays(3), 'status' => 'in_work',
|
||||
'master_id' => $user->id,
|
||||
]);
|
||||
|
||||
// A done labor: 2h × 400 = 800 → 30% = 240
|
||||
WorkOrderWork::create([
|
||||
'work_order_id' => $wo->id, 'master_id' => $user->id,
|
||||
'name' => 'X', 'hours' => 2, 'price_per_hour' => 400, 'status' => 'done',
|
||||
]);
|
||||
|
||||
// An installed part: sell 350 - buy 200 = 150 margin × 1 qty → 10% = 15
|
||||
WorkOrderPart::create([
|
||||
'work_order_id' => $wo->id,
|
||||
'name' => 'P', 'qty' => 1, 'sell_price' => 350, 'buy_price' => 200,
|
||||
'status' => 'installed',
|
||||
]);
|
||||
|
||||
// Adjustments for the period
|
||||
PayrollAdjustment::create([
|
||||
'user_id' => $user->id, 'period' => '2026-06',
|
||||
'type' => 'bonus', 'amount' => 500, 'date' => today(),
|
||||
]);
|
||||
PayrollAdjustment::create([
|
||||
'user_id' => $user->id, 'period' => '2026-06',
|
||||
'type' => 'fine', 'amount' => 100, 'date' => today(),
|
||||
]);
|
||||
PayrollAdjustment::create([
|
||||
'user_id' => $user->id, 'period' => '2026-06',
|
||||
'type' => 'advance', 'amount' => 200, 'date' => today(),
|
||||
]);
|
||||
|
||||
$run = app(PayrollCalculator::class)->compute($user->id, '2026-06');
|
||||
|
||||
// base 5000 + works 240 + parts 15 + bonus 500 - fine 100 - advance 200 = 5455
|
||||
$this->assertEquals(5455.0, (float) $run->total);
|
||||
$this->assertEquals(5000.0, (float) $run->base);
|
||||
$this->assertEquals(240.0, (float) $run->works_pct_amount);
|
||||
$this->assertEquals(15.0, (float) $run->parts_pct_amount);
|
||||
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
|
||||
public function test_compute_ignores_other_periods_and_other_users(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-06-15');
|
||||
$ctx = $this->bootTenant();
|
||||
$me = $this->makeMaster();
|
||||
$other = $this->makeMaster();
|
||||
EmployeeProfile::create(['user_id' => $me->id, 'base_salary' => 1000, 'works_pct' => 100, 'parts_pct' => 0]);
|
||||
|
||||
$c = Client::create(['name' => 'X', 'phone' => '+37300000000', 'type' => 'individual', 'status' => 'active']);
|
||||
$v = Vehicle::create(['client_id' => $c->id, 'make' => 'X', 'model' => 'Y', 'plate' => 'NOISE']);
|
||||
$wo = WorkOrder::create([
|
||||
'number' => WorkOrder::generateNumber($ctx['company']->id),
|
||||
'client_id' => $c->id, 'vehicle_id' => $v->id,
|
||||
'opened_at' => today(), 'status' => 'in_work',
|
||||
]);
|
||||
|
||||
// Another master's work — must be excluded.
|
||||
WorkOrderWork::create([
|
||||
'work_order_id' => $wo->id, 'master_id' => $other->id,
|
||||
'name' => 'X', 'hours' => 5, 'price_per_hour' => 400, 'status' => 'done',
|
||||
]);
|
||||
// Adjustment for a different period — must be excluded.
|
||||
PayrollAdjustment::create([
|
||||
'user_id' => $me->id, 'period' => '2026-05',
|
||||
'type' => 'bonus', 'amount' => 9999, 'date' => today()->subMonth(),
|
||||
]);
|
||||
|
||||
$run = app(PayrollCalculator::class)->compute($me->id, '2026-06');
|
||||
$this->assertEquals(1000.0, (float) $run->total, 'only base — no own works, no current-period adjustments');
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
|
||||
private function bootTenant(): array
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'pr-' . uniqid(),
|
||||
'name' => 'Payroll Co', 'status' => 'active',
|
||||
]);
|
||||
app(TenantManager::class)->setCurrent($company);
|
||||
return compact('company');
|
||||
}
|
||||
|
||||
private function makeMaster(): User
|
||||
{
|
||||
return User::create([
|
||||
'name' => 'M', 'email' => 'm-' . uniqid() . '@example.com',
|
||||
'password' => bcrypt('x'), 'role' => 'master', 'status' => 'active',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user