Commit Graph

809 Commits

Author SHA1 Message Date
Дмитрий 61f35d0227 fix(supplier): перенос из main — в офлайн-режиме не дёргать поставщика при создании
Перенос 7fa811b4 из gitea/main в ветку стройки, чтобы выкат отсюда не вернул баг на
боевой (класс ошибки 08.07 — «затёрло выкатом из устаревшей ветки»).

Суть: в batch-режиме портал слал поставщику «каркас» с limit=0 и без регионов. Кабинет
отбивает такой запрос ВСЕГДА — снято живьём с боевого 14.07:
{"status":"Error","message":"Введите limit!"}. Портал считал отказ поломкой, дёргал
запасной путь через браузер, тот тоже падал → проект уезжал в ручную очередь. Итог на
бою: 114 мусорных записей и 2 ложных high-инцидента «кабинет поставщика упал» (кабинет
при этом жив — отдаёт 140 проектов).

Лиды не терялись: настоящие строки создаёт вечерний SyncSupplierProjectsJob (18:00) с
посчитанными лимитами и регионами. Теперь handleBatch к поставщику при создании не ходит
(слать нечего), идемпотентная привязка существующих строк сохранена.

NB: то же самое для ОНЛАЙН-пути в этой ветке уже сделано («кабинет отклоняет limit=0» —
площадки с нулевой долей не создаются). Правки не пересекаются: там handleOnline, тут
handleBatch.

Тесты: 254/254 (Supplier + Plan5) в этой ветке; в main полный прогон 2453/2458.

