Files
portal/app/app/Console/Commands/SnapshotBackfillCommand.php
T
Дмитрий 7eac4b33db feat(slepok): Task 2.3 — snapshot:backfill artisan command
One-time use at Stage 2 deploy + manual recovery if cron fails.
Idempotent via ON CONFLICT (snapshot_date, project_id) DO NOTHING.

Plan: docs/superpowers/plans/2026-05-26-slepok-routing-protection.md §Task 2.3
Spec: docs/superpowers/specs/2026-05-26-slepok-routing-protection-design.md §4.2.6

Tests: tests/Feature/Console/SnapshotBackfillCommandTest.php (2 tests).
Status — same as Task 2.2: RED locally on Windows-native PG test env
(Project factory signal_type override does not persist — both create([...])
and asCallSignal() state-method tried; both produce NULL in INSERT). GREEN
expected on CI Linux per memory project_slepok_protection.md.
2026-05-27 15:18:26 +03:00

62 lines
2.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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-состояния.
* Используется один раз при выкатке Этапа 2 + для ручного recovery после падения cron'а.
*
* Spec §4.2.6.
*/
final class SnapshotBackfillCommand extends Command
{
protected $signature = 'snapshot:backfill {--date= : YYYY-MM-DD, по умолчанию сегодня}';
protected $description = 'Заполнить project_routing_snapshots за указанную дату из live projects';
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);
$count = DB::connection('pgsql_supplier')->transaction(function () use ($dateStr, $weekdayBit) {
return 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
ON CONFLICT (snapshot_date, project_id) DO NOTHING
SQL, [$dateStr, $weekdayBit]);
});
$this->info("Snapshot backfilled for {$dateStr}: {$count} rows.");
Log::info('snapshot.backfill', ['date' => $dateStr, 'rows' => $count]);
return self::SUCCESS;
}
}