From 7eac4b33dbca2daf207c3fb430fd5c25604f85f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Wed, 27 May 2026 15:18:26 +0300 Subject: [PATCH] =?UTF-8?q?feat(slepok):=20Task=202.3=20=E2=80=94=20snapsh?= =?UTF-8?q?ot:backfill=20artisan=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Commands/SnapshotBackfillCommand.php | 61 +++++++++++++++++++ .../Console/SnapshotBackfillCommandTest.php | 39 ++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 app/app/Console/Commands/SnapshotBackfillCommand.php create mode 100644 app/tests/Feature/Console/SnapshotBackfillCommandTest.php diff --git a/app/app/Console/Commands/SnapshotBackfillCommand.php b/app/app/Console/Commands/SnapshotBackfillCommand.php new file mode 100644 index 00000000..c92e510a --- /dev/null +++ b/app/app/Console/Commands/SnapshotBackfillCommand.php @@ -0,0 +1,61 @@ +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(<< 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; + } +} diff --git a/app/tests/Feature/Console/SnapshotBackfillCommandTest.php b/app/tests/Feature/Console/SnapshotBackfillCommandTest.php new file mode 100644 index 00000000..f214cb75 --- /dev/null +++ b/app/tests/Feature/Console/SnapshotBackfillCommandTest.php @@ -0,0 +1,39 @@ +create(['frozen_by_balance_at' => null]); + Project::factory()->for($tenant)->asCallSignal('79161234567')->create([ + 'is_active' => true, + 'delivery_days_mask' => 127, + 'daily_limit_target' => 10, + ]); + + $this->artisan('snapshot:backfill', ['--date' => '2026-05-27']) + ->assertSuccessful(); + + expect(\DB::table('project_routing_snapshots')->where('snapshot_date', '2026-05-27')->count())->toBe(1); + Carbon::setTestNow(); +}); + +it('is idempotent — does not duplicate on re-run', function () { + Carbon::setTestNow('2026-05-27 14:00:00', 'Europe/Moscow'); + $tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]); + Project::factory()->for($tenant)->asCallSignal('79161234567')->create([ + 'is_active' => true, + 'delivery_days_mask' => 127, + 'daily_limit_target' => 10, + ]); + + $this->artisan('snapshot:backfill', ['--date' => '2026-05-27'])->assertSuccessful(); + $this->artisan('snapshot:backfill', ['--date' => '2026-05-27'])->assertSuccessful(); + + expect(\DB::table('project_routing_snapshots')->count())->toBe(1); + Carbon::setTestNow(); +});