← К списку версий
v0.91.0
02.10.2026
MINOR
Одна синхронизация людей, проектов и задач: без почты создаём, смену статуса показываем в письме
ТЗ 272 «Одна кнопка и правдивые письма». Люди без email молча пропускались, а смена статуса задачи нигде не сохранялась — платформа отдаёт только текущий статус, поэтому письмо об изменении просто не могло появиться. Теперь: клиент без email создаётся с пустой почтой (появится в проектах и списках, писем не получит); каждый переход статуса «было → стало» пишется при синхронизации в новую таблицу task_status_changes и выводится в отчётных письмах плейсхолдером {{status_changes_list}}; сопоставление человека с клиентом вынесено в один сервис PersonMatcher для превью и импорта сразу. Попутно исправлена ошибка сопоставления по кириллице: сравнение шло через SQL LOWER(), который в SQLite не понижает регистр кириллицы, из-за чего людей без почты создавали заново при каждом импорте. Версия v0.91.0.
Что сделано
- Человек без email создаётся - Раньше он просто исчезал: шаг 1 показывал его в блоке «не будет добавлен», шаг 2 пропускал. Теперь клиент создаётся с пустым email (поле стало nullable), попадает в проекты и списки; писем он не получает, потому что отбор идёт по наличию адреса. Оба мастера — «Синхронизация всех людей» и «Синхронизация людей с платформы задач» — показывают его отдельным блоком «добавятся, но писем не получат».
- История смены статуса задачи - Новая таблица task_status_changes (task_id, project_id, from_status, to_status, source, origin, changed_at). Заполняется в момент синхронизации — это единственное место, где известны оба значения. Переход из закрытой задачи в историю пишется, но письмо не отправляет: это факт, а не повод слать «задача открыта заново».
- Письма показывают смену статусов - ReportDispatcher собирает блок «Изменился статус задач» за отчётный период (не больше 20 переходов) и подставляет его через новый плейсхолдер {{status_changes_list}} в шаблоны report-wednesday и report-friday. Пустой период даёт пустую строку — лишний заголовок в письме не появляется. Переходы по внутренним (невидимым клиенту) задачам в письмо не попадают.
- Одно сопоставление вместо двух - Новый сервис App\Services\Sync\PersonMatcher используют и предпросмотр (DiffEngine), и импорт (SyncManager), и оба контроллера: порядок поиска один — внешний id человека (= id клиента в MY IPD), затем email, затем имя вместе с организацией. Одного имени без организации недостаточно: однофамильцев с разными компаниями склеивать нельзя.
- Исправлена ошибка сопоставления по кириллице - Сравнение имени и email шло через SQL LOWER()/TRIM(), а SQLite не понижает регистр кириллических букв: «Человек Без Почты» никогда не совпадал с «человек без почты», и при каждом импорте создавался новый клиент. Сравнение перенесено в PHP с нормализацией пробелов и регистра.
- Организация и адреса больше не теряются - Организация человека с платформы пишется в clients.company — по ней он и находится в следующих импортах. Обновление email и списка emails происходит только когда платформа реально прислала адреса: иначе у человека без почты на площадке стирались бы адреса, добавленные в MY IPD.
Проверено
- Полный набор тестов зелёный - 398 тестов, падений нет. Обновлены тесты двух мастеров под новое правило (люди без email больше не пропускаются), добавлены TaskStatusChangeSyncFeatureTest (3 теста: переход записан со старым и новым статусом, при неизменившемся статусе история пуста, переоткрытая задача не шлёт письмо) и три теста на блок смены статусов в WeeklyReportCompositionTest (внутренняя задача скрыта, переход вне периода не показывается).
- Миграции применены на TEST - clients.email стал nullable, создана task_status_changes, в существующие шаблоны report добавлен плейсхолдер.
Результат
- Одна кнопка синхронизирует людей, проекты и задачи; человек без почты не теряется, а клиент видит в отчёте, какие задачи сменили статус.