Дмитрий
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
Дмитрий
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
Дмитрий
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
Дмитрий
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
Дмитрий
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
Дмитрий
d369383c7d
feat(supplier): supplier_manual_sync_queue table (Tier 3 queue)
...
SaaS-level (без tenant_id, без RLS, как supplier_csv_reconcile_log).
+3 CHECK (platform/operation/status), +2 индекса, +2 FK
(project_id→projects CASCADE, resolved_by_user_id→users SET NULL).
Миграция через DB::unprepared (PG prepared statement не разрешает multi-SQL).
schema.sql bumped v8.24 → v8.25 (64 base tables / 121 indexes / 40 RLS).
SchemaDeltaTest обновлён под новые метрики (63→64 tables, 119→121 indexes).
§15.2 pre-flight: rebase на origin/main f7f37fb выполнен до коммита.
Spec §4.5. Task 3 of 12. Регрессия: schema+delta тесты 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-19 12:55:06 +03:00
Дмитрий
ed8ec89bcc
feat(supplier): supplier_leads.vid -> nullable для CSV-recovered лидов
...
Резервный CSV-канал (Путь 2): отчёт поставщика «Запрос номеров» не
содержит vid -> CSV-recovered лиды имеют vid=NULL. UNIQUE-индекс
idx_supplier_leads_vid_unique сохранён (PostgreSQL NULL != NULL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-18 17:20:28 +03:00
Дмитрий
5e73e0cf0f
feat(deals): schema — lead_statuses funnel 14->5 (new/viewed/in_progress/won/lost)
2026-05-18 03:42:41 +03:00
Дмитрий
ec6ebc57e0
merge: C9 — Plan 6 регионы субъект-уровня в портал
...
# Conflicts:
# app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
# db/CHANGELOG_schema.md
# db/schema.sql
2026-05-17 09:30:21 +03:00
Дмитрий
98549c52be
fix(import): Task 1 code-review — убран фантомный GRANT-блок + усилен UNIQUE-тест
...
Code-review Task 1: явный per-table GRANT-блок для import_unknown_statuses
использовал несуществующие роли (crm_app_admin / crm_readonly). Реальные роли —
crm_app_user / crm_admin_user / crm_migrator / crm_audit_writer /
crm_supplier_worker (db/00_create_roles.sql). Блок удалён целиком из
db/02_grants.sql и db/schema.sql: import_unknown_statuses — обычная
tenant-scoped таблица, покрыта umbrella GRANT ... ON ALL TABLES +
ALTER DEFAULT PRIVILEGES (как import_log), явный per-table grant не нужен.
ImportSchemaTest: UNIQUE-тест усилен — проверяет состав колонок
(status_ru, tenant_id), а не только наличие constraint'а типа 'u'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 17:20:12 +03:00
Дмитрий
70f8b210f4
feat(import): H1+H2 — схема import_unknown_statuses + enrichment import_log
...
Sprint 4 Task 1 (schema delta §6):
- H1: новая таблица import_unknown_statuses (RLS tenant_isolation,
UNIQUE(tenant_id,status_ru), FK→tenants/import_log/lead_statuses/users)
- H2: +5 колонок import_log (entity_type, source_system, mapping_config,
unknown_statuses_count, dry_run)
- schema.sql v8.20→v8.21 (64 таблицы / 118 индексов / 40 RLS-политик)
- db/CHANGELOG_schema.md v8.21 entry
- db/02_grants.sql v8.21 section (crm_app_user/crm_app_admin/crm_readonly)
- migrate: hasTable/hasColumn guards (fresh-safe)
- tests: 3 Pest-теста (ImportSchemaTest) + SchemaDeltaTest v8.21 metrics
- ide-helper: _ide_helper.php + _ide_helper_models.php (были отсутствуют
в worktree, phpstan падал молча из-за missing scanFiles entry)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 17:01:51 +03:00
Дмитрий
c4876410ea
db(schema): v8.20 — add projects.regions INT[] for subject-level filtering
...
Adds INT[] column + GIN index to support 89-code regions (Plan 6).
region_mask/region_mode kept for backward-compat (DEPRECATED, removal in Plan 6.5).
Empty array semantically equivalent to legacy region_mask=255 (all of Russia).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-15 04:52:19 +03:00
Дмитрий
e746b3c9a4
chore(cleanup): dead code removal + DemoSeeder env-conditional + schema header drift
...
Closes Audit #3 P2 batch (knip dead exports/components, DemoSeeder
hygiene, schema header drift).
- Remove app/resources/js/views/admin/AdminPlaceholderView.vue
(unreferenced placeholder view — confirmed via repo-wide grep, only
doc references remain)
- npm uninstall concurrently (no script invoked it; --legacy-peer-deps
for Histoire 1.0-beta.1 peerDep quirk)
- 12 unused exports → internal types (remove `export` keyword):
- api/admin.ts: AdminTenantsStats, ApiTenantMetrics,
ApiAdminBillingSummary, ApiAdminIncidentsSummary
- api/notifications.ts: NotificationEvent
- api/reports.ts: ApiReportType, ApiReportFormat, ApiReportParameters,
ReportCounts, ReportQuota
- composables/mockBilling.ts: TxType
- composables/useStatusPill.ts: StatusPillSlug
All 12 are used INSIDE their own file (response shapes), just not
exported externally — converting to internal types satisfies knip
without losing type-checking inside the file.
- DatabaseSeeder::run() — DemoSeeder runs only in local+testing envs
(`migrate:fresh --seed` in dev now produces demo tenant + admin@demo.local
+ 3 projects + ~14 demo deals; prod environments skip)
- db/schema.sql header line 4: «62 базовые таблицы» → «63 базовые
таблицы (61 regular + 2 partitioned parents: deals + supplier_lead_costs)»
Closes schema header drift finding from Phase 3.
Verification:
- vue-tsc --noEmit: 0 errors
- ESLint on touched files: 0 errors
- Pest --parallel: 742/739/3sk/0 failed (identical to baseline, no regressions)
- 2243 assertions / 34.46s
Plan: docs/superpowers/plans/2026-05-14-audit3-deferred-fixes.md Task 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 08:28:44 +03:00
Дмитрий
9d2e7270de
feat(projects): Plan 5 Task 3 — store + StoreProjectRequest + ProjectService::create
...
- StoreProjectRequest: 3-way conditional validation (site domain regex, call 7\d{10}, sms senders required)
- ProjectService::create(): max_projects limit check via Tenant.limits JSONB + dispatch SyncSupplierProjectJob
- ProjectController: constructor DI + store() method returning 201
- SyncSupplierProjectJob: stub (Task 4 полная реализация)
- POST /api/projects route inside auth:sanctum+tenant group (name projects.store)
- Migration add_limits_to_tenants: JSONB DEFAULT '{}' per-tenant limits column
- Tenant model: limits added to fillable + casts as array
- schema.sql/CHANGELOG: tenants.limits documented in v8.20
- phpstan-baseline: +8 actingAs entries for new test file
- Quirk: region_mode in request uses 'include'/'exclude' (schema CHECK) not 'all'/'whitelist' (plan spec typo)
- Quirk: Project::first() → Project::where('signal_identifier','x.ru')->latest()->first() (no RefreshDatabase, persistent test DB)
- 8/8 ProjectsStoreTest passed; 699/706 total (4 pre-existing failures unchanged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 18:29:54 +03:00
Дмитрий
144d4cbb98
feat(db): Plan 5 Task 1 — schema delta v8.19 → v8.20 + Project.archived_at
...
Schema delta (1 правка в db/schema.sql):
- projects + archived_at TIMESTAMPTZ NULL — soft archive flow (отличие от
is_active=false который = pause).
Метрики: 62 базовых таблицы / 117 индексов / 39 RLS (без изменений).
Сопутствующие правки:
- db/CHANGELOG_schema.md — v8.20 entry.
- app/Models/Project — fillable+casts: archived_at datetime + scopeActive +
scopeArchived (whereNull/whereNotNull archived_at).
- Migration guard: Schema::hasColumn() проверка перед ALTER TABLE — предотвращает
"duplicate column" после migrate:fresh (schema.sql v8.20 уже содержит колонку).
Tests:
- ArchivedAtTest.php — 2 it() блоков: archived_at колонка timestamptz + fillable/casts.
- pest --filter=ArchivedAtTest: 2/2 PASS (4 assertions, 485 ms).
- Full suite: 689/686+3 skipped/0 failed (2094 assertions, 84638 ms).
Quirk зафиксирован: Schema::getColumnType('projects', 'archived_at') → 'timestamptz'
(не 'timestamp') — PostgreSQL TIMESTAMPTZ → Doctrine/Laravel native type string.
План spec ожидал 'timestamp', скорректировано в тесте с комментарием.
Spec: docs/superpowers/specs/2026-05-10-claude-brain-extraction-design.md (Plan 5).
Plan: docs/superpowers/plans/2026-05-10-claude-brain-extraction.md Task 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 17:39:46 +03:00
Дмитрий
e5ee9dce0d
fix(db): Plan 4 Task 1 code-review fixes (4 Important issues)
...
- chk_lead_charges_prepaid_zero_price moved inline в CREATE TABLE lead_charges
(consistent с ~30 другими CHECK constraint'ами в schema.sql).
- LeadCharge.casts() — убран no-op 'charge_source' => 'string' (Eloquent
возвращает VARCHAR как string без cast'а; consistent с SupplierLead.platform).
- SchemaDeltaTest — добавлен uses(DatabaseTransactions::class) для tests 1+2
(rollback после теста, project convention LeadChargeTest/PricingTierTest).
- SchemaDeltaTest test #5 — замена destructive migrate:fresh на static parse
count(CREATE TABLE) / count(CREATE INDEX) / count(CREATE POLICY) в schema.sql.
Устраняет cross-test coupling в sequential pest run; параллельно убирает
LARAVEL_PARALLEL_TESTING skip — теперь все 5 тестов выполняются в parallel.
Метрики из static parse: 62 base tables / 117 indexes / 39 RLS policies
(совпадают с schema v8.19, spec §2.4).
All 5 SchemaDeltaTest assertions still pass. No new schema changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 08:51:18 +03:00
Дмитрий
a907fea031
feat(db): Plan 4 Task 1 — schema delta v8.18 → v8.19 + models/seeder
...
Schema delta (4 правки в db/schema.sql):
- tenants + delivered_in_month INT NOT NULL DEFAULT 0 CHECK (>=0) — month
counter для PricingTierResolver O(1) lookup на горячем пути.
- lead_charges + charge_source VARCHAR(8) DEFAULT 'rub' CHECK IN ('prepaid','rub')
+ ALTER ADD CONSTRAINT chk_lead_charges_prepaid_zero_price (prepaid → price=0).
- supplier_leads + recovered_from_csv_at TIMESTAMPTZ + partial index
WHERE recovered_from_csv_at IS NOT NULL.
- Новая таблица supplier_csv_reconcile_log (SaaS-level, без RLS) + 2 индекса
(started_at DESC, partial status WHERE status IN ('drift_alert','failed')).
Метрики: 61 → 62 базовых таблиц / 114 → 117 индексов / 39 RLS (без изменений).
Сопутствующие правки:
- db/CHANGELOG_schema.md — v8.19 entry (18 записей).
- db/02_grants.sql — GRANT SELECT,INSERT,UPDATE on supplier_csv_reconcile_log
+ GRANT USAGE,SELECT on sequence для crm_supplier_worker.
- app/Models/Tenant — fillable+casts: delivered_in_month integer.
- app/Models/LeadCharge — fillable+casts: charge_source string.
- app/Models/SupplierLead — fillable+casts: recovered_from_csv_at datetime.
- LeadChargeFactory — defaults charge_source='rub' + prepaid() state.
- PricingTierSeeder (новый) — 7 ступеней дефолтного тарифа (placeholder,
Plan 4 Открытый вопрос #1 : 100/200/400/800/1500/3000/∞ leads at
50000/45000/40000/35000/30000/27000/25000 копеек).
- DatabaseSeeder — call PricingTierSeeder; убран broken Laravel scaffold
User::factory(['name' => ...]) (наша схема first_name/last_name).
Tests (Plan 4 surface):
- SchemaDeltaTest.php — 5 it() блоков: delivered_in_month CHECK, charge_source
CHECK на prepaid+zero-price, recovered_from_csv_at колонка, reconcile_log
таблица+status CHECK, migrate:fresh idempotency (skip in parallel).
- pest --filter=SchemaDeltaTest: 5/5 PASS (9 assertions, 2076 ms).
- pest --filter='Tenant|LeadCharge|SupplierLead|Plan4': 111/111 PASS.
CI gates:
- composer pint: passed.
- composer stan: passed (0 errors above baseline — @phpstan-ignore-next-line
на $this->markTestSkipped в Pest closure rebound context).
Verify:
- migrate:fresh --seed на DB_DATABASE=liderra_testing: 0 errors, 754 ms.
- PricingTier::count() = 7.
Концерны (НЕ блокируют Task 1):
- pest --parallel: 617/622 PASS + 4 skipped + 1 flaky FAIL — flaky test
колеблется между ProjectExtensionsTest::supplierB1_B2_B3_relations
(SupplierProjectFactory race: closure пикает signal_type=sms до override
platform=B1 → CHECK chk_supplier_projects_b1_not_for_sms violation)
и NewLeadNotificationTest::webhook_дубль_Биз_19 (известный microsecond
precision quirk в anti-spam exclusion, memory feedback_environment.md).
Оба теста существуют на main (HEAD 0802f7c = plan4-billing branch HEAD
до этого commit'а), Plan 4 их не трогает. Фиксы — вне Task 1 scope.
Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §2.
Plan: docs/superpowers/plans/2026-05-11-plan4-billing-csv-admin-plan.md Task 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 08:38:38 +03:00
Дмитрий
fb55bfdd1f
feat(db): supplier_leads + projects.delivered_today + 2 system_settings (v8.18)
...
Plan 2/5 Task 1 — слой данных для supplier-webhook flow.
- supplier_leads (SaaS-level, без RLS) — raw payload incoming webhook'ов
- projects.delivered_today — дневной счётчик для проверки daily quota
- system_settings: supplier_webhook_secret + supplier_ip_allowlist
Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §5-§6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 18:07:31 +03:00
Дмитрий
001d7819bf
fix(supplier): close code-review BLOCKER+WARN — FK + CHECK + resolver guard
...
Code-review subagent (CV.12 в Plan 1) нашёл 1 BLOCKER + 2 actionable WARNINGs:
1. **BLOCKER** — projects.supplier_b{1,2,3}_project_id были голыми BIGINT без
REFERENCES, вопреки явному комментарию «FK добавятся в Task 2». Task 2
создал supplier_projects, но FK на projects не вернул. Можно было записать
произвольный BIGINT в эти колонки.
Fix: ALTER TABLE projects ADD CONSTRAINT … FOREIGN KEY … ON DELETE SET NULL
для всех трёх + 3 partial index (WHERE NOT NULL) для FK lookup.
2. **WARNING** (Project-level B1+SMS guard) — CHECK существовал только на
supplier_projects; Project::create(['signal_type'=>'sms','supplier_b1_project_id'=>…])
проходил вопреки spec §2.2 «B1 не поддерживает СМС».
Fix: ADD CONSTRAINT chk_projects_b1_not_for_sms
CHECK (signal_type <> 'sms' OR supplier_b1_project_id IS NULL).
3. **WARNING** (resolver collision) — SupplierProjectResolver::resolveOrStub
firstOrCreate на (platform, unique_key) без signal_type → при коллизии
unique_key возвращал чужую запись с другим signal_type без ошибки.
Fix: после firstOrCreate проверяется match signal_type, иначе DomainException.
+1 тест на collision.
Schema bumped v8.16 → v8.17. Метрики: 60 таблиц / 111 индексов (+3) / 39 RLS.
Pest: 500/498 passed (+1 collision test). Larastan 0 errors. Pint clean.
Spec: §2.1, §2.2
Plan: Task 2 (закрытие code-review CV.12)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 17:32:02 +03:00