Files
portal/app/app/Console/Commands/SnapshotRebuildCommand.php
T

83 lines
3.6 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Перестраивает project_routing_snapshots за указанную дату из текущего
* live-состояния, ПЕРЕЗАПИСЫВАЯ существующий snapshot.
*
* В отличие от `snapshot:backfill` (идемпотентный — ON CONFLICT DO NOTHING),
* `snapshot:rebuild` всегда сначала DELETE'ит существующий snapshot за дату,
* затем создаёт новый. Используется для manual recovery после падения
* `SnapshotProjectRoutingJob` cron'а с уже частично записанным snapshot'ом
* (см. Task 2.10, Spec §4.2.6 fail-loud strategy).
*
* Fail-loud strategy:
* 1. Heartbeat alarm via SchedulerHeartbeatTracker (Task 2.4).
* 2. LeadRouter Log::error on missing snapshot (Task 2.5).
* 3. Manual recovery: `php artisan snapshot:rebuild --date=YYYY-MM-DD`.
*
* NO fallback to live projects — explicit downtime + alert is safer
* than silent regression.
*/
final class SnapshotRebuildCommand extends Command
{
protected $signature = 'snapshot:rebuild {--date= : YYYY-MM-DD, по умолчанию сегодня}';
protected $description = 'Перестроить project_routing_snapshots за указанную дату (DELETE+INSERT, для recovery)';
public function handle(): int
{
$dateStr = (string) ($this->option('date') ?? Carbon::today('Europe/Moscow')->toDateString());
$date = Carbon::parse($dateStr, 'Europe/Moscow');
$weekdayBit = 1 << ($date->isoWeekday() - 1);
// NB: НЕ оборачиваем в ->transaction() — это recovery-команда, half-done state
// допустим (retry восстанавливает; на проде admin контроль). Wrapper конфликтует
// с tests SharesSupplierPdo (shared PDO + nested transaction levels).
$deleted = DB::connection('pgsql_supplier')
->table('project_routing_snapshots')
->where('snapshot_date', $dateStr)
->delete();
$inserted = DB::connection('pgsql_supplier')->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
SQL, [$dateStr, $weekdayBit]);
$this->info("Snapshot rebuilt for {$dateStr}: deleted={$deleted}, inserted={$inserted}.");
Log::warning('snapshot.rebuild', [
'date' => $dateStr,
'deleted' => $deleted,
'inserted' => $inserted,
]);
return self::SUCCESS;
}
}