Решение заказчика 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Lefthook-pint не перестейджит — приводим к канону версии из c366614f. Без изменений логики. Один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Task 0.4: middleware EnsureSalesUser (гейт зоны /api/sales — user('sales') активен, иначе 401, alias 'sales-portal'). Трейт ScopesSalesOwnership: менеджер видит только свои tenant_id из sales_client_assignments, начальник (head) — всех (null=без ограничения). Тест SalesOwnershipScopeTest 6/6, всего sales 18/18. Larastan чистый. Один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 0.3: guard 'sales' (driver sanctum, provider sales_users). Тест SalesGuardTest 3/3 (valid Bearer→200, без токена→401, мусор→401). Миграция personal_access_tokens (Sanctum Bearer; раньше не было — основной кабинет SPA cookie), DDL через pgsql_supplier, гранты crm_admin_user, CHANGELOG v8.60. Larastan baseline: +SalesGuardTest (Pest TestCall false-pos), SetTenantContext int→mixed (второй provider расширил тип $request->user()). План: admin-db ДО auth:sales. Один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 0.2: SalesUser (Authenticatable + HasApiTokens, isHead), SalesTariff, SalesClientAssignment (snapshot тарифа), SalesAttachmentRequest, SalesPayout (append-only через триггер БД). Тест tests/Feature/Sales/SalesModelsTest.php — 9 passed. Разрешение хозяина: один эскейп на сессию.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>