Backend-ревью: register/resend применял только cooldown 60с, но не часовой
лимит 5/час (spec §7.6) — можно было слать код 1/мин бесконечно. Добавлен
RateLimiter по ключу email|ip (как в registerStart). +тест throttle для start.
Larastan baseline перегенерирован (новый тест добавил postJson-вызовы).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhoneNormalizer (RU-телефон → 7XXXXXXXXXX) + Mailable RegisterEmailVerificationCode
с 6-значным кодом + эндпоинты register/start|verify|resend: pending-регистрация
в сессии (паттерн 2FA), email_verified_at=now() при verify, rate-limit на start +
cooldown 60с на resend, лимит 5 попыток ввода кода. Телефон обязателен, нормализуется
в 7XXXXXXXXXX. deptrac: разрешён Request→Service. Старый одношаговый register пока
сохранён (удаляется отдельной задачей Task 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Джоб создания/правки проекта запускается из очереди, где SetTenantContext не
отрабатывает (нет app.current_tenant_id GUC). Под боевой ролью crm_app_user первый
же Project::find() падал SQLSTATE 42704 (unrecognized configuration parameter
app.current_tenant_id) за ~2мс — до контакта с поставщиком: проект у поставщика не
создавался, в UI вечный «Sync pending». На dev не всплывало (postgres superuser
обходит RLS). Единственный supplier-flow джоб, который был на дефолтном подключении.
Фикс: const DB_CONNECTION = 'pgsql_supplier' + все DB-операции через ::on()/
DB::connection() — как у SyncSupplierProjectsJob/DeleteSupplierProjectJob/CsvReconcileJob.
Тесты: SupplierConnectionTest +constant-assert; SyncSupplierProjectJobTest
+поведенческий connection-assert (DB::listen → projects-запросы на pgsql_supplier);
Plan5/SyncSupplierProjectJobTest +SharesSupplierPdo (джоб теперь пишет через
pgsql_supplier → нужен shared PDO под DatabaseTransactions).
Проверено вживую на тест-сервере: проекты 14/15 синхронизированы, 6 доноров у
crm.bp-gr.ru (12742042-44 / 12766120-22), aggregateSyncStatus=ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
На 2 vCPU/2GB YC VM холодный старт Chromium в refresh-session ~65s wall-clock,
не укладывался в прежние 75s (60s Node + 15s buffer). Поднято до 180s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Закрывается крестиком, закрытие запоминается в localStorage. Чисто фронтенд (информация, без блокировок, без бэкенда). +3 Vitest.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Allows SaaS-admin area in non-local/testing envs only when SAAS_ADMIN_TEST_BYPASS=true.
Default false -> production unaffected. Remove after Yandex 360 SSO (Б-1 + DO-4).
TDD: tests/Feature/Middleware/EnsureSaasAdminTest.php (2 passing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleOnline/syncGroup: сверка external_id со списком живых проектов портала (listProjects); пересоздание удалённых на портале доноров in-place без удаления записей (на supplier_projects могут висеть лиды/списания). online-режим заполняет supplier_b1/b2/b3_project_id, чтобы UI sync-бейдж не залипал в pending. +3 Pest.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DeleteSupplierProjectJob: если после удаления Лидерра-проекта у донора
(supplier_project) не осталось других потребителей (pivot
project_supplier_links) — удаляет его у поставщика и локально;
если потребители есть — НЕ удаляет, диспатчит SyncSupplierProjectsJob.
2 Pest-теста (no-consumers / remaining-consumers) GREEN.
phpstan-baseline: +once() Mockery chain (аналог andThrow baseline).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AppLayout брал заголовок топбара только из sidebar navItems → /reminders и
/import (которых нет в боковом меню) показывали fallback «Страница». Добавлен
fallback на route.meta.title перед «Страница». TDD: AppLayout.spec.ts (RED→GREEN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DashboardPageHead показывал захардкоженное «Доброе утро, Иван» любому
пользователю. Теперь имя берётся из auth-store (first_name), а приветствие —
по времени суток (ночь/утро/день/вечер). Fallback «коллега» при отсутствии user.
TDD: DashboardPageHead.spec.ts (RED→GREEN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Реальный портал отдаёт rt-projects-load с name='B1_<id>' / 'B2_<id>' / 'B3_<id>'
и чистым идентификатором в поле 'content'. Старое matching по name === uniqueKey
никогда не совпадало с реальным ответом → idMap пустой → SyncSupplierProjectJob
молча выходил, ничего не записав в БД, а на портале оставались orphan-группы.
Объясняет ранее задокументированное в ЭТАЛОН «проект 5 вылечен вручную —
усыновлены 3 портальные записи». Заказчик обходил тот же баг руками.
Фикс — matching по content с fallback на name, чтобы мок-тесты с упрощённым
форматом (без content) продолжали работать; реалистичная фикстура добавлена
в SupplierPortalClientMultiFlagTest.
Verified:
- Pest supplier suite (SyncSupplierProjectJob/SyncSupplierProjectsJob/multi-flag): 16/16 passed
- E2E live на crm.bp-gr.ru: ProjectService::create + sync → supplier_projects записаны
с ext_id, pivot заполнен, портал имеет 3 группы B1/B2/B3
- Multi-tenant ночной батч с computeOrder проверен на 79991177889 (T1+T2+T3+T4
на одном identifier — формула max(max, ceil(Σ/3)) сходится с фактом)
Закрывает два бага sync поставщика, обнаруженные при live-проверке создания
проекта «мой номер» (call, 79135191264, лимит 15, дни Пн-Пт):
1. SyncSupplierProjectJob хардкодил workdays=[1..7] в 7 местах и в DTO для
portal, и в supplier_projects.current_workdays. Заменено на реальную маску
через приватный workdaysFromMask() (зеркало bitmaskToList ночного батча).
2. forceFill в update-path online mode не включал current_workdays — после
первого create со старыми [1..7] последующий ресинк не подтягивал
реальные дни в локальную БД (на portal летели корректные, в нашей таблице
оставались stale).
3. ProjectService::update() ресинкал только при смене sms_*/signal_identifier/
regions. Добавлены daily_limit_target и delivery_days_mask — поставщик
видит новый лимит и дни сразу, не дожидаясь ночного батча 18:00 МСК.
Тесты:
- SyncSupplierProjectJobTest: +2 specs (real-workdays create-path, update-path
current_workdays refresh).
- ProjectsUpdateTest: «without resync» переписан в name-only, +2 specs
(daily_limit_target и delivery_days_mask change → resync).
- Pest 146/146 (Supplier + Plan5/Projects scope), Pint passed, Larastan 0.
Live-ресинк проекта id=5 «мой номер» в dev DB выполнен — current_workdays
теперь [1,2,3,4,5], HTTP ушёл к crm.bp-gr.ru с теми же днями.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Портал crm.bp-gr.ru возвращает status=Doubles при попытке создать
вторую группу с тем же unique_key. Старый код делал одну B1/B2/B3-группу
на каждый регион проекта — вторая группа молча пропадала.
Теперь оба джоба (SyncSupplierProjectJob + SyncSupplierProjectsJob)
формируют ровно одну группу на идентификатор со всеми регионами:
- regions=[82,83] → tag='РФ', regions=[82,83] в одной группе
- regions=[] → tag='РФ', regions=[] (вся РФ)
- regions=[82] → tag='Москва', regions=[82]
subject_code=null во всех supplier_projects и project_supplier_links.
ProjectService::update() теперь триггерит SyncSupplierProjectJob
при изменении поля regions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Активатор v-menu внутри position:fixed v-app-bar уезжает off-screen под
prefers-reduced-motion:reduce (умолчание Windows Server). Подключён
repositionMenuAfterOpen к обоим меню топбара через @update:model-value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
План 4 Task 4 эпика project-migration-redesign.
- NewProjectDialog: отдельный чекбокс «Вся РФ» (89 субъектов в autocomplete
без sentinel сохранены) + inline v-alert предупреждение + подтверждение.
- Взаимоисключение: выбор субъектов снимает «Вся РФ» и наоборот.
- Гейт submit: блок если ни субъектов, ни подтверждённой «Вся РФ»
(errors.regions = «Выберите регион...»); «Вся РФ» -> regions=[] на API.
- Лейбл autocomplete «Регионы» (убрано «(пусто = вся РФ)»).
- watch immediate:true — инициализация vsyaRf/edit-prefill при mount
(чинит EditProjectDialog submit при модальном открытии).
- Vitest 3/3 новых + 22 passed соседних (NewProject/Edit/ProjectsView) без регрессий.
План 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).