Commit Graph

30 Commits

Author SHA1 Message Date
Дмитрий 16aa00a0e4 feat(sales): Этап 2 — сервис-канал поиск→портал (managers + ingest)
Портал публикует /api/sales/integration/{managers,prospects} под сервис-токеном
(X-Sales-Token, config sales.integration_token). ingest создаёт карточки stage=new
с полным payload, дедуп по (sales_user_id, inn|phone), assigned_by=начальник.
Гейты: 8/8 Pest, Larastan 0. Финдер-сторона — следующим коммитом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:13:59 +03:00
Дмитрий 2f6e88e784 feat(sales): замечания владельца по воронке — богатая карточка, сортировка созвонов, счётчики фильтра
Три правки после демо Этапа 1 (все по TDD):
1. Карточка показывает ВСЕ данные поиска из payload (тип ProspectPayload 1:1 с
   dataclass Firm; computed infoRows рендерит юрлицо, директора+личный ИНН,
   контакты, бюджет Директа вилкой, каналы/коллтрекинг, оценку). Демо-сидер
   кладёт полный синтетический payload.
2. Начальнику отдаётся manager_counts; фильтр показывает «Имя (N)».
3. Колонка «Переговоры» сортируется по next_call_at ASC (просроченные сверху).

Гейты: бэк 12/12, фронт 19/19, Larastan 0. НЕ выкачено (ждём разрешения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:52:54 +03:00
Дмитрий d6cddf73a6 feat(sales): artisan sales:prospects-demo — демо-карточки воронки
Этап 1 Task 8. По 2 карточки на каждую из 8 стадий у указанного менеджера
(для показа досок). Baseline под artisan() Pest-паттерн.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:35:52 +03:00
Дмитрий 9d8fd8d48c feat(sales): API воронки — список (свои/все+фильтр) + результаты разговора
Этап 1 Task 3–7. GET /api/sales/prospects (менеджер видит свои; начальник —
все + ?manager_id). PATCH /prospects/{id} — переговоры (next_call_at),
недозвон (причина), отказ (причина; запрещён из stage=user). ownership 403.
8 тестов зелёные. Baseline Larastan под Pest-паттерны нового файла.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:33:29 +03:00
Дмитрий 4b7463d1d9 feat(sales): модель SalesProspect + фабрика
Этап 1 Task 2. Дефолт stage=new в $attributes (доступен до refresh).
ide-helper мисин добавлен в локальный стаб (регенерация всех моделей).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:24:53 +03:00
Дмитрий ad50861ed5 fix(sales): тариф не сохранялся — первая ступень «0—1» (from=0) отклонялась валидацией
Форма «Тарифы менеджеров» дефолтит первую ступень с 0 (с момента привязки),
и расчёт вознаграждения (tenure стартует с 1) с from=0 работает верно, но
валидатор params.periods.*.from требовал min:1 → «Сохранить» падало 422
(«Поле params.periods.0.from должно быть не менее 1»). min:1 → min:0.

TDD: тест store с первой ступенью from=0 → 201 (падал 422 до фикса).
Baseline Larastan: actingAs 20→21 (добавлен один тест, квирк Pest+Larastan п.25).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:15:10 +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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 2e3089e207 feat(sales): EnsureSalesUser + ScopesSalesOwnership
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>
2026-06-30 12:43:31 +03:00
Дмитрий 0a842fc7a2 feat(sales): guard sales (sanctum) + provider + таблица токенов
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>
2026-06-30 12:36:13 +03:00
Дмитрий 2f692a0555 feat(sales): Eloquent-модели портала продаж
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>
2026-06-30 12:08:52 +03:00