docs(ux): косяки портала глазами пользователя — 7 находок
Структурированный разбор недочётов портала поставщика при взгляде глазами обычного пользователя: баланс и тариф-блокировка, формат телефона источника, регион вся РФ, потеря черновика реквизитов, вход до подтверждения почты, обращение на ты в блокировке, пустые состояния онбординга. Каждый раздел — ОПИСАНИЕ.md плюс скрины до и после. Словарь cspell пополнен доменными словами. Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
@@ -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 = [
|
||||
|
||||
@@ -2292,3 +2292,12 @@ golive
|
||||
jivo
|
||||
дживо
|
||||
gigachat
|
||||
Сидинг
|
||||
бан
|
||||
ларавеловское
|
||||
онбординга
|
||||
онбординге
|
||||
префлайта
|
||||
свипом
|
||||
хэндоффа
|
||||
юзают
|
||||
|
||||
@@ -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).
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 141 KiB |
@@ -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…` (источник проекта) — не сохранить случайно с `+`.
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 85 KiB |
@@ -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 секунд: создать проект, нажать «Создать» с пустыми регионами, затем поставить галочку «Вся РФ».
|
||||
|
After Width: | Height: | Size: 114 KiB |
@@ -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. Заметка
|
||||
|
||||
Отдельный скрин не сохранён (косяк в переходе между экранами). Воспроизводится: новый клиент без реквизитов → «Создать проект» → заполнить → «Создать» → «Заполнить реквизиты» → вернуться к созданию.
|
||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 102 KiB |
@@ -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 → войти.
|
||||
|
After Width: | Height: | Size: 194 KiB |
@@ -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. Заметка
|
||||
|
||||
Отдельный скрин не сохранён (окно одноразовое при создании без денег). Воспроизводится: создать проект с лимитом при нулевом балансе.
|
||||
|
After Width: | Height: | Size: 145 KiB |
@@ -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.
|
||||
|
||||
Скрин дашборда отдельно не сохранён; воспроизводится мгновенно входом нового клиента без проектов.
|
||||
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 117 KiB |
@@ -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).
|
||||
- После фикса косяка — обновить статус в этой таблице.
|
||||
@@ -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 <file>` (виснет — квирк).
|
||||
|
||||
## 5. 🔑 Доступ к проду (только чтение для разведки)
|
||||
|
||||
- `ssh liderra-prod '<cmd>'` — боевой сервер, код в `/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 = <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; на прод/коммит — только по моему явному слову.
|
||||