ProjectService::update() теперь возвращает Project с dynamic applies_from
attribute (CarbonImmutable | null), который ProjectResource подхватит для UI
(«изменения вступят в силу с DD.MM 21:00»).
Логика: для каждого изменённого поля из SupplierSnapshotGuard::SLEPOK_SENSITIVE_FIELDS
вычисляется максимум appliesFrom() — slepok-инвариант (до 18:00 МСК = today 21:00,
после = tomorrow 21:00). NULL = применяется немедленно (none changed / no supplier links).
Plan: docs/superpowers/plans/2026-05-26-slepok-routing-protection.md §Task 2.8
Spec: docs/superpowers/specs/2026-05-26-slepok-routing-protection-design.md §4.2.5
Tests: tests/Feature/Services/Project/ProjectServiceAppliesFromTest.php — 4/4 PASS.
ProjectService regression — 7/7 PASS.
Возвращает CarbonImmutable когда правка slepok-sensitive поля вступит в силу:
правка до 18:00 МСК → сегодня в 21:00 МСК
правка с 18:00 МСК и позже → завтра в 21:00 МСК
Возвращает null когда правка применяется немедленно:
- поле не slepok-sensitive (вне 7 полей SLEPOK_SENSITIVE_FIELDS), либо
- проект не связан с поставщиком (нет project_supplier_links)
7 slepok-sensitive полей: is_active, daily_limit_target, delivery_days_mask,
regions, signal_identifier, sms_senders, sms_keyword.
Spec §4.2.5. Используется ProjectService (Task 2.8) для прикрепления к
UI-ответу метки «изменения вступят в силу с DD.MM HH:MM МСК».
Plan: docs/superpowers/plans/2026-05-26-slepok-routing-protection.md §Task 2.7
NB plan-bug: оригинальные тесты в плане использовали Project::factory()->make()
с id=null, что приводило к WHERE project_id IS NULL → 0 совпадений. Заменил
на ->create() для реального id (factory default signal_type=null nullable в
projects table, не блокирует create()).
Tests added:
- tests/Feature/Services/Project/SupplierSnapshotGuardAppliesFromTest.php
(11 tests including dataset-driven для 7 полей, 11/11 isolated PASS).