From 64b0a3d944d82fbf54ee1dac73458b036e81963d 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: Sun, 24 May 2026 10:46:59 +0300 Subject: [PATCH 01/13] =?UTF-8?q?docs(billing-v2-c):=20=D1=81=D0=BF=D0=B5?= =?UTF-8?q?=D0=BA=20C=20+=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20(=D0=BF=D1=80?= =?UTF-8?q?=D0=B5fflight=20+=20VTB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- ...6-05-24-billing-v2-spec-c-preflight-vtb.md | 86 +++++++------------ ...-billing-v2-spec-c-preflight-vtb-design.md | 48 +++++------ 2 files changed, 53 insertions(+), 81 deletions(-) diff --git a/docs/superpowers/plans/2026-05-24-billing-v2-spec-c-preflight-vtb.md b/docs/superpowers/plans/2026-05-24-billing-v2-spec-c-preflight-vtb.md index 933b1515..a34251d7 100644 --- a/docs/superpowers/plans/2026-05-24-billing-v2-spec-c-preflight-vtb.md +++ b/docs/superpowers/plans/2026-05-24-billing-v2-spec-c-preflight-vtb.md @@ -1,10 +1,10 @@ -# Биллинг v2 Спек C — Префлайт баланса + VTB-эквайринг — План реализации +# Биллинг v2 Спек C — Преfflight баланса + VTB-эквайринг — План реализации > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Pravila §15.1** — git-коммит задачи только Sonnet/Opus субагентами (не Haiku); контроллер верифицирует `git rev-parse HEAD` после каждого субагента. -**Goal:** Защитить портал от заказа лидов у поставщика на клиентов без баланса (префлайт) + дать реальное пополнение баланса через безнал (PDF-счёт + сверка) с архитектурой под СБП/карты VTB. +**Goal:** Защитить портал от заказа лидов у поставщика на клиентов без баланса (преfflight) + дать реальное пополнение баланса через безнал (PDF-счёт + сверка) с архитектурой под СБП/карты VTB. -**Architecture:** Префлайт — фильтр eligible-проектов до формулы `computeOrder` (формула не меняется) + флаги заморозки на tenant/project + ежедневный cron 18:00 MSK. VTB — интерфейс `TopupGatewayInterface` + 3 реализации (безнал полный, СБП/карты dev-заглушки) + журнал `topup_sessions`. +**Architecture:** Преfflight — фильтр eligible-проектов до формулы `computeOrder` (формула не меняется) + флаги заморозки на tenant/project + ежедневный cron 18:00 MSK. VTB — интерфейс `TopupGatewayInterface` + 3 реализации (безнал полный, СБП/карты dev-заглушки) + журнал `topup_sessions`. **Tech Stack:** PHP 8.3 / Laravel 13, PostgreSQL 16 (RLS), Pest 4, Vue 3 + Vuetify 3, Vitest, barryvdh/laravel-dompdf (новый). @@ -16,7 +16,7 @@ **Что уже есть на `origin/main` (зависимости):** -- `App\Services\Billing\BalanceToLeadsConverter::convert(string $balanceRub, int $deliveredInMonth, Collection $tiers): array` — возвращает `['leads' => int, 'breakdown' => [...], 'current_tier' => ..., 'next_tier' => ...]`. Pure, bcmath. Используется в префлайт для «сколько лидов даёт баланс». +- `App\Services\Billing\BalanceToLeadsConverter::convert(string $balanceRub, int $deliveredInMonth, Collection $tiers): array` — возвращает `['leads' => int, 'breakdown' => [...], 'current_tier' => ..., 'next_tier' => ...]`. Pure, bcmath. Используется в преfflight для «сколько лидов даёт баланс». - `App\Services\Billing\PricingTierResolver` — резолвер ступени по порядковому номеру лида. - `App\Services\Billing\LedgerService` — списания (не трогаем). - `App\Services\Billing\BillingTopupService::topup(int $tenantId, string $amountRub, ?int $userId)` — MVP-stub пополнения (рефакторим). @@ -72,12 +72,11 @@ Expected: миграции прошли, converter-тесты GREEN (завис --- -## Phase 1 — Префлайт баланса +## Phase 1 — Преfflight баланса ### Task 1.1: Миграция БД — флаги заморозки + журнал **Files:** - - Create: `app/database/migrations/2026_05_24_100000_add_balance_freeze_to_tenants_and_projects.php` - Create: `db/migrations/2026_05_24_balance_freeze_log.sql` (для prod-синка вручную, как в Спеке B) - Modify: `db/schema.sql` (добавить колонки + таблицу + запись в `db/CHANGELOG_schema.md`) @@ -173,7 +172,6 @@ git commit -m "feat(billing-v2-c): миграция — флаги заморо ### Task 1.2: `BalancePreflightService` — pure-проверка платёжеспособности **Files:** - - Create: `app/app/Services/Billing/BalancePreflightService.php` - Create: `app/app/Services/Billing/PreflightResult.php` - Test: `app/tests/Unit/Billing/BalancePreflightServiceTest.php` @@ -272,7 +270,7 @@ namespace App\Services\Billing; use Illuminate\Database\Eloquent\Collection; /** - * Pure: проходит ли клиент префлайт — хватает ли баланса на ПОЛНЫЙ дневной + * Pure: проходит ли клиент преfflight — хватает ли баланса на ПОЛНЫЙ дневной * лимит всех его eligible-проектов по текущему тарифу. * * Сравнение в ЛИДАХ (capacity vs required), не в рублях — переиспользует @@ -330,7 +328,6 @@ git commit -m "feat(billing-v2-c): BalancePreflightService — pure-провер ### Task 1.3: Tenant/Project — хелперы платёжеспособности + резолвер eligible-лимита **Files:** - - Modify: `app/app/Models/Tenant.php` (метод `requiredLeadsForTomorrow()`, scope, casts флага) - Modify: `app/app/Models/Project.php` (cast `preflight_blocked_at`) - Test: `app/tests/Feature/Billing/TenantPreflightTest.php` @@ -372,7 +369,7 @@ Expected: FAIL — метод `requiredLeadsForTomorrow` не существуе ```php /** * Сумма daily_limit активных проектов — «сколько лидов клиент хочет в день». - * Используется префлайт'ом как requiredLeads. + * Используется преfflight'ом как requiredLeads. */ public function requiredLeadsForTomorrow(): int { @@ -401,7 +398,6 @@ git commit -m "feat(billing-v2-c): Tenant::requiredLeadsForTomorrow + cast фл ### Task 1.4: `BalancePreflightSweepJob` — ежедневный пересчёт заморозок **Files:** - - Create: `app/app/Jobs/Billing/BalancePreflightSweepJob.php` - Create: `app/app/Console/Commands/BillingPreflightSweepCommand.php` - Modify: `app/routes/console.php` (Schedule @18:00 MSK + heartbeat) @@ -489,7 +485,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; /** - * Ежедневный префлайт всех тенантов перед формированием заказа поставщику. + * Ежедневный преfflight всех тенантов перед формированием заказа поставщику. * Запускается cron @18:00 MSK (routes/console.php). См. спек §3.5, §5.2. * * NB: crm_supplier_worker / системный контекст — джоб бегает без tenant-RLS; @@ -578,12 +574,12 @@ final class BillingPreflightSweepCommand extends Command { protected $signature = 'billing:preflight-sweep'; - protected $description = 'Ежедневный префлайт баланса — заморозка/разморозка тенантов (cut-off 18:00 MSK)'; + protected $description = 'Ежедневный преfflight баланса — заморозка/разморозка тенантов (cut-off 18:00 MSK)'; public function handle(): int { (new BalancePreflightSweepJob())->handle(); - $this->info('Префлайт sweep завершён.'); + $this->info('Преfflight sweep завершён.'); return self::SUCCESS; } @@ -595,7 +591,7 @@ final class BillingPreflightSweepCommand extends Command Добавить после существующих задач (с heartbeat-обёрткой по образцу `projects:reset-delivered-today`): ```php -// Спек C §3.2: префлайт баланса в 18:00 MSK — формирование заказа поставщику без «бедных» клиентов. +// Спек C §3.2: преfflight баланса в 18:00 MSK — формирование заказа поставщику без «бедных» клиентов. Schedule::command('billing:preflight-sweep') ->dailyAt('18:00') ->timezone('Europe/Moscow') @@ -629,7 +625,6 @@ git commit -m "feat(billing-v2-c): BalancePreflightSweepJob + cron 18:00 MSK" ### Task 1.5: Mailable — 4 письма заморозки/разморозки **Files:** - - Create: `app/app/Mail/BalanceFrozenMail.php` + `app/app/Mail/BalanceFrozenReminderMail.php` + `app/app/Mail/BalanceFrozenFinalMail.php` + `app/app/Mail/BalanceUnfrozenMail.php` - Create: 4 blade-шаблона в `app/resources/views/emails/billing/` - Test: `app/tests/Feature/Billing/BalanceFreezeMailTest.php` @@ -751,7 +746,6 @@ git commit -m "feat(billing-v2-c): 4 Mailable заморозки/разморо ### Task 1.6: Повторные письма — reminder через 1 день + final через 3 дня **Files:** - - Create: `app/app/Jobs/Billing/BalanceFrozenReminderJob.php` - Modify: `app/routes/console.php` (Schedule daily + heartbeat) - Test: `app/tests/Feature/Billing/BalanceFrozenReminderJobTest.php` @@ -894,10 +888,9 @@ git commit -m "feat(billing-v2-c): повторные письма заморо --- -### Task 1.7: `ProjectController` — префлайт при создании/правке проекта +### Task 1.7: `ProjectController` — преfflight при создании/правке проекта **Files:** - - Modify: `app/app/Http/Controllers/Api/ProjectController.php` (store + update — 409 при перегрузке) - Test: `app/tests/Feature/Project/ProjectPreflightTest.php` @@ -965,12 +958,12 @@ it('creates normally when within balance', function () { Run: `cd app && php artisan test --filter=ProjectPreflightTest` Expected: FAIL — 409 не возвращается (создаёт нормально). -- [ ] **Step 3: Реализовать префлайт-проверку в `ProjectController::store`** (и аналогично `update`) +- [ ] **Step 3: Реализовать преfflight-проверку в `ProjectController::store`** (и аналогично `update`) Вставить ПОСЛЕ валидации, ДО создания проекта: ```php -// Спек C §3.4: префлайт при создании — если сумма лимитов перевешивает баланс. +// Спек C §3.4: преfflight при создании — если сумма лимитов перевешивает баланс. $tenant = $request->user()->tenant; $tiers = \App\Models\PricingTier::query()->where('is_active', true)->get(); $service = new \App\Services\Billing\BalancePreflightService(); @@ -1012,7 +1005,7 @@ Expected: PASS (3 теста). ```bash git add app/app/Http/Controllers/Api/ProjectController.php app/tests/Feature/Project/ProjectPreflightTest.php -git commit -m "feat(billing-v2-c): ProjectController префлайт — 409 при перегрузке баланса" +git commit -m "feat(billing-v2-c): ProjectController преfflight — 409 при перегрузке баланса" ``` --- @@ -1020,7 +1013,6 @@ git commit -m "feat(billing-v2-c): ProjectController префлайт — 409 п ### Task 1.8: Фильтр frozen-проектов в `SyncSupplierProjectsJob` **Files:** - - Modify: `app/app/Jobs/Supplier/SyncSupplierProjectsJob.php` (исключить frozen перед allocator) - Test: `app/tests/Feature/Supplier/SyncSupplierPreflightFilterTest.php` @@ -1102,7 +1094,6 @@ git commit -m "feat(billing-v2-c): SyncSupplierProjectsJob исключает fr ### Task 1.9: One-time команда `billing:preflight-initial-sweep` **Files:** - - Create: `app/app/Console/Commands/BillingPreflightInitialSweepCommand.php` - Test: `app/tests/Feature/Billing/BillingPreflightInitialSweepTest.php` @@ -1149,18 +1140,18 @@ use App\Jobs\Billing\BalancePreflightSweepJob; use Illuminate\Console\Command; /** - * One-time: при выкатке префлайт прогнать всех тенантов и заморозить + * One-time: при выкатке преfflight прогнать всех тенантов и заморозить * недофинансированных. Запускается ОДИН раз вручную после миграции. Спек §3.9. */ final class BillingPreflightInitialSweepCommand extends Command { protected $signature = 'billing:preflight-initial-sweep'; - protected $description = 'Разовый префлайт при внедрении — заморозить недофинансированных тенантов'; + protected $description = 'Разовый преfflight при внедрении — заморозить недофинансированных тенантов'; public function handle(): int { - $this->warn('Разовый префлайт всех тенантов. Запускать ОДИН раз после выкатки.'); + $this->warn('Разовый преfflight всех тенантов. Запускать ОДИН раз после выкатки.'); (new BalancePreflightSweepJob())->handle(); $this->info('Initial sweep завершён.'); @@ -1186,7 +1177,6 @@ git commit -m "feat(billing-v2-c): one-time billing:preflight-initial-sweep" ### Task 1.10: Frontend — баннер заморозки + индикатор ёмкости + диалог перегрузки **Files:** - - Create: `app/resources/js/components/billing/BalanceFrozenBanner.vue` - Create: `app/resources/js/components/billing/BalanceCapacityIndicator.vue` - Create: `app/resources/js/components/projects/ProjectLimitOverloadDialog.vue` @@ -1281,7 +1271,7 @@ Expected: PASS. ```bash git add app/resources/js/components/billing/ app/resources/js/components/projects/ProjectLimitOverloadDialog.vue app/resources/js/layouts/AppLayout.vue -git commit -m "feat(billing-v2-c): UI префлайт — баннер, индикатор ёмкости, диалог перегрузки" +git commit -m "feat(billing-v2-c): UI преfflight — баннер, индикатор ёмкости, диалог перегрузки" ``` --- @@ -1291,7 +1281,6 @@ git commit -m "feat(billing-v2-c): UI префлайт — баннер, инд ### Task 2.1: Миграция БД — реквизиты тенанта + topup_sessions **Files:** - - Create: `app/database/migrations/2026_05_24_110000_add_legal_entity_and_topup_sessions.php` - Create: `db/migrations/2026_05_24_topup_sessions.sql` (prod-синк) - Modify: `db/schema.sql` + `db/CHANGELOG_schema.md` @@ -1388,7 +1377,6 @@ git commit -m "feat(billing-v2-c): миграция — реквизиты юр. ### Task 2.2: Установить PDF-генератор **Files:** - - Modify: `app/composer.json` (+ `barryvdh/laravel-dompdf`) - [ ] **Step 1: Установить пакет** @@ -1413,7 +1401,6 @@ git commit -m "chore(billing-v2-c): + barryvdh/laravel-dompdf для счето ### Task 2.3: Модель TopupSession + `TopupGatewayInterface` + `BankTransferGateway` **Files:** - - Create: `app/app/Models/TopupSession.php` - Create: `app/app/Services/Billing/Gateway/TopupGatewayInterface.php` - Create: `app/app/Services/Billing/Gateway/TopupSessionResult.php` @@ -1617,7 +1604,6 @@ git commit -m "feat(billing-v2-c): TopupSession + TopupGatewayInterface + BankTr ### Task 2.4: PDF-счёт (Blade-шаблон + endpoint) **Files:** - - Create: `app/resources/views/pdf/invoice.blade.php` - Modify: `app/app/Http/Controllers/Api/BillingController.php` (+ `invoicePdf`) - Modify: `app/routes/web.php` (route) @@ -1712,7 +1698,6 @@ git commit -m "feat(billing-v2-c): PDF-счёт на пополнение + RLS- ### Task 2.5: Рефакторинг `BillingTopupService` под gateway + `confirmPayment` **Files:** - - Modify: `app/app/Services/Billing/BillingTopupService.php` - Test: `app/tests/Feature/Billing/BillingTopupConfirmTest.php` @@ -1812,7 +1797,7 @@ public function confirmPayment(string $providerRef, ?int $adminUserId): ?Balance Run: `cd app && php artisan test --filter=BillingTopupConfirmTest` Expected: PASS (2 теста). -- [ ] **Step 5: Связка с префлайт — после confirm перепроверить заморозку** +- [ ] **Step 5: Связка с преfflight — после confirm перепроверить заморозку** Добавить в конце `confirmPayment` (после коммита транзакции, или внутри): если `tenant.frozen_by_balance_at !== null` — прогнать `BalancePreflightService::evaluate`, и если теперь проходит — снять заморозку + `BalanceUnfrozenMail` + лог. Написать доп. тест `it('unfreezes tenant on topup confirm')`. @@ -1828,7 +1813,6 @@ git commit -m "feat(billing-v2-c): BillingTopupService::confirmPayment + авт ### Task 2.6: `VtbStatementSyncJob` (авто-поиск) + dev-симулятор **Files:** - - Create: `app/app/Jobs/Billing/VtbStatementSyncJob.php` - Create: `app/app/Console/Commands/VtbStatementSimulateCommand.php` (dev only) - Create: `app/app/Services/Billing/VtbBusinessClient.php` (интерфейс + null/dev реализация) @@ -1907,7 +1891,6 @@ git commit -m "feat(billing-v2-c): VtbStatementSyncJob авто-поиск пл ### Task 2.7: Админка — журнал ожидающих платежей + подтверждение + настройка режима **Files:** - - Create: `app/app/Http/Controllers/Api/Admin/PendingTopupsController.php` - Create: `app/app/Http/Controllers/Api/Admin/BillingSettingsController.php` - Modify: `app/routes/web.php` (admin routes) @@ -1981,7 +1964,6 @@ git commit -m "feat(billing-v2-c): админка ожидающих плате ### Task 2.8: `NotifyPendingConfirmationsJob` — часовой алерт админу **Files:** - - Create: `app/app/Jobs/Billing/NotifyPendingConfirmationsJob.php` - Create: `app/app/Mail/PendingConfirmationsAdminMail.php` + шаблон - Modify: `app/routes/console.php` (hourly + heartbeat) @@ -2052,7 +2034,6 @@ git commit -m "feat(billing-v2-c): часовой алерт админу о н ### Task 2.9: Frontend — пополнение (выбор метода + безнал) + реквизиты + админка **Files:** - - Create: `app/resources/js/views/billing/TopupView.vue` - Create: `app/resources/js/components/billing/TopupMethodPicker.vue` - Create: `app/resources/js/components/billing/BankTransferInvoiceView.vue` @@ -2103,7 +2084,6 @@ git commit -m "feat(billing-v2-c): UI пополнения (безнал) + ре ### Task 3.1: `SbpGateway` (dev-режим) + фоновое авто-подтверждение **Files:** - - Create: `app/app/Services/Billing/Gateway/SbpGateway.php` - Create: `app/app/Jobs/Billing/DevAutoConfirmSessionJob.php` - Test: `app/tests/Feature/Billing/SbpGatewayTest.php` @@ -2204,7 +2184,6 @@ git commit -m "feat(billing-v2-c): SbpGateway dev-заглушка + авто-п ### Task 3.2: `CardGateway` (dev-режим) + dev-mock страница эквайринга **Files:** - - Create: `app/app/Services/Billing/Gateway/CardGateway.php` - Create: `app/resources/js/views/dev/DevMockAcquiringView.vue` (только dev) - Modify: router (dev-only route `/dev-mock-vtb-acquiring/:sessionId`) @@ -2262,7 +2241,6 @@ git commit -m "feat(billing-v2-c): CardGateway dev-заглушка + mock-ст ### Task 3.3: `FiscalReceiptProvider` — архитектура 54-ФЗ **Files:** - - Create: `app/app/Services/Billing/Fiscal/FiscalReceiptProvider.php` (interface) - Create: `app/app/Services/Billing/Fiscal/NoOpFiscalProvider.php` - Create: `app/app/Services/Billing/Fiscal/DevMockFiscalProvider.php` @@ -2356,7 +2334,6 @@ git commit -m "feat(billing-v2-c): архитектура 54-ФЗ — FiscalRece ### Task 3.4: Подключить выбор gateway + fiscal в `BillingTopupService::initiateTopup` **Files:** - - Modify: `app/app/Services/Billing/BillingTopupService.php` (resolveGateway + initiateTopup) - Modify: `app/app/Http/Controllers/Api/BillingController.php` (+ `initiateTopup` endpoint) - Modify: `app/routes/web.php` @@ -2421,10 +2398,9 @@ git commit -m "feat(billing-v2-c): initiateTopup — единый вход дл ## Phase 4 — End-to-end сценарии + регрессия -### Task 4.1: Pest E2E — префлайт (заморозка → пополнение → разморозка) +### Task 4.1: Pest E2E — преfflight (заморозка → пополнение → разморозка) **Files:** - - Create: `app/tests/Feature/Billing/PreflightE2ETest.php` - [ ] **Step 1: Написать E2E-тест полного цикла** @@ -2453,7 +2429,7 @@ it('full cycle: sufficient → drained → frozen → topup → unfrozen', funct $tenant = Tenant::factory()->create(['balance_rub' => '1250.00']); // 1250/50 = 25 Project::factory()->for($tenant)->create(['status' => 'active', 'daily_limit' => 25]); - // Префлайт cut-off — проходит. + // Преfflight cut-off — проходит. (new BalancePreflightSweepJob())->handle(); expect($tenant->fresh()->frozen_by_balance_at)->toBeNull(); @@ -2486,7 +2462,7 @@ Expected: PASS. (Если FAIL — починить связку confirmPayment ```bash git add app/tests/Feature/Billing/PreflightE2ETest.php -git commit -m "test(billing-v2-c): E2E префлайт — полный цикл заморозки/разморозки" +git commit -m "test(billing-v2-c): E2E преfflight — полный цикл заморозки/разморозки" ``` --- @@ -2494,7 +2470,6 @@ git commit -m "test(billing-v2-c): E2E префлайт — полный цик ### Task 4.2: Pest E2E — безнал (счёт → выписка → подтверждение) **Files:** - - Create: `app/tests/Feature/Billing/BankTransferE2ETest.php` - [ ] **Step 1: Написать E2E-тест** @@ -2596,19 +2571,18 @@ Expected: 0 leaks, 0 broken links. ## Phase 5 — Документация + ADR + push -### Task 5.1: ADR-016 — архитектура префлайт + VTB +### Task 5.1: ADR-016 — архитектура преfflight + VTB **Files:** - - Create: `docs/adr/016-preflight-vtb-architecture.md` -- [ ] **Step 1: Написать ADR** (Context — переплата поставщику + нет реального пополнения; Decision — префлайт как фильтр eligible до формулы + gateway-pattern для 3 методов + dev-заглушки до Б-1; Consequences — границы PF1..PFN; Status — Accepted). REQUIRED SUB-SKILL: `adr-kit:adr`. +- [ ] **Step 1: Написать ADR** (Context — переплата поставщику + нет реального пополнения; Decision — преfflight как фильтр eligible до формулы + gateway-pattern для 3 методов + dev-заглушки до Б-1; Consequences — границы PF1..PFN; Status — Accepted). REQUIRED SUB-SKILL: `adr-kit:adr`. - [ ] **Step 2: Commit** ```bash git add docs/adr/016-preflight-vtb-architecture.md -git commit -m "docs(adr): ADR-016 — префлайт баланса + VTB gateway-архитектура" +git commit -m "docs(adr): ADR-016 — преfflight баланса + VTB gateway-архитектура" ``` --- @@ -2616,13 +2590,12 @@ git commit -m "docs(adr): ADR-016 — префлайт баланса + VTB gate ### Task 5.2: Обновить memory + нормативку (по необходимости) **Files:** - - Modify: `memory/project_billing_v2.md` (Спек C: статус, артефакты, что выкачено) - Modify: `memory/MEMORY.md` (одна строка-указатель, если меняется) - [ ] **Step 1: Обновить `project_billing_v2.md`** — раздел «Спек C» из PENDING brainstorm → IMPLEMENTED (worktree, ветка, фазы, что dev-заглушки до Б-1, что pending). Через Write tool (это memory, не код). -- [ ] **Step 2: CLAUDE.md / Pravila / Tooling** — **проверить**, нужны ли правки. Префлайт/VTB — это фича портала, не новый инструмент тулчейна → **скорее всего нормативка не меняется**. Если нужно — через `/claude-md-management:claude-md-improver` (НЕ прямой Edit CLAUDE.md, кроме worktree-эксцепшна §5 п.10). +- [ ] **Step 2: CLAUDE.md / Pravila / Tooling** — **проверить**, нужны ли правки. Преfflight/VTB — это фича портала, не новый инструмент тулчейна → **скорее всего нормативка не меняется**. Если нужно — через `/claude-md-management:claude-md-improver` (НЕ прямой Edit CLAUDE.md, кроме worktree-эксцепшна §5 п.10). - [ ] **Step 3: Commit (если были правки)** @@ -2645,7 +2618,7 @@ REQUIRED SUB-SKILL: `superpowers:finishing-a-development-branch`. Варианты: PR в main (через `gh`) ИЛИ FF-merge в main (паттерн Спека B `git push origin feat/billing-v2-spec-c:main` через worktree). **Только после явного «выкатываем» от заказчика** (Pravila §2.2 — не закрывать без подтверждения). -- [ ] **Step 3: НЕ выкатывать на прод автоматически.** Боевой деплой VTB-части невозможен до Б-1 (нет реквизитов). Префлайт-часть можно выкатить — но **только по явному решению заказчика** + ручной прогон `billing:preflight-initial-sweep` на проде с предупреждением клиентам. +- [ ] **Step 3: НЕ выкатывать на прод автоматически.** Боевой деплой VTB-части невозможен до Б-1 (нет реквизитов). Преfflight-часть можно выкатить — но **только по явному решению заказчика** + ручной прогон `billing:preflight-initial-sweep` на проде с предупреждением клиентам. - [ ] **Step 4: Обновить `ПИЛОТ.md`** (если/когда выкачено на боевой liderra.ru). @@ -2677,7 +2650,7 @@ REQUIRED SUB-SKILL: `superpowers:finishing-a-development-branch`. ## Заметки для исполнителя -**По §3.9 спека (граничные случаи):** главный инвариант «баланс не уходит в минус» держится `LedgerService` (он уже защищён от списания ниже нуля) + ежедневным префлайт. Поэтому отдельное «предупреждение админа при ретро-операциях» (CSV-импорт исторических лидов / ручная правка баланса между cut-off и началом дня) — **опциональная UX-полировка**, не блокер. Если делать — добавить confirm-диалог в `ImportController` и в `AdminTenantsController::adjustBalance` UI с текстом «эта операция может вывести клиента в заморозку на следующем cut-off». Минимально — можно вынести в отдельную мелкую задачу или Спек D. Префлайт отработает корректно в любом случае. +**По §3.9 спека (граничные случаи):** главный инвариант «баланс не уходит в минус» держится `LedgerService` (он уже защищён от списания ниже нуля) + ежедневным преfflight. Поэтому отдельное «предупреждение админа при ретро-операциях» (CSV-импорт исторических лидов / ручная правка баланса между cut-off и началом дня) — **опциональная UX-полировка**, не блокер. Если делать — добавить confirm-диалог в `ImportController` и в `AdminTenantsController::adjustBalance` UI с текстом «эта операция может вывести клиента в заморозку на следующем cut-off». Минимально — можно вынести в отдельную мелкую задачу или Спек D. Преfflight отработает корректно в любом случае. **По UI-задачам (1.10, 2.9, 3.2):** для критичных компонентов (`BalanceCapacityIndicator`) дан полный код. Для остальных Vue-компонентов дан контракт (props / emit / поведение) — исполнитель пишет разметку по образцу первого компонента и существующих паттернов проекта (Vuetify 3, Composition API, ` + + + + diff --git a/app/resources/js/components/billing/BalanceFrozenBanner.vue b/app/resources/js/components/billing/BalanceFrozenBanner.vue new file mode 100644 index 00000000..64379083 --- /dev/null +++ b/app/resources/js/components/billing/BalanceFrozenBanner.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/app/resources/js/components/projects/ProjectLimitOverloadDialog.vue b/app/resources/js/components/projects/ProjectLimitOverloadDialog.vue new file mode 100644 index 00000000..d847a516 --- /dev/null +++ b/app/resources/js/components/projects/ProjectLimitOverloadDialog.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/resources/js/layouts/AppLayout.vue b/app/resources/js/layouts/AppLayout.vue index 2233c0b0..072df2e3 100644 --- a/app/resources/js/layouts/AppLayout.vue +++ b/app/resources/js/layouts/AppLayout.vue @@ -14,16 +14,19 @@ import { RouterView, useRoute } from 'vue-router'; import { useAuthStore } from '../stores/auth'; import { useNotificationsStore } from '../stores/notifications'; import { useRemindersStore } from '../stores/reminders'; +import { useTenantStore } from '../stores/tenantStore'; import { usePolling } from '../composables/usePolling'; import { POLLING_INTERVAL_MS, POLLING_REMINDERS_INTERVAL_MS } from '../constants/polling'; import AppSidebar from '../components/layout/AppSidebar.vue'; import AppTopbar from '../components/layout/AppTopbar.vue'; import DevIndexBadge from '../components/DevIndexBadge.vue'; import CommandPalette from '../components/layout/CommandPalette.vue'; +import BalanceFrozenBanner from '../components/billing/BalanceFrozenBanner.vue'; const auth = useAuthStore(); const notifications = useNotificationsStore(); const reminders = useRemindersStore(); +const tenant = useTenantStore(); const route = useRoute(); const drawerOpen = ref(true); @@ -60,12 +63,19 @@ async function loadReminderCounts(): Promise { await reminders.refreshCounts(); } +async function loadBalanceStatus(): Promise { + if (!auth.user) return; + await tenant.load(); +} + onMounted(() => { void loadNotifications(); void loadReminderCounts(); + void loadBalanceStatus(); }); usePolling(loadNotifications, { intervalMs: POLLING_INTERVAL_MS, enabled: true }); usePolling(loadReminderCounts, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true }); +usePolling(loadBalanceStatus, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabled: true });