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.
72 lines
2.9 KiB
PHP
72 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, Queueable, InteractsWithQueue, 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]);
|
|
}
|
|
}
|