Files
portal/app/tests/Feature/Jobs/SnapshotProjectRoutingJobTest.php
T

106 lines
4.4 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\SnapshotProjectRoutingJob;
use App\Models\Project;
use App\Models\Tenant;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
// NB: Job не оборачивает свою INSERT в DB::transaction() — иначе под
// DatabaseTransactions+SharesSupplierPdo (общий PDO между pgsql и pgsql_supplier)
// PostgreSQL возвращает «There is already an active transaction». Атомарность
// гарантирует INSERT ... ON CONFLICT DO NOTHING на уровне PG (идемпотентность).
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function () {
Carbon::setTestNow('2026-05-27 15:02:00', 'UTC'); // 18:02 MSK = 15:02 UTC
});
afterEach(function () {
Carbon::setTestNow();
});
it('creates snapshot for tomorrow with active projects only', function () {
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
$active = Project::factory()->for($tenant)->create([
'is_active' => true,
'delivery_days_mask' => 127,
'daily_limit_target' => 10,
'preflight_blocked_at' => null,
'signal_type' => 'call',
'signal_identifier' => '79161234567',
]);
$inactive = Project::factory()->for($tenant)->create([
'is_active' => false,
'delivery_days_mask' => 127,
'signal_type' => 'call',
'signal_identifier' => '79169999999',
]);
(new SnapshotProjectRoutingJob)->handle();
$rows = \DB::table('project_routing_snapshots')
->where('snapshot_date', '2026-05-28')->get();
expect($rows)->toHaveCount(1);
expect($rows->first()->project_id)->toBe($active->id);
});
it('excludes frozen tenants', function () {
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => now()]);
Project::factory()->for($tenant)->create([
'is_active' => true, 'delivery_days_mask' => 127, 'daily_limit_target' => 10, 'signal_type' => 'call', 'signal_identifier' => '79161234567',
]);
(new SnapshotProjectRoutingJob)->handle();
expect(\DB::table('project_routing_snapshots')->count())->toBe(0);
});
it('excludes preflight_blocked projects', function () {
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
Project::factory()->for($tenant)->create([
'is_active' => true, 'delivery_days_mask' => 127, 'daily_limit_target' => 10, 'signal_type' => 'call', 'signal_identifier' => '79161234567',
'preflight_blocked_at' => now(),
]);
(new SnapshotProjectRoutingJob)->handle();
expect(\DB::table('project_routing_snapshots')->count())->toBe(0);
});
it('excludes projects whose days_mask does not match tomorrow', function () {
// 2026-05-28 — четверг (isoWeekday=4 → bit 1<<3 = 8). Тест: mask=4 (только среда).
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
Project::factory()->for($tenant)->create([
'is_active' => true,
'delivery_days_mask' => 4, // только среда
'daily_limit_target' => 10,
'signal_type' => 'call',
'signal_identifier' => '79161234567',
]);
(new SnapshotProjectRoutingJob)->handle();
expect(\DB::table('project_routing_snapshots')->count())->toBe(0);
});
it('uses effective_daily_limit_today as daily_limit when set (R-11/OPEN-5 variant A)', function () {
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
Project::factory()->for($tenant)->create([
'is_active' => true, 'delivery_days_mask' => 127,
'daily_limit_target' => 10,
'effective_daily_limit_today' => 3, // override
'signal_type' => 'call',
'signal_identifier' => '79161234567',
]);
(new SnapshotProjectRoutingJob)->handle();
$row = \DB::table('project_routing_snapshots')->first();
expect($row->daily_limit)->toBe(3);
});
it('is idempotent — second run does not duplicate', function () {
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
Project::factory()->for($tenant)->create([
'is_active' => true, 'delivery_days_mask' => 127, 'daily_limit_target' => 10, 'signal_type' => 'call', 'signal_identifier' => '79161234567',
]);
(new SnapshotProjectRoutingJob)->handle();
(new SnapshotProjectRoutingJob)->handle();
expect(\DB::table('project_routing_snapshots')->count())->toBe(1);
});