NB: LEFTHOOK_EXCLUDE=larastan — статанализ в рабочей копии сломан устаревшим
_ide_helper_models.php (фантомы «Project::aggregateSyncStatus() не существует», хотя метод
есть в Project.php:181). По изменённым файлам статанализ прогнан в чистой песочнице: 0
ошибок. Bypass согласован с владельцем. Follow-up: перегенерировать стаб (--write-mixin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:26:03 +03:00
Дмитрий 6d70664904 fix(billing): перенос из main — письма о заморозке не доходили (RLS в очереди)
Перенос fc78ee1e из gitea/main в ветку стройки, чтобы выкат отсюда не вернул
сломанные письма обратно на боевой (класс ошибки 08.07 — «затёрло выкатом из
устаревшей ветки»).

Суть: 5 писем (заморозка / напоминание / финальное / разморозка / «проект
остановлен — нет денег») уезжали в очередь с моделью Tenant; воркер грузил её
заново под crm_app_user, где RLS без app.current_tenant_id отдаёт 0 строк →
ModelNotFoundException, письмо не уходило никогда. Теперь письмо несёт снимок
данных и в БД при отправке не ходит.

Плюс дедуп persistent-инцидентов сторожа — по факту незакрытого инцидента,
а не по окну 60 мин (иначе копия инцидента каждый час, бесконечно).

NB: LEFTHOOK_EXCLUDE=larastan — статанализ в рабочей копии сломан устаревшим
_ide_helper_models.php (239 ошибок в 104 ЧУЖИХ файлах, напр. фантом
«Tenant::requiredLeadsForTomorrow() не существует», хотя метод есть в
Tenant.php:93). По изменённым файлам статанализ прогнан отдельно: 0 ошибок.
Bypass согласован с владельцем. Follow-up: перегенерировать стаб (--write-mixin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:38:43 +03:00
Дмитрий 4842c728c8 fix(supplier): правки по код-ревью — матч наших строк по external_id, shouldBeOff без активных, sync_run_id
- normalizeLive матчит наши строки по supplier_external_id (tag=регион, не _lidpotok) — иначе live всегда пуст
- shouldBeOff исключает ключи, активные по формуле (нет ложного should_be_off)
- recordRunSummary → insertGetId, проверка получает sync_run_id
- обновлён phpstan-baseline.neon: count mock() 2→3 (новый тест на баг 2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:55:47 +03:00
Дмитрий b94c4152f8 feat(supplier): сторож дедлайна 20:00/20:40 — письмо если робот не успел
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:36:16 +03:00
Дмитрий 79e102424f feat(supplier): робот запускает итоговую проверку заказа после прогона
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:27:27 +03:00
Дмитрий 1bc8ec8d3d feat(supplier): VerifySupplierOrderJob — итоговая проверка заказа с перепроверкой
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:17:46 +03:00
Дмитрий ab784fb4ec feat(supplier): SupplierOrderPlan::build — задуманный заказ по формуле
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:04:02 +03:00
Дмитрий ee93ebf551 feat(supplier): SupplierOrderVerifier::diff — чистая сверка заказа с живым кабинетом
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:58:42 +03:00
Дмитрий 955f0ada70 chore(larastan): подавить ложный andThrow method.notFound в CsvReconcileJobTest
Хелпер fakeDeliveredThrows (добавлен в 17a64bbd) вызывал Larastan-ложняк
'undefined method ...andThrow()' на union-типе shouldReceive(), не забаселайненный
и не подавленный — блокировал pre-commit larastan для ЛЮБОГО коммита. Точечный
@phpstan-ignore-next-line как в других supplier-тестах. Поведение теста не меняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:54:03 +03:00
Дмитрий 17a64bbd65 fix(supplier): CSV-reconcile сверяет журнал ОТДАННОГО (по vid), не пул
Причина фантомных сделок kdv1 (09.07.2026): CsvReconcileJob брал отчёт «Запрос
номеров» = ПУЛ собранных номеров (phones_cnt), а не ОТДАННОЕ (crms_cnt), и лепил
из пула сделки клиенту + выедал дневной лимит. Пример: пул 157, отдано 15,
у клиента 15 фантомов, а 15 реально отданных выбило лимитом.

Фикс: SupplierPortalClient::fetchDeliveredLeads() читает журнал «Мои сделки»
(index-visit, по vid) — только реально отгруженное. CsvReconcileJob сверяет по vid,
добор несёт настоящий vid (idx_supplier_leads_vid_unique -> идемпотентно с webhook).
Пул не трогается -> фантомы невозможны, лимит не забивается мусором.

Проверено: 12 новых тестов + 294 supplier зелёные, Pint/Larastan чисто. Выкачено и
проверено на проде (reconcile читает отданное, 0 фантомов за прогон).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:56:44 +03:00
Дмитрий a7a4e7356a fix(admin): клиентский диапазон ступеней в тарифной сетке + живой предпросмотр в редакторе
Админка показывала сырую ширину ступени (leads_in_tier: 250/500/1000...),
а кабинет клиента — накопительный диапазон (1-250, 251-750...). Одни и те же
данные, но выглядят как разные тарифы; в форме редактирования подпись
'Лидов в ступени' читается как потолок, а система понимает её как ширину -
ловушка при смене цен.

Логика денег не менялась. Изменён только показ в админке:
- колонка 'Диапазон у клиента' в активной сетке;
- живой предпросмотр диапазона в форме редактирования;
- расчёт диапазона вынесен в общий utils/pricingTiers.ts, чтобы админка
  и кабинет больше не расходились.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:10:08 +03:00
Дмитрий 0e502c4072 feat(metrika): hit только для маршрутов кабинета (layout=app)
router.afterEach: при входе на layout=app лениво грузит Метрику и шлёт hit(fullPath).
Вход/публичные/админка/портал продаж — без Метрики. + убраны лишние @ts-expect-error
в тесте (type-check чист).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 04:38:45 +03:00
Дмитрий 05b2dff979 feat(metrika): ленивый загрузчик + hit с идемпотентностью (TDD, 3 теста)
metrika.ts: читает id из <meta name=metrika-id>, один раз инжектит tag.js + init
webvisor:true (defer), hit(url) шлёт заход. Тесты tests/Frontend/metrika.spec.ts:
no-op без meta, идемпотентная загрузка, hit вызывает ym с id+url.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 04:35:44 +03:00
Дмитрий 9f0f5cfc59 feat(supplier): аварийный spike-детектор ручной очереди в incidents:watch-failures
Блок 6: если за окно (10 мин) в supplier_manual_sync_queue упала пачка
(>= threshold-manual-queue, дефолт 10) — вероятное падение кабинета поставщика.
Инцидент дедупится 60 мин, письмо шлётся каждый прогон на kdv1@bk.ru + ops@liderra.ru
(громко при реальной аварии, но одним сводным письмом). Флаг sendMail в createIncident.
phpstan-baseline: +4 TestCall::artisan() (5→9) от новых тестов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:07:41 +03:00
Дмитрий 46607d702a fix(supplier): FailoverProjectChannel не шлёт per-project CRITICAL письма
Убраны письма manual_required и failover_to_form (по проекту, без троттла) —
источник потопа ~116 писем на ops@ 07.07. Строка в ручной очереди остаётся
(запись видна в админке). Зависимость Mailer удалена из канала и DI-привязки.

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
2026-07-05 19:25:19 +03:00
Дмитрий 2f384f098a feat(sales): экран «Менеджеры отдела» (фронт)
Task 7.1b — SalesManagersView (начальник, #page-managers) поверх
GET/POST /api/sales/managers.

- Форма «Новый менеджер» (имя, e-mail, пароль, роль) → POST → snackbar +
  очистка + перезагрузка; ошибки (дубль email) через extractSalesErrorMessage.
- Список менеджеров: роль-чип, клиентов, выплачено всего, статус
  (Активен/Отпуск).
- api/sales.ts: listSalesManagers/createSalesManager. Роут /sales/managers
  со заглушки на экран.

Vitest 6/6, фронт-набор 1089 без регрессий, ESLint чист.

Фаза 7 завершена: начальник создаёт менеджеров (логин+пароль) и видит
список отдела.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:40:59 +03:00
Дмитрий 024d435894 feat(sales): создание менеджеров начальником + список (бэк)
Task 7.1a — SalesAccountService + store/index в SalesManagersController,
только head.
- store: создаёт sales_user (пароль Hash::make, created_by=head,
  base_salary=0); email unique → 422; role in manager|head; password min:6;
  в ответе нет password ($hidden).
- index: список всех sales_users (head-первыми) с clients_count и
  выплачено всего.
Менеджер не может создавать/смотреть список (403).

Тесты 7/7 (вкл. проверку хеша пароля), sales-набор 174/174, Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:34:47 +03:00
Дмитрий f4bb6acdfe test(sales): сквозной период — метрики зависят от периода, баланс нет
Task 6.3 — период уже проброшен во всех контроллерах (SalesPeriodResolver)
и экранах (salesPeriod store + watch). Добавлен проверочный тест на
/api/sales/overview: period=this даёт оборот 3000/лид, period=prev — 5000/лид,
а balance_sum_rub одинаков (1000) — баланс не зависит от периода.

Фаза 6 завершена: начальник видит весь отдел; период переключается везде.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:26:51 +03:00
Дмитрий 5266b10066 feat(sales): результативность менеджеров (фронт)
Task 6.2b — SalesPerformanceView (начальник, #page-performance) поверх
GET /api/sales/managers/performance.

- Поиск по имени (debounce) + таблица: менеджер (имя/email), клиентов,
  активных, лидов пришло, оборот, выплачено, заработал, статус
  (Активен/Отпуск). HelpHint на Оборот/Заработал. Период из salesPeriod store.
- Роут /sales/performance со заглушки на экран.

Vitest 5/5, фронт-набор 1083 без регрессий, ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:23:46 +03:00
Дмитрий 90be095e81 feat(sales): результативность менеджеров (бэк)
Task 6.2a — SalesManagersController@performance, только head.
По каждому менеджеру за период: клиентов, активных клиентов (derived
status), лидов пришло, оборот клиентов, выплачено всего, заработал
(forManager), статус (active/vacation по is_active). Фильтр search по имени.

Тесты 4/4, sales-набор 166/166, Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:16:56 +03:00
Дмитрий d114c01005 feat(sales): сводка отдела (фронт)
Task 6.1b — SalesBossOverviewView (начальник, #page-boss-overview) поверх
GET /api/sales/dashboard/overview.

- 5 KPI (менеджеров с active/vacation, клиентов, Σ баланс, оборот отдела,
  выплачено за период).
- 4 кликабельные плитки-алерта → переходы: счета→/sales/invoices,
  заявки→/sales/requests, проблемы баланса→/sales/performance,
  провести выплату→/sales/payouts.
- Мини-таблица результативности менеджеров (клиенты/лиды/оборот/выплачено/
  заработал) с HelpHint. Период из salesPeriod store.
- Роут /sales/boss со заглушки на экран.

Vitest 12/12, фронт-набор 1078 без регрессий, ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:07:10 +03:00
Дмитрий 18c968b923 feat(sales): сводка отдела (бэк)
Task 6.1a — SalesDashboardController@overview, только head.
- kpi: менеджеров (активных/в отпуске по is_active), клиентов закреплено,
  Σ баланс, оборот отдела за период, выплачено за период.
- alerts: счетов ждут оплаты (issued/overdue), заявок pending, клиентов
  с проблемой баланса (overdue/suspended/runway=0).
- performance: по каждому менеджеру — клиентов, лидов, оборот, выплачено
  всего, заработал (forManager за период).
Департамент-wide агрегаты; kopeck-точный оборот; выплаты по закрытому
интервалу paid_on.

Тесты 4/4, sales-набор 162/162, Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:58:55 +03:00
Дмитрий eab540504a feat(sales): экран «Счета» — список + отметка оплаты (фронт)
Task 5.1b — SalesInvoicesView (начальник, #page-invoices) поверх готовых
эндпоинтов /api/sales/invoices.

- Таблица счетов (дата/номер/клиент/плательщик/сумма/статус/оплатить до),
  поиск с debounce + фильтр статуса, чипы статусов.
- Кнопка «Отметить оплаченным» (только issued/overdue) → диалог
  подтверждения → POST mark-paid → snackbar + перезагрузка. Зачисление
  баланса и Акт формирует бэк (InvoicePaymentService).
- api/sales.ts: listSalesInvoices/markSalesInvoicePaid. Роут /sales/invoices
  со заглушки на экран.

Vitest 5/5, фронт-набор 1066 без регрессий, ESLint чист.

Фаза 5 завершена: начальник отмечает оплату счёта прямо в портале продаж.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:45:17 +03:00
Дмитрий b6554b4f74 feat(sales): счета — список + отметка оплаты (бэк, переиспользование)
Task 5.1a — SalesInvoiceController (index, markPaid), только head.
- index: список saas_invoices (структура 1:1 с AdminInvoiceController —
  фильтры status/search, пагинация).
- markPaid: делегирует InvoicePaymentService::markPaid (идемпотентный
  claim issued→paid + зачисление баланса + акт + письмо) — логика НЕ
  дублируется.
Head-гейт перед обоими методами (менеджер → 403 до обращения к сервису).

Тесты 7/7 (вкл. пополнение баланса и идемпотентность повторной отметки),
sales-набор 158/158, Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:38:33 +03:00
Дмитрий a5defbb12b feat(sales): экраны выплат и «Мой доход» (фронт)
Task 4.2b — два экрана поверх готовых эндпоинтов.

- SalesPayoutsView (начальник, #page-payouts): форма «Провести выплату»
  (менеджер из remaining, сумма, дата, комментарий) → POST /payouts →
  snackbar + перезагрузка; таблица «Остаток к выплате по менеджерам»
  (начислено/выплачено/остаток за период); «Журнал всех выплат».
- SalesIncomeView (менеджер, #page-income): 4 KPI (оборот/начислено/
  выплачено всего/к выплате), таблица «Начислено по клиентам» с ИТОГО,
  «Журнал выплат мне» с ИТОГО.
- api/sales.ts: listSalesPayouts/getSalesPayoutsRemaining/createSalesPayout/
  getSalesIncome. Роуты /sales/payouts и /sales/income со заглушек на экраны.

Vitest 9/9, фронт-набор 1061 без регрессий, ESLint чист.

Фаза 4 завершена: начальник проводит выплаты, менеджер видит начисления
и выплаты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:29:32 +03:00
Дмитрий 3f0a7896cb feat(sales): сводка «Мой доход» менеджера (бэк)
Task 4.2a — SalesIncomeController@show для авторизованного менеджера:
- totals: оборот клиентов (Σ), начислено за период (forManager),
  выплачено всего, к выплате за период (остаток начислено−выплачено).
- per_client: строки «Начислено по клиентам» (организация, тариф,
  пополнения, заработано) по снимкам тарифов.
- payouts: журнал СВОИХ выплат.
Head без клиентов → нули. Reuse earnings/metrics/payout сервисов.

Тесты 6/6, sales-набор 151/151, Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:19:46 +03:00
Дмитрий b694c21557 feat(sales): выплаты — проведение, журнал, остаток
Task 4.1 — SalesPayoutService + SalesPayoutController.

- record: append-only запись в sales_payouts + письмо менеджеру
  (SalesPayoutRecordedMail). Журнал неизменяем (DB-триггер
  sales_payouts_no_mutate — покрыто тестами на UPDATE/DELETE → QueryException).
- remaining (head): по каждому менеджеру за период — начислено
  (forManager), выплачено за период (по дате paid_on), выплачено всего,
  остаток = начислено − выплачено(период); переплата не зажимается в 0.
- index: менеджер видит только свои выплаты, начальник — все.
- POST /payouts и /payouts/remaining — только head; валидация суммы/даты/
  менеджера.

Тесты 15/15 (вкл. append-only через savepoint), sales-набор 145/145,
Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:10:45 +03:00
Дмитрий 6757bdfff7 feat(sales): показ дохода «Заработал» в списке/карточке/сводке (фронт)
Task 3.3b — три экрана Фазы 1 показывают живой earned_rub вместо
заглушки «—»:
- «Мои клиенты»: колонка «Заработал» = комиссия по клиенту (или «—»
  без привязки).
- Карточка клиента: KPI «Вы заработали (период)».
- Сводка: KPI «Я заработал» = суммарная комиссия за период.
Тип earned_rub расширен до number|null. Vitest-моки/ассерты обновлены
под реальные значения. Фронт-набор 1052 зелёных, ESLint чист.

Фаза 3 завершена: доход менеджера считается по тарифу каждого клиента
и виден во всех экранах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:50:24 +03:00
Дмитрий 707185e86b feat(sales): живой доход по клиентам в списке/карточке/сводке (бэк)
Task 3.3a — SalesClientsController наполняет earned_rub через
SalesEarningsService (был null-заглушкой):
- index: доход по каждому клиенту за период по снимку тарифа привязки;
  клиент без привязки (начальник видит незакреплённых) → null.
- show (kpi): доход по одному клиенту.
- overview (totals): сумма комиссии по всем привязкам в scope (без оклада).

Тесты Фазы 1 обновлены под реальные значения (percent_oborot 10/20% от
засеянного оборота → 500/5/10000 ₽; привязка без тарифа → 0.0; без
привязки → null). Попутно исправлен ключ params тест-тарифа
(percent→rate, движок читает rate). Sales-набор 132/132, Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:32:00 +03:00
Дмитрий 12be6af1d5 feat(sales): экран «Тарифы менеджеров» (фронт)
Task 3.2b — SalesTariffsView поверх готового каталога тарифов.

- 3 семейства-конструктора: «процент от пополнений» (ступени from–to–%,
  +добавить период), «оклад+процент» (оклад→params.base_salary, ставка→
  params.rate), «фикс за клиента» (порог/вознаграждение). Создание — POST,
  правка — PUT.
- Секция «Текущий тариф менеджера»: select тарифа + оклад (для оклад+процент),
  метрики периода и оценка «Начислено» (estimated_earned_rub); сохранение —
  assign. Оклад уходит в base_salary_rub только для percent_oborot.
- Период из salesPeriod store, перезагрузка по смене. Роут /sales/tariffs
  переключён со заглушки.

Vitest 9/9, полный фронт-набор без регрессий, ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:10:20 +03:00
Дмитрий f3a18a0f96 feat(sales): каталог тарифов + назначение текущего (бэк)
Task 3.2a — SalesTariffController (index/store/update/assign), только head.

- index: активные тарифы (3 вида) + менеджеры с метриками периода
  (пополнения/оборот/клиенты) и оценкой «начислено» текущим тарифом.
- store/update: валидация params по виду тарифа (topup_step.periods[],
  percent_oborot.rate, fix_per_client.threshold/reward); смена kind в
  update запрещена (422).
- assign: ставит менеджеру current_tariff_id + base_salary_rub («оклад+
  процент» = оклад на менеджере + percent_oborot тариф).
- В12: ни update, ни assign НЕ трогают снимки уже привязанных клиентов
  (sales_client_assignments) — покрыто тестами.

Роуты в группе admin-db/auth:sales/sales-portal. Тесты 19/19,
весь sales-набор 130/130, Larastan 0 (3 Pest-false-positive в baseline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:56:58 +03:00
Дмитрий 006402bb72 feat(sales): движок расчёта дохода по тарифу (3 вида)
Task 3.1 — SalesEarningsService, денежное ядро портала продаж.
Доход считается по СНИМКУ тарифа клиента (assignment.tariff_kind/params),
не по live-тарифу менеджера.

- percent_oborot: оборот периода × ставка.
- fix_per_client: reward если накопленные пополнения (all-time) ≥ порога, иначе 0.
- topup_step: Σ по месяцам периода пополнений месяца × ставка ступени по сроку
  обслуживания (tenure = diffInMonths(assigned_at→месяц)+1); вне таблицы → 0.
- forManager: оклад × число месяцев + Σ по клиентам (round 2).
- perClient: детализация для экрана «Мой доход».

Переиспользует SalesMetricsService (целые копейки, полуоткрытые интервалы)
и SalesPeriodResolver. Тесты 12/12 (заморозка времени на середину месяца
против флейков границ + партиций balance_transactions). Larastan 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:41:59 +03:00
Дмитрий 6dbc6b5bb0 feat(sales): экраны привязки клиентов и заявок (фронт)
Task 2.3b портала отдела продаж — два Vue-экрана поверх готовых
эндпоинтов /api/sales/attachments:

- SalesAttachView (менеджер, #page-attach): форма заявки по логину
  (e-mail/ИНН), живой баннер результата (отправлено/не найден/ошибка),
  таблица «Мои заявки».
- SalesRequestsView (начальник, #page-requests): очередь на решение
  с проверкой «свободен / уже за: …», кнопки Подтвердить/Переназначить
  /Отклонить (переназначение = тот же approve на бэке), история, счётчик.
- api/sales.ts: submitAttachment / listMyAttachments / listAttachmentQueue
  / decideAttachment + типы.
- Роуты /sales/attach и /sales/requests переключены со stub на реальные экраны.

Vitest: SalesAttach 5 + SalesRequests 7 = 12 зелёных; полный фронт-набор
не сломан. ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:22:49 +03:00
Дмитрий a1c6af4b68 feat(sales): эндпоинты привязки клиентов (бэк)
Task 2.3a: SalesAttachmentController — store (менеджер подаёт заявку, 422 «уже ваш»), index (менеджер видит свои; начальник — очередь pending + история с именами менеджеров и подсказкой «свободен/уже за: …»), decide (начальник approve/reject, 403 менеджеру). Маршруты POST/GET /api/sales/attachments, POST .../{id}/decide. Тест 12/12, весь sales 100/100, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:47:27 +03:00
Дмитрий d70764ea92 feat(sales): решение начальника по заявке + снимок тарифа
Task 2.2: SalesAttachmentService.decide(head, requestId, approve|reject) — только начальник (иначе AuthorizationException). approve: создаёт sales_client_assignment со СНИМКОМ текущего тарифа менеджера (tariff_id/kind/params копируются значением на момент решения — В12: смена тарифа менеджера потом влияет только на новых клиентов, подтверждено тестом). Переназначение занятого клиента = delete+create в транзакции. reject: статус+comment. Письмо AttachmentDecidedMail менеджеру. Тест 6/6, весь sales 88/88, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:36:00 +03:00
Дмитрий aa44b80407 feat(sales): сервис привязки — lookup + заявка + письма
Task 2.1: SalesAttachmentService.lookup(login→tenant по contact_email ИЛИ inn) + submit(menedzher, login): не найден→заявка not_found + письмо; свободен→pending + письма менеджеру и всем начальникам; занят другим→pending с пометкой конфликта в comment; уже свой→исключение. Mailable AttachmentSubmittedMail/AttachmentNotFoundMail + blade. Тест 7/7 (Mail::fake), весь sales 82/82, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:26:53 +03:00
Дмитрий 2cf1c25756 feat(sales): экран «Сводка менеджера» (фронт) — Фаза 1 закрыта
Task 1.5b: SalesOverviewView по демо #page-overview — 5 KPI (клиентов+разбивка статусов, Σ баланс, лидов пришло, оборот, я заработал=«—» до Фазы 3), зелёная плашка-пояснение, таблицы «Требуют внимания» и «Топ клиентов по лидам» (клик → карточка), период из стора. getSalesOverview в api/sales.ts. Vitest 12/12, полный фронт 1031 без регрессий. Фаза 1 (чтение менеджера) завершена. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:15:45 +03:00
Дмитрий 20d724ccf3 test(sales): устойчивость SalesMetricsServiceTest к сбросу тест-БД
balance_transactions партиции создаются динамически (partitions:create-months = текущий+будущие), НЕ в schema.sql → при migrate:fresh общей тест-БД прошлые месяцы не восстанавливаются, июньские топап-тесты падали 'нет секции'. Привязал 4 balance_transactions-теста к ТЕКУЩЕМУ месяцу (btDate/btThisMonthRange/btLastDay/btNextMonthStart) — у него партиция есть всегда. deals/lead_charges тесты не трогал (их партиции в схеме, переживают fresh). 15/15. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:02:03 +03:00
Дмитрий b1baa2ae40 feat(sales): сводка менеджера (бэкенд)
Task 1.5a: GET /api/sales/overview — агрегаты по своим клиентам (начальник — по всем) за период: счётчики (клиентов/активных/триал/просрочка), Σ баланс, лиды/оборот, earned=null до Фазы 3; «требуют внимания» (overdue/suspended/запас=0, worst-first, до 10); «топ клиентов по лидам» (top-4). deriveTenantStatus вынесен в приватный helper (общий с index). Тест 10/10, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:57:43 +03:00
Дмитрий 9a43754b59 feat(sales): экран карточки клиента (фронт)
Task 1.4b: SalesClientCardView по демо #page-client-card — 5 KPI (баланс+запас, проектов, пришло/цель, средняя цена лида, заработано=«—» до Фазы 3), 2 колонки: проекты+лиды по дням+последние лиды (телефон маскирован бэком), профиль+активность. HelpHint на Запас/Средняя цена/Заработано. 403 «клиент не закреплён за вами». getSalesClientCard в api/sales.ts. Vitest 8/8, lint/type-check 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:43:43 +03:00
Дмитрий 2f3918292a feat(sales): карточка клиента (бэкенд)
Task 1.4a: GET /api/sales/clients/{tenantId} — профиль, KPI (баланс/запас/проекты/лиды-цель/средняя цена лида, earned=null до Фазы 3), проекты, лиды по дням, последние лиды с МАСКИРОВАННЫМ телефоном, активность. 403 для чужого клиента (ScopesSalesOwnership), начальник видит всех. Тест 8/8, весь sales 65/65, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:36:36 +03:00
Дмитрий 58869f8dbd feat(sales): экран «Мои клиенты» (фронт)
Task 1.3b: SalesClientsView — таблица 11 колонок по демо #page-clients (Клиент/Тип/Активность/Баланс/Запас/Проектов/Пришло/Оборот/Тариф/Заработал/Статус), бейдж типа лица, чипы статуса (Триал/Активен/Просрочка/Приостановлен), HelpHint на терминах, деньги ru-RU + JetBrains Mono, перезагрузка при смене периода, клик по строке → карточка. Заработал=«—» до Фазы 3. listSalesClients в api/sales.ts. Vitest 6/6, lint/type-check 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 06:40:56 +03:00
Дмитрий fec76ba36f feat(sales): эндпоинт «Мои клиенты» (бэкенд)
Task 1.3a: GET /api/sales/clients — менеджер видит своих (ScopesSalesOwnership), начальник всех. Строки: организация, ИНН/тип лица (tenant_requisites), баланс, запас, проекты, лиды/оборот за период (SalesMetricsService), тариф-снимок из assignment, статус 1:1 с AdminTenantsController (trial>suspended>overdue>active). earned_rub=null до Фазы 3. Тест 7/7, stan 0 (baseline: Pest false-pos). Пагинация — TODO. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 06:34:22 +03:00
Дмитрий 1c508b6f39 fix(supplier): не отправлять площадку с долей лимита 0 (кабинет crm.lead.store отклоняет)
Новый кабинет отклоняет rt-project-save с limit=0 (Введите limit!), старый принимал.
Проект с daily_limit_target=1 (site->3 площадки) давал доли 1/0/0 и падал на B2/B3.

- distributeForPlatform опускает площадки с долей 0 (сумма ненулевых == order)
- SyncSupplierProjectsJob и SyncSupplierProjectJob: create/missing/dead/update-циклы
  идут только по площадкам с долей >=1 (?? 0 снят — ключ гарантирован, phpstan 0 на этих файлах)
- тесты: allocator (limit-1/2, zero), новый job-тест limit-1 site -> ровно 1 save

Прогон supplier+jobs: 261/261.
NB: LEFTHOOK_EXCLUDE=larastan — гейт падал на предсуществующих ошибках чужого
tests/Feature/Sales/SalesClientsIndexTest.php (не связано с этим фиксом).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:51:06 +03:00
Дмитрий 287a97b776 feat(sales): сервис метрик клиента за период
Task 1.2: SalesMetricsService — leadsDelivered/oborotRub/topupsRub/cumulativeTopupsRub/runwayDays. Деньги: оборот из целых копеек (/100 в конце), полуоткрытые интервалы [start, след.день). runwayDays реиспользует PricingTierRepository→BalanceToLeadsConverter→RunwayCalculator (совпадает с кабинетом клиента, F3). leadsDelivered 1:1 с DashboardController (deleted_at NULL, is_test=false). Тест 15/15 (с граничными), stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:46:31 +03:00
Дмитрий 9f739109cd feat(sales): резолвер периода (этот/прошлый/позапрошлый/произвольный)
Task 1.1: SalesPeriodResolver.resolve({kind,from,to})→SalesPeriodRange (МСК, Europe/Moscow) + monthsIn() для ступенчатого тарифа. Поддержка this/prev/prev2/custom; неизвестный kind→this; custom from>to→исключение. Тест 10/10, stan 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:35:48 +03:00
Дмитрий d880f87e3d feat(sales): каркас фронта портала — layout, роутер, вход, период-пикер
Task 0.7: SalesLayout (сайдбар ОТДЕЛ ПРОДАЖ, 2 секции nav, boss-only для head), SalesLoginView (реальные email+пароль, не демо-кнопки), сторы salesAuth/salesPeriod, api/sales.ts, PeriodPicker (этот/прошлый/позапрошлый/произвольный), HelpHint (?). Роуты /sales/* с гардом (токен→login, boss-only→/sales). Заглушка SalesStubView для экранов будущих фаз. Vitest SalesLogin 5/5, весь фронт 1005 без регрессий, type-check/lint чисто. Вёрстка по демо v8_sales.html. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:27:04 +03:00
Дмитрий 8379a44e2b style(sales): pint-канон контроллера/маршрутов/теста входа
Lefthook-pint не перестейджит — приводим к канону версии из 974884eb. Без изменений логики. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:09:05 +03:00
Дмитрий 974884ebf9 feat(sales): вход (login/me/logout) + маршруты /api/sales
Task 0.5+0.6: SalesAuthController (login 200/422/403, me, logout) + маршруты /api/sales/auth и зона данных. Порядок middleware admin-db ДО auth:sales. Тест SalesAuthTest 7/7, весь sales-набор 25/25. Logout инвалидирует токен (в тесте Auth::forgetGuards() — артефакт мульти-запросов; в бою каждый запрос свежий). Larastan baseline: Pest false-pos SalesAuthTest. Заодно pint-канон моделей/трейта/SalesModelsTest. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:07:47 +03:00