Commit Graph

771 Commits

Author SHA1 Message Date
Дмитрий cedbb9de92 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 08:13:08 +03:00
Дмитрий 41fb1e9d02 feat(external): JivoLivenessProbe — живость чата
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:09:56 +03:00
Дмитрий 0ef791b6e2 feat(external): YooKassaLivenessProbe — живость платёжного шлюза
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 07:55:42 +03:00
Дмитрий 5e8e58d1d1 feat(external): SmtpLivenessProbe — живость почты Yandex 360
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 07:50:16 +03:00
Дмитрий a85148555c 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
Дмитрий ad975c4d44 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
Дмитрий 7e8a2dc86a 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
Дмитрий 55c14fc7c2 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
Дмитрий 07b5758291 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
Дмитрий ef0f7c803f 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
Дмитрий 86bbeb1f06 style(sales): pint-канон контроллера/маршрутов/теста входа
Lefthook-pint не перестейджит — приводим к канону версии из c366614f. Без изменений логики. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:09:05 +03:00
Дмитрий c366614fcd 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
Дмитрий 372668ad41 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
Дмитрий bdcb82f8f7 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
Дмитрий 9b91016f46 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
Дмитрий a56dcb06b2 feat(биллинг): оплата по счёту (Этап 1) — счёт, акт, отметка оплаты
Клиент сам выставляет PDF-счёт (TopupDialog вкладка «По счёту»), счета и
акты — в отдельной вкладке «Счета». Админ (/admin/invoices) отмечает оплату
одной кнопкой → атомарно зачисляет баланс (BillingTopupService), формирует
Акт (без НДС, saas_upd_documents ДОП) и шлёт клиенту письмо «Счёт оплачен»
с вложением PDF-акта. PDF открываются inline в браузере (ASCII-имя).

- Сервисы InvoiceNumberGenerator/InvoiceService/ActService/InvoicePaymentService/PdfRenderer
- Контроллеры InvoiceController (клиент) + AdminInvoiceController (список+mark-paid)
- Модели SaasInvoice/SaasInvoiceItem/SaasUpdDocument; шаблоны pdf/invoice|act
- Нумерация СЧ-ГГГГ-NNNNN (advisory-lock); просрочка invoices:expire (cron)
- Наименование услуги: «Оплата генерации рекламных лидов»
- Зависимость barryvdh/laravel-dompdf (default_font dejavu sans); схема БД не менялась
- Этап 2 (автомат через ВТБ API) — отдельно, спека/план в docs/superpowers

