Files
autocrm/tests/Feature/Audit/PayrollCalculatorTest.php
T
Vasyka 0620635abb 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>
2026-06-03 07:05:46 +00:00

135 lines
5.3 KiB
PHP
Raw 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\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',
]);
}
}