87336f74dc
Daily 18:02 MSK job: captures eligible projects state into project_routing_snapshots for tomorrow date. Filters frozen tenants, preflight_blocked projects, weekday_mask. Carries effective_daily_limit_today (R-11/OPEN-5 var A). Idempotent via INSERT ON CONFLICT DO NOTHING. Spec section 4.2.2.
106 lines
4.4 KiB
PHP
106 lines
4.4 KiB
PHP
<?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);
|
|
});
|