Files
portal/app/app/Jobs/SnapshotProjectRoutingJob.php
T
Дмитрий f94552d452 WIP чекпойнт: lead-region/supplier бэкенд + фронт-редизайн + Pint + тесты
92 файла одной пачкой. Исключены чужие зоны: CLAUDE.md, .claude/settings.json, docs/observer/.pii-counters.json.
gitleaks staged: no leaks found. Не верифицировано тестами - сохранение труда в историю.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 05:17:12 +03:00

73 lines
2.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Daily 18:02 МСК snapshot — фиксирует состояние всех eligible Лидерра-проектов
* на завтрашний день (slepok №NЛ по канону спека §0).
* Spec: docs/superpowers/specs/2026-05-26-slepok-routing-protection-design.md §4.2.2.
*/
final class SnapshotProjectRoutingJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public const DB_CONNECTION = 'pgsql_supplier'; // BYPASSRLS
public function handle(): void
{
$snapshotDate = Carbon::tomorrow('Europe/Moscow')->toDateString();
$weekdayBit = 1 << (Carbon::tomorrow('Europe/Moscow')->isoWeekday() - 1);
// NB: Без внешнего transaction() — атомарность гарантирует INSERT ... ON CONFLICT
// на уровне PG. Внешний transaction() ломается при тестах под DatabaseTransactions
// + SharesSupplierPdo (общий PDO pgsql/pgsql_supplier → PG ругается «active transaction»).
$exists = DB::connection(self::DB_CONNECTION)
->table('project_routing_snapshots')
->where('snapshot_date', $snapshotDate)
->exists();
if ($exists) {
Log::info('snapshot.already_exists', ['date' => $snapshotDate]);
return;
}
$count = DB::connection(self::DB_CONNECTION)->insert(<<<'SQL'
INSERT INTO project_routing_snapshots (
snapshot_date, project_id, tenant_id,
daily_limit, delivery_days_mask, regions,
signal_type, signal_identifier, sms_senders, sms_keyword,
expected_volume
)
SELECT
?::date,
p.id, p.tenant_id,
COALESCE(p.effective_daily_limit_today, p.daily_limit_target),
p.delivery_days_mask,
p.regions,
p.signal_type, p.signal_identifier, p.sms_senders, p.sms_keyword,
COALESCE(p.effective_daily_limit_today, p.daily_limit_target)
FROM projects p
INNER JOIN tenants t ON t.id = p.tenant_id
WHERE p.is_active = true
AND (p.delivery_days_mask & ?::int) <> 0
AND p.preflight_blocked_at IS NULL
AND t.frozen_by_balance_at IS NULL
AND t.deleted_at IS NULL
ON CONFLICT (snapshot_date, project_id) DO NOTHING
SQL, [$snapshotDate, $weekdayBit]);
Log::info('snapshot.created', ['date' => $snapshotDate, 'rows' => $count]);
}
}