Тема
ТЗ — Клиентский кабинет B2C (cms/cabinet-b2c)
Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: несколько Статус: ТЗ к разработке
Назначение и возможности
Зона 3 кабинета (Inertia + React SSR, см. /cms-v2/client-cabinets): личный кабинет физического лица поверх API-слоя ядра. Базовая площадка, поверх которой другие модули расширяют секции через реестр (заказы, бонусы, документы и т.д. — отдельные модули).
- Профиль: контакты, аватар, смена email/телефона с подтверждением, удаление аккаунта (152-ФЗ);
- заказы: список, детализация, статусы в реальном времени (интеграция
cms/commerce-orders); - адреса: адресная книга, адрес по умолчанию, привязка к ПВЗ;
- избранное: вишлист, списки, сравнение;
- бонусы: баланс, история начислений/списаний (если подключён модуль лояльности);
- подписки на уведомления: каналы и типы событий, управление согласиями;
- секции кабинета расширяются модулями через
CabinetSectionContract(реестр), без правки ядра кабинета; - управление сессиями: список активных устройств, «выйти со всех устройств».
Зависимости и выключение
requires: ядро (auth, API-слой) · suggests: cms/commerce-orders (секция заказов), cms/two-factor (доп. шаг подтверждения на смену email/удаление аккаунта), cms/dadata (suggest-provider — автозаполнение и нормализация адресов), cms/consents (детальный журнал согласий и политика хранения; без модуля работает упрощённая анонимизация на базовых примитивах согласий ядра) · provides: cabinet-shell
Поведение при выключении: раздел кабинета недоступен целиком (редирект на публичный сайт), заказы/профиль остаются доступны через админку/Filament для сотрудников; данные пользователей сохраняются и восстанавливаются при повторном включении. Деградация по каждому suggests-модулю — см. «Крайние случаи».
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_cabinet_addresses | id, user_id, label, address_json, is_default | адресная книга |
cms_cabinet_favorites | id, user_id, favoritable_type, favoritable_id | избранное, полиморфное |
cms_cabinet_sessions | id, user_id, device_hash, ip, last_used_at | активные сессии для управления устройствами |
cms_cabinet_verification_codes | id, user_id, type, target_value, code_hash, expires_at, attempts, confirmed_at | подтверждение смены email/телефона (type: email/phone) |
cms_cabinet_deletion_requests | id, user_id, requested_at, scheduled_purge_at, status, cancelled_at | grace-период удаления аккаунта |
user_id — FK constrained()->index() на всех таблицах; address_json — json()->nullable() + cast array. Индекс (favoritable_type, favoritable_id) в cms_cabinet_favorites под полиморфную выборку; уникальный составной индекс (user_id, favoritable_type, favoritable_id) — не даёт задвоить избранное. В cms_cabinet_verification_codes — уникальный частичный индекс (user_id, type) там, где confirmed_at IS NULL (одна активная заявка на тип), индекс по expires_at под джобу очистки. status в cms_cabinet_deletion_requests — PHP Enum (pending/cancelled/completed).
ПДн-паспорт. Персональные данные: address_json (адрес доставки/проживания), ip/device_hash в cms_cabinet_sessions (техданные устройства пользователя), target_value (email/телефон) в cms_cabinet_verification_codes. Срок хранения: адреса и избранное — пока аккаунт активен + 30 дней после завершённого удаления (окно на споры по заказам), далее анонимизация; cms_cabinet_sessions — 90 дней от last_used_at (джоба PurgeExpiredCabinetSessions); cms_cabinet_verification_codes — до expires_at
- 7 дней (окно антифрод-аудита), затем purge. Модуль реализует хуки ядра «выгрузить всё по субъекту» (профиль, адреса, избранное, история сессий) и «забыть по запросу» (обезличивание адресов,
ip → null,device_hashпересоленный) согласно 152-ФЗ и политикеcms/consents.
Входные и выходные данные
Входы:
| Источник | Поля | Чем валидируется |
|---|---|---|
Форма профиля (PUT /api/v1/cabinet/me) | name, phone_display, avatar_media_id | FormRequest whitelist; avatar_media_id — существующий объект медиатеки текущего пользователя |
Запрос смены email (POST /api/v1/cabinet/me/email) | new_email | FormRequest: формат, уникальность, rate-limit на пользователя |
Подтверждение email (POST /api/v1/cabinet/me/email/confirm) | code | FormRequest: TTL email_change_confirmation_ttl_minutes, лимит попыток из attempts |
Запрос смены телефона (POST /api/v1/cabinet/me/phone) | new_phone | FormRequest: формат RU-номера, rate-limit |
Подтверждение телефона (POST /api/v1/cabinet/me/phone/confirm) | code | FormRequest: код из SMS, TTL, лимит попыток |
Адрес (POST/PUT /api/v1/cabinet/addresses) | label, address_json{city,street,house,apartment,zip}, is_default | FormRequest whitelist полей address_json; нормализация через suggest-provider (cms/dadata), если подключён |
Избранное (POST /api/v1/cabinet/favorites) | favoritable_type, favoritable_id | FormRequest: favoritable_type — whitelist известных типов, favoritable_id — существующая сущность |
Настройки уведомлений (PUT /api/v1/cabinet/notifications) | channel, event_type, enabled | FormRequest whitelist каналов/типов из реестра NotificationDispatch |
Запрос удаления аккаунта (POST /api/v1/cabinet/me/delete) | password, reason? | FormRequest: подтверждение пароля текущего пользователя |
Отмена удаления (POST /api/v1/cabinet/me/delete/cancel) | — | только владелец, в пределах grace-периода |
Отзыв сессии (DELETE /api/v1/cabinet/sessions/{id}) | id (path) | scoped-запрос WHERE user_id = $currentUser->id |
Всё, что не перечислено выше, модуль обязан отвергать (whitelist-принцип §11 стандарта): неизвестные поля профиля, произвольный favoritable_type, чужой media_id.
Выходы:
| Потребитель | Данные | Формат |
|---|---|---|
| React-кабинет (Inertia, зона 3) | профиль, адреса, избранное, сессии, настройки уведомлений | REST-конверт {data, meta} |
cms/commerce-orders (при включении) | секция «Заказы» в кабинете | рендер через CabinetSectionContract |
cms/consents | факт запроса/отмены/завершения удаления аккаунта | события CabinetAccountDeletion* |
NotificationDispatch (ядро) | письмо/SMS подтверждения email/телефона, уведомление на старый email о смене | вызов контракта, log-fallback при выключенном notification-channel |
Filament (cabinet-b2c.manage) | карточка активности пользователя (последний вход, заказы) | admin-view, не публичный API |
cms/audit (если включён) | факты смены email/телефона, отзыва сессии | событие → аудит-лог |
Настройки (группа cabinet-b2c)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
cabinet-b2c.registration_enabled | bool | true | нет | Разрешить самостоятельную регистрацию |
cabinet-b2c.require_phone_verification | bool | true | нет | Обязательное подтверждение телефона |
cabinet-b2c.sections_enabled | array | ["profile","orders","addresses","favorites"] | нет | Активные секции кабинета |
cabinet-b2c.max_favorites_per_user | int | 500 | нет | Лимит позиций избранного на пользователя |
cabinet-b2c.max_addresses_per_user | int | 20 | нет | Лимит адресов в адресной книге |
cabinet-b2c.pending_registration_ttl_hours | int | 24 | нет | TTL незавершённой регистрации (email/телефон не подтверждён) |
cabinet-b2c.email_change_confirmation_ttl_minutes | int | 60 | нет | Срок жизни кода подтверждения новой почты |
cabinet-b2c.code_resend_cooldown_seconds | int | 60 | нет | Минимальный интервал между повторной отправкой кода |
cabinet-b2c.deletion_grace_period_days | int | 14 | нет | Окно на отмену запроса удаления аккаунта |
cabinet-b2c.self_deletion_enabled | bool | true | нет | Kill-switch: разрешить самостоятельное удаление аккаунта без выключения модуля |
Достижение лимита (max_favorites_per_user, max_addresses_per_user) отдаёт понятную ошибку 422 и метрику, не 500 и не тихое обрезание списка.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/cabinet/me | auth | Данные текущего профиля |
| PUT | /api/v1/cabinet/me | auth | Обновление профиля |
| POST | /api/v1/cabinet/me/email | auth, rate-limit | Запрос смены email (письмо-подтверждение на новый + уведомление на старый) |
| POST | /api/v1/cabinet/me/email/confirm | auth | Подтверждение нового email кодом |
| POST | /api/v1/cabinet/me/phone | auth, rate-limit | Запрос смены телефона |
| POST | /api/v1/cabinet/me/phone/confirm | auth | Подтверждение телефона кодом |
| GET | /api/v1/cabinet/addresses | auth | Адресная книга |
| POST | /api/v1/cabinet/addresses | auth | Добавление адреса |
| PUT | /api/v1/cabinet/addresses/{id} | auth (владелец) | Изменение адреса |
| DELETE | /api/v1/cabinet/addresses/{id} | auth (владелец) | Удаление адреса |
| GET | /api/v1/cabinet/favorites | auth | Список избранного |
| POST | /api/v1/cabinet/favorites | auth | Добавить в избранное |
| DELETE | /api/v1/cabinet/favorites/{id} | auth (владелец) | Убрать из избранного |
| GET/PUT | /api/v1/cabinet/notifications | auth | Настройки подписок на уведомления |
| GET | /api/v1/cabinet/sessions | auth | Список активных сессий |
| DELETE | /api/v1/cabinet/sessions/{id} | auth (владелец) | Завершить сессию устройства |
| POST | /api/v1/cabinet/me/delete | auth, подтверждение пароля | Запрос удаления аккаунта |
| POST | /api/v1/cabinet/me/delete/cancel | auth (владелец) | Отмена удаления в пределах grace-периода |
Коллекции (addresses, favorites, sessions) — keyset-пагинация (не OFFSET).
Компоненты
Filament: карточка пользователя со сводкой активности кабинета (последний вход, заказы). Реестр CabinetSectionContract для расширения секций модулями. Команды: cms:cabinet-b2c:doctor --json.
Демо-контент: сидер демо-пользователя кабинета с заполненными адресами, избранным и историей сессий — галерея /_gallery и playground показывают все секции без ручного ввода. Фронтенд-бюджет: React-остров кабинета грузится отдельным чанком (не в общий бандл темы), тяжёлые виджеты секций (карта ПВЗ) — lazy-load по видимости, формы доступны с клавиатуры, поля — с label.
События и обмен
| Событие | Когда | Payload |
|---|---|---|
CabinetProfileUpdated | изменён профиль | user_id, changed_fields[] |
CabinetEmailChangeRequested | запрошена смена email | user_id, old_email, new_email |
CabinetEmailChanged | новый email подтверждён и применён | user_id, old_email, new_email |
CabinetPhoneChangeRequested | запрошена смена телефона | user_id, new_phone |
CabinetPhoneChanged | телефон подтверждён и применён | user_id |
CabinetSessionRevoked | завершена сессия устройства | user_id, session_id |
CabinetAccountDeletionRequested | запрошено удаление аккаунта | user_id, scheduled_purge_at |
CabinetAccountDeletionCancelled | пользователь отменил удаление в grace-периоде | user_id |
CabinetAccountDeleted | grace-период истёк, обезличивание завершено | user_id |
Слушает: — (модуль сам не подписывается на чужие события; интеграции — через provides и сервис-вызовы, см. таблицу ниже). Provides-контракт cabinet-shell — реестр CabinetSectionContract потребляется модулями-расширениями (заказы, бонусы, документы).
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/commerce-orders | provides (cabinet-shell, реестр CabinetSectionContract) | cms/commerce-orders → cabinet-b2c | секция «Заказы» регистрируется в кабинете, рендерится сервисом заказов |
cms/consents | событие | cabinet-b2c → cms/consents | CabinetAccountDeletionRequested/…Cancelled инициируют/останавливают обезличивание заказов и отзывов |
cms/two-factor | сервис-вызов (мягкая зависимость, suggests) | cabinet-b2c → cms/two-factor | доп. шаг подтверждения на смену email/удаление аккаунта; при выключении шаг пропускается |
cms/dadata | provides-контракт suggest-provider | cabinet-b2c → cms/dadata | нормализация и автоподсказка адреса при вводе; при выключении — обычный текстовый ввод |
NotificationDispatch (ядро) | сервисный контракт | cabinet-b2c → ядро | письма/SMS подтверждения, уведомление о смене email на старый адрес; log-fallback без notification-channel |
RequestContext (ядро) | сервисный контракт | ядро → cabinet-b2c | текущий пользователь/site_id для скоупа всех запросов |
cms/audit (если включён) | событие | cabinet-b2c → cms/audit | факты смены email/телефона, отзыва сессии попадают в журнал безопасности |
cms/attack-monitor (если включён) | событие | cabinet-b2c → cms/attack-monitor | серии неудачных подтверждений кода — сигнал возможного брутфорса |
| React-кабинет (зона 3) | внешний канал REST /api/v1 | cabinet-b2c → фронтенд | headless-рендер всех секций |
Фоновая работа
Очередь cabinet-b2c (низкий приоритет), джобы — идемпотентны, расписание через ScheduleRegistrar:
PurgeExpiredCabinetSessions— ежедневно, чиститcms_cabinet_sessionsстарше 90 дней отlast_used_at;PurgeExpiredPendingRegistrations— ежедневно, помечаетexpiredнезавершённые регистрации старшеpending_registration_ttl_hours(обрабатывается ядром auth, модуль участвует стороной секции профиля);PurgeExpiredVerificationCodes— ежедневно, чиститcms_cabinet_verification_codesстаршеexpires_at + 7 дней;FinalizeAccountDeletion— ежедневно, для заявок с истёкшимscheduled_purge_atи статусомpendingзапускает обезличивание (сервис-вызовcms/consents, при отсутствии модуля — базовый механизм ядра), ключ идемпотентности —user_id(повторный прогон не дублирует эффект), издаётCabinetAccountDeleted.
Операции профиля/адресов/избранного/сессий синхронны в рамках HTTP-запроса — фоновая работа только у ретеншна и финализации удаления.
Эксплуатация (ранбук). Метрики: число отправленных/подтверждённых кодов, доля неудачных подтверждений (детект брутфорса), число отозванных сессий, длительность FinalizeAccountDeletion. Алерты: доля неудачных подтверждений кода выше порога, отставание очереди cabinet-b2c от расписания.
| Симптом | Что проверить | Команда |
|---|---|---|
| Код подтверждения email/телефона не приходит | доступность NotificationDispatch / log-fallback вместо реальной отправки | cms:cabinet-b2c:doctor --json |
| Сессии не отзываются на клиенте | рассинхрон cms_cabinet_sessions и токенов Sanctum | cms:cabinet-b2c:doctor --json |
| Подсказки адреса не работают | доступность cms/dadata и suggest-provider | cms:dadata:doctor --json |
| Аккаунты не удаляются по истечении grace-периода | отставание очереди cabinet-b2c, ошибки FinalizeAccountDeletion | cms:cabinet-b2c:doctor --json, логи очереди |
Бэкап/рестор: в бэкап попадают все таблицы модуля и связанные медиа-аватары. После рестора cms_cabinet_sessions не восстанавливается принудительно (устаревшие записи просто expire по TTL — не критично для консистентности); cms_cabinet_verification_codes с истёкшим expires_at можно не восстанавливать — пользователь запросит код заново.
Производительность и кеш
Ожидаемые объёмы: сотни тысяч — единицы миллионов пользователей на инсталляцию, в среднем 2–5 адресов и до нескольких сотен позиций избранного на пользователя. Горячий путь — открытие кабинета: bootstrap-запрос GET /api/v1/cabinet/me на каждой странице React-острова, бюджет ≤3 запроса (профиль + счётчик избранного + активные секции реестра, без N+1 по адресам/сессиям — они грузятся отдельно по требованию вкладки). Настройки читаются из кеша группы — 0 запросов на горячем пути (контрактный тест).
Критичные индексы — см. «Модель данных»: user_id на всех таблицах (изоляция по владельцу — она же и главный вектор безопасности, и главный индекс производительности), составной уникальный (user_id, favoritable_type, favoritable_id) — защищает и от дублей, и от полного скана при проверке «уже в избранном».
Теги cabinet-b2c:profile:<user_id>, cabinet-b2c:favorites:<user_id> — приватный кеш уровня пользователя (короткий TTL), инвалидируется событиями CabinetProfileUpdated и изменением избранного. Персональные данные кабинета не попадают в общий page-cache — сегментация по правилам ядра (§10 стандарта), не самодельная: страница кабинета в принципе не участвует в page-cache сайта, только в приватном кеше по user_id.
Безопасность
IDOR — главный вектор для кабинета. Каждый эндпоинт обязан использовать scoped-запрос через владельца (WHERE user_id = $currentUser->id), а не голый Model::find($id): адреса, избранное, сессии, заявки на смену email/телефона — доступ к чужой записи по перебору id должен возвращать 404, а не 403 (не подтверждать существование чужого ресурса). Контрактный тест перебирает id соседнего пользователя на каждом эндпоинте.
Смена email/телефона — двойное подтверждение для email: письмо на старый адрес — уведомление (не блокирует, информирует о попытке смены), письмо на новый адрес — подтверждение с кодом; смена применяется только после подтверждения нового адреса. Телефон — однократное подтверждение SMS-кодом. Ни одна смена не применяется мгновенно без верификации.
Удаление аккаунта — не каскадное: запрос переводит аккаунт в grace-период (deletion_grace_period_days), по истечении — обезличивание (не hard delete) по политике cms/consents: заказы и отзывы сохраняются обезличенными (ссылка на пользователя разрывается, факт покупки/отзыва — нет), финансовые записи не трогаются (append-only, специфика §4 стандарта). Kill-switch self_deletion_enabled позволяет временно отключить самостоятельное удаление (например, при расследовании инцидента) без выключения модуля.
Управление сессиями реально инвалидирует токен/сессию устройства при отзыве. Rate-limit на запрос/подтверждение кода (по пользователю и по IP) — защита от перебора кода и спама письмами/SMS.
Матрица ролей:
| Действие | Владелец аккаунта | Сотрудник (cabinet-b2c.manage) | studio |
|---|---|---|---|
| Просмотр/редактирование своего профиля, адресов, избранного | ✅ | — | — |
| Просмотр профиля любого пользователя (обращение в поддержку) | — | ✅ | ✅ |
| Отзыв чужой активной сессии (при компрометации аккаунта) | — | ✅ | ✅ |
| Запрос/отмена удаления своего аккаунта | ✅ | — | — |
Отключение kill-switch self_deletion_enabled | — | — | ✅ |
| Принудительное восстановление обезличенного аккаунта (инцидент) | — | — | ✅ |
Права: cabinet-b2c.view.own, cabinet-b2c.manage (для сотрудников через админку).
UX-требования
Для пользователя кабинета: форма профиля/адреса при ошибке валидации (422) сохраняет уже введённые значения (клиентское состояние не сбрасывается); индикатор загрузки/skeleton вместо пустого экрана на bootstrap-запросе; список сессий явно подсвечивает «это устройство»; форма смены email/телефона показывает, на какой адрес уйдёт письмо/SMS, до отправки; кабинет адаптивен (мобильная раскладка секций, не только десктоп — большинство визитов в личный кабинет с мобильных устройств); формы доступны с клавиатуры.
Для админа/сотрудника студии: пустая адресная книга/избранное в Filament-карточке — с подсказкой, а не голым пустым списком; отзыв сессии пользователя сотрудником — подтверждение действия («вы точно хотите завершить сессию клиента?»); ошибки на человеческом языке («пользователь уже подтвердил email» вместо кода исключения); принудительное восстановление аккаунта — отдельное необратимо-звучащее действие с явным подтверждением и записью в аудит.
Крайние случаи и типовые баги
- Двойной сабмит формы смены email → повторный
POSTс тем жеnew_emailне создаёт вторую заявку, обновляет существующую (уникальный частичный индекс(user_id, type)по неподтверждённым). - Код подтверждения истёк → код невалиден, ошибка «код устарел», доступна повторная отправка не чаще
code_resend_cooldown_seconds. - Подтверждён новый email, старый не открыл письмо → это ожидаемо: письмо на старый адрес — уведомление, не условие применения смены; смена уже произошла по факту подтверждения нового адреса.
cms/two-factorвыключен посреди активного challenge (пользователь начал смену email с 2FA-шагом, модуль отключили) → деградация: шаг 2FA пропускается, операция продолжается по обычной проверке (пароль/код), не зависает и не падает 500.cms/dadataнедоступен/таймаут при вводе адреса → поле остаётся обычным текстовым вводом без подсказок, форма адреса не блокируется (согласовано сdocs/dadata.md).- Удаление аккаунта с активными неисполненными заказами → запрос уходит в grace-период, заказы не удаляются каскадно; финализация обезличивания откладывается, пока у пользователя есть заказы в незавершённом статусе (сервис-вызов к
cms/commerce-ordersпередFinalizeAccountDeletion, если модуль подключён). - Незавершённая регистрация (email/телефон не подтверждён) → аккаунт существует в состоянии
pendingдоpending_registration_ttl_hours, далее —expired; email из истёкшей заявки можно использовать повторно для новой регистрации; повторная отправка кода — под тем жеcode_resend_cooldown_seconds. - Пустые и огромные данные избранного/адресной книги → пустое состояние с подсказкой «добавить адрес» / «сохранять товары в избранное», не пустая таблица без объяснения; на другом полюсе — тысячи позиций избранного не деградируют список благодаря keyset-пагинации, а
max_favorites_per_userдаёт понятную ошибку при достижении лимита вместо тихого обрезания. - Ловушка измерения city — адрес привязан к ПВЗ конкретного
city_id; на сайте безcms/multicityполе nullable, адресная книга работает без города (контрактный тест гоняется в обоих режимах, §4 стандарта). - ⚠️ Противоречие:
require_phone_verification = true, но в инсталляции не подключён ни одинnotification-channelдля SMS → регистрация с обязательным подтверждением телефона блокируется без явного объяснения пользователю и админу. Разрешение:cms:cabinet-b2c:doctorпри enable/health-чеке проверяет наличие резолвящегосяnotification-channelдля SMS при включённомrequire_phone_verificationи алертит явной ошибкой в админке, а не тихим 500 на форме регистрации. - Гонка «выйти со всех устройств» и параллельного запроса с другого устройства → отзыв сессии атомарен по
session_id; повторный/параллельный запрос на уже отозванную сессию получает 404, а не 500. - Модуль
cabinet-b2cвыключен, пока пользователь внутри кабинета → следующий запрос к/api/v1/cabinet/*отдаёт редирект/понятную ошибку, а не 500; открытая вкладка показывает fallback-экран «раздел временно недоступен» вместо белого экрана.
Донорский код
| Что взять | Путь |
|---|---|
| Подходы к личному кабинету | несколько проектов студии (профиль, адреса, избранное) |
Миграция legacy: команда cms:cabinet-b2c:import-legacy --source=<профиль> переносит профиль/адреса/избранное со старой платформы клиента, маппинг по external_id на пользователя ядра; идемпотентна (повторный прогон обновляет, не дублирует), --dry-run с отчётом расхождений. Обязательна для клиентских переездов с donor-проектов студии, где кабинет уже существовал.
Тесты и приёмка
- [ ] Контрактный тест: пользователь видит и редактирует только свои данные (IDOR-тест перебора
idсоседа на каждом эндпоинте: адреса, избранное, сессии, заявки смены email/телефона — 404, не 403); - [ ] Смена email требует подтверждения нового адреса и применяется только после него; письмо на старый адрес — уведомление, не блокирует смену;
- [ ] Смена телефона требует подтверждения кодом, не применяется мгновенно;
- [ ] Двойной сабмит запроса смены email не создаёт вторую заявку (идемпотентность);
- [ ] Удаление аккаунта уходит в grace-период, не удаляет данные каскадно; отмена в пределах периода восстанавливает аккаунт без следов удаления;
- [ ]
FinalizeAccountDeletionидемпотентна (повторный прогон не дублирует эффект) и соответствует политикеcms/consents(обезличивание, а не hard delete при активных заказах); - [ ] Незавершённая регистрация истекает по TTL, email освобождается для повторной попытки; повторная отправка кода ограничена cooldown;
- [ ] Реестр секций позволяет модулю добавить пункт меню кабинета без правки кода ядра кабинета;
- [ ] Деградация при выключении редиректит на публичный сайт без 500; деградация при выключении
cms/two-factor/cms/dadataне блокирует операции профиля/адреса; - [ ] Управление сессиями реально инвалидирует токен/сессию устройства при отзыве; гонка параллельного отзыва отдаёт 404, не 500;
- [ ] Лимиты
max_favorites_per_user/max_addresses_per_userотдают 422 с понятной ошибкой при достижении; - [ ] Хуки «выгрузить всё по субъекту» и «забыть по запросу» покрыты тестом (ПДн-паспорт);
- [ ] Контрактный набор
cms-testingзелёный, testbench-изоляция пакета, feature-тест на каждый роут; - [ ] Тестовая БД только
cabinet-b2c_test;migrate:fresh/refresh/reset,db:wipeзапрещены.