Закрывает замечания заказчика (22.05.2026) по проектам/поставщику. Все 4 куска
имеют общий корень: online-синхронизация одного проекта работала с данными ЭТОГО
проекта, а не пересчитывала всю «группу» (проекты разных tenant'ов с одним
identifier) — отсюда переплата ×3 при изменении лимита, затирание регионов/дней
группы, неотправленная пауза, и осиротевшие проекты при смене источника.
1. Групповой пересчёт в SyncSupplierProjectJob::handleOnline (#1 при изменении,
#2 дни, #3 регионы, C2/C3): union regions, computeOrder eligible,
distributeForPlatform — те же расчёты, что в ночном syncGroup. Online и
ночной теперь дают идентичный supplier-state, расхождение устранено.
2. Пауза #10:
- ProjectController::toggleActive — диспатчит SyncSupplierProjectJob;
- ProjectService::bulkPauseResume — диспатчит sync per project;
- DTO status вычисляется из groupActive (paused когда группа без активных);
- sp.inactive_since пишется при пересинке (для UI/DTO консистентности).
3. Смена источника #8/#9 в ProjectService::update:
- до update снимается старый buildUniqueKeyAgnostic;
- если изменился — отвязываем старые supplier_projects от этого project
(pivot + legacy FK), DeleteSupplierProjectJob удаляет их у поставщика
при отсутствии других потребителей, либо пересинкает агрегат.
4. Перенос auto-link корня из feat/root-domain-auto-link: новый
App\Support\SupplierIdentifier::extractRootDomain + блоки auto-link в
обоих джобах (online + nightly).
Тесты: TDD на каждый кусок. SyncSupplierProjectJobTest +2 (group recompute,
pause). ProjectUpdateDedupTest +1 (source detach + cleanup dispatch).
ProjectsActionsTest +2 (toggle + bulk pause dispatches).
Регрессия: 186/186 passed (Project/Plan5/Projects + Supplier), 502 assertions.
Деплой: дельтой на боевой (база = root-domain ветка; на боевом джобы СТАРЕЕ
main, deliver через копию изменённых файлов + config:cache + restart queue).
План: docs/superpowers/plans/2026-05-22-замечания-проекты-чеклист.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Закрывает два бага 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>
UX-request 18.05.2026 (п.9):
- ProjectDetailsDrawer (правая панель на /projects) теперь редактирует
signal_identifier для site (домен) и call (телефон 7\d{10}); для sms —
sms_senders+sms_keyword (как раньше).
- Поле «Источник» отображается **только** в карточке проекта (read-only
в drawer сделки на /deals — Task 2 закрыл).
Backend:
- UpdateProjectRequest: condition-based валидация по signal_type из БД
(site domain regex, call 11-digit 7\d{10}; sms — без новых правил)
- ProjectService::update: убран signal_identifier из silent-drop;
$needsResync расширен на signal_identifier → SyncSupplierProjectJob
signal_type остаётся immutable (менять тип проекта — отдельная задача).
Larastan baseline bumped (ProjectsUpdateTest: actingAs 8→12 для 4 новых тестов).
Pest tests/Feature/Plan5/Projects/ProjectsUpdateTest 12/12.
Vitest 33 passes на Project-spec'ах. Build 2.03s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C1: ProjectResource не возвращал regions → edit-диалог/drawer затирали
сохранённые регионы при сохранении. +поле в toArray().
C2: +integration-тест outbound regions[] через полный SyncSupplierProjectsJob::handle().
I1: расскип NewProjectDialog payload-теста (regions в POST).
I2: assert data.regions в ProjectsStore/UpdateTest (ловит C1 на backend-уровне).
I4: docblock — bulkUpdateRegions legacy (region_mask, не влияет на outbound до Plan 6.5).
M1: CHANGELOG v8.22 — исправлен неверный пример регионов (Москва=82).
Регрессия: Pest 905/902/3sk/0, Vitest 104f/884/3sk/0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Корень: dev-БД `liderra` создавалась с LC_CTYPE=C — lower()/upper() не
делает case-folding для кириллицы, `ILIKE '%сп%'` на «Окна СПб» = 0 строк.
Test-БД с Russian_Russia.1251 маскировала проблему.
Системный fix: dev-БД пересоздана через `LOCALE_PROVIDER icu ICU_LOCALE 'und'`
(PG 16+ ICU collation, кросс-платформенно). Точечный COLLATE-workaround не
понадобился — все 5 ILIKE-endpoint'ов теперь работают с кириллицей без
правки кода. CTO-20 закрыт в реестре v1.81; команда CREATE DATABASE с ICU
зафиксирована для prod-deploy.
Сопутствующее:
- ProjectsView clearable: workaround `::after content '✕'` + видимость
через `.v-field--dirty` (mdi-* font не подключён в проекте — CTO-19
заведён в реестре).
- LookupsTest: удалён stale case `GET /api/projects?tenant_id=N`,
заменённый auth:sanctum-роутом в Plan 5.
- Pest +1 регрессионный тест (`search is case-insensitive for Cyrillic`)
в ProjectsListShowTest, 10/10 / 37 assertions.
- phpstan-baseline регенерирован (3 actingAs + удалённый case).
- cspell-words: +Регистронезависимый, +und.
- app/.backups/ в gitignore.
Verify:
- Pest --parallel: 742 passed / 1 flaky error (CsvReconcileJobTest cache
race, в изоляции 2/2 PASS) / 3 skipped.
- Browser: «сп» и «окн» возвращают «Окна СПб».
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- BulkProjectActionRequest: add update_regions/update_days/update_limit actions, scope.filter, withValidator for ids-or-scope + delta/replace mutual exclusion
- ProjectBulkActionsTest: 4 new tests (3 pass, 1 todo pending Task 2 service handler)
- ProjectsActionsTest: update > 100 ids limit test to match new max:500
- phpstan-baseline: add 4 actingAs false-positive entries for new test file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
I-1/M-1: introduce resolvedSupplierProjects() private helper on Project
model; rewrite aggregateSyncStatus(), aggregateLastSyncedAt(),
getSupplierLinks() to read from eager-loaded supplierB1/B2/B3 relations
instead of SupplierProject::find() — eliminates up to 120 SELECTs/page.
I-2: aggregateLastSyncedAt() now uses sortBy(timestamp) instead of
Collection::min() on Carbon objects (string-comparison was unreliable).
M-2: add explanatory comment on intval+array_filter silent-drop behaviour
in the ?ids batch-fetch path.
M-3: new test — ?ids batch silently excludes foreign-tenant project IDs.
M-4: new test — show returns 200 for archived project (read preserved).
PHPStan baseline updated: 2 new test functions raise actingAs() count 7→9.
Tests: 9/9 passed (33 assertions). Larastan: 0 errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>