Commit Graph

343 Commits

Author SHA1 Message Date
Дмитрий 228f6dc031 fix(auth): A6 review — ранний return при redirect на /login (Sprint 5A) 2026-05-17 02:23:26 +03:00
Дмитрий eaac2cf329 feat(auth): A6 — реальный обратный отсчёт TOTP-окна в 2FA (Sprint 5A) 2026-05-17 02:23:26 +03:00
Дмитрий 7e7ccfeedf test(auth): A5 — regression generic fallback ForgotPassword (Sprint 5A) 2026-05-17 02:23:26 +03:00
Дмитрий 35ef205c0f feat(auth): A4 — ResetPassword ошибка несовпадения паролей (Sprint 5A) 2026-05-17 02:23:26 +03:00
Дмитрий 7efab319e7 feat(auth): A1 — Yandex 360 SSO disabled + tooltip (Sprint 5A) 2026-05-17 02:23:26 +03:00
Дмитрий 0456f4f98a fix(import): final review — /import в явный список Route::view
Final review (🟢 low): SPA-маршрут /import работал через Route::fallback,
но все остальные app-маршруты перечислены явно в Route::view-блоке
(CLAUDE.md документирует явный список как намеренный паттерн — catch-all
перехватывал бы _test/* runtime-роуты Pest). /import добавлен в список
для консистентности и устойчивости.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:33:15 +03:00
Дмитрий 321cf2b22c fix(import): final review — tenant-изоляция import_unknown_statuses под BYPASSRLS
Final review нашёл: HistoricalImportService::loadStatusOverrides и
persistUnknownStatuses запрашивали import_unknown_statuses без явного
where(tenant_id), полагаясь на RLS через SET LOCAL. Но queue worker на prod
работает под crm_supplier_worker — BYPASSRLS-роль (00_create_roles.sql §5),
SET LOCAL не фильтрует → cross-tenant утечка: импорт тенанта A мог подхватить
resolved-маппинг тенанта B и инкрементировать его occurrences.

Добавлен явный where(tenant_id) в обе выборки (конвенция defense-in-depth
00_create_roles.sql:64 — WHERE-фильтры обязательны под BYPASSRLS). +тест
cross-tenant изоляции (red-green verified: без фикса 'Архив' тенанта A
получал status 'closed' из чужого маппинга).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:31:56 +03:00
Дмитрий 4c92d564de feat(import): маршрут /import + сайдбар + инструкция H9
- router/index.ts: добавлен маршрут /import (name=import, layout=app,
  requiresAuth=true, transition=ld-route-fadeup, devIndex=29)
- AppSidebar.vue: пункт «Импорт данных» (mdi-database-import-outline)
  добавлен в группу «Работа» следом за Дашборд
- router.spec.ts: TDD-кейс маршрута /import (layout=app, requiresAuth=true)
- docs/Как_перенести_данные_из_crm-bp-gr.md: инструкция H9 (4 шага + таблица ошибок)
- cspell-words.txt: добавлены формы глагола «замапить»

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:14:04 +03:00
Дмитрий a500882c55 refactor(import): Task 10 code-review — POLL_INTERVAL_MS константа
Code-review Task 10 (🟡): магическое число 2000 (интервал polling'а) вынесено
в именованную константу POLL_INTERVAL_MS — паттерн файла (как в DashboardView).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:08:33 +03:00
Дмитрий e8d0b61bdd feat(import): ImportView — экран импорта CSV
TDD: spec (3 tests) first, then component.
ImportView.vue: upload form + polling + history table + unknown-statuses banner.
Uses api/imports (uploadImport/listImports/getImport/getUnknownStatuses).
setInterval callback wrapped in named async fn (pollOnce) — no eslint-disable needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:05:15 +03:00
Дмитрий 4cccf9ef22 feat(import): api/imports.ts + UnknownStatusesDialog (wizard маппинга)
- api/imports.ts: типы ImportLogResource/UnknownStatus/StatusMapping,
  функции uploadImport/listImports/getImport/getUnknownStatuses/resolveUnknownStatuses
  (apiClient из ./client, стиль api/dashboard.ts)
- UnknownStatusesDialog.vue: wizard маппинга незамапленных статусов воронки
  (ТЗ §6.4/§6.6), 14 канонических slug, defineExpose(selection, save)
- Vitest 3/3 (tests/Frontend/UnknownStatusesDialog.spec.ts)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:58:33 +03:00
Дмитрий f78466aefa test(import): Task 9 code-review — cross-tenant тест ImportController::show
Code-review Task 9 (🟡): добавлен тест защиты show() — пользователь одного
тенанта получает 403 при запросе import_log другого тенанта (покрывает
abort_if defense-in-depth в ImportController::show). phpstan-baseline
регенерирован — инкремент count ложного TestCall-срабатывания (квирк 25).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:52:31 +03:00
Дмитрий f88d4b2cd4 feat(import): ImportController + маршруты /api/imports
Task 9 Sprint 4: ImportController с 5 методами (store/index/show/
unknownStatuses/resolveUnknownStatuses), 2 FormRequest (StoreImportRequest
/ ResolveUnknownStatusesRequest), 5 маршрутов в routes/web.php под
auth:sanctum+tenant. Defense-in-depth: явный where(tenant_id) поверх RLS
(postgres superuser обходит BYPASSRLS на dev — паттерн DealController).
Тест 8/8, Larastan baseline regen (только TestCall false positives).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:45:47 +03:00
Дмитрий 3fde0eab51 feat(import): ImportLeadsJob — queued-обработчик CSV-импорта
ShouldQueue-job: читает CSV через Storage::disk('local'), парсит через
CsvLeadsParser, импортирует через HistoricalImportService (4 аргумента),
обновляет import_log (pending→processing→done|failed), шлёт
ImportCompletedNotification. RLS через SET LOCAL в каждой транзакции.
tries=1 (идемпотентность на уровне строк, повторный прогон искажает
счётчики — авто-ретрай отключён). Larastan: 0 новых ошибок.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:31:19 +03:00
Дмитрий 4f6739aef1 feat(import): Mailable ImportCompletedNotification
Task 8 — email-уведомление пользователю по завершении CSV-импорта
исторических лидов (ТЗ §6.6). Два исхода: done (счётчики строк) /
failed (сообщение об ошибке). Blade-шаблон markdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:23:00 +03:00
Дмитрий 95be14559a fix(import): Task 6 code-review — final-класс + честное имя поля errors
Code-review Task 6 (non-blocking 🟡): HistoricalImportService объявлен final
(симметрия с ImportResult, утилитарный сервис без наследования). Ключ ошибки
upsert'а переименован 'line' → 'source_crm_id' — поле хранит идентификатор из
исходной CRM, а не файловую строку (в отличие от CsvParseResult::errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:17:59 +03:00
Дмитрий 7217a3a081 feat(import): HistoricalImportService — идемпотентный upsert лидов
Реализован HistoricalImportService с ImportResult DTO и 7 feature-тестами
(TDD). Идемпотентный upsert через pg_advisory_xact_lock + webhook_dedup_keys;
создание партиций через MonthlyPartitionManager; напоминания; unknown-статусы
с tenant-переопределениями; dry_run режим; historical_import tx без списания
баланса. Попутный fix CarbonImmutable-петли в MonthlyPartitionManager::ensureRange.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:10:23 +03:00
Дмитрий 6233083038 fix(import): Task 5 code-review — final-класс CsvLeadsParser + self::EXPECTED_COLUMNS
Code-review Task 5 (non-blocking 🟡): CsvLeadsParser объявлен final (симметрия
с DTO ParsedLeadRow/CsvParseResult, утилитарный класс без наследования);
строка ошибки про число колонок использует self::EXPECTED_COLUMNS вместо
литерала 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:51:27 +03:00
Дмитрий 3e23773359 feat(import): CsvLeadsParser + DTO ParsedLeadRow/CsvParseResult
Парсер CSV-выгрузки лидов crm.bp-gr.ru (ТЗ §6.2/§6.3): срезает UTF-8 BOM,
разбирает строки через str_getcsv, валидирует телефон (7XXXXXXXXXX) и даты
(Y/m/d H:i:s), срезает префикс B[123]_ из названия проекта. Невалидные
строки не роняют парсинг — собираются в errors[] с абсолютным номером строки.
Тесты: 5/5 (unit, без DB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:47:15 +03:00
Дмитрий f4241e1f09 feat(import): сервис StatusRuToSlugMapper (ТЗ §6.4)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:42:49 +03:00
Дмитрий 2f4575b8a0 feat(import): сервис MonthlyPartitionManager + рефактор partitions:create-months
Выносит DDL-логику создания месячных RANGE-партиций из команды
PartitionsCreateMonths в переиспользуемый сервис MonthlyPartitionManager.
Сервис используется командой (DRY) и будет использован HistoricalImportService
для партиций под исторические даты CSV.

- MonthlyPartitionManager::ensureRange(table, from, to) — гарантирует партиции
  под диапазон дат, идемпотентно; отвергает незарегистрированные таблицы
- MonthlyPartitionManager::ensureMonth(table, monthStart) — одна партиция
- PartitionsCreateMonths рефакторена: убраны PARTITIONED_TABLES, partitionExists(),
  use DB; inject MonthlyPartitionManager через handle()
- Test: MonthlyPartitionManagerTest (3 теста, DatabaseTransactions — DDL откат)
- Regression: PartitionsCreateMonthsTest (4 теста) — зелёный, поведение не изменилось

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:37:29 +03:00
Дмитрий cf95b87f30 feat(import): Eloquent-модели ImportLog + ImportUnknownStatus
- ImportLog: $attributes зеркалят DB DEFAULT'ов (status/entity_type/dry_run),
  CREATED_AT/UPDATED_AT=null (таблица использует started_at/finished_at),
  casts для mapping_config (array) и dry_run (boolean)
- ImportUnknownStatus: scope unresolved() (whereNull mapped_to_slug),
  BelongsTo tenant
- Фабрики ImportLogFactory + ImportUnknownStatusFactory
- Тест ImportModelsTest (2/2, DatabaseTransactions, idempotent)
- ide-helper:models перегенерирован под новые модели
- phpstan-baseline регенерирован (квирк 25: TestCall::$tenant/$user)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:29:33 +03:00
Дмитрий b2086cd2aa 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
Дмитрий fada3f348c 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
Дмитрий b24277efc3 feat(api): J1 — auth:sanctum+tenant middleware на /api/deals*
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:18:13 +03:00
Дмитрий 5ce5d3aa64 feat(api): J2 — стаб-гейт EnsureSaasAdmin на /api/admin/* 2026-05-16 15:01:07 +03:00
Дмитрий 9e8c6aea84 feat(admin): G5/G6 frontend — incident detail view + РКН-notify 2026-05-16 14:09:53 +03:00
Дмитрий b0826ecc0c feat(admin): G6 backend — incident РКН-notify endpoint 2026-05-16 14:09:53 +03:00
Дмитрий f9e3a82b4c feat(admin): G5 backend — incident detail endpoint 2026-05-16 14:09:53 +03:00
Дмитрий 5f28c0136c fix(admin): G4 review — real AxiosError in error test + balance/NaN guards + a11y 2026-05-16 14:09:53 +03:00
Дмитрий 4fcf642963 feat(admin): G4 frontend — billing row-actions menu + dialogs 2026-05-16 14:09:53 +03:00
Дмитрий a57f9cc511 fix(admin): G4 review — lockForUpdate on refund balance + self-contained tariff tests 2026-05-16 14:09:53 +03:00
Дмитрий 1b29e986d1 feat(admin): G4 backend — billing tenant actions (status/refund/tariff)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 14:09:53 +03:00
Дмитрий 9ccd960714 feat(reports): кнопка «Скачать» → signed download URL (F2 frontend) 2026-05-16 12:45:51 +03:00
Дмитрий a132b93390 fix(reports): download/downloadUrl отклоняют expired-job по expires_at (F2 review fixup) 2026-05-16 12:42:00 +03:00
Дмитрий 7fc5c09ab7 feat(reports): download endpoint + signed URL 24ч (F2 backend) 2026-05-16 12:36:08 +03:00
Дмитрий d1a5888315 feat(reports): BillingSummaryProvider + isSupported всех 4 типов (F1 закрыт) 2026-05-16 12:28:57 +03:00
Дмитрий 5521d172b6 feat(reports): SourcesSummaryProvider — агрегат сделок по utm_source (F1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:21:51 +03:00
Дмитрий 05b8051303 feat(reports): ManagersSummaryProvider — агрегат сделок по менеджерам (F1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:14:49 +03:00
Дмитрий b5a8ad6e0a fix(deals): router в DealsViewRedesign.spec + idempotency guard + watch-test (C8/F3 review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий 56b4c3e282 feat(deals): deep-link /deals?openId= из напоминаний и колокольчика (audit C8/F3)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий fbb346feeb fix(dashboard): скрыть Live-бейдж при ошибке + formatRub guard + test hardening (C1 review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий 3c3fd248e9 fix(dashboard): восстановить tenant-guard в load() + auth.user в тесте (C1 review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий b6489f7c7d feat(dashboard): DashboardView на real API /api/dashboard/summary (audit C1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий 1000775df9 fix(dashboard): activity-бакеты в MSK + deltaBlock для leads + test hardening (J3 review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий 2a7d6a91e4 fix(dashboard): runway_days опирается на фикс. 7д-окно, не на range (J3 review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:09 +03:00
Дмитрий a1ae5f9aeb feat(dashboard): GET /api/dashboard/summary — агрегат KPI/баланса/активности (audit J3)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:41:08 +03:00
Дмитрий f53d46b1d1 docs(admin): AdminLayout JSDoc — 4→7 nav-пунктов (Sprint 3A final review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:15:11 +03:00
Дмитрий b940243b34 docs+test(admin): ImpersonationBanner — убрать stale JSDoc + тест poll→render (B5 review fixup)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:09:29 +03:00
Дмитрий 6098bb0b77 revert(nav): откат B1 «Напоминания» в сайдбаре — конфликт с решением заказчика
Откат 4342dc7. Audit B1 предлагал вернуть «Напоминания» в сайдбар, но
пункт был намеренно убран по требованию заказчика (commit 5c8ad27
«sidebar cleanup»; тест AppLayout.spec.ts фиксирует «Напоминания убраны
по требованию заказчика»). Решение заказчика 2026-05-16: B1 → won't-do,
пункт остаётся убранным. Восстанавливает зелёный AppLayout.spec.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:04:13 +03:00