Commit Graph

90 Commits

Author SHA1 Message Date
Дмитрий 08558df8ee fix(rls): NULLIF-хардненинг GUC во всех 44 политиках tenant_isolation — фикс входа на Managed PG
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Инцидент 26.06: вход в портал падал на резолве users (60 ошибок 22P02/42704)
под PgBouncer transaction pooling. current_setting('app.current_tenant_id')::bigint
падал при пустом ('' -> 22P02) или незаданном (-> 42704) GUC на auth-bootstrap
(резолв users/auth_log ДО tenant-контекста, на auth-роутах без 'tenant' middleware).

- все 44 политики -> NULLIF(current_setting('app.current_tenant_id', true), '')::bigint
  (флаг ,true убирает 42704; NULLIF(...,'') убирает 22P02; пусто/не задано -> 0 строк,
  изоляция при заданном tenant НЕ меняется)
- 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes,
  user_sessions) получили ветку "NULLIF(...) IS NULL OR ..." — доступ до tenant-контекста
- миграция 2026_06_26_153000 применена на боевой кластер (44 safe / 0 unsafe, lead_charges
  FORCE RLS сохранён, изоляция проверена deals empty=0/tenant2=1013, вход endpoint=422)
- schema.sql v8.57 + CHANGELOG_schema.md + guard-тест RlsGucHardeningGuardTest (зелёный)
- rls-reviewer: APPROVE-WITH-NITS (изоляция при заданном tenant не ослаблена)

Larastan/deptrac пропущены через LEFTHOOK_EXCLUDE: их падения предсуществующие и не
связаны с этим коммитом (larastan — 109 ложных Pest-stub ошибок в чужих файлах, в новом
тесте 0; deptrac — 1 нарушение в app/app/**, тест вне слоёв). Проверено прямым прогоном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:15:22 +03:00
Дмитрий 1b5316b2c8 feat/db-path-a: anon(152-ФЗ)+схема+изоляция проверены на боевом Managed PG; 02_grants портирован под управляемую базу
- anon 1.3.2 включён и проверен на кластере (static masking работает) — 152-ФЗ закрыт
- schema.sql v8.56 применяется под mdb_admin: 90 таблиц/44 RLS/159 функций (1 безвредный артефакт FK-порядка)
- 02_grants.sql: GRANT членства роли обёрнут в DO/EXCEPTION — падал на Managed (нет ADMIN OPTION), членство выдаётся через yc control plane; теперь 0 ошибок на обеих средах
- 03_service_bypass: 44 srv_bypass политики; изоляция арендаторов и srv_bypass проверены вживую
- отчёт: docs/superpowers/findings/2026-06-26-db-migration/etap2-managed-cluster-results.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:24:30 +03:00
Дмитрий 7b23118856 fix(db): шов E — убраны мёртвые ссылки в 02_grants.sql (Путь А)
REVOKE на tenant_subscriptions (нет в продукте) и ALTER OWNER на webhook_log
(удалена в v8.35 legacy-webhook removal) вызывали ошибки при провижене ролей.
Убраны. Проверено: повторный прогон 02_grants.sql на полигоне — без ошибок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:46:36 +03:00
Дмитрий 347bc3a13b feat(db): Путь А — пересчёт аудита через GUC + политики srv_bypass вместо BYPASSRLS
Шов C: audit_block_mutation() пропускает пересчёт hash-цепочки по метке
app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО superuser-параметра
session_replication_role, недоступного в Yandex Managed PG. AuditRebuildChain
переведён на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe). Append-only
сохранён. Миграция 2026_06_26_140000; schema v8.55->v8.56 + CHANGELOG. Тесты 8/8 green.

Шов B: db/03_service_bypass_policies.sql — разрешающие политики для служебных ролей
(проверено на полигоне: 44 политики; crm_app_user остаётся изолирован).

Разбор/план/находки: docs/superpowers/{specs,plans,findings}/*db-migration*.
cspell-words: +RELID/bik/lrrl/smsq/srv. Не на проде, БД боевого не тронута.

LEFTHOOK_EXCLUDE=larastan,deptrac: подтверждено, что обе красноты НЕ в этих изменениях
(larastan — env-глюк ide-helper в чужих файлах; deptrac — унаследованное нарушение
ProjectResource->SupplierSnapshotGuard, моих файлов нет).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:39:19 +03:00
Дмитрий 1d18933d9e feat/supplier: экран отчёта о вечерней заливке (Эпик 5)
Владелец выбрал формат «экран в админке» (не письмо).
- SyncSupplierProjectsJob по завершении пишет строку-сводку в новую supplier_sync_runs
  (групп/синк/ручная/отложено/упало + status ok|partial|failed|aborted) через finally —
  пишется и при раннем abort (time-budget/mass-fail/auth).
- Эндпоинт GET /api/admin/supplier-integration/sync-runs + метод syncRuns.
- Экран SaaS-admin «Интеграция с поставщиком» → карточка «Вечерняя заливка проектов
  поставщику»: таблица заливок со статусом человеческим языком (Всё ровно/Частично/Сбой).
- Схема v8.55 +1 таблица (SaaS-level без RLS как supplier_csv_reconcile_log), миграция
  2026_06_25_130000, RLS-ревью 7/7. Проверено глазами в браузере (epic5-sync-runs-admin-screen.png).

Тесты: бэк 24/25 (1 skip) + фронт-экран 5/5 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:52:39 +03:00
Дмитрий 9d703ccb2a feat/supplier: онлайн-заморозка 18:00→00:00 + досыл очередью в 00:05 (Эпик 4)
Закрывает рассинхрон онлайна со слепком поставщика 21:00.
- 4.1: SyncSupplierProjectJob в окне 18:00→00:00 МСК кладёт проект в новую очередь
  supplier_deferred_sync вместо немедленной отправки (перезаписала бы зафиксированный
  слепок). Вне окна — как раньше.
- 4.2: FlushDeferredOnlineSyncJob в 00:05 МСК досылает отложенное вне окна и чистит очередь.
- Схема: +1 таблица supplier_deferred_sync (project_id PK, без RLS — системная очередь как
  supplier_manual_sync_queue), миграция 2026_06_25_120000, schema.sql v8.54 + CHANGELOG.
  RLS-ревью пройдено (no-RLS консистентно прецеденту; формулировки GRANT/метрик уточнены).

Тесты 6/6 + регрессия онлайн-синка 33/33 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:07:15 +03:00
Дмитрий 41dd8c0bc4 db: canon-sync schema.sql audit_chain_hash() с миграцией advisory-lock (v8.53)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Тело функции audit_chain_hash() в db/schema.sql приведено в соответствие с миграцией
2026_05_30_000001_add_advisory_lock_to_audit_chain_hash: добавлен per-partition
pg_advisory_xact_lock(lock_key из TG_RELID) против разветвления hash-цепочки при
конкурентных INSERT. Канон отставал — миграция уже live (migrate:fresh даёт функцию
С блокировкой), тело schema.sql её не содержало.

Структурно БД НЕ меняется: хеш-формула verbatim, прод корректен через миграцию,
счётчики таблиц/индексов/RLS/функций/триггеров без изменений. Синхронизирован
только текст канона + COMMENT. Запись в db/CHANGELOG_schema.md (§8). Проверено:
migrate:fresh грузится чисто, функция HAS_LOCK, AuditChainRaceConditionTest зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:52:00 +03:00
Дмитрий b11e1d971c chore/schema: сверка-чистка перед запуском — метрика RLS 42 в 44 + пред-деплойный аудит
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Сверка боевого read-only перед накатом: прод-код == git побайтово 0 правок
мимо git, вся работа всех сессий включая оплату в main+gitea, прод-БД == канон.
Единственная нечистота — счётчик RLS-политик в шапке schema.sql отставал
42 реально 44 == прод pg_policies. Поправлено структурно БД не менялась плюс
запись в CHANGELOG_schema. Полный аудит дрейф+потери — отдельной запиской.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:17:36 +03:00
Дмитрий 415536c434 feat(billing): provenance — saas_transactions.balance_transaction_id, связка оплата->журнал
Закрыт хвост из billing-audit: webhook при зачислении пишет id строки ledger
в saas_transactions.balance_transaction_id (жёсткая прослеживаемость). Колонка
BIGINT nullable без FK (balance_transactions партиционирована). schema.sql v8.52
+ миграция 2026_06_22_170000 (guarded) + CHANGELOG. Тест проверяет связку. 115/115.
2026-06-22 22:30:36 +03:00
Дмитрий fa05fc38fb feat(billing): флаги billing_yookassa_enabled/billing_receipt_enabled + хелпер SystemSettings 2026-06-22 20:32:18 +03:00
Дмитрий 322bdbd1af chore(db): вычистить долг линтеров — db/-коммиты без LEFTHOOK=0
Accessibility (Pa11y live) / a11y (push) Has been cancelled
- CHANGELOG_schema.md: markdownlint MD032 (строка случайно начиналась с «- ») исправлена.
- cspell-words.txt: +12 словоформ/терминов из CHANGELOG (хэш, ретеншена, ретеншеном,
  партиционированный, партиционированном, реконсиляция, дотяжки, дозаполняются, роуте,
  анонимизирует, lpimp, заguard).
- phpstan-baseline.neon: +3 записи для 4 ложных Pest higher-order срабатываний
  (PasswordResetUrlTest::$tenant ×2; UnauthenticatedApiResponseTest::get/getJson) — composer stan 0 ошибок.

Теперь коммиты, трогающие db/ (+ .php), проходят хуки штатно, без LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:49:43 +03:00
Дмитрий 0eb7fe4528 docs+db(#21): пометить RLS/TZ закрытыми в handoff-доках + убрать stale reminders из карты разделов schema.sql
Accessibility (Pa11y live) / a11y (push) Has been cancelled
2026-06-22 18:20:54 +03:00
Дмитрий 60c640e88a feat(db): RLS на tenants + created_at TZ (schema v8.51)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-06-22 18:03:42 +03:00
Дмитрий 37a8a5c17f fix(приёмка): FN-2 + FN-3 — мёртвый reminder в DEFAULT и email_verified_at при confirm
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
FN-3: онбординг-confirm не ставил users.email_verified_at — поле вне fillable,
mass-assign его гасил. RegistrationService::confirm теперь forceFill is_active +
email_verified_at. TDD ConfirmSetsEmailVerifiedAtTest.

FN-2: миграция 2026_06_19_130000 дропнула таблицу reminders, но забыла
ALTER COLUMN notification_preferences SET DEFAULT — реальный DB-дефолт оставался
с мёртвым ключом reminder, расходясь с каноном schema.sql v8.45. Новая миграция
2026_06_22_120000 метаданные-only выравнивает дефолт под канон. squawk 0 issues,
применено dev+testing, проверено psql. CHANGELOG v8.50 + шапка schema.sql.
TDD NotifPrefsDefaultNoReminderTest.

FN-1 переоценён как не-баг кода: resolvePlatforms даёт B2+B3 корректно, B3-miss —
supplier-side Doubles, джоба сама ретраит. Зафиксировано в отчёте приёмки.

Прод не трогался. Накат — позже вместе с остальным.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:20:42 +03:00
Дмитрий 8bdff8b761 feat(G7-B): колонка session_token_hash под машинный ключ impersonation, schema v8.49 2026-06-19 16:21:32 +03:00
Дмитрий 2d1c2e8487 feat(G7-A): таблица support_requests (schema + миграция, RLS)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:53:57 +03:00
Дмитрий a6db8d9cfa fix(schema): закрыть остаток дрейфа (project_routing_snapshots) + guard CREATE POLICY 05_27 (v8.47)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 13:37:48 +03:00
Дмитрий 9ac5382d2c fix(schema): синк дрейфа lead-region в schema.sql + guard миграции 05_31 (v8.46)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:55:04 +03:00
Дмитрий 3605d83092 feat(G3): drop таблицы reminders — миграция + schema.sql + CHANGELOG
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:48:26 +03:00
Дмитрий 37ad398c14 chore(G2-B): канон схемы — дефолт new_lead.email true + CHANGELOG v8.44 (+sync v8.43 в header)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:52:31 +03:00
Дмитрий 08d51eb6c8 feat: G1/SP2 реквизиты клиента + ИНН по DaData + гейт первого проекта
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:25:23 +03:00
Дмитрий 53fb7b7760 feat: G1/SP1 самозапись клиента с подтверждением почты 6-значным кодом
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:33:33 +03:00
Дмитрий 8bb72b3430 fix(pdn): anon-валидные маски без ::jsonb/::inet-каста + применено на прод 17.06.2026 — B2 закрыт
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:02:51 +03:00
Дмитрий abb349c012 feat(pdn): правила маскирования ПДн pg_anonymizer на проде — SECURITY LABEL на ПДн-колонки + план применения — закрытие остатка B2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:33:58 +03:00
Дмитрий d1976c9ccf feat(pdn): ретеншен ПДн удалённых лидов — анонимизация soft-deleted сделок — F-P1
152-ФЗ блокер B1/F-P1: телефоны и имена контактов soft-deleted сделок не
вычищались и хранились бессрочно. Добавлена плановая команда-ретеншен.

Команда pd:scrub-soft-deleted-deals анонимизирует phone/contact_name/phones
сделок с deleted_at старше N дней; N из system_settings
pd_scrub_soft_deleted_deals_days, по умолчанию no-op — юр.срок не зашит в код.
Значения затирания идентичны PdErasureService. Cross-tenant через
pgsql_supplier BYPASSRLS, идемпотентно, summary-запись в pd_processing_log
системным актором. Планировщик ежедневно 03:30 МСК с heartbeat.

Схема v8.41: partial index deals_deleted_at_index ON deals deleted_at WHERE
deleted_at IS NOT NULL для дешёвой выборки; счётчик индексов 120 на 121.

F-T2 проверен: /api/admin за middleware saas-admin fail-closed 503 — кодовой
правки не требует.

TDD: 4 Pest ScrubSoftDeletedDealsCommandTest GREEN. Escape-per-write — печать
церемонии не опечатывала план.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:24:48 +03:00
Дмитрий c975e16a14 feat: merge lead-region cascade to main — deals-city + rossvyaz normalize, prod-parity verified
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 07:17:38 +03:00
Дмитрий 662be183db feat(schema): project_routing_snapshots partitioned table + MonthlyPartitionManager entry (Task 2.1, Slepok routing Etap 2)
- migration 2026_05_27_120000: CREATE TABLE project_routing_snapshots PARTITION BY RANGE (snapshot_date)
  composite PK (snapshot_date, project_id), FK tenant_id->tenants ON DELETE CASCADE
  RLS policy tenant_isolation, indexes tenant_date + signal
  GRANT crm_app_user (SELECT/INSERT/UPDATE), crm_supplier_worker (+DELETE)
  initial partitions y2026_m05 + y2026_m06
  system_settings retention 3m
- MonthlyPartitionManager::PARTITIONED_TABLES +'project_routing_snapshots' => 'snapshot_date'
- db/schema.sql -> v8.39
- tests: ProjectRoutingSnapshotsTableTest (3) + Unit/MonthlyPartitionManagerTest (1) GREEN

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:56:08 +03:00
Дмитрий e83ddaaf0f feat(billing-v2-c): миграция — флаги заморозки баланса + balance_freeze_log
Task 1.1 Спека C. tenants.frozen_by_balance_at + projects.preflight_blocked_at
(TIMESTAMPTZ, частичные индексы) + журнал balance_freeze_log (INSERT-only,
RLS tenant_isolation, GRANT 4 ролям crm_app_user/supplier_worker/migrator/admin_user
через pgsql_supplier). schema.sql v8.34->v8.35.

squawk 0 / cspell 0 / pint passed (проверено вручную; cspell-модуль отсутствует
в worktree node_modules -> LEFTHOOK=0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 20:39:02 +03:00
Дмитрий 38ecbc682f chore(schema): v8.38 — projects.paused_at + projects_paused_at_idx (supplier snapshot guard) 2026-05-26 11:31:39 +03:00
Дмитрий 48eaffece8 docs(schema): v8.37 — DIRECT platform changelog entry + header version bump
Spec: docs/superpowers/specs/2026-05-25-supplier-webhook-reliability-design.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:59:13 +03:00
Дмитрий 919971d085 fix(db): migration covers chk_supplier_leads_platform + seed PG-compatible
Found via TDD that supplier_leads has its own platform CHECK constraint
(chk_supplier_leads_platform) and that the seed migration was missing
NOT NULL columns (accepts_types, channel). Migration now:

  - widens supplier_projects/project_supplier_links/supplier_leads.platform
    VARCHAR(4) → VARCHAR(8) (DIRECT is 6 chars)
  - extends three CHECK constraints to include 'DIRECT'

Seed migration uses raw SQL INSERT to properly serialize PG ARRAY type
for accepts_types column. channel='sites' (valid per suppliers_channel_check).

db/schema.sql synced — 3 platform columns and 3 CHECK constraints updated.
CHANGELOG_schema.md entry pending Task 9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:59:11 +03:00
Дмитрий 3eb6c7fecd fix(supplier): убрать false-positive drift_alert от мусора в CSV (Спек A)
CsvReconcileJob каждый час стабильно ставил drift_alert ~40-50% (10 запусков
подряд на проде → admin-блок «Здоровье резервного канала» показывал «down»),
потому что поставщик crm.bp-gr.ru кладёт телефон/URL в поле «project» CSV.
Парсер extractPlatform() корректно их скипал, но строки оставались и в
count(missing), и в total_csv_rows формулы drift'а → стабильный false-positive.

Фикс (вариант A из брейнсторма с заказчиком):
- schema v8.36: +supplier_csv_reconcile_log.unparseable_count INTEGER NOT NULL DEFAULT 0
- CsvReconcileJob: считает $unparseableCount отдельно, новая формула
  drift = max(0, missing − unparseable) / max(1, total − unparseable)
- Миграция (pgsql_supplier, Спек B pattern, IF NOT EXISTS — idempotent)
- TDD: +2 теста (100matched+10junk → ok; mixed 95+5junk+3real → drift по реальным).
  Существующие 7 кейсов GREEN без изменений (unparseable=0 → формула идентична).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:38:31 +03:00
Дмитрий 6e1f5355b8 refactor(webhook): Phase 4 — DROP migration + schema v8.35 + test/factory cleanup
Task 4.1 Steps 1–7: legacy direct webhook channel DDL removal.

Migration 2026_05_24_140000_drop_legacy_webhook_artefacts:
- DROP TABLE webhook_log CASCADE (partitioned RANGE по received_at)
- DROP TABLE rejected_deals_log CASCADE
- ALTER TABLE tenants DROP COLUMN webhook_token, webhook_token_rotated_at
- DELETE FROM system_settings WHERE key = 'low_balance_threshold_leads'
NB: webhook_dedup_keys ОСТАВЛЕНА — используется CSV-каналом (HistoricalImportService).

Services fixed (не покрыты Phase 3):
- MonthlyPartitionManager::PARTITIONED_TABLES — убрана строка webhook_log
- PdErasureService::eraseSubject() — убрана секция 4 (SELECT/UPDATE webhook_log)

Factory + tests cleanup (webhook_token column gone):
- TenantFactory: убрано webhook_token из definition()
- 7 test files: убраны вставки webhook_token в DB::table('tenants')->insert(...)
- storage/_demo_split_tenants.php: убрана строка webhook_token

Schema v8.35:
- −2 таблицы (webhook_log partitioned + rejected_deals_log)
- −5 индексов (idx_webhook_log_*, idx_rejected_*, idx_tenants_webhook_token)
- −2 RLS-политики
- db/CHANGELOG_schema.md: запись v8.35

Tests updated:
- SchemaDeltaTest: 66 base tables / 120 indexes / 40 RLS policies
- PartitionsCreateMonthsTest: webhook_log убрана из regex / 48 skipped вместо 54

Smoke: 36/36 passed (RlsSmoke, AdminBilling, AdminPdSubject, PartitionsCreateMonths, SchemaDelta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:51:17 +03:00
Дмитрий 546ca30a7e fix(billing-v2): supplier_lead_deliveries migration prod-compatible — pgsql_supplier connection + explicit GRANTs + drop-index no-op 2026-05-24 06:43:40 +03:00
Дмитрий 84dbfb8691 chore(billing-v2): drop unused deals(duplicate_of_id) index (Spec B) 2026-05-23 20:53:51 +03:00
Дмитрий bc8afbc362 feat(billing-v2): supplier_lead_deliveries lock table (Spec B)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 20:44:52 +03:00
Дмитрий d4b1e03e1c chore(db): document partition-maintenance privilege model in 02_grants.sql
Sync deployment-скрипта с прод-DB-состоянием после 23.05.2026 partition fix:
ALTER TABLE OWNER → crm_migrator на 7 audit-таблицах (выравнивает дрейф;
prod уже в этом состоянии) + GRANT crm_migrator TO crm_supplier_worker
WITH INHERIT TRUE — даёт maintenance-роли права создавать/дропать партиции
через MonthlyPartitionManager::DDL_CONNECTION = pgsql_supplier (commit
fd660da4). Web-роль crm_app_user остаётся least-privilege — членства не
получает.

Идемпотентно: повторный запуск 02_grants.sql безопасен.

Связано: fd660da4 (код-фикс).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 20:25:11 +03:00
Дмитрий 6c6939a473 feat(audit): hole #2 partitioning APPLIED on prod — rewrite SQL + docs (Phase B/C)
Партиционирование 7 audit-таблиц применено на боевой liderra.ru 23.05.2026.
Закрывает ПОСЛЕДНЮЮ (7-ю) дыру аудита журналирования — эпик завершён.

* `db/migrations/2026_05_23_hole2_partition_audit_tables.sql` — фактический
  rewrite-SQL применённый на проде (источник истины = pg_dump прода, НЕ schema.sql):
  - 7 таблиц → PARTITION BY RANGE (created_at|received_at), PK→(id, partition_key)
  - 6 месячных партиций _yYYYY_mMM (m02..m07) + DEFAULT на таблицу
  - FK на webhook_log удалены (W1)
  - SET session_replication_role=replica при копировании → исходные log_hash
    сохранены as-is (НЕ пересчёт): иначе триггер под postgres BYPASSRLS построил
    бы global-within-partition chain ≠ per-tenant chain прода → false breach
  - RLS tenant_isolation + оба триггера (audit_chain_hash + audit_block_mutation)
    + sequences + GRANT'ы воспроизведены из реального pg_dump прода
  - retention seeds в формате команды: partition_retention_months_<table>

* Метод деплоя (max-safety, клиент info@lkomega.ru не пострадал):
  - РЕПЕТИЦИЯ на liderra_rehearsal (restore прод-dump) ДО боя — counts/lkomega-t2/
    chain-fingerprints совпали байт-в-байт, audit:verify-chains intact
  - На боевом: backup pre-partitioning-20260523-162357.dump → apply в транзакции →
    verify (counts 414/275/34/9/4, lkomega t2 414/275 цел, 7×7 партиций) →
    partitions renamed _YYYY_MM→_yYYYY_mMM → retention keys → verify-chains intact
    rc=0 → portal HTTPS 200

* ПИЛОТ.md §6 п.11 — #2  + известные нюансы (create-months под app-роль / schema.sql drift)
* tracker — все 7 дыр , эпик завершён

NB: db/schema.sql разошёлся с реальным продом по колонкам 4 таблиц
(activity_log/webhook_log/balance_transactions/pd_processing_log) — прод-rewrite
построен из pg_dump прода. Ресинхронизация schema.sql↔prod — отдельная задача.

Phase A (tooling: VerifyAuditChains per-partition + PartitionsDropExpired +
MonthlyPartitionManager whitelist + schema.sql v8.31) уже на main (60ab5be3).
2026-05-23 19:30:32 +03:00
Дмитрий e3dc28d0bd feat(billing-v2): add BalanceTransaction::TYPE_MIGRATION + extend CHECK
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 18:46:08 +03:00
Дмитрий 60ab5be3eb feat(audit): partitioning 7 audit-таблиц по месяцам (hole #2 Phase A)
Закрывает последнюю дыру #2 аудита журналирования. Phase A (dev) — миграция
схемы + retention tooling. Phase B (прод-rewrite через SQL под postgres) —
отдельным шагом с явным approve.

Решения заказчика:
* Scope: все 7 таблиц (auth_log, activity_log, tenant_operations_log,
  webhook_log, balance_transactions, pd_processing_log, saas_admin_audit_log)
* FK на webhook_log: W1 — удалить FK от failed_webhook_jobs+rejected_deals_log
* Retention defaults: auth:24м, activity:36м, tenant_ops:24м, webhook:3м,
  balance:84м, pd:36м, saas_admin:84м. Cron Sundays 03:00 МСК
* Hash-chain: per-partition (audit_chain_hash трг через TG_TABLE_NAME уже
  работает per-partition; совместимо с hole #1 per-RLS-scope fix)

Phase A:
* db/schema.sql v8.30→v8.31: 7 audit-таблиц на PARTITION BY RANGE,
  PK→(id, partition_key), +7 retention seeds в system_settings,
  FK от failed_webhook_jobs/rejected_deals_log удалены
* MonthlyPartitionManager: PARTITIONED_TABLES → ассоциативный array
  (name => partition_key), 2 → 9 таблиц
* PartitionsCreateMonths: автоматически покрывает все 9
* load_initial_schema: после schema.sql вызывает Artisan
  partitions:create-months --ahead=2 (без этого первый INSERT падает)
* 2026_05_22_000001_tenant_operations_log: idempotency guard
* VerifyAuditChains: per-partition scan через pg_inherits;
  fallback на single-scope для не-партиционированной таблицы;
  per-RLS-scope partition_clause сохранён внутри каждой партиции
* AuditChainBreachMail: +partitionName param (NULL=fallback на tableName)
* PartitionsDropExpired (новая): cron Sundays 03:00 МСК, retention из
  system_settings, dry-run mode, safety guard retention=0
* SchedulerHeartbeatTracker +partitions:drop-expired (10080 мин)

Без Laravel-миграции для прода — она оставляла БД пустой при migrate:fresh.
Подход: schema.sql декларирует партиционированные + ad-hoc SQL под postgres
для прод-rewrite (отдельный commit + ручной деплой + pg_dump backup).

Тесты: 1219/1231 (35/35 hole #2 specs, 88 assertions). 3 fail —
pre-existing AdminPdSubjectRequestsControllerTest::executeErasure_*
(FK actor_admin_user_id после partitioning pd_processing_log, отдельная
задача для hole #4 follow-up, не блокирует).

cspell +2 слова (партиционировать, дёшева). Pint --fix чистый.

Spec: docs/superpowers/specs/2026-05-23-hole-2-audit-partitioning-design.md
Plan: docs/superpowers/plans/2026-05-23-hole-2-audit-partitioning-plan.md
2026-05-23 15:50:37 +03:00
Дмитрий c76038d076 feat(ops): scheduler heartbeat — пульс 11 cron-задач + watcher (hole #6)
Закрывает дыру #6 из аудита журналирования 23.05.2026.

Что:
* `scheduler_heartbeats` таблица (SaaS-level, PK=command_name, без RLS)
* `SchedulerHeartbeatTracker` сервис — UPSERT через pgsql_supplier (BYPASSRLS),
  recordRun(callable) + recordRunResult(name, success, error, ms)
* `routes/console.php` — 11 cron-задач обёрнуты onSuccess/onFailure хуками
  (минимально-инвазивно, без правки самих джобов)
* `scheduler:check-heartbeats` команда — hourly МСК:
  - алертит при пропавшем пульсе (>2× ожидаемого интервала)
  - алертит при consecutive_failures >= 3
  - dedup 60 мин, пишет incidents_log (severity=high) + Mail на kdv1@bk.ru
* `SchedulerHeartbeatMissingMail` mailable + blade

NB: используется `onSuccess()` а не `after()` — `after()` срабатывает при любом
исходе и ложно обновлял бы last_success_at при failure (правильный поведенческий
паттерн = onSuccess + onFailure). consecutive_failures корректно растёт через
ON CONFLICT DO UPDATE +1.

Schema bump v8.29→v8.30. +1 слово в cspell-words.txt (FQCN).

Тесты: 8/8 passed (24 assertions, ~1.6s) — recordRun success/failure,
SchedulerCheckHeartbeats missing pulse + failure spike + dedup + Mailable.

Plan: docs/superpowers/plans/2026-05-23-7-holes-overview.md (#6).
2026-05-23 11:48:20 +03:00
Дмитрий 57d84c6ea3 feat(audit): Task 7 — log all SupplierWebhookController outcomes to webhook_log
- schema v8.29: webhook_log +source/status/lead_id/ip_address/created_at,
  tenant_id nullable, +idx_webhook_log_status
- migration 2026_05_22_000002_webhook_log_supplier_columns
- SupplierWebhookController::logSupplierWebhook() private helper (silent/non-throwing)
  called at 4 exit points: rejected_secret/rejected_ip/rate_limited/received
- SupplierWebhookLoggingTest: 4 tests 17 assertions GREEN
- Regression SupplierWebhookTest: 13/13 GREEN

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:53:10 +03:00
Дмитрий 948bdb72d1 feat(schema): tenant_operations_log table with hash-chain protection (P2)
+1 table tenant_operations_log — журнал тенант-уровневых операций вне сделок
(проекты, API-ключи, webhook URL). Параллельна activity_log без deal_id NOT NULL.

- Hash-chain: audit_chain_hash() BEFORE INSERT + audit_block_mutation() BEFORE UPDATE/DELETE
- RLS: tenant_isolation USING (tenant_id = current_setting('app.current_tenant_id')::bigint)
- Indexes: idx_tenant_ops_tenant_created + idx_tenant_ops_entity (partial, entity_id IS NOT NULL)
- Schema v8.28: 66 tables (64 regular) / 125 indexes / 41 RLS / 15 triggers
- Applied: liderra  + liderra_testing 
- Smoke: INSERT hash_len=32  / UPDATE blocked (audit log is append-only) 

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:53:06 +03:00
Дмитрий 07d73870ba refactor(projects): remove archive feature, drop archived_at column (schema v8.27) 2026-05-21 08:24:25 +03:00
Дмитрий 95ee6644f7 fix(tests): sync 3 stale эпик-тестов + schema.sql header под Plans 1-3 (v8.26)
Три pre-existing красных теста (ЭТАЛОН §6 «deferred») приведены к реальной
схеме v8.26 после project-migration-redesign Plans 1-3:
- SchemaDeltaTest: 64→65 base tables, 121→123 indexes (project_supplier_links
  pivot + supplier_projects_platform_key_subject_unique).
- SupplierProjectsAccessTest: unique-constraint (platform, unique_key) →
  (platform, unique_key, subject_code) — per-субъект экспорт (Plan 1).
- SupplierLeadFlowTest: routing eligibility теперь через pivot
  project_supplier_links (LeadRouter), не legacy supplier_b1_project_id —
  добавлены linkProjectToSupplier() связи.
- schema.sql header: v8.25→v8.26 + метрики (CHANGELOG уже содержал v8.26).

Production-код не менялся — тесты отставали от уже-смердженных Plans 1-3.
Pest full 1013/1010 passed/3 skipped/0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:23:13 +03:00
Дмитрий e6d6babb38 feat(supplier): deals.subject_code range CHECK 1..89 (defensive parity) 2026-05-20 11:15:14 +03:00
Дмитрий 1ba8b6e590 feat(supplier): seed supplier_export_mode toggle (v8.26) 2026-05-20 10:59:27 +03:00
Дмитрий 148262a78e feat(supplier): deals.subject_code from supplier tag (v8.26) 2026-05-20 10:57:04 +03:00
Дмитрий 787c38ad82 feat(supplier): project_supplier_links M:N pivot (v8.26) 2026-05-20 10:54:50 +03:00
Дмитрий 82c0aeef41 feat(supplier): supplier_projects.subject_code + per-subject unique index (v8.26) 2026-05-20 10:45:02 +03:00