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.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 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 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>
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>
6-я плитка «👥 Клиенты» со светофором (amber если есть спящие) + drill:
KPI за период (всего активных / новых / заходили / получали лиды / платили),
список новых клиентов (с датой входа/лидами/балансом) и «спящих» (активные
без входа 14+ дней или ни разу = не активировались). Клик по строке → карточка
клиента. Backend: clients() endpoint + clientsTile в summary (cross-tenant через
pgsql_admin); сигналы — users.last_login_at, deals, balance_transactions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 read-only эндпоинта под группой [saas-admin,admin-db] (cross-tenant через
pgsql_admin): L1 сводка (Финансы+Здоровье), L2 Финансы (KPI+внимание+топ),
L2 Здоровье (6 подсистем+светофор). TDD, 83 admin-теста зелёные. baseline:
+3 Pest getJson false-positive. Без маржи, без новых таблиц.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bootstrap: alias admin-db=UseAdminConnection; web.php: группа saas-admin теперь
['saas-admin','admin-db'] (swap default→pgsql_admin после гейта). Тест: admin-db
в пайплайне /api/admin/tenants, saas-admin не потерян.
SharesAdminPdo (зеркало SharesSupplierPdo) применён глобально к Feature suite
(Pest.php): admin-db висит на всей группе → admin-эндпоинты в тестах читают
через pgsql_admin (separate PDO) и не видели бы засеянные в транзакции данные;
sharing PDO даёт cross-connection visibility. baseline: +trait.unused
(Pest применяет трейт в рантайме, phpstan не видит uses() из Pest.php).
261 supplier+admin тестов зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дружелюбный переключатель ВКЛ/ВЫКЛ флага routing_match_by_snapshot для владельца — без правки БД и без 30-символьного основания общего edit-flow. GET/POST source-edit-flag в AdminSupplierIntegrationController пишут в system_settings type=bool + audit-журнал. На экране карточка с VSwitch и диалогом подтверждения, бамп ключа возвращает тумблер к факту при отмене. TDD: 5 эндпоинт-тестов + фронт-спек. Larastan чист, baseline дополнен Pest-шумом. Проверено глазами через Playwright.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Владелец выбрал формат «экран в админке» (не письмо).
- SyncSupplierProjectsJob по завершении пишет строку-сводку в новую supplier_sync_runs
(групп/синк/ручная/отложено/упало + status ok|partial|failed|aborted) через finally —
пишется и при раннем abort (time-budget/mass-fail/auth).
- Эндпоинт GET /api/admin/supplier-integration/sync-runs + метод syncRuns.
- Экран SaaS-admin «Интеграция с поставщиком» → карточка «Вечерняя заливка проектов
поставщику»: таблица заливок со статусом человеческим языком (Всё ровно/Частично/Сбой).
- Схема v8.55 +1 таблица (SaaS-level без RLS как supplier_csv_reconcile_log), миграция
2026_06_25_130000, RLS-ревью 7/7. Проверено глазами в браузере (epic5-sync-runs-admin-screen.png).
Тесты: бэк 24/25 (1 skip) + фронт-экран 5/5 зелёные. Под LEFTHOOK=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Публичный роут /api/webhook/payment (CSRF-exempt). Cross-tenant поиск через
pgsql_supplier (BYPASSRLS), зачисление под SET LOCAL app.current_tenant_id,
атомарный claim pending->success (идемпотентность), защита от несовпадения
суммы, делегирование зачисления BillingTopupService.
Именованные лимитеры auth-login/auth-2fa/auth-password (perMinute 20 by IP) в AppServiceProvider; throttle-middleware на login/forgot/reset/2fa-verify/recovery в web.php. Закрывает per-IP объёмный перебор. Pest tests/Feature/Auth 97/97 GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec C §3.6/§6.2. Бэкенд: GET /api/billing/balance-status (frozen + capacity + required + дефицит ₽/leads), Pest 6. Фронт: BalanceFrozenBanner (в AppLayout, глобально), BalanceCapacityIndicator (в BillingView под балансом), ProjectLimitOverloadDialog (409-перехват в NewProjectDialog: save-blocked/set-zero), tenantStore + api getBalanceStatus. Vitest +18.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Закрывает дыру #4 аудита журналирования. Объём по выбору заказчика — МИНИМУМ:
✅ Админ-API + кнопка в админке для удаления ПДн субъекта
✅ Сервис анонимизации (users + supplier_leads + deals + webhook_log)
✅ Журнал факта удаления в pd_processing_log
❌ БЕЗ формы самообслуживания на стороне субъекта
❌ БЕЗ email-подтверждения
❌ БЕЗ 30-дневного SLA (trigger deadline_at уже в схеме)
Что добавлено:
* Eloquent-модель `App\Models\PdSubjectRequest` (таблица уже была в схеме)
* Сервис `App\Services\Pd\PdErasureService::eraseSubject()`:
- cross-tenant через pgsql_supplier (BYPASSRLS)
- транзакционно (rollback при ошибке)
- users: email→erased-{id}@deleted.local, first_name→Удалено, last_name→null,
phone→+7000{id}
- supplier_leads: phone→+7000XXXXXXX, raw_payload→{erased:true}
- deals: phone→+7000XXXXXXX, contact_name→Удалено (только если есть phone)
- webhook_log: batched UPDATE по 500, raw_payload→{erased,erased_at}
- pd_processing_log запись action=deleted за каждого user/lead с
actor_admin_user_id (hash-chain audit_chain_hash триггером сам подписывает)
- При requestId — pd_subject_requests SET status=completed, completed_at,
response_text счёт
* Контроллер `AdminPdSubjectRequestsController`: index/show/store/executeErasure
* Маршруты под middleware(saas-admin): GET/POST /api/admin/pd-subject-requests,
GET /{id}, POST /{id}/erase
* Vue: `AdminPdSubjectRequestsView` (Quiet Luxury, таблица + диалог создания +
кнопка Анонимизировать для request_type=deletion); ESLint требует
v-slot:[`item.X`]= вместо #item.X для динамических slot-имён с точкой
* Пункт меню в AdminLayout.vue + route /admin/pd-subject-requests
NB: реальная схема — users.first_name/last_name/phone/email; supplier_leads
имеет только phone (нет contact_*); deals имеет phone+contact_name (нет
contact_email); webhook_log JSONB. PdErasureService адаптирован под факт.
Тесты: 12/12 passed (63 assertions, ~2.6s) — index pagination, store +
deadline trigger (+30 дней), eraseSubject анонимизация user/lead/deal/log,
pd_processing_log запись, request status→completed, отклонение
не-deletion типов, gate saas-admin, InvalidArgumentException.
Plan: docs/superpowers/plans/2026-05-23-7-holes-overview.md (#4).
План 4 Task 2 эпика project-migration-redesign.
- AdminSupplierIntegrationController +projectsIndex (список supplier_projects
+ кто заказывал через pivot project_supplier_links -> projects -> tenants
organization_name + дата последней поставки = max supplier_leads.received_at
+ subject_name из RussianRegions::CODE_TO_NAME, «РФ» при NULL subject_code).
- +projectsDestroy (bulk-delete: deleteProject на портале, затем локально;
pivot снимается CASCADE; сбой строки не прерывает batch -> failures[]).
- Routes: GET /projects, POST /projects/delete в admin-группе.
- Pest 5/5 (26 assertions). phpstan-baseline +9 ignore (Pest TestCall).
GET /api/admin/supplier-integration/manual-queue — pending список (limit 100).
POST /manual-queue/{id}/resolve — оператор пометил, что вручную создал проект
на портале; reconcile через channel->listProjects() по (platform, signal_type,
unique_key), 409 если не найден.
ОТКЛОНЕНИЕ ОТ plan Step 10.3: план писал portal external_id прямо в
projects.supplier_b*_project_id (FK на local supplier_projects.id) — FK
violation. Resolve делает firstOrCreate local supplier_projects row с
verified external_id, в FK пишет local id.
Routes — в группе saas-admin (web.php, EnsureSaasAdmin стаб). Task 10 of 12.
Tests 4/4 (index pending / exclude resolved / resolve match / resolve 409).
Spec §4.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final review (🟢 low): SPA-маршрут /import работал через Route::fallback,
но все остальные app-маршруты перечислены явно в Route::view-блоке
(CLAUDE.md документирует явный список как намеренный паттерн — catch-all
перехватывал бы _test/* runtime-роуты Pest). /import добавлен в список
для консистентности и устойчивости.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>