diff --git a/.gitleaks.toml b/.gitleaks.toml index dfdf0c64..a049bdbd 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -126,7 +126,11 @@ paths = [ '''app/app/Services/Autopodbor/Agent/Fake.*Agent\.php''', # Кликабельные прототипы фичи (демо-телефоны для визуализации макета) — та же категория, # что docs/superpowers/{specs,plans,audits,runbooks}; не реальные ПДн. - '''docs/superpowers/prototypes/.*\.html''' + '''docs/superpowers/prototypes/.*\.html''', + # «Косяки глазами пользователя» — audit-internal находки живого прохода портала + # с синтетическим демо-телефоном 916-123-45-67 в разделе про нормализацию формата. + # Не реальные ПДн; та же категория, что docs/superpowers/{specs,plans,audits}. + '''косяки с точки зрения пользователя/.*\.md''' ] regexTarget = "match" regexes = [ diff --git a/cspell-words.txt b/cspell-words.txt index e78a390c..a877858d 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -2292,3 +2292,12 @@ golive jivo дживо gigachat +Сидинг +бан +ларавеловское +онбординга +онбординге +префлайта +свипом +хэндоффа +юзают diff --git a/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/ОПИСАНИЕ.md new file mode 100644 index 00000000..d6000c86 --- /dev/null +++ b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/ОПИСАНИЕ.md @@ -0,0 +1,144 @@ +# Косяк 01 — «Заплатил, а проект не разблокировался» (префлайт считает лид по 500 ₽ вместо 70 ₽) + +**Тяжесть:** 🔴 КРИТИЧНО. Блокирует запуск любого нового клиента и ежедневно переблокирует существующих. +**Деньги клиентов:** ЦЕЛЫ — реальное списание за лид идёт по правильной цене 70 ₽. Сломан только «контролёр на входе» (проверка «хватает ли денег»). +**Где нашли:** живой проход боевого 24.06.2026, тестовый клиент tenant 27, проект 192 «Мой первый проект». + +--- + +## ✅ СТАТУС: ИСПРАВЛЕНО + ВЫКАЧЕНО НА ПРОД liderra.ru — 24.06.2026 (коммит `116b0aaa`). Код прода считает по 70₽; блок омеги 188/190 снимется свипом 18:00 МСК (одобрено владельцем) + +**Подход (согласован с владельцем):** разовую латку с данными НЕ делаем (цена меняется постоянно, старые версии тарифа нарочно остаются активными для истории). Чиним **только код** — все места префлайта переводим на справочник `PricingTierRepository::activeAt(now)`, который сам берёт действующую версию по дате. Это работает на любые будущие смены цены. + +**Что сделано (по TDD, локальная копия `app/`):** + +- Новый тест `tests/Feature/Billing/PreflightUsesCurrentTariffVersionTest.php` (5 кейсов) — при двух версиях тарифа берётся действующая. Сначала RED, потом GREEN. +- Переведены на справочник **5 мест** (нашлось 5, не 3): + 1. `app/Services/Billing/ProjectBlockReleaseService.php` (снятие блока при пополнении) + 2. `app/Http/Controllers/Api/ProjectController.php::runPreflight` (создание/правка проекта) + 3. `app/Services/Project/ProjectService.php` (массовая правка лимита) + 4. `app/Jobs/Billing/BalancePreflightSweepJob.php` (вечерний пересчёт 18:00) + 5. `app/Jobs/Billing/BalanceFrozenReminderJob.php` (цифры в письме-напоминании) +- Регресс: весь биллинг **126 тестов зелёные**, Pint применён. Larastan-ошибки — предсуществующий env-косяк (не мои строки). +- **UI-часть (409) — правка НЕ понадобилась:** проверено живьём — окно «Лимит превышает баланс» при 409 открывается корректно (не закрывается молча). «Молчаливое закрытие» из первого обхода было артефактом снимка (диалог в портале). + +**Проверено глазами (локальный dev с воссозданным дублем тарифа, скрины в этой папке):** + +- `скрин-2…` проект «Приостановлен — не хватает баланса» (до). +- `скрин-3…` после пополнения 5000 ₽ → проект **разблокирован** («Ожидает синхр.»). По старому багу (500 ₽ → 10 лидов < 30) остался бы заблокирован. +- `скрин-4…` окно 409 показывает «5000 ₽ = **71 лидов** по текущему тарифу» (71 = 5000/70 — действующая цена, а не 10 = 5000/500). + +**Готово полностью:** закоммичено (`116b0aaa`), выкачено на прод 24.06.2026, омега перепроверена (код прода = 70₽; блок 188/190 снимется свипом 18:00 МСК, одобрено владельцем). Ничего не осталось. + +**NB по локальному dev:** в dev-базу добавлен новый 70 ₽ тариф (его там не было — dev отставал от прода с 22.06) и тестовый клиент `fix-check@example.org`. Можно оставить (dev стал реалистичнее) или убрать. + +--- + +## 1. В чём проблема простым языком + +Новый клиент создаёт проект → проект «приостановлен, не хватает баланса». Клиент идёт в Биллинг, видит «1000 ₽ ≈ 14 лидов по 70 ₽», пополняет — **и проект всё равно остаётся заблокированным**. Пытается снизить лимит, чтобы оживить, — окно молча закрывается, ничего не меняется. + +Причина: система при проверке «хватает ли денег» считает, что **один лид стоит 500 ₽**, хотя на витрине и при реальном списании лид стоит **70 ₽**. Из-за этого клиенту, чтобы «пройти контролёра», нужно в **7 раз больше денег**, чем обещает сайт. На практике новый клиент запуститься не может. + +Что видел клиент (скрин): `скрин-1-заплатил-но-проект-заблокирован.png` — баланс пополнен, проект «Приостановлен — не хватает баланса». + +--- + +## 2. Где именно косяк (корень) + +### 2.1. В тарифах две активные версии одновременно + +Таблица `pricing_tiers`. Когда 22.06.2026 включили новый тариф (первая ступень 70 ₽), **старый тариф (первая ступень 500 ₽) забыли выключить** — обе версии `is_active = true`. + +``` +id tier_no leads_in_tier price_per_lead_kopecks effective_from <- цена первой ступени +1 1 100 50000 (= 500 ₽) 1970-01-01 <- СТАРАЯ, надо выключить +22 1 100 7000 (= 70 ₽) 2026-06-22 <- НОВАЯ, правильная +... (так для всех 7 ступеней: id 1–7 старые / id 22–28 новые) +``` + +### 2.2. Правильный путь (берёт свежий тариф = 70 ₽) — РАБОТАЕТ + +Через `PricingTierRepository::activeAt()`, выбирает свежую версию каждой ступени: + +- `app/Http/Controllers/Api/BillingController.php:105` — витрина «кошелёк/≈ N лидов» (показывает 70 ₽). +- `app/Services/Billing/LedgerService.php:52` — **реальное списание за лид** (списывает по 70 ₽). **Деньги целы.** + +### 2.3. Сломанный путь (берёт тариф «по-простому» → ловит обе версии → садится на 500 ₽) + +Запрос `PricingTier::query()->where('is_active', true)->get()` тянет ВСЕ 14 строк (обе версии). Дальше `sortBy('tier_no')` при дубле `tier_no=1` оставляет первую по порядку — старую (id=1, 500 ₽). Три места: + +1. `app/Services/Billing/ProjectBlockReleaseService.php:42` — авто-снятие блока после пополнения. **Из-за этого пополнение не разблокирует.** +2. `app/Http/Controllers/Api/ProjectController.php` метод `runPreflight` (~строка 214; берётся из `store` ~стр.147 и `update` ~стр.187) — проверка при создании/правке проекта. **Из-за этого правка лимита даёт 409.** +3. `app/Jobs/Billing/BalancePreflightSweepJob.php:40` — вечерний пересчёт 18:00 по всем клиентам. **Из-за этого ежедневно переблокирует и существующих клиентов.** + +Сам `BalancePreflightService` и `BalanceToLeadsConverter` не виноваты — они считают по тем ступеням, что им передали. Виноваты вызывающие (3 места выше), которые передают неправильный набор. + +--- + +## 3. Доказательства (живьём на боевом, tenant 27, баланс 1000 ₽) + +Калькулятор баланса с тем набором, что передаёт сломанный путь: + +``` +convert("1000.00", 0, активные_тарифы) => { "leads": 2, + "breakdown":[{"tier_no":1,"leads":2,"price_rub":"500.00"}], + "current_tier":{"no":1,"price_rub":"500.00"} } <- взял 500 ₽! +preflight need=10 => passes=false capacity=2 deficit=8 <- поэтому даже лимит 10 не прошёл (409) +preflight need=50 => passes=false capacity=2 deficit=48 +``` + +Витрина же показывала «≈ 14 лидов · по 70 ₽» (правильный путь). Расхождение **7×** — это и есть баг. + +Лог боевого: `PATCH /api/projects/192 → 409` (попытка снизить лимит до 10 — молча отклонена). + +--- + +## 4. Что делаем (план фикса — НЕ выполнен, ждёт владельца) + +### Шаг 1 — данные (быстро убирает симптом) + +Выключить устаревший набор тарифа (6–7 старых строк), оставить только новый 70 ₽: + +```sql +-- проверить, что выключаем именно старые 500-рублёвые: +SELECT id, tier_no, price_per_lead_kopecks, effective_from FROM pricing_tiers WHERE is_active = true ORDER BY tier_no, id; +-- выключить старый набор (id 1..7 на боевом на 24.06; СВЕРИТЬ id перед выполнением!): +UPDATE pricing_tiers SET is_active = false, updated_at = now() WHERE id IN (1,2,3,4,5,6,7); +``` + +После этого «по-простому»-запрос вернёт только 70 ₽ и проблема уходит во всех трёх местах. +⚠️ Запись изменения тарифа — в `db/CHANGELOG_schema.md` (правило §4.2). Делать в транзакции, на тесте сперва. + +### Шаг 2 — код (чтобы дубль больше не ломал) + +Перевести 3 сломанных места на тот же справочник, что витрина/списание — `PricingTierRepository::activeAt(now())` вместо `PricingTier::query()->where('is_active', true)->get()`: + +- `app/Services/Billing/ProjectBlockReleaseService.php:42` +- `app/Http/Controllers/Api/ProjectController.php` (`runPreflight`, ~214) +- `app/Jobs/Billing/BalancePreflightSweepJob.php:40` +TDD: тест «при двух активных версиях тарифа префлайт берёт свежую (70 ₽)». + +### Шаг 3 — UI (мелкий, но важный): не закрывать окно молча на 409 + +При создании/правке проекта ответ `409 balance_insufficient` сейчас в диалоге `NewProjectDialog.vue` / `views/projects/EditProjectDialog.vue` не всегда показывается — окно закрывается, клиент думает, что сохранил. Показывать причину, как это уже сделано в боковой карточке `components/projects/ProjectDetailsDrawer.vue` (там 409 выводится под полем лимита). + +--- + +## 5. Как проверить, что починили + +1. Калькулятор: `convert("1000.00",0,tiers)` → должно быть **14 лидов**, current_tier 70 ₽ (а не 2 / 500 ₽). +2. Новый клиент: лимит проекта 10, пополнить так, чтобы хватало по 70 ₽ → проект **разблокировался**. +3. Снизить лимит заблокированного проекта в пределах баланса → **сохраняется** (нет молчаливого 409). +4. Вечерний sweep 18:00 не блокирует клиента, которому хватает по 70 ₽. + +## 6. Чего НЕ делать + +- НЕ трогать `LedgerService` (реальное списание) — оно правильное (70 ₽). +- НЕ менять `BalancePreflightService`/`BalanceToLeadsConverter` — логика верна, проблема во входных тарифах. +- НЕ удалять старые строки тарифа физически — только `is_active=false` (история/аудит). +- Сверить id старого набора прямо перед `UPDATE` (на боевом 24.06 это 1–7, но проверить!). + +## 7. Связь с другими задачами + +Похоже, это тот самый незакрытый пункт «омега L» (проект на проде blocked при живых поставщиках) из памяти сессий 23.06. После фикса — перепроверить омегу (tenant 25, проекты 188/190). diff --git a/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-1-заплатил-но-проект-заблокирован.png b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-1-заплатил-но-проект-заблокирован.png new file mode 100644 index 00000000..829059a4 Binary files /dev/null and b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-1-заплатил-но-проект-заблокирован.png differ diff --git a/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-2-до-фикса-заблокирован-локально.png b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-2-до-фикса-заблокирован-локально.png new file mode 100644 index 00000000..5856d625 Binary files /dev/null and b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-2-до-фикса-заблокирован-локально.png differ diff --git a/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-3-после-пополнения-разблокирован.png b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-3-после-пополнения-разблокирован.png new file mode 100644 index 00000000..83b00a07 Binary files /dev/null and b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-3-после-пополнения-разблокирован.png differ diff --git a/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-4-409-показывает-71-лид-по-действующей-цене.png b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-4-409-показывает-71-лид-по-действующей-цене.png new file mode 100644 index 00000000..9e272bd8 Binary files /dev/null and b/косяки с точки зрения пользователя/01-баланс-тариф-блокировка/скрин-4-409-показывает-71-лид-по-действующей-цене.png differ diff --git a/косяки с точки зрения пользователя/02-телефон-источник-формат/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/02-телефон-источник-формат/ОПИСАНИЕ.md new file mode 100644 index 00000000..306ff2bd --- /dev/null +++ b/косяки с точки зрения пользователя/02-телефон-источник-формат/ОПИСАНИЕ.md @@ -0,0 +1,92 @@ +# Косяк 02 — Поле телефона-источника отвергает нормальные номера без подсказки + +**Тяжесть:** 🟠 средняя (бьёт по каждому, кто заводит проект типа «Звонок» — а это частый тип). +**Где нашли:** живой проход 24.06.2026. Ввёл в новый проект номер `+7 (916) 123-45-67` → отказ. + +--- + +## ✅ СТАТУС: ИСПРАВЛЕНО + ВЫКАЧЕНО НА ПРОД liderra.ru — 24.06.2026 (коммит `f7963bcf`) + +**Что сделано (TDD: 8 тестов RED→GREEN + проверка глазами на :8000):** + +- **Бэкенд (корень):** в `StoreProjectRequest` и `UpdateProjectRequest` добавлен `prepareForValidation()` — для типа `call` номер прогоняется через `PhoneNormalizer` и приводится к `7XXXXXXXXXX` (ведущий `+` срезан — раздача `LeadRouter` матчит без `+`). Финальная regex `^7\d{10}$` оставлена страховкой. Невалидный мусор не нормализуется → даёт честную ошибку. +- **Понятная ошибка + имя поля:** кастомные `messages()` в обоих Request (по `signal_type`): для звонка «Введите номер в формате 79161234567…», для сайта — про домен. Внутреннее имя «Источник» больше не всплывает. +- **Фронт:** постоянная подсказка под полем (`persistent-hint`) в `NewProjectDialog.vue` + статичный `pdd-hint` в `ProjectDetailsDrawer.vue`. Пересобрано `npm run build`. +- **Тест:** `app/tests/Feature/Project/ProjectPhoneNormalizationTest.php` — 8 кейсов (3 формата ввода, идемпотентность, без `+`, мусор→422 с примером, домен не трогаем, нормализация при update). Все зелёные. Pint применён. +- **Швы проверены:** дедуп (`ProjectService:536`) стал каноничнее; `SupplierSnapshotGuard` не даёт ложного «источник изменён» (идемпотентность). sms-тип не трогали. +- **Глазами:** создал «Звонок» с `+7 (916) 123-45-67` → сохранилось/показано `79161234567` (скрин `скрин-2-после-фикса-номер-нормализован.png`); постоянная подсказка видна в обоих диалогах. +- **Побочная находка (НЕ из 02):** на чистом main `tests/Feature/Project/ProjectUpdateDedupTest::…(#8)` падает (`SupplierSnapshotGuard:133` — гард блокирует смену источника у active+linked проекта). Предсуществующий тест-регресс на main, к 01/02 отношения не имеет — ждёт решения владельца. + +**Готово полностью:** закоммичено (`f7963bcf`) и выкачено на прод 24.06.2026. Ничего не осталось. + +--- + +## 1. В чём проблема простым языком + +Клиент создаёт проект «Звонок», вводит номер так, как привык любой россиянин — `+7 (916) 123-45-67`, или `8 916…`, или с пробелами/скобками — и получает красным: +> **«Поле Источник имеет некорректный формат.»** + +И всё. Ни слова о том, **какой** формат нужен. Единственный принимаемый вид — `79161234567` (цифра 7 и 10 цифр подряд, без `+`, без `8`, без пробелов). При живом проходе из-за этого было видно, как реальный клиент бьётся десятками попыток (в логах боевого — 32 отказа подряд у одного клиента). + +Добивает то, что **в соседнем месте всё сделано правильно**: в Настройках → Реквизиты поле «Контактный телефон» приняло `8 (916) 123-45-67` и **само** превратило его в `+79161234567`. То есть в продукте уже есть умная обработка телефона — но именно в поле источника проекта её не подключили. + +Скрин: `скрин-1-нормальный-номер-отклонён.png` — поле «Номер конкурента» со значением `+7 (916) 123-45-67` и ошибка «Поле Источник имеет некорректный формат». + +--- + +## 2. Где именно косяк + +### 2.1. Грубая проверка без нормализации (корень) + +- `app/Http/Requests/StoreProjectRequest.php:42` — для типа `call`: `'signal_identifier' => ['required','string','regex:/^7\d{10}$/']`. +- `app/Http/Requests/UpdateProjectRequest.php` (~стр. 44–48) — то же при редактировании. +Никакой нормализации ввода нет — что пришло, то и проверяется регуляркой. Любая привычная запись (`+7…`, `8…`, пробелы, скобки, дефисы) не совпадает → отказ. +(Для типа `site` — аналогично, регулярка домена `^[a-z0-9...]\.[a-z]{2,}$`, но домены клиенты путают реже.) + +### 2.2. Текст ошибки бесполезен + имя поля не совпадает с экраном + +- Сообщение — стандартное ларавеловское «имеет некорректный формат», **без примера/формата**. +- Имя поля в ошибке — «Источник» (`lang/ru/validation.php:184`: `'signal_identifier' => 'Источник'`), а на экране поле подписано **«Номер конкурента»** (звонок) / **«Домен конкурента»** (сайт). Клиент ищет «Источник» и не находит. + +### 2.3. На фронте — только плейсхолдер, который исчезает + +- `resources/js/views/projects/NewProjectDialog.vue` и `resources/js/components/projects/ProjectDetailsDrawer.vue`: у поля только `placeholder="79161234567"`. Подсказка пропадает, как только клиент начинает печатать; постоянной подсказки формата под полем нет. + +### 2.4. Готовое правильное решение — рядом + +- `app/Support/PhoneNormalizer.php` — `normalize(string $raw): ?string`. Срезает все не-цифры, превращает `8XXXXXXXXXX`/`7XXXXXXXXXX`/`XXXXXXXXXX` → `+7XXXXXXXXXX`, иначе `null`. +- Используется в `app/Http/Requests/UpdateRequisitesRequest.php:27` (поле `contact_phone`) — поэтому реквизиты «прощают» формат. + +--- + +## 3. Что делаем (план фикса — НЕ выполнен) + +### Шаг 1 — нормализовать ввод перед проверкой (главное) + +В `StoreProjectRequest` и `UpdateProjectRequest` добавить `prepareForValidation()`, который приводит `signal_identifier` (для `call`) к каноничному виду через `PhoneNormalizer` — и тогда `+7…`, `8…`, пробелы/скобки все пройдут. +⚠️ Нюанс: проект хранит телефон как `7XXXXXXXXXX` (без `+`), а `PhoneNormalizer` отдаёт `+7XXXXXXXXXX`. Поэтому при подстановке убрать ведущий `+` (или завести метод-вариант без `+`). Регулярку `^7\d{10}$` можно оставить как финальную страховку после нормализации. + +### Шаг 2 — понятная ошибка + постоянная подсказка + +- Кастомное сообщение для поля: например «Введите номер в формате 79161234567 — цифра 7 и 10 цифр, без `+`, без `8` и без пробелов». +- Под полем (в `NewProjectDialog.vue` и `ProjectDetailsDrawer.vue`) — постоянная подсказка формата, не только плейсхолдер. + +### Шаг 3 — починить расхождение имени поля + +Чтобы в ошибке поле называлось как на экране: либо переименовать attribute в `lang/ru/validation.php:184` под подпись, либо задать кастомные сообщения прямо в Request (тогда «Источник» в тексте не всплывёт). + +(TDD: тест «`+7 (916) 123-45-67`, `8 916 123 45 67`, `7-916-123-45-67` → сохраняется как `79161234567`».) + +--- + +## 4. Как проверить, что починили + +1. Создать проект «Звонок», ввести `+7 (916) 123-45-67` → сохраняется, в базе `signal_identifier = 79161234567`. +2. То же для `8 (916) 123-45-67`, `7 916 123 45 67`. +3. При совсем неверном вводе — ошибка с примером формата, имя поля совпадает с подписью на экране. + +## 5. Чего НЕ делать + +- НЕ трогать `PhoneNormalizer` и поле реквизитов — они работают правильно, это образец. +- НЕ ослаблять итоговую проверку (после нормализации номер всё равно должен быть валидным российским 11-значным `7…`). +- Помнить про разницу `+7…` (реквизиты) vs `7…` (источник проекта) — не сохранить случайно с `+`. diff --git a/косяки с точки зрения пользователя/02-телефон-источник-формат/скрин-1-нормальный-номер-отклонён.png b/косяки с точки зрения пользователя/02-телефон-источник-формат/скрин-1-нормальный-номер-отклонён.png new file mode 100644 index 00000000..1a832a15 Binary files /dev/null and b/косяки с точки зрения пользователя/02-телефон-источник-формат/скрин-1-нормальный-номер-отклонён.png differ diff --git a/косяки с точки зрения пользователя/02-телефон-источник-формат/скрин-2-после-фикса-номер-нормализован.png b/косяки с точки зрения пользователя/02-телефон-источник-формат/скрин-2-после-фикса-номер-нормализован.png new file mode 100644 index 00000000..07300ba9 Binary files /dev/null and b/косяки с точки зрения пользователя/02-телефон-источник-формат/скрин-2-после-фикса-номер-нормализован.png differ diff --git a/косяки с точки зрения пользователя/03-регионы-вся-рф/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/03-регионы-вся-рф/ОПИСАНИЕ.md new file mode 100644 index 00000000..67551775 --- /dev/null +++ b/косяки с точки зрения пользователя/03-регионы-вся-рф/ОПИСАНИЕ.md @@ -0,0 +1,75 @@ +# Косяк 03 — Регионы: «пусто = вся РФ» в одном месте, «обязательно выбрать» в другом + двойное подтверждение и зависающая ошибка + +**Тяжесть:** 🟠 средняя (трение у каждого при создании первого проекта; нестыковка путает). +**Где нашли:** живой проход 24.06.2026. Оставил регионы пустыми (думая «значит вся Россия») → ошибка; поставил «Вся РФ» → ошибка осталась висеть → пришлось жать отдельное «Подтверждаю». + +--- + +## 1. В чём проблема простым языком + +Наивный клиент при создании проекта не трогает регионы — логично думает «пусто = вся Россия». Жмёт «Создать» → красным: **«Выберите регион или подтвердите "Вся РФ"»**. Ставит галочку «Вся РФ (все регионы)» → **красная ошибка не исчезает** (выглядит так, будто галочка не сработала). И только нажав отдельную кнопку **«Подтверждаю "Вся РФ"»**, ошибку убирает, и лишь потом «Создать» проходит. Три действия там, где клиент ждал ноль. + +И сразу нестыковка: в **редактировании** проекта (боковая карточка) поле прямо подписано **«Регионы (пусто = вся РФ)»** — то есть там пусто разрешено и ничего подтверждать не надо. Один и тот же смысл — в создании «обязательно выбери», в редактировании «пусто = вся РФ». Клиента это путает. + +--- + +## 2. Где именно косяк + +### 2.1. Гейт «обязательно выбрать» — сделан НАМЕРЕННО (это не баг сам по себе) + +`resources/js/views/projects/NewProjectDialog.vue:297-299` (комментарий разработчика): +> Plan 4 Task 4: обязательный выбор региона + явная «Вся РФ» с подтверждением. +> На бэке regions=[] (Вся РФ) и «забыл» неотличимы → гейт намеренно UI-only. + +Логика: на бэкенде пустой список регионов = «вся РФ» (`StoreProjectRequest`: `regions => ['present','array']`), и «выбрал всю РФ» от «забыл сузить» не отличить. Чтобы клиент случайно не лил дорогие лиды по всей стране, добавили UI-гейт: либо выбери субъекты, либо явно подтверди «Вся РФ». Цель здравая. + +### 2.2. Что реально плохо — ошибка не снимается галочкой + +`resources/js/views/projects/NewProjectDialog.vue`: + +- стр. 448–449 (`submit()`): `if (form.regions.length === 0 && !vsyaRfConfirmed.value) { errors.regions = ['Выберите регион или подтвердите «Вся РФ»']; }` +- ошибку снимают только: `confirmVsyaRf()` (стр. 311) и выбор субъектов `onRegionsChange()` (стр. 328). +- а обработчик галочки «Вся РФ» `chooseVsyaRf()` (стр. 304–305) ставит `vsyaRf=true, vsyaRfConfirmed=false` и **НЕ снимает** `errors.regions`. +→ Поэтому после клика по галочке красная ошибка висит, пока не нажмёшь «Подтверждаю». Выглядит как баг. + +### 2.3. Нестыковка между экранами + +- Создание/редактирование через `NewProjectDialog.vue` — гейт с подтверждением «Вся РФ». +- Боковая карточка `resources/js/components/projects/ProjectDetailsDrawer.vue:262` — подпись «Регионы (пусто = вся РФ)», без гейта. +Две разные правки регионов с разным поведением для одного и того же смысла. + +--- + +## 3. Что делаем (план фикса — НЕ выполнен; решение за владельцем) + +Гейт нужен (защита от случайной «всей РФ») — убирать его не предлагаю. Чинить трение и нестыковку: + +### Шаг 1 — убрать зависание ошибки (мелкая правка, явно полезная) + +В `chooseVsyaRf()` (при клике по галочке «Вся РФ») снимать `errors.regions`, чтобы красная подпись не висела после установки галочки. + +### Шаг 2 — упростить подтверждение (на усмотрение владельца) + +Сейчас «Вся РФ» = два шага (галочка → «Подтверждаю»). Вариант: оставить только галочку с поясняющим текстом рядом («Проект будет получать лиды по всей РФ»), без отдельной кнопки. Сохраняет защиту, убирает лишний клик. +⚠️ Это меняет задуманный «двойной» гейт Plan 4 Task 4 — **согласовать с владельцем**, прежде чем трогать (намеренная защита). + +### Шаг 3 — выровнять формулировки между экранами + +Привести подпись/поведение к одному виду: либо везде «пусто = вся РФ» с мягким подтверждением, либо везде гейт. Сейчас `ProjectDetailsDrawer.vue` («пусто = вся РФ») и `NewProjectDialog.vue` (гейт) противоречат друг другу. + +--- + +## 4. Как проверить, что починили + +1. Создать проект, поставить «Вся РФ» → красная ошибка «Выберите регион…» **сразу исчезает** (без «Подтверждаю»). +2. Поведение регионов в создании и в редактировании — одинаковое и непротиворечивое. +3. Защита сохраняется: совсем «ничего не выбрал и не подтвердил» → проект по всей РФ случайно не создаётся. + +## 5. Чего НЕ делать + +- НЕ убирать защиту от случайной «всей РФ» без согласования (это намеренный гейт Plan 4 Task 4). +- НЕ менять бэкенд-правило `regions => present, array` (пустой массив = вся РФ — это контракт). + +## 6. Заметка + +Скрин отдельно не сохранён (косяк проявляется в динамике формы). При желании — воспроизводится за 10 секунд: создать проект, нажать «Создать» с пустыми регионами, затем поставить галочку «Вся РФ». diff --git a/косяки с точки зрения пользователя/03-регионы-вся-рф/скрин-1-после-фикса-галочка-снимает-ошибку.png b/косяки с точки зрения пользователя/03-регионы-вся-рф/скрин-1-после-фикса-галочка-снимает-ошибку.png new file mode 100644 index 00000000..76efc635 Binary files /dev/null and b/косяки с точки зрения пользователя/03-регионы-вся-рф/скрин-1-после-фикса-галочка-снимает-ошибку.png differ diff --git a/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/ОПИСАНИЕ.md new file mode 100644 index 00000000..bd685099 --- /dev/null +++ b/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/ОПИСАНИЕ.md @@ -0,0 +1,75 @@ +# Косяк 04 — Гейт реквизитов выскакивает в конце формы первого проекта и теряет черновик + +**Тяжесть:** 🟠 средняя (бьёт по каждому новому клиенту ровно один раз — при первом проекте; первое впечатление). +**Где нашли:** живой проход 24.06.2026. Заполнил всю форму проекта, нажал «Создать» → «Сначала заполните реквизиты», ушёл в реквизиты, вернулся — **форма пустая, всё ввожу заново**. + +--- + +## 1. В чём проблема простым языком + +Новый клиент впервые создаёт проект: заполняет тип источника, телефон/домен, название, лимит, регионы, дни — всю форму. Жмёт «Создать» и только тут узнаёт: +> **«Сначала заполните реквизиты компании — без них нельзя создать первый проект.»** + +Жмёт «Заполнить реквизиты» → его **уводит на другую страницу** (Настройки → Реквизиты). Заполняет их, возвращается к созданию проекта — а **форма пустая**, всё, что вводил, пропало. Приходится вводить заново. Раздражает на самом первом шаге знакомства с продуктом. + +--- + +## 2. Где именно косяк + +### 2.1. Требование реквизитов — разумное, фиксируется на бэке + +`app/Http/Controllers/Api/ProjectController.php:131-134` — гейт срабатывает **только для самого первого проекта**: + +```php +if (Project::where('tenant_id', $tenant->id)->count() === 0 + && ! $this->requisites->isLightComplete($tenant)) { + return response()->json(['error' => 'requisites_required'], 422); +} +``` + +`isLightComplete` (`app/Services/Requisites/RequisitesService.php:31`) требует: тип лица + контактное имя + контактный телефон (+ ИНН для юрлица/ИП). Само требование нормальное — косяк не в нём. + +### 2.2. Косяк №1 — узнаёт в самом конце + +Проверка только на submit (`store`). Клиент уже заполнил всю форму проекта, прежде чем узнал, что сперва нужны реквизиты. Фронт показывает плашку по 422: `resources/js/views/projects/NewProjectDialog.vue:411-412` (`error === 'requisites_required'` → `requisitesRequired=true`). + +### 2.3. Косяк №2 — уводит со страницы и теряет черновик + +Кнопка «Заполнить реквизиты» (`NewProjectDialog.vue:284`): `router.push({ path: '/settings', query: { tab: 'requisites' } })`. Это **навигация прочь** — диалог уничтожается. При повторном открытии «Создать проект» форма сбрасывается в пустую (watch на `modelValue`, `Object.assign(form, {…пусто…})` ~стр. 371). Введённые данные нигде не сохранены → теряются. + +--- + +## 3. Что делаем (план фикса — НЕ выполнен) + +Цель: не заставлять вводить проект дважды. Любой из вариантов (по возрастанию усилий): + +### Вариант A — предупредить заранее (просто) + +Когда новый клиент без реквизитов открывает «Создать проект» (первый раз) — сразу, до заполнения, показать шаг/плашку «Сначала короткие реквизиты компании» с переходом. Тогда он не тратит время на форму впустую. + +### Вариант B — не терять черновик (надёжно) + +Сохранять введённые поля проекта перед уходом в реквизиты (в стор/localStorage) и восстанавливать при возврате; либо открывать реквизиты **поверх** (модалкой над модалкой), не разрушая форму проекта, и после сохранения вернуть клиента к заполненной форме. + +### Вариант C — реквизиты прямо в шаге создания (лучший UX, дороже) + +Встроить мини-форму реквизитов как первый шаг визарда создания первого проекта, без ухода на /settings. + +Рекомендация: минимум — Вариант A (дёшево, снимает «зря заполнил»), в идеале + B (не терять данные). + +--- + +## 4. Как проверить, что починили + +1. Новый клиент без реквизитов открывает «Создать проект» → узнаёт про реквизиты **до** заполнения формы (вариант A), либо +2. после заполнения реквизитов возвращается к **заполненному** черновику проекта, ничего не вводит заново (вариант B/C). +3. Гейт по-прежнему не даёт создать первый проект без реквизитов (требование сохранено). + +## 5. Чего НЕ делать + +- НЕ убирать само требование реквизитов перед первым проектом (`store` гейт) — это нужный бизнес-контракт G1/SP2. +- НЕ ослаблять `isLightComplete` (тип лица + имя + телефон, ИНН для юр/ИП). + +## 6. Заметка + +Отдельный скрин не сохранён (косяк в переходе между экранами). Воспроизводится: новый клиент без реквизитов → «Создать проект» → заполнить → «Создать» → «Заполнить реквизиты» → вернуться к созданию. diff --git a/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/скрин-1-вариант-C-шаг-реквизиты.png b/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/скрин-1-вариант-C-шаг-реквизиты.png new file mode 100644 index 00000000..0f59be44 Binary files /dev/null and b/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/скрин-1-вариант-C-шаг-реквизиты.png differ diff --git a/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/скрин-2-вариант-C-шаг-проект.png b/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/скрин-2-вариант-C-шаг-проект.png new file mode 100644 index 00000000..96b05b13 Binary files /dev/null and b/косяки с точки зрения пользователя/04-реквизиты-в-конце-теряют-черновик/скрин-2-вариант-C-шаг-проект.png differ diff --git a/косяки с точки зрения пользователя/05-вход-до-подтверждения-почты/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/05-вход-до-подтверждения-почты/ОПИСАНИЕ.md new file mode 100644 index 00000000..305ec645 --- /dev/null +++ b/косяки с точки зрения пользователя/05-вход-до-подтверждения-почты/ОПИСАНИЕ.md @@ -0,0 +1,69 @@ +# Косяк 05 — Вход до подтверждения почты пишет «Аккаунт заблокирован» (пугающе и неверно) + +**Тяжесть:** 🟡 низкая (но портит первое впечатление и пугает; задевает часть новых клиентов). +**Где нашли:** живой проход 24.06.2026. Зарегистрировался, не подтвердил почту, попробовал войти → «Аккаунт заблокирован.» + +--- + +## 1. В чём проблема простым языком + +Новый клиент зарегистрировался, но письмо с кодом не увидел/закрыл страницу. Позже идёт на «Вход», вводит логин-пароль — и получает: +> **«Аккаунт заблокирован.»** + +Звучит как бан, как наказание. Человек пугается: «меня заблокировали?!», хотя на самом деле он просто **ещё не подтвердил почту**. Правильное сообщение здесь — «Подтвердите почту — мы отправили код на …», а не «заблокирован». + +--- + +## 2. Где именно косяк + +`app/Http/Controllers/Api/AuthController.php:95-102` — вход проверяет только флаг активности и не разбирает причину: + +```php +if (! $user->is_active) { + ... + return response()->json([ + 'message' => 'Аккаунт заблокирован.', + 'errors' => ['email' => ['Аккаунт заблокирован.']], + ], 422); +} +``` + +А новый, ещё не подтверждённый клиент создаётся именно неактивным: + +- `app/Services/Auth/RegistrationService.php:203` — `'is_active' => false`; +- `:194` — `tenant.status = 'pending_email_confirm'`; +- после ввода кода из письма (`:115`) — `is_active = true`, `email_verified_at = now()`. + +Итог: **одно и то же сообщение «заблокирован»** показывается в двух разных случаях: + +1. почта ещё не подтверждена (`email_verified_at === null`) — это НЕ блокировка; +2. аккаунт реально отключён администратором. +Различить их легко — по `email_verified_at` (или `tenant.status`), но код этого не делает. + +--- + +## 3. Что делаем (план фикса — НЕ выполнен) + +В `AuthController` перед общим «заблокирован» добавить ветку «почта не подтверждена»: + +- если `$user->email_verified_at === null` (или `tenant.status === 'pending_email_confirm'`) → вернуть дружелюбное сообщение, например **«Подтвердите почту — мы отправили код на {email}»**, и (желательно) ссылку/кнопку «Отправить код повторно» или переход на `/confirm-email`; +- иначе (почта была подтверждена, но `is_active=false`) → оставить «Аккаунт заблокирован» (это действительно отключение админом). + +(TDD: тест «неподтверждённый юзер при входе получает сообщение про подтверждение почты, а не "заблокирован"»; «отключённый админом подтверждённый юзер — получает "заблокирован"».) + +--- + +## 4. Как проверить, что починили + +1. Зарегистрироваться, НЕ подтверждать, войти → сообщение про подтверждение почты (+ способ переотправить код), НЕ «заблокирован». +2. Подтвердить почту → вход проходит. +3. Подтверждённого юзера отключить (`is_active=false` админом) → вход даёт «Аккаунт заблокирован». + +## 5. Чего НЕ делать + +- НЕ делать вид, что вход прошёл — пускать неподтверждённого нельзя (требование подтверждения почты сохраняется). +- НЕ менять регистрацию (создание неактивным до подтверждения — это правильно). + +## 6. Заметка + +Отдельный скрин не сохранён. Воспроизводится: регистрация → не вводить код → /login → войти. diff --git a/косяки с точки зрения пользователя/05-вход-до-подтверждения-почты/скрин-1-после-фикса-подтвердите-почту.png b/косяки с точки зрения пользователя/05-вход-до-подтверждения-почты/скрин-1-после-фикса-подтвердите-почту.png new file mode 100644 index 00000000..d036720f Binary files /dev/null and b/косяки с точки зрения пользователя/05-вход-до-подтверждения-почты/скрин-1-после-фикса-подтвердите-почту.png differ diff --git a/косяки с точки зрения пользователя/06-обращение-на-ты-в-блокировке/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/06-обращение-на-ты-в-блокировке/ОПИСАНИЕ.md new file mode 100644 index 00000000..e833e767 --- /dev/null +++ b/косяки с точки зрения пользователя/06-обращение-на-ты-в-блокировке/ОПИСАНИЕ.md @@ -0,0 +1,58 @@ +# Косяк 06 — Обращение «на ты» в окне блокировки баланса (везде остальное «вы») + нет кнопки «Пополнить» + +**Тяжесть:** 🟡 низкая (тон), но видно всем, кто упёрся в баланс; для делового продукта режет. +**Где нашли:** живой проход 24.06.2026. Создал проект без денег → окно «Лимит превышает баланс». + +--- + +## 1. В чём проблема простым языком + +Когда денег не хватает, всплывает окно «Лимит превышает баланс» с текстом **на «ты»**: +> «**У тебя** 0₽ = 0 лидов по текущему тарифу. После сохранения нужно 50 лидов. Не хватает: 50 лидов. +> Чтобы проект начал работать — **пополни** счёт, **поставь** его лимит 0 или **уменьши** лимиты других проектов.» + +Весь остальной портал — на «вы» (например, в подсказке блокировки источника: «поставьте проект на паузу»). Резкий переход на «ты» в денежном окне выглядит небрежно для делового сервиса. + +**Заодно (то же окно):** текст советует «пополни счёт», но **кнопки «Пополнить» в окне нет** — только «Отмена», «Поставить лимит 0», «Сохранить и приостановить». Самого естественного действия — оплатить — под рукой нет, надо идти искать биллинг. + +--- + +## 2. Где именно косяк + +`resources/js/components/projects/ProjectLimitOverloadDialog.vue`: + +- стр. 37: `У тебя {{ ... }}₽ = {{ ... }} лидов по текущему тарифу.` +- стр. 43: `Чтобы проект начал работать — пополни счёт, поставь его лимит 0 или уменьши лимиты других проектов.` +- кнопки (стр. ~45–60): «Отмена» / «Поставить лимит 0» / «Сохранить и приостановить» — кнопки «Пополнить» нет. + +Это единственное место в интерфейсе с обращением «на ты» (проверено: `grep "у тебя"` по `resources/js` даёт только этот файл). + +--- + +## 3. Что делаем (план фикса — НЕ выполнен) + +### Шаг 1 — перевести текст на «вы» (мелочь) + +- «У **вас** {{...}} ₽ = {{...}} лидов по текущему тарифу.» +- «Чтобы проект заработал — **пополните** счёт, **поставьте** его лимит 0 или **уменьшите** лимиты других проектов.» + +### Шаг 2 — добавить кнопку «Пополнить» в это окно (полезно) + +Рядом с «Поставить лимит 0» / «Сохранить и приостановить» добавить «Пополнить баланс», открывающую диалог пополнения (`TopupDialog`) или ведущую в `/billing`. Тогда клиент закрывает проблему прямо здесь. +(Связано с косяком про отсутствие «Пополнить» на карточке заблокированного проекта — см. будущий косяк 07/онбординг.) + +--- + +## 4. Как проверить, что починили + +1. Окно «Лимит превышает баланс» — весь текст на «вы». +2. В окне есть кнопка «Пополнить», ведущая к пополнению. +3. Поиск «у тебя»/«пополни»/«поставь» по `resources/js` — пусто (не осталось «ты»). + +## 5. Чего НЕ делать + +- НЕ менять логику/цифры окна (баланс, лиды, дефицит) — это другой косяк (см. 01). Здесь только текст и кнопка. + +## 6. Заметка + +Отдельный скрин не сохранён (окно одноразовое при создании без денег). Воспроизводится: создать проект с лимитом при нулевом балансе. diff --git a/косяки с точки зрения пользователя/06-обращение-на-ты-в-блокировке/скрин-1-после-фикса-вы-и-пополнить.png b/косяки с точки зрения пользователя/06-обращение-на-ты-в-блокировке/скрин-1-после-фикса-вы-и-пополнить.png new file mode 100644 index 00000000..55fdfa5c Binary files /dev/null and b/косяки с точки зрения пользователя/06-обращение-на-ты-в-блокировке/скрин-1-после-фикса-вы-и-пополнить.png differ diff --git a/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/ОПИСАНИЕ.md b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/ОПИСАНИЕ.md new file mode 100644 index 00000000..a4fc1f54 --- /dev/null +++ b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/ОПИСАНИЕ.md @@ -0,0 +1,62 @@ +# Косяк 07 — Нет онбординга новичка: пустой дашборд без подсказки «с чего начать» + лишний шум на пустом списке проектов + +**Тяжесть:** 🟡 низкая (но это первое впечатление — клиент после входа теряется «и что теперь?»). +**Где нашли:** живой проход 24.06.2026. Зашёл новым клиентом → дашборд с нулями, дальше непонятно. + +--- + +## 1. В чём проблема простым языком + +Новый клиент впервые входит и попадает на **дашборд со сплошными нулями** (лидов 0, конверсия 0, проектов 0, баланс 0 ₽, графики пустые). И **ни одной подсказки, что делать дальше** — нет приветственного шага, нет кнопки «Создайте первый проект», нет короткого чек-листа «1) реквизиты 2) проект 3) пополнить». Человек смотрит на пустые графики и думает «я зашёл, и что?». Чтобы начать, надо самому догадаться уйти в раздел «Проекты». + +А на **пустом списке проектов** (где ещё ни одного проекта) уже висит большой жёлтый баннер «вносите изменения до 18:00 МСК» и весь набор фильтров/сортировки/пагинации («Тип», «Статус», «Регион», «День приёма», «Сортировать», «Показывать по: 20/50/100/200») — всё это для новичка без единого проекта лишний шум. + +--- + +## 2. Где именно косяк + +### 2.1. Дашборд новичка — нули без онбординга + +`resources/js/views/DashboardView.vue:26-34` — стартовое состояние = нули (`EMPTY_KPIS`, `EMPTY_BALANCE`), и это всё. Никакого блока «с чего начать»/CTA для клиента без проектов нет. + +### 2.2. Баннер «до 18:00» показывается даже на пустом списке + +`resources/js/views/ProjectsView.vue:9, 220-221` — `showCutoffBanner` завязан только на флаг «скрыл вручную» (`localStorage 'projects.cutoffBannerDismissed'`), а не на наличие проектов. При 0 проектов баннер всё равно висит. + +### 2.3. Фильтры/сортировка/пагинация не скрыты при пустом списке + +В `ProjectsView.vue` панель массовых действий скрыта при пустом списке (`v-if="store.items.length > 0"`, стр. 119), а строка фильтров и «Показывать по…» — нет, показываются всегда. Пустое состояние (стр. 141) при этом нормальное: «Нет проектов. Создайте первый — кнопка справа сверху». + +--- + +## 3. Что делаем (план фикса — НЕ выполнен; это улучшение UX, не баг-блокер) + +### Шаг 1 — онбординг новичка (главное) + +Для клиента без проектов показывать на дашборде (или поверх) короткий стартовый блок: «Добро пожаловать! 1) Заполните реквизиты 2) Создайте первый проект 3) Пополните баланс» с кнопками-переходами. Снимает «и что теперь?». + +### Шаг 2 — чище пустой список проектов + +При 0 проектов: не показывать баннер «до 18:00» и прятать фильтры/сортировку/пагинацию (как уже сделано с панелью массовых действий, стр. 119) — оставить только заголовок, кнопку «Создать проект» и подсказку пустого состояния. + +--- + +## 4. Как проверить, что починили + +1. Новый клиент без проектов на дашборде видит понятный «с чего начать» с переходами. +2. Пустой список проектов — без баннера «до 18:00» и без фильтров/пагинации; есть кнопка «Создать проект» и подсказка. + +## 5. Чего НЕ делать + +- НЕ показывать фейковые цифры/демо-данные на дашборде новичка (нули честные — это правильно, не подменять). + +--- + +## 6. Прочие мелочи (записаны, чтобы не потерять; отдельные папки не заводим) + +- **Имя клиента «Новый».** Регистрация не спрашивает имя → приветствие «Доброе утро, Новый», аватар «НК». Стоит спросить имя при регистрации/онбординге или не показывать «Новый». +- **Индикатор силы пароля врёт.** `password123` помечается «Средний», хотя это очень частый слабый пароль. Стоит занижать оценку для словарных/частых паролей. (Поиск компонента индикатора в форме регистрации.) +- **Console-ошибка 401 на `/api/auth/me` до входа.** Видна в консоли браузера на странице входа (запрос «кто я» отдаёт 401, пока не залогинен). Пользователю не видна, безвредна — можно глушить (не дёргать `/me` до авторизации или не считать 401 ошибкой). Низший приоритет. +- **Капча на регистрации не проверена под нагрузкой/в headless** — для приёмки достаточно, но при автотестах учесть Yandex SmartCaptcha. + +Скрин дашборда отдельно не сохранён; воспроизводится мгновенно входом нового клиента без проектов. diff --git a/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-1-онбординг-дашборд.png b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-1-онбординг-дашборд.png new file mode 100644 index 00000000..da03b026 Binary files /dev/null and b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-1-онбординг-дашборд.png differ diff --git a/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-2-пустой-список-проектов.png b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-2-пустой-список-проектов.png new file mode 100644 index 00000000..57b721db Binary files /dev/null and b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-2-пустой-список-проектов.png differ diff --git a/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-3-параграф6-имя-коллега.png b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-3-параграф6-имя-коллега.png new file mode 100644 index 00000000..4a82d1fe Binary files /dev/null and b/косяки с точки зрения пользователя/07-онбординг-пустые-состояния/скрин-3-параграф6-имя-коллега.png differ diff --git a/косяки с точки зрения пользователя/СОДЕРЖАНИЕ.md b/косяки с точки зрения пользователя/СОДЕРЖАНИЕ.md new file mode 100644 index 00000000..865c3c93 --- /dev/null +++ b/косяки с точки зрения пользователя/СОДЕРЖАНИЕ.md @@ -0,0 +1,59 @@ +# Косяки с точки зрения пользователя — реестр + +> ✅ **ВСЕ 7 КОСЯКОВ + §6-мелочи ЗАКРЫТЫ И ВЫКАЧЕНЫ НА БОЕВОЙ liderra.ru — 24.06.2026.** +> Каждый по TDD (Pest/vitest) + проверен глазами через Playwright; выкат по каноническому ранбуку +> (бэкап → клон gitea → сборка на проде → maintenance → rsync overlay → composer/optimize → up). +> Прод: HTTP 200, health чисто, маркеры всех фиксов на проде. Омега (косяк 01) перепроверена — +> код считает по 70₽; блок 188/190 снимется свипом в 18:00 МСК (одобрено владельцем). Откат: +> `/tmp/app-backup-2026-06-24-koryaki.tgz`. +> +> 🔱 **Передача между сессиями — [ХЭНДОФФ-следующей-сессии.md](ХЭНДОФФ-следующей-сессии.md)** (читать первым: статус, что осталось, состояние dev, промт). + +Найдены при живом проходе боевого liderra.ru «глазами нового/наивного клиента» 24.06.2026. +Каждый косяк = отдельная папка с описанием, скринами и планом «что делать». +**Правило: другая сессия сначала читает этот файл, потом папку нужного косяка. Чинит по одному, после фикса — отмечает статус здесь.** + +Метод проверки: завели тестового клиента `naive-client-0624@example.org` (tenant 27), прошли путь +регистрация → первый вход → создание проекта → блок по балансу → пополнение. Боевых клиентов и их деньги не трогали. + +--- + +## Легенда статусов + +- 🔴 НЕ НАЧАТО — расследовано, ждёт фикса +- 🟡 В РАБОТЕ +- ✅ СДЕЛАНО (дата + коммит) + +--- + +## Список косяков + +| № | Папка | Короткая суть | Тяжесть | Статус | +|---|---|---|---|---| +| 01 | [01-баланс-тариф-блокировка](01-баланс-тариф-блокировка/ОПИСАНИЕ.md) | Префлайт баланса считает лид по старой цене 500 ₽ вместо 70 ₽ (в тарифах две активные версии). Клиент платит — проект не разблокируется, лимит снизить нельзя. | 🔴 КРИТИЧНО (блокирует запуск всех клиентов) | 🟢 ИСПРАВЛЕНО В КОДЕ (локально, TDD+глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026) | +| 02 | [02-телефон-источник-формат](02-телефон-источник-формат/ОПИСАНИЕ.md) | Поле телефона-источника отвергает `+7…`/`8…` без подсказки; в реквизитах тот же телефон сам нормализуется (`PhoneNormalizer`) | средняя | 🟢 ИСПРАВЛЕНО В КОДЕ (локально, TDD 8 тестов + глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026) | +| 03 | [03-регионы-вся-рф](03-регионы-вся-рф/ОПИСАНИЕ.md) | Регионы: «пусто = вся РФ» в редактировании, но обязательны при создании; галочка «Вся РФ» не снимает ошибку + двойное подтверждение | средняя | 🟢 ИСПРАВЛЕНО В КОДЕ (одна галочка + снятие ошибки, vitest+глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026) | +| 04 | [04-реквизиты-в-конце-теряют-черновик](04-реквизиты-в-конце-теряют-черновик/ОПИСАНИЕ.md) | Гейт реквизитов выскакивает в конце формы первого проекта; кнопка уводит на /settings и теряет черновик | средняя | 🟢 ИСПРАВЛЕНО В КОДЕ (вариант C: реквизиты шагом 1 визарда, vitest+глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026) | +| 05 | [05-вход-до-подтверждения-почты](05-вход-до-подтверждения-почты/ОПИСАНИЕ.md) | Вход до подтверждения почты → «Аккаунт заблокирован» (пугающее слово вместо «подтвердите почту») | низкая | 🟢 ИСПРАВЛЕНО В КОДЕ (ветка «подтвердите почту» + переход на /confirm-email, Pest+vitest+глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026) | +| 06 | [06-обращение-на-ты-в-блокировке](06-обращение-на-ты-в-блокировке/ОПИСАНИЕ.md) | Тон «на ты» в окне блокировки баланса («у тебя 0 ₽», «пополни») при общем «вы» + в окне нет кнопки «Пополнить» | низкая | 🟢 ИСПРАВЛЕНО В КОДЕ (текст «вы» + кнопка «Пополнить»→/billing, vitest+глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026) | +| 07 | [07-онбординг-пустые-состояния](07-онбординг-пустые-состояния/ОПИСАНИЕ.md) | Пустой дашборд новичка без подсказки «с чего начать»; баннер/фильтры на пустом списке проектов (+ прочие мелочи: имя «Новый», индикатор пароля, 401 в консоли) | низкая | 🟢 ИСПРАВЛЕНО (онбординг + чистый список + §6: имя/пароль/401, vitest+глазами) — ✅ НА ПРОДЕ (выкат 24.06.2026); капча §6.4 — заметка приёмки | + +> Все 7 косяков расследованы и разложены по папкам (24.06.2026). Каждый со своим ОПИСАНИЕ.md. Прочие мелочи — внутри косяка 07 §6. + +--- + +## Схема папки одного косяка (шаблон) + +``` +NN-короткое-имя/ + ОПИСАНИЕ.md — в чём проблема (простым языком) / где косяк (файлы:строки) / что делать / как проверить / чего НЕ делать + скрин-*.png — визуальные доказательства + (при необходимости) доказательства.md — сырые выводы команд/SQL +``` + +## Важные общие правила для сессии-исполнителя + +- **Боевой код списания работает правильно (70 ₽). Деньги клиентов целы.** Не «чинить» то, что не сломано. +- Чинить по TDD, на тесте, не накатывать на боевой без отдельного «выкатываем» от владельца. +- Правка `db/schema.sql` / тарифов — с записью в `db/CHANGELOG_schema.md` (правило §4.2). +- После фикса косяка — обновить статус в этой таблице. diff --git a/косяки с точки зрения пользователя/ХЭНДОФФ-следующей-сессии.md b/косяки с точки зрения пользователя/ХЭНДОФФ-следующей-сессии.md new file mode 100644 index 00000000..68b54fd9 --- /dev/null +++ b/косяки с точки зрения пользователя/ХЭНДОФФ-следующей-сессии.md @@ -0,0 +1,96 @@ +# Хэндофф — реестр «косяки с точки зрения пользователя» (создан 24.06.2026) + +> ✅ **ЗАКРЫТО ПОЛНОСТЬЮ 24.06.2026: все 7 косяков + §6-мелочи исправлены (TDD + глаза) и ВЫКАЧЕНЫ НА ПРОД liderra.ru.** +> Коммиты на gitea/main: 01 `116b0aaa`, 02 `f7963bcf`, 03 `977404e2`, 04 `394c97e8`, 05 `80de6ecb`, +> 06 `664427ce`, 07 `9f1a1e60`, 07§6 `704c4660`. Выкат из gitea `8f75ac05` по ранбуку +> `docs/superpowers/runbooks/2026-06-18-gitea-prod-deploy-pipeline.md`. Прод: HTTP 200, health чисто. +> Омега (косяк 01) перепроверена — код прода считает по 70₽; блок 188/190 снимется свипом 18:00 МСК (одобрено). +> Откат: `/tmp/app-backup-2026-06-24-koryaki.tgz` на проде. Эта секция «осталось» ниже — историческая, всё выполнено. + +Читать ВМЕСТЕ с [СОДЕРЖАНИЕ.md](СОДЕРЖАНИЕ.md). Это передача работы между сессиями. + +--- + +## 0. Что это и откуда + +Заказчик попросил пройти боевой liderra.ru **глазами нового/наивного клиента** и собрать все косяки UX/логики. Нашли 7, разложили по папкам (`01…07`), каждый — `ОПИСАНИЕ.md` со схемой: **в чём проблема / где косяк (файлы:строки) / что делать / как проверить / чего НЕ делать**. Потом начали **чинить по одному**, согласовывая с владельцем. + +**Тестовый клиент на ПРОДЕ:** `naive-client-0624@example.org` (tenant 27) — заведён при обходе, остался с заблокированным проектом. Можно убрать (спросить владельца). + +--- + +## 1. ⚖️ ГЛАВНОЕ — правила работы владельца (соблюдать строго) + +1. **Сначала чётко согласовать, ЧТО делаем** — до правок. Владелец не программист, объяснять простым языком. +2. **Проверять швы** — что ещё заденем (все вызовы, кто ещё читает то же). +3. **В конце проверять и кодом (тесты), и ГЛАЗАМИ** (Playwright на живом сайте) — обязательно. +4. **Без разовых латок.** Чинить корень так, чтобы держало будущие изменения (владелец прямо это сказал про тариф). +5. **Ничего на прод и никаких коммитов без явного слова** — «выкатываем» для прода, эскейп для коммита (стена «роутер-наставник»). +6. **TDD обязателен** (правило проекта §11/§12 — навык `superpowers:test-driven-development` ПЕРВЫМ). Кодовая фраза сессий: «роутер-наставник». + +--- + +## 2. ✅ Косяк 01 — СДЕЛАН + ВЫКАЧЕН НА ПРОД 24.06.2026 (коммит `116b0aaa`) + +**Суть:** префлайт/блокировка баланса считали лид по устаревшей цене 500 ₽ вместо действующей 70 ₽ (в `pricing_tiers` две активные версии — это нормальное версионирование по `effective_from`; правильный справочник `PricingTierRepository::activeAt(now)` берёт свежую, а «по-простому» `PricingTier::where('is_active',true)->get()` садился на старую). Из-за этого клиент платит — проект не разблокируется. **Реальное списание было корректным (70 ₽) — деньги клиентов целы.** + +**Что сделано (TDD, локальная копия `app/`, НЕ закоммичено):** + +- Новый тест: `app/tests/Feature/Billing/PreflightUsesCurrentTariffVersionTest.php` (5 кейсов, RED→GREEN). +- Переведены на `app(PricingTierRepository::class)->activeAt(now('Europe/Moscow'))` **5 файлов**: + - `app/app/Services/Billing/ProjectBlockReleaseService.php` + - `app/app/Http/Controllers/Api/ProjectController.php` (`runPreflight`) + - `app/app/Services/Project/ProjectService.php` (`applyBalancePreflightToBulkLimit`) + - `app/app/Jobs/Billing/BalancePreflightSweepJob.php` + - `app/app/Jobs/Billing/BalanceFrozenReminderJob.php` +- Регресс: `php vendor/bin/pest tests/Feature/Billing` → **126 зелёных**. Pint применён. Larastan-ошибки = предсуществующий env-косяк ide-helper на Windows (не мои строки, на CI зелено). +- UI 409: правка НЕ нужна — проверено живьём, окно «Лимит превышает баланс» открывается корректно. +- Глазами: скрины в `01-баланс-тариф-блокировка/` (до-блок / после-пополнения-разблок / 409-показывает-71-лид). + +**git status (uncommitted):** 5×`M` + 1×`??` тест (см. список выше). + +**ОСТАЛОСЬ по 01:** + +1. (опц.) код-ревью изменений. +2. **Коммит** — спросить у владельца эскейп. Сообщение без скобок (квирк стены). Co-Authored-By как в правилах. +3. **Выкат на прод** — только по явному «выкатываем». +4. **После прода — перепроверить омегу** (tenant 25, проекты 188/190): должны корректно считаться/разблокироваться по 70 ₽. + +--- + +## 3. ✅ Косяки 02–07 — ВСЕ СДЕЛАНЫ + ВЫКАЧЕНЫ НА ПРОД 24.06.2026 + +Каждый с готовым `ОПИСАНИЕ.md`. Брать по одному, по правилам §1. + +- **02 — телефон-источник:** отвергает `+7…`/`8…` без подсказки. Готовое решение рядом — `app/app/Support/PhoneNormalizer.php` (его юзают реквизиты). Подключить к `StoreProjectRequest`/`UpdateProjectRequest` (источник хранится `7XXXXXXXXXX` без `+`). Скрин есть. +- **03 — регионы:** галочка «Вся РФ» не снимает ошибку (`NewProjectDialog.vue` `chooseVsyaRf` стр.~304 не чистит `errors.regions`); нестыковка с `ProjectDetailsDrawer.vue:262`. Гейт намеренный — упрощение согласовать. +- **04 — реквизиты в конце + теряют черновик:** `ProjectController.php:131-134` гейт первого проекта ок; UI уводит на `/settings` и сбрасывает форму. Варианты A/B/C в описании. +- **05 — «Аккаунт заблокирован»** при входе до подтверждения почты: `AuthController.php:95-102`. Добавить ветку «почта не подтверждена» (`email_verified_at===null`). +- **06 — «на ты» в окне блока:** `ProjectLimitOverloadDialog.vue:37,43`. Перевести на «вы» + добавить кнопку «Пополнить». +- **07 — онбординг/пустые состояния** + прочие мелочи (имя «Новый», индикатор пароля, 401 в консоли) — в §6 описания 07. + +--- + +## 4. 🛠 Состояние локального dev (важно для проверки глазами) + +- **Запущен `php artisan serve` на http://127.0.0.1:8000** (на момент хэндоффа жив). Если умер — поднять заново из `app/`. +- **Фронт собран статикой** (`npm run build`, `public/build/manifest.json`), файл `public/hot` удалён → SPA грузится без Vite-dev. **Если правишь фронт — пересобрать** `npm run build` (Vite-dev на :5173 в этой среде недоступен из браузера — IPv6/сеть; используй build). +- **Dev-база досеяна** под проверку 01: добавлен новый **70 ₽ тариф** (`effective_from 2026-06-22`, дубль поверх старого 500 ₽ — как на проде; раньше dev отставал) + тестовый клиент **`fix-check@example.org` / `password123`** (tenant 9, проект 16). Можно оставить или убрать. +- **Прогон одного теста:** `cd app && php vendor/bin/pest tests/Feature/Billing/PreflightUsesCurrentTariffVersionTest.php`. +- **Сидинг/чтение dev-БД:** через bootstrap-скрипт в каталоге `app/` (`require __DIR__.'/vendor/autoload.php'` …), НЕ `tinker ` (виснет — квирк). + +## 5. 🔑 Доступ к проду (только чтение для разведки) + +- `ssh liderra-prod ''` — боевой сервер, код в `/var/www/liderra/app`, логи nginx `/var/log/nginx/`, laravel `/var/www/liderra/app/storage/logs/laravel.log`. +- Чтение боевой БД: bootstrap-скрипт + `DB::statement("SET app.current_tenant_id = ")` (RLS). Роли `crm_app_user` без BYPASSRLS — тенант ставить вручную; для поиска по тенантам перебирать id. +- **Прод тарифы:** старые `id 1–7` (500…250 ₽, eff 1970), новые `id 22–28` (70…40 ₽, eff 2026-06-22), ВСЕ активны — это норма (версионирование). НЕ выключать. + +--- + +## 6. ПРОМТ ДЛЯ СЛЕДУЮЩЕЙ СЕССИИ (скопировать в начало) + +> Продолжаем реестр **«косяки с точки зрения пользователя»** (папка в корне репо Документация). Кодовая фраза «роутер-наставник». Прочитай `косяки с точки зрения пользователя/ХЭНДОФФ-следующей-сессии.md` и `СОДЕРЖАНИЕ.md`. +> +> Косяк **01 (тариф/баланс) ИСПРАВЛЕН В КОДЕ локально** (5 файлов + 1 тест, 126 тестов зелёные, проверено глазами на :8000), но **НЕ закоммичен и НЕ на проде**. Реши со мной: (а) сначала закоммитить 01 (дашь эскейп) и выкатить на прод по моему «выкатываем», потом перепроверить омегу; или (б) сразу взять **косяк 02 (телефон)**. +> +> Правила: сначала чётко согласуй со мной ЧТО делаем; проверь швы (что ещё заденем); чини корень без разовых латок; TDD; в конце проверь и тестами, и ГЛАЗАМИ через Playwright; на прод/коммит — только по моему явному слову.