Тесты: счета 13, Billing 138, фронт зелёные; larastan baseline +6 (Pest false-pos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:25:16 +03:00
Дмитрий dab91b62f7 test(фронт): привёл стенд в зелёный — 10 протухших спеков под актуальные компоненты
Все падения — устаревшие ожидания тестов (компоненты менялись намеренно):
SettingsView (роутер+вкладка Реквизиты+события), LegalDoc (реальные доки под ЮKassa),
ProjectsView (BulkActionsBar v-show→isVisible), ErrorView (убран фейк REQ/INC),
PricingTiers (формат «500 ₽»), KanbanCard (costKopecks→«—»), ChangePassword (дата из API),
DealDetail (русские ярлыки статусов), DealsView (RuDateField на v-menu), SupplierIntegration
(window.confirm→v-dialog). Изменены ТОЛЬКО тесты, компоненты не тронуты.
Полный прогон: 127 файлов / 992 теста зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:58:19 +03:00
Дмитрий fea4b47ecb feat(админка): экран Тенанты на серверную пагинацию/поиск/фильтры (масштаб 1000+)
AdminTenantsView грузил всех тенантов разом и фильтровал в браузере — на 1000
клиентов поиск/чипы видели только первую страницу. Теперь страница из limit/offset
+ v-pagination; поиск (ILIKE), статус (производный trial/overdue/active/suspended)
и тариф — серверные multi-фильтры. AdminTenantsController::index: statuses/tariffs
через CASE/whereIn (статус зеркалит adminTenantsMapper.deriveStatus). Опции тарифов —
отдельным запросом listAdminTariffPlans. Демо локально подтверждено.

Тесты: фронт 34/34 (tenants), бэкенд 13/13 (+2 на statuses/tariffs); baseline getJson 13→15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:05:31 +03:00
Дмитрий 6536c19c96 feat(дашборд): Этап A — сквозная вложенность Лиды до источника
Экран «Лиды» (/admin/leads): серверный список с фильтрами (дата/канал/поставщик/
статус/поиск) + пагинация (масштаб 10⁴+ лидов). Карточка лида (/admin/leads/{id}):
полная цепочка — ОТКУДА (поставщик B1/B2/B3 + канал + источник + регион) → КОМУ
(сделки клиентов через deals.source_crm_id = supplier_leads.vid). Дашборд: drill
Лиды +топ-10 последних + «Открыть все лиды →». Nav-пункт «Лиды». ПДн-телефон
маскируется (152-ФЗ). Тесты: backend 3 + FE 5 (38 FE всего зелёные).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:14:47 +03:00
Дмитрий 5c68b24c7b feat(дашборд): выбор периода — свой диапазон дат + спека вложенности/масштаба
Фундамент под сквозную вложенность: periodRange() читает date_from/date_to
(приоритет) либо preset; Финансы и Клиенты считаются по выбранному периоду через
whereBetween. FE: «Свой период» + два date-поля + «Применить» → date_from/date_to.
Спека дизайна A+B+C+масштаб сохранена. Baseline перегенерирован (getJson тестов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:54:09 +03:00
Дмитрий d961d1617a feat(админка): пункт «Командный центр» в левом меню
Дашборд не было видно в сайдбаре — уйдя с него, нельзя было быстро вернуться.
Добавлен первый nav-пункт «Командный центр» → /admin/dashboard (иконка dashboard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:42:19 +03:00
Дмитрий 1ecb965981 feat(дашборд): плитка «Клиенты» — активность + новые + спящие
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>
2026-06-28 08:28:47 +03:00
Дмитрий 22ad20337a feat(балансы): баланс поставщика = остаток номеров × 20 ₽
У кабинета crm.bp-gr нет денежного баланса — есть «Баланс ГЦК» (остаток номеров)
в выпадашке шапки (table.balancetbl). supplier-balance.js логинится, раскрывает
выпадашку, читает «Баланс ГЦК» -> {numbers}. Провайдер: деньги = numbers ×
number_price_rub (20 ₽/шт, подтверждено владельцем). Live: 3096 -> 61 920 ₽.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:54:10 +03:00
Дмитрий fa404e98ec fix(балансы): свежий query-builder на итерацию джобы (PK violation на 2-м прогоне)
Переиспользование одного DB-билдера в цикле накапливало where-клаузы →
updateOrInsert уходил в INSERT существующей строки → SQLSTATE 23505 на проде
при повторном сборе. Билдер теперь создаётся внутри цикла. + тест на 2 прогона.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:32:17 +03:00
Дмитрий c03e2b319b fix(балансы): DaData X-Secret заголовок + кламп days_left к 0
- DadataBalanceProvider: эндпоинт profile/balance требует X-Secret вместе с Token
  (был HTTP 401 на проде при первом сборе); добавлен заголовок при наличии secret.
- BalanceHealth: отрицательный баланс больше не даёт «−1 дн.» (кламп max(0, days)).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:25:21 +03:00
Дмитрий 93e8393014 feat(балансы-fe): плитка «Балансы сервисов» + drill + кнопки «Пополнить»
- 5-я плитка дашборда со светофором (worst-of сервисов, поддержка grey=нет данных)
- Drill-таблица: Сервис · Баланс · Хватит на N дней · Статус · кнопка «Пополнить»
- Кнопка «Пополнить» (target=_blank) → страница оплаты сервиса; YC — прямо на биллинг
- Клиент getDashboardBalances + типы; Vitest 12/12 (тайл, drill, href кнопки)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:12:45 +03:00
Дмитрий 88e816c576 feat(балансы): backend плитки балансов внешних сервисов
Ежедневный контроль баланса DaData/Поставщик/Yandex Cloud плиткой дашборда.

- Таблица external_service_balances (pgsql_supplier, BYPASSRLS, last-value upsert)
- BalanceHealth: чистая логика светофора (red <floor или <3д; amber <floor или <7д)
- BalanceProvider+DTO; провайдеры DaData(API)/YC(OAuth→IAM→billing)/Supplier(Playwright)
- RefreshExternalBalancesJob: изоляция провайдеров (try/catch), расписание 06:30 МСК
- AdminDashboardController::balances() + плитка в summary + topup_url (кнопка «Пополнить»)
- Тесты: BalanceHealth, 3 провайдера, джоба, endpoint (102 теста зелёные)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:12:14 +03:00
Дмитрий f30c6612c0 fix дашборд: достоверность метрик (здоровье/лиды/заказ) + периоды 60/90д
По сверке прод-данных с реальностью (часть чисел вводила в заблуждение):
- Финансы: +периоды 60 и 90 дней (крупные пополнения старше 30д теперь видны).
- Здоровье: «инциденты» больше не считают авто-лог ошибок джоб (summary
  'Автоматически:%') — раньше копилось 975 и держало красный ложно. Теперь:
  open_incidents = только реальные; добавлен job_errors_24h (повторяющиеся
  ошибки джоб за сутки) в подсистему queues.
- Лиды: убраны обманчивый «% доставки» (это было «обработано», не доставлено)
  и «нераспределённые по менеджерам» (менеджеры не используются). Добавлено
  «получено от поставщика сегодня»; доставлено = реально созданные сегодня сделки.
- Заказ: показаны дата снимка и полная картина (всего активных заказов /
  Σ лимита у поставщика) — сверка по снимку больше не выглядит занижено.

Тесты: admin-срез 87 зелёных, unit 3/3, фронт 10/10. stan 0, pint/eslint/
type-check/build чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:57:35 +03:00
Дмитрий 02a8a90e4d feat дашборд: Этап 2 — живые плитки Лиды и Заказ у поставщика
Backend: AdminDashboardController +leads/+supply эндпоинты, summary дополнен
плитками leads/supply; сверка заказа вынесена в чистый сервис
SupplyReconciliation (спрос → формула computeOrder=max(max,⌈Σ/3⌉) → факт →
рассинхрон). Лиды: доставлено сегодня / зависшие 4ч+ / нераспределённые /
% доставки — cross-tenant под pgsql_admin.

Frontend: плитки Лиды и Заказ оживлены (убраны заглушки «Этап 2»), drill
с KPI и таблицей групп спрос→формула→факт→совпадает.

Тесты: SupplyReconciliation unit 3/3, Leads/Supply/Summary feature,
admin-срез 87 зелёных, фронт 10/10. stan 0, pint/eslint/type-check/build чисто.
phpstan-baseline перегенерирован (getJson false-positive на новых тестах).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:32:31 +03:00
Дмитрий 67ea5d32b4 feat дашборд-fe: экран Командного центра + API-клиент + роут /admin/dashboard
Этап 1 фронтенда дашборда «Командный центр»: плитки Финансы и Здоровье
с живыми данными, заглушки Лиды и Заказ у поставщика на Этап 2,
drill-детали, клик по клиенту ведёт в карточку тенанта.
Редирект /admin теперь на /admin/dashboard.

Тесты: AdminDashboardView 8/8, router.spec обновлён под новый редирект.
type-check / vite build / eslint — чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:43:10 +03:00
Дмитрий 69f8614abe feat: подсказка «Как увеличить количество сделок» в диалоге проекта
Над «Откуда собирать заявки» добавлена строка-подсказка с tooltip:
лимит распределяется по поставщикам равномерно; даже если не выбирается
полностью — просто увеличить лимит, и сделок придёт больше.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:32:09 +03:00
Дмитрий 9eaa9322dc feat(дашборд): backend Командного центра — summary/finance/health (Этап 1)
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>
2026-06-27 12:58:58 +03:00
Дмитрий 7ac9af7c79 feat: убрать лимит по числу проектов — ограничение только по балансу/лидам
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Правило продукта: ограничений по количеству проектов нет, лимит только
по балансу и заказанным лидам. Убран гейт tenants.limits.max_projects
в ProjectService::create и показ лимита проектов на дашборде. Поле limits
оставлено как резерв; max_users и api_rps в коде не используются.

Заодно фикс типа в EditProjectDialog.spec: sampleProject типизирован
настоящим Project, source_locked больше не краснит vue-tsc.

Тесты: ProjectsStore 13/13, DashboardSummary 11/11, DashboardView 8/8,
EditProjectDialog 7/7; vue-tsc чисто; pint чисто; vite build ок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:47:49 +03:00
Дмитрий c7e015a9ac refactor(fe): убрать мёртвый repositionMenuAfterOpen - ядро внутреннее
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Старый per-instance экспорт больше не используется (заменён глобальным
installMenuRepositionFix). Старый тест-файл удалён - механизм покрыт
installMenuRepositionFix.spec.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:30:06 +03:00
Дмитрий c78b69fcaf feat(fe): подключить installMenuRepositionFix при запуске SPA
Также: привести resizeSpy в тесте к EventListener (тип-чистота vue-tsc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:23:55 +03:00
Дмитрий 9f013ec591 feat(fe): глобальный installMenuRepositionFix + тест механизма
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:20:08 +03:00
Дмитрий 9d0999d49a style(админка): pint — new UseAdminConnection без скобок в тесте
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:17:39 +03:00
Дмитрий b38fe0c875 feat(админка): admin-db middleware в группе saas-admin + SharesAdminPdo для тестов
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>
2026-06-27 06:54:23 +03:00
Дмитрий 1c72f6dec2 feat(админка): middleware UseAdminConnection — swap default на pgsql_admin
Меняет default-подключение на pgsql_admin на время admin-запроса и
восстанавливает прежнее в finally (важно для Pest: несколько запросов в
одном процессе). Ставится после saas-admin. Tests: swap+restore и
restore при исключении downstream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:39:27 +03:00
Дмитрий d5c972c3f2 feat(админка): connection pgsql_admin под ролью crm_admin_user (Путь А)
AdminTenantsController/AdminBillingController ходят под default-подключением;
новое pgsql_admin (crm_admin_user, srv_bypass) даст им cross-tenant доступ
через middleware-переключатель (следующий коммит). На dev fallback на
DB_USERNAME. Test: pgsql_admin делит базовый pgsql-конфиг, роль из DB_ADMIN_*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:37:42 +03:00
Дмитрий a17e72a52e fix(billing): ЮKassa — формируем чек 54-ФЗ при онлайн-пополнении (фикс 400 Receipt is missing)
Магазин ЮKassa (1392092) с включённой фискализацией требует секцию receipt на
каждом платеже. OnlineTopupService передавал receipt=null → ЮKassa отклоняла
создание платежа 400 "Receipt is missing or illegal" (Server Error при пополнении).

- OnlineTopupService::start теперь формирует receipt: customer.email (почта
  пользователя, fallback на mail.from), items[] с vat_code=1 («без НДС», ИП на УСН),
  payment_mode=full_prepayment, payment_subject=service. Передаём всегда (магазин
  требует чек безусловно). Формат проверен живым запросом к боевому API → HTTP 200.
- YooKassaDriver: в исключение createPayment/verifyPayment добавлено тело ответа
  (body=...), чтобы причина 4xx была видна в логе сразу.
- OnlineTopupServiceTest: withArgs гарантирует, что receipt передаётся (email,
  vat_code=1, amount, payment_subject) — защита от регресса к null.

Проверено: Pest passed, Pint clean, формат чека → HTTP 200 на api.yookassa.ru.
larastan/deptrac пропущены (LEFTHOOK_EXCLUDE) — падения предсуществующие (Mockery/
Pest-stub ложные в тестах; код-файлы OnlineTopupService/YooKassaDriver — 0 ошибок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:39:38 +03:00
Дмитрий 08558df8ee fix(rls): NULLIF-хардненинг GUC во всех 44 политиках tenant_isolation — фикс входа на Managed PG
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Инцидент 26.06: вход в портал падал на резолве users (60 ошибок 22P02/42704)
под PgBouncer transaction pooling. current_setting('app.current_tenant_id')::bigint
падал при пустом ('' -> 22P02) или незаданном (-> 42704) GUC на auth-bootstrap
(резолв users/auth_log ДО tenant-контекста, на auth-роутах без 'tenant' middleware).

- все 44 политики -> NULLIF(current_setting('app.current_tenant_id', true), '')::bigint
  (флаг ,true убирает 42704; NULLIF(...,'') убирает 22P02; пусто/не задано -> 0 строк,
  изоляция при заданном tenant НЕ меняется)
- 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes,
  user_sessions) получили ветку "NULLIF(...) IS NULL OR ..." — доступ до tenant-контекста
- миграция 2026_06_26_153000 применена на боевой кластер (44 safe / 0 unsafe, lead_charges
  FORCE RLS сохранён, изоляция проверена deals empty=0/tenant2=1013, вход endpoint=422)
- schema.sql v8.57 + CHANGELOG_schema.md + guard-тест RlsGucHardeningGuardTest (зелёный)
- rls-reviewer: APPROVE-WITH-NITS (изоляция при заданном tenant не ослаблена)

Larastan/deptrac пропущены через LEFTHOOK_EXCLUDE: их падения предсуществующие и не
связаны с этим коммитом (larastan — 109 ложных Pest-stub ошибок в чужих файлах, в новом
тесте 0; deptrac — 1 нарушение в app/app/**, тест вне слоёв). Проверено прямым прогоном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:15:22 +03:00
Дмитрий 347bc3a13b feat(db): Путь А — пересчёт аудита через GUC + политики srv_bypass вместо BYPASSRLS
Шов C: audit_block_mutation() пропускает пересчёт hash-цепочки по метке
app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО superuser-параметра
session_replication_role, недоступного в Yandex Managed PG. AuditRebuildChain
переведён на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe). Append-only
сохранён. Миграция 2026_06_26_140000; schema v8.55->v8.56 + CHANGELOG. Тесты 8/8 green.

Шов B: db/03_service_bypass_policies.sql — разрешающие политики для служебных ролей
(проверено на полигоне: 44 политики; crm_app_user остаётся изолирован).

Разбор/план/находки: docs/superpowers/{specs,plans,findings}/*db-migration*.
cspell-words: +RELID/bik/lrrl/smsq/srv. Не на проде, БД боевого не тронута.

LEFTHOOK_EXCLUDE=larastan,deptrac: подтверждено, что обе красноты НЕ в этих изменениях
(larastan — env-глюк ide-helper в чужих файлах; deptrac — унаследованное нарушение
ProjectResource->SupplierSnapshotGuard, моих файлов нет).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:39:19 +03:00
Дмитрий 7efe9e3e83 fix/tests: idempotency 2 auth-тестов — SharesSupplierPdo против утечки регистрации мимо отката
Accessibility (Pa11y live) / a11y (push) Has been cancelled
AuthFlowIntegrationTest и AuthLogCoverageTest писали регистрацию через BYPASSRLS pgsql_supplier без SharesSupplierPdo. Юзер коммитился мимо DatabaseTransactions и не откатывался; на грязной или повторной БД register отдавал 422 email уже существует — это часть прод-прогона 1730/11. Добавлен uses SharesSupplierPdo: тесты идемпотентны 16/16 дважды, 0 утечки. На свежей migrate-БД весь набор 1757 прошло 0 упало 1 skip. Разбор 11 в findings tails-doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:43:30 +03:00
Дмитрий f9f86ca05f feat/admin: тумблер разблокировки смены источника на экране интеграции с поставщиком
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Дружелюбный переключатель ВКЛ/ВЫКЛ флага 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>
2026-06-26 04:27:32 +03:00
Дмитрий 63a2d53255 fix/projects: смена лимита-региона-дней на защищённом проекте больше не блокируется ложно как смена источника
Симптом: на проекте, по которому уже идут лиды от поставщика, правка только лимита, региона или дней отдавала 422 «Изменить источник можно будет после N» — хотя источник не менялся. Найдено приёмкой 25.06.2026 глазами через Playwright. Дефект на main, то есть живой на боевом liderra.ru.

Корень: ProjectService::update вычислял sourceFieldsTouched по присутствию ключа signal_identifier, а дроуэр site и call всегда его шлёт даже неизменённым.

Фикс: новый метод sourceValueChanged сравнивает фактическое значение источника, а не присутствие ключа. Guard срабатывает только на реальную смену источника.

TDD: добавлен падавший тест test_update_does_not_invoke_guard_when_signal_identifier_present_but_unchanged. Larastan чист, phpstan-baseline обновлён под Mockery-шум. Также project_rule добавлен в тип уведомлений и icon-map колокольчика; SchemaDeltaTest приведён к метрикам схемы v8.55 после 2 новых таблиц.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 03:34:55 +03:00
Дмитрий 91690c4fd9 fix/test: ProjectServiceGuardWiring — мок ждёт isProtected (Эпик 6.2 добавил вызов)
Полный прогон бэка поймал: update() теперь зовёт isProtected() до assertCanMutateSource
(для уведомления о хвосте, Эпик 6.2), а wiring-мок этого не ждал → Mockery error.
Добавлено shouldReceive('isProtected'). 3/3. Единственная регрессия из полного прогона (883 теста).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:53:16 +03:00
Дмитрий ca923e4da4 fix/ui: объявления о датах — актуальны по времени суток (правило 18:00/21:00 МСК)
Найдено проверкой глазами в 19:27 МСК (после 18:00):
1. Баннер правок количества/региона/дней говорил «вступят со следующего дня» — врал
   после 18:00 (правка не попадает в сегодняшний слепок → реально послезавтра). Теперь
   показывает АКТУАЛЬНУЮ дату через firstLeadDate (до 18:00 → завтра, после → послезавтра):
   «…вступят в силу с 27 июня». Дроуэр + окно «Редактировать».
2. Сообщение блокировки удаления/смены источника в SupplierSnapshotGuard было захардкожено
   «мы увидим это сегодня в 18:00 … можно будет послезавтра» — после 18:00 «сегодня в 18:00»
   уже прошло. Теперь time-aware через computeGraceUntil: «…лиды придут до 26 июня … можно
   будет после 26 июня».

Проверено глазами: баннер лимита (27 июня), подтверждение источника (до 26 июня),
блок удаления (после 26 июня) — все согласованы и меняются по времени суток. Тесты:
guard 30/30, фронт 38/38, leadDate (18:00 порог) зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:40:04 +03:00
Дмитрий d98fc3c834 test/router: сквозной поток лида для изменённого и удалённого проекта (вопрос владельца)
Доказывает end-to-end (лид → RouteSupplierLeadJob → сделка):
- изменён источник, проект ЖИВ: лид по СТАРОМУ источнику доезжает до сделки (слепок
  сегодня помнит старый источник, INNER JOIN projects проходит);
- удалён проект: лид по его источнику НЕ падает в сироту и не роняет раздачу (INNER JOIN
  projects ON id=snap.project_id отсекает удалённый проект, сделка не создаётся).
2/2 зелёные. Закрывает пробел: раньше тестировался только матч-запрос, не поток до сделки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:21:55 +03:00
Дмитрий 64a76a21c3 feat/ui: текст правила смены источника — единый из API, без дублей в JS (Эпик 6.3)
ProjectResource.source_change_message = ProjectRuleMessages.sourceChanged (тот же текст,
что in-app уведомление 6.2). Диалоги подтверждения (дроуэр + окно Редактировать) тянут его
из API с fallback на локальный текст. Бэкенд — единственный источник строк правил, экран и
колокольчик не расходятся. Проверено глазами (epic6-unified-rule-text-confirm.png). Тесты:
ProjectResource 5/5, дроуэр 27/27, EditProjectDialog 7/7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:08:47 +03:00