Commit Graph

103 Commits

Author SHA1 Message Date
Дмитрий d4e055d4f5 feat(supplier): таблица supplier_order_checks — история итоговой проверки заказа
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:55:09 +03:00
Дмитрий daa41bcecf feat(sales): пересмотр состава тарифов портала продаж — 2 вида
Решение заказчика 03.07.2026: два вида тарифа вместо трёх — суточный
оклад daily_salary и процент от пополнений topup_step. Убраны
percent_oborot и fix_per_client.

Миграция 2026_07_03_120000 меняет CHECK sales_tariffs.kind, освобождает
FK sales_users.current_tariff_id и sales_client_assignments.tariff_id,
удаляет тарифы уходящих видов. SalesEarningsService для уходящих видов
возвращает 0 по default-ветке. Снимки привязок не трогаются.

Обновлены контроллеры тарифов и дохода, сервисы Earnings и Metrics,
фронт SalesTariffsView и api/sales.ts, демо-сид, тесты бэка и фронта,
CHANGELOG схемы v8.61 и спека портала.

Не на проде: ветка feat/sales-portal-demo.

Проверка: Sales Feature 169/169, Vitest SalesTariffs 10/10, Larastan 0.

Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
2026-07-05 19:25:19 +03:00
Дмитрий 5a590259fd feat(sales): демо-сид портала продаж (dev/test only)
Task 8.2 — SalesDemoSeeder: начальник Орлов + 3 менеджера (Лебедев в
отпуске) + 4 тарифа (3 вида) + привязки (к существующим тенантам,
round-robin, снимок тарифа) + выплаты как в прототипе.
- Защита от прода: throw если app env = production.
- Идемпотентно (firstOrCreate/updateOrCreate; выплаты только если их нет —
  append-only). DatabaseSeeder не трогается — запуск вручную
  `php artisan db:seed --class=SalesDemoSeeder`.

NB: запускать в dev-БД, НЕ в liderra_testing (портит baseline dept-count
тестов). Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:56:07 +03:00
Дмитрий bdcb82f8f7 feat(sales): guard sales (sanctum) + provider + таблица токенов
Task 0.3: guard 'sales' (driver sanctum, provider sales_users). Тест SalesGuardTest 3/3 (valid Bearer→200, без токена→401, мусор→401). Миграция personal_access_tokens (Sanctum Bearer; раньше не было — основной кабинет SPA cookie), DDL через pgsql_supplier, гранты crm_admin_user, CHANGELOG v8.60. Larastan baseline: +SalesGuardTest (Pest TestCall false-pos), SetTenantContext int→mixed (второй provider расширил тип $request->user()). План: admin-db ДО auth:sales. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:36:13 +03:00
Дмитрий b0794fbef6 feat(sales): схема портала отдела продаж — 5 таблиц + append-only выплаты
Task 0.1: миграция sales_tariffs/sales_users/sales_client_assignments/sales_attachment_requests/sales_payouts (SaaS-level, без RLS, фильтр по владению в коде). Append-only триггер. Гранты crm_admin_user. DDL через pgsql_supplier. CHANGELOG_schema v8.59. Разрешение хозяина: один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:57:22 +03:00
Дмитрий 88e816c576 feat(балансы): backend плитки балансов внешних сервисов
Ежедневный контроль баланса DaData/Поставщик/Yandex Cloud плиткой дашборда.

- Таблица external_service_balances (pgsql_supplier, BYPASSRLS, last-value upsert)
- BalanceHealth: чистая логика светофора (red <floor или <3д; amber <floor или <7д)
- BalanceProvider+DTO; провайдеры DaData(API)/YC(OAuth→IAM→billing)/Supplier(Playwright)
- RefreshExternalBalancesJob: изоляция провайдеров (try/catch), расписание 06:30 МСК
- AdminDashboardController::balances() + плитка в summary + topup_url (кнопка «Пополнить»)
- Тесты: BalanceHealth, 3 провайдера, джоба, endpoint (102 теста зелёные)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:12:14 +03:00
Дмитрий 7ac9af7c79 feat: убрать лимит по числу проектов — ограничение только по балансу/лидам
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Правило продукта: ограничений по количеству проектов нет, лимит только
по балансу и заказанным лидам. Убран гейт tenants.limits.max_projects
в ProjectService::create и показ лимита проектов на дашборде. Поле limits
оставлено как резерв; max_users и api_rps в коде не используются.

Заодно фикс типа в EditProjectDialog.spec: sampleProject типизирован
настоящим Project, source_locked больше не краснит vue-tsc.

Тесты: ProjectsStore 13/13, DashboardSummary 11/11, DashboardView 8/8,
EditProjectDialog 7/7; vue-tsc чисто; pint чисто; vite build ок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:47:49 +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
Дмитрий 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
Дмитрий 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
Дмитрий fec15a3703 fix(migration): guard CREATE POLICY в tenant_requisites (DROP IF EXISTS) — чинит migrate:fresh
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Accessibility (Pa11y live) / a11y (push) Has been cancelled
G1-миграция guard'ила CREATE TABLE, но не CREATE POLICY (в PG нет IF NOT EXISTS) → коллизия с политикой из schema.sql на migrate:fresh. Тот же дрейф-класс. Теперь migrate:fresh зелёный целиком.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:58:37 +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
Дмитрий 582c02d4a7 feat(G3): убрать reminder из дефолтов notification_preferences (factory+seeder)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:38:58 +03:00
Дмитрий 34981da707 feat(G3): удалить контроллер/роуты/модель/команду/письмо напоминаний
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:37:57 +03:00
Дмитрий c049ab49b6 test(G2-B): UserFactory зеркалит новый дефолт new_lead.email true
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:53:36 +03:00
Дмитрий 01dffd6b30 feat(G2-B): миграция — дефолт new_lead.email true + дотяжка существующих
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:53:07 +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
Дмитрий 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
Дмитрий 9fe5b5f229 imitation phase1 merge
# Conflicts:
#	app/app/Console/Commands/PhoneRangesImportCommand.php
#	app/app/Jobs/RouteSupplierLeadJob.php
#	app/app/Services/LeadRegionResolver.php
#	app/database/migrations/2026_05_31_100000_create_phone_ranges_and_resolution_log.php
#	app/tests/Feature/Console/PhoneRangesImportCommandTest.php
#	app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php
#	app/tests/Feature/PartitionsCreateMonthsTest.php
#	cspell-words.txt
#	docs/observer/STATUS.md
#	tools/enforce-powershell-gate.test.mjs
#	tools/enforce-router-gate.mjs
#	tools/enforce-router-gate.test.mjs
#	tools/enforce-tdd-gate.test.mjs
#	tools/enforce-tdd-real-test-verifier.mjs
#	tools/enforce-tdd-real-test-verifier.test.mjs
#	tools/enforce-verify-record.test.mjs
#	tools/mcp-tool-classifier.test.mjs
#	tools/shell-content-rules.test.mjs
2026-06-17 08:03:35 +03:00
Дмитрий f94552d452 WIP чекпойнт: lead-region/supplier бэкенд + фронт-редизайн + Pint + тесты
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>
2026-06-17 05:17:12 +03:00
Дмитрий c4cb766d25 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:28:33 +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
Дмитрий 22f6178b2b fix(migrations): clean migrate:fresh resilience (partition parent guard + delta idempotency)
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.
2026-06-03 20:01:55 +03:00
Дмитрий f8d89e81d1 revert(imitation): drop out-of-scope migration edits from 7c5ca7f6 + dead webhook_log test
Reverts 7c5ca7f6 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).
2026-06-03 18:58:13 +03:00
Дмитрий 7c5ca7f688 chore(imitation): Task 0.5 — test env, reference-seed base, migrate:fresh resilience 2026-06-03 16:49:23 +03:00
Дмитрий ec21971888 feat(region): schema migration + MonthlyPartitionManager registration 2026-06-01 07:12:08 +03:00
Дмитрий ba3dbbd9be fix(audit): add pg_advisory_xact_lock to audit_chain_hash trigger
Closes race condition where concurrent INSERT workers (webhook handlers)
all read the same prev_hash before any commits, causing hash chain to
branch. Advisory lock key is derived from the partition OID (TG_RELID):
  lock_key := ('x' || lpad(to_hex(TG_RELID::int), 16, '0'))::bit(64)::bigint

This serializes INSERTs to the SAME partition without blocking concurrent
INSERTs to DIFFERENT partitions (distinct OIDs → distinct lock keys).

Hash formula: verbatim unchanged from db/schema.sql:3107-3127:
  digest(COALESCE(prev_hash, ''::bytea) || NEW::text::bytea, 'sha256')

Tested: pg_locks advisory lock presence test passes (pg_advisory_xact_lock
visible in pg_locks during INSERT transaction).

Refs: docs/superpowers/plans/2026-05-29-audit-chain-race-fix.md Task 2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 09:20:56 +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
Дмитрий 8b6b410119 feat(projects): add paused_at column for supplier-snapshot guard 2026-05-26 11:06:42 +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
Дмитрий 9a56d92440 fix(db): widen supplier_*.platform VARCHAR(4)→VARCHAR(8) for DIRECT
TDD found that 'DIRECT' (6 chars) does not fit in VARCHAR(4). Three columns
need widening: supplier_projects.platform, project_supplier_links.platform,
supplier_leads.platform. supplier_manual_sync_queue.platform was already
VARCHAR(8). Done in the same migration as CHECK extension — single
atomic deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:59:00 +03:00
Дмитрий 0e5f47c5e9 feat(db): seed suppliers.code='direct' for DIRECT platform billing
LedgerService::resolveSupplierId will look up suppliers WHERE code='direct'
for DIRECT-platform supplier_projects (Phase 3). cost_rub matches B1 (same
supplier company, different lead-routing channel).

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:58:58 +03:00
Дмитрий cbfb504a54 feat(db): extend supplier_projects.platform CHECK to include DIRECT
Adds DIRECT value to chk_supplier_projects_platform and chk_psl_platform
constraints. DIRECT represents supplier projects without B[123]_ prefix
(e.g. client.carmoney.ru, cashmotor.ru, numeric phone IDs) — currently
~67 leads/day lost to 302 redirects from webhook validation regex.

Schema-only change; no code yet uses DIRECT — code changes follow in
subsequent commits. Migration is forward-compatible: old code continues
to work with B1/B2/B3 rows.

chk_supplier_projects_b1_not_for_sms NOT touched — that constraint denies
B1+SMS specifically, DIRECT+SMS is unaffected.

Spec: docs/superpowers/specs/2026-05-25-supplier-webhook-reliability-design.md §3 Phase 3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:58:57 +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
Дмитрий d377d97737 fix(migration): 2026_05_22_000002 — use pgsql_supplier connection (owner-rights fix)
Миграция падала на проде:
  SQLSTATE[42501]: Insufficient privilege: must be owner of table webhook_log

Причина: default connection 'pgsql' (crm_app_user) не имеет owner-прав на
webhook_log (owner — crm_migrator). Заменено на 'pgsql_supplier'
(BYPASSRLS-роль crm_supplier_worker) — паттерн Спека B Phase 1 (commit 546ca30a),
который выработан ровно под эту проблему prod-ролей.

Эта миграция блокировала выкатку legacy-webhook-removal (Phase 6 deploy
24.05.2026, отменено rollback'ом). После fix миграция применится
no-op (webhook_log будет дропнут моей миграцией 2026_05_24_140000
сразу после).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 19:29:05 +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
Дмитрий d726d92427 refactor(billing-v2): seeders/factories — drop prepaid balance_leads defaults 2026-05-23 18:46:17 +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