X-Frame-Options SAMEORIGIN + X-Content-Type-Options nosniff + Referrer-Policy на все web-ответы (go-live аудит 17.06). CSP вынесен отдельно (SPA Vue+Vuetify). TDD-тест на публичном /.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Строка-приветствие показывала захардкоженную рыбу: +3 новых лида с утра,
сегодня 11 / вчера 38, средняя стоимость 2 248 руб. Числа ни к чему не были
привязаны — остаток прототипа Sprint 4.
Бэкенд: DashboardController.summary отдаёт avg_lead_cost_rub — среднее
фактически списанных rub-сумм за окно периода: AVG price_per_lead_kopecks
WHERE charge_source rub делить на 100; null если в окне нет rub-списаний.
Тот же источник, что карточка сделки F2.
Фронт: DashboardPageHead принимает пропы сегодня/вчера/средняя; сегодня и
вчера берутся из activity.points последняя точка сегодня; средняя из
avg_lead_cost_rub, прочерк при null. Размытое +3 с утра убрано.
TDD: 2 Pest DashboardSummaryTest 10/10 + 4 vitest DashboardPageHead;
полная фронт-сюита 959 passed / 3 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
История транзакций в обзоре биллинга показывала пустой столбец «Операция»:
списания за лид LedgerService создаёт без description, а таблица выводила
поле как есть без запасного текста. Добавлен ярлык по типу операции
с приоритетом сохранённого description. Косметика отображения,
денежных значений не касается. TDD: 2 vitest, 955 passed / 3 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дашборд считал «хватит на дни» от legacy balance_leads (≈0 для рублёвых тенантов)
и расходился с биллингом. Введён общий RunwayCalculator; оба контроллера считают
runway от affordable leads (рубли→лиды по тарифу, BalanceToLeadsConverter). Фронт
DashboardView больше не режет число дней до 7 сегментов полосы. TDD: 4 Pest нового
сервиса + обновлён DashboardSummary + 1 vitest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend: GET /api/deals/{id} отдаёт cost_kopecks — снимок rub-списания из
lead_charges по deal_id, либо null для prepaid/не списано. Frontend: ApiDeal.cost_kopecks
→ MockDeal.costKopecks → карточка DealDetailBody показывает formatCost(costKopecks/100)
либо прочерк вместо вводящего в заблуждение 0 рублей. TDD: 3 Pest + 4 vitest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Поле Город добавлено в секцию Параметры DealDetailBody со значением deal.city,
прочерк при пустом. TDD: 2 теста в DealDetailBody.spec.ts. Чистое отображение,
денежных полей не касается.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DealDetailDrawer: default для tenantId (require-default-prop). AdminPdSubjectRequestsView: v-slot:[...] в #[...] (v-slot-style, auto-fix). 2 region-спека: disable-комментарий no-explicit-any для VueWrapper-кастов F-3 - по конвенции 9 соседних тестов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 файла одной пачкой. Исключены чужие зоны: CLAUDE.md, .claude/settings.json, docs/observer/.pii-counters.json.
gitleaks staged: no leaks found. Не верифицировано тестами - сохранение труда в историю.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-contained app-namespace artisan command (NEVER on production) that funds local imitation clients on a shared B2 supplier, disables DaData (region from tag), rebuilds the routing snapshot, then injects synthetic leads through the real RouteSupplierLeadJob so deals/charges/notifications appear for hands-on UI review. The lead payload encodes the supplier unique_key as a domain so RouteSupplierLeadJob re-resolves the real supplier (parseProjectField then resolveOrStub). Test asserts exit 0 + new deals.
ImitationTestCase::seedPhoneRange used non-existent columns (range_from/range_to/region_name) and omitted the required import_id FK, so every Россвязь-branch test that called it failed. Now seeds a phone_ranges_imports anchor row and inserts phone_ranges with the real columns (def_code/from_num/to_num/operator/region/subject_code/imported_at/import_id), mirroring the verified RossvyazPrefixLookup parsing. Found during Task 5.
Restores a working migrate:fresh without the reverted blanket catch-all. (1) MonthlyPartitionManager::ensureMonth skips a partitioned table whose parent does not exist yet (targeted pg_class relkind='p' guard) instead of crashing — the initial schema-load runs partitions:create-months before later delta-migrations create their own partitioned tables. (2) migration 0001 runs with $withinTransaction=false so the schema.sql DDL is committed before partitions:create-months opens its second pgsql_supplier connection. (3) re-applies the clean idempotency guards on add_balance_freeze (DROP POLICY IF EXISTS) and add_paused_at (column/index existence checks) since schema.sql already contains those objects. migrate:fresh now rebuilds liderra_testing cleanly; MonthlyPartitionManagerTest 15/15 incl. new resilience guard test.
Reverts 6f81d136 production-migration edits (load_initial_schema withinTransaction=false + try/catch, idempotency guards) and coupled migrate:fresh guard tests to baseline; imitation suite uses DatabaseTransactions on a pre-migrated DB so the reverted migrate:fresh resilience is not needed for Phase 1. Also drops the orphaned ensureMonth webhook_log test (webhook_log removed from PARTITIONED_TABLES in 2026_05_24_140000_drop_legacy_webhook_artefacts).
deals:backfill-region-city fills deals.city from the lead resolved_subject_code (deals -> supplier_lead_deliveries -> supplier_leads) for deals where city is still empty, idempotently and across all tenants (BYPASSRLS). --dry-run reports the count without writing. Whitelisted in artisan-run.yml (dry-run read-only; real run requires confirm_apply). TDD: +4 tests GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The UI «Город» column binds to deals.city but nothing ever populated it — the region was only stored as a numeric code on supplier_leads + the resolution log. RouteSupplierLeadJob now writes the resolved subject name (RussianRegions::CODE_TO_NAME) into deals.city on deal creation (the lead's real region, even if subject_code is substituted on routing step 3), and updates it in the CSV-merge branch when the webhook resolution outranks the tag. New deals now display the region. TDD: +2 tests in RouteSupplierLeadJobTest; 24 job tests GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test env (`SharesSupplierPdo` trait + postgres superuser) обходит RLS, поэтому
trigger `audit_chain_hash()` в тестах пишет global chain, не per-tenant. Это
расхождение с prod (где RLS активен и trigger пишет per-tenant) валидно — но
делает pre-rebuild sanity-check невыполнимым assumption'ом.
Multi-tenant test теперь проверяет только self-consistency post-rebuild:
rebuild должен produce chain matching своему partition_clause.
Pre-Task-4 (global LAG): post-rebuild verify с PARTITION BY tenant_id → mismatch
→ RED (текущее состояние).
Post-Task-4 (per-tenant LAG): post-rebuild verify с PARTITION BY tenant_id →
match → GREEN.
Prod RLS-aware trigger semantics валидируется live `audit:verify-chains`, не в
этом тесте.
Ref: docs/superpowers/plans/2026-05-29-audit-rebuild-per-tenant-fix.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 3 плана 2026-05-29-audit-rebuild-per-tenant-fix.md.
3 новых сценария в AuditRebuildChainTest.php:
1. multi-tenant — 2 tenants, 4 rows interleaved, rebuild from firstId →
chain должна остаться intact per-tenant. RED: fails на pre-rebuild
sanity-check (preMismatches=1) — в test env trigger пишет НЕ per-tenant
chain (SharesSupplierPdo trait → BYPASSRLS). Task 4 имплементер должен
разобрать: либо trigger в test env починить (RLS-aware), либо тест
адаптировать к фактической семантике pgsql_supplier.
2. BYPASSRLS auth_log — INSERT direct через pgsql_supplier, partition_clause=''
(global chain within partition). Сейчас PASS случайно (single global LAG
совпадает с tенущим rebuild semantics).
3. single-row partition — 1 tenant, 1 row, rebuild → должна работать.
Сейчас PASS случайно.
+ new const AUTH_LOG_ROW_EXPR mirror'ит AuditChainConfig::TABLES['auth_log'].
Регрессия narrow: 7 tests / 6 passed / 1 failed (RED expected).
Ref: docs/adr/ADR-018-audit-chain-per-tenant-semantics.md
docs/superpowers/plans/2026-05-29-audit-rebuild-per-tenant-fix.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 2 плана 2026-05-29-audit-rebuild-per-tenant-fix.md.
Regression-safe refactor: drop private TABLE_CONFIG const + buildRowExpression()
helper, заменить на чтение AuditChainConfig::TABLES (создан в Task 1, commit
4cfd9f6b) + AuditChainConfig::rowExpression($table). Поведение не изменилось —
тот же baseline regression Pest (9 passed pre-refactor → 10 passed post-refactor;
+1 = регрессия-guard VerifyAuditChainsTest.php flipped fail→pass; 2 pre-existing
errors orthogonal к Task 2).
VerifyAuditChainsTest.php — TDD regression guard на cleanness рефактора: проверяет
полноту AuditChainConfig::TABLES (6 таблиц), корректность rowExpression() для
всех таблиц, и отсутствие private TABLE_CONFIG const после refactor'а.
Ref: docs/adr/ADR-018-audit-chain-per-tenant-semantics.md
docs/superpowers/plans/2026-05-29-audit-rebuild-per-tenant-fix.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>