Перейти к основному содержимому

UC-CHS-007. Администратор ведёт справочники банков и отельеров

Назначение

Администратор платформы ведёт платформенные справочники BankEntity и HotelierEntity через Admin API шахматки (create, search/list, get by id, update, delete = архив, lookup list, lookup by id), а менеджеры застройщика и агенты используют lookup-методы этих справочников. Административный контур — отдельный за пределами identity (ADR-0053), обращается по m2m. Сценарий относится к контексту chessboard.

Metadata

ПолеЗначение
IDUC-CHS-007
Контекстchessboard (см. docs/06-services/chessboard/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАдминистратор платформы (запрос приходит в chessboard по m2m от админ-бэкенда)
Permissions / scopechessboard.banks.manage / chessboard.hoteliers.manage — m2m-клиент админ-бэкенда (client-credentials, ADR-0053); scope = платформа. Lookup для менеджеров застройщика и агентов — chessboard.references.lookup
TenantНе применяется: справочники платформенные, не tenanted (ADR-0052 п.3)
Record-levelНе применяется

Предусловие

  • Админ-бэкенд аутентифицирован m2m-токеном с permission chessboard.banks.manage / chessboard.hoteliers.manage; конечный администратор авторизован в admin API (его собственная identity, ADR-0053), его id прокидывается в actor-stamping.
  • Лого предварительно загружено в storage/CDN — в команду передаётся только LogoFileId.

Триггер

ПолеЗначение
ИсточникДействие администратора (admin-интерфейс → админ-бэкенд → m2m-вызов в chessboard)
Триггерящий элементРаздел «Справочники» → «Банки» / «Отельеры» → «Создать» / «Изменить» / «Архивировать»

Основной Поток

Сценарий одинаков для банка и отельера (парные справочники одной формы); ниже — банк, дельта отельера — в A5. Ведение — простой CRUD над anemic-справочником (bank.md) без доменных событий и Kafka-публикаций (simplicity-first, ADR-0056; потребители читают lookup-API синхронно).

  1. Список. GET /admin/banks?search=&page=&pageSize= — постранично активные записи (DeletedAt == null, фильтр по имени, сортировка по Name); поля BankModel: Id, Name, LogoFileId, CreatedAt, UpdatedAt, Version, RowVersion. Флаг includeArchived = true показывает и архивные.
  2. Создание. POST /admin/banks (Name, LogoFileId). Вход: Name 1..256; LogoFileId — валидный Guid или null. Имя уникально среди активных; занято → CHESSBOARD_BANK_NAME_TAKEN (INV-BNK-1; unique-индекс ux_banks_name страхует гонку). Создаётся запись (CreatedByUserId = admin-user, Version = 1). Результат — 201 Created.
  3. Правка. PUT /admin/banks/{id} (Name, LogoFileId, RowVersion) — проверка уникальности нового имени (INV-BNK-1), Version + 1, UpdatedByUserId = admin-user. Устаревший RowVersion409. Результат — 200 OK.
  4. Архив («delete» в терминах формы). DELETE /admin/banks/{id} — soft-delete: DeletedAt = now, DeletedByUserId = admin-user; физического удаления нет — на банк ссылаются Project.Financing.*BankIds и сделки crm (INV-BNK-2). Результат — 204 No Content. Ссылки остаются валидными; lookup-списки и public-проекции архивные исключают.
  5. Карточка. GET /admin/banks/{id} (включая архивные) → 200 / 404.
  6. Lookup для доменных сценариев. GET /developers/banks/lookup?search= и GET /developers/banks/lookup/{id} (продублировано в agents area; permission chessboard.references.lookup) — только активные (DeletedAt == null, сортировка по Name), компактная модель {Id, Name, LogoFileId}; используется вкладкой «Финансирование» проекта (UC-CHS-001, INV-PRJ-7) и оформлением сделок в crm.

Альтернативные Потоки

A1. Дубль названия

  1. Имя совпало с активной записью (или гонку поймал unique-индекс ux_banks_name / ux_hoteliers_name) → CHESSBOARD_BANK_NAME_TAKEN / CHESSBOARD_HOTELIER_NAME_TAKEN (INV-BNK-1 / INV-HTL-1), 409. Админ меняет название.
  2. Имя совпало с архивной записью — создание допустимо (уникальность — среди deleted_at is null); при желании админ восстанавливает архивную (A3).

A2. Архив отельера с активными проектами

  1. У отельера есть активные проекты (HotelierId == id, DeletedAt == null) → CHESSBOARD_HOTELIER_HAS_PROJECTS (INV-HTL-2), 409 — FK projects.hotelier_id физический, архив оператора с живыми отелями сломал бы INV-PRJ-4. Админ сперва переназначает отельера в проектах.
  2. Для банка такого guard-а нет (ссылки — jsonb без FK): архив всегда проходит, проекции фильтруют (INV-BNK-2).

A3. Восстановление из архива

  1. POST /admin/banks/{id}/restore — guard уникальности имени среди активных (иначе CHESSBOARD_BANK_NAME_TAKEN), DeletedAt = null, DeletedByUserId = null, Version + 1.

A4. Конкурентная правка двумя админами

  1. Устаревший RowVersion409; admin-интерфейс перечитывает карточку и повторяет.

A5. Дельта отельера

  1. Все шаги идентичны с заменой справочника/кодов: справочник отельеров, CHESSBOARD_HOTELIER_NAME_TAKEN, guard архива INV-HTL-2 (A2); lookup отельеров используется формой проекта для выбора HotelierId (UC-CHS-001 A2, INV-PRJ-4).

Кросс-сервисный контракт, идемпотентность и наблюдаемость

Синхронное чтение потребителями (без событий). Справочники маленькие и меняются редко, поэтому интеграция — только синхронный lookup-API, без Kafka-реплики (simplicity-first, ADR-0056). Потребители: catalog (название+лого банка/отельера в карточке проекта), crm (банк эскроу-счёта сделки) — резолвят запись по id в момент построения проекции / оформления сделки.

Поведение при архиве и устаревании (что видит потребитель).

  • Lookup-списки и lookup-by-id для developers/agents отдают только активные записи; архивная запись исчезает из выбора и из public-проекции карточки проекта — на месте архивированного банка/отельера поле просто не отображается, при этом ссылка в Project.Financing/projects.hotelier_id остаётся валидной (INV-BNK-2 / INV-HTL-2).
  • Потребитель, которому нужно показать уже сосланную архивную запись (например, crm для банка эскроу сделки, начатой до архива), обязан сохранить снапшот названия/лого в момент связывания либо запросить запись admin-путём получения по id (он возвращает и архивные, шаг 5) — устаревание разрешается на стороне потребителя; активные lookup-ответы шахматки архивными записями не «засоряются».
  • Отельер защищён строже банка: архив запрещён при живых ссылающихся проектах (INV-HTL-2, FK) — «исчезновения» оператора из карточки апарт-отеля не происходит. Банк допускает архив при живых ссылках (jsonb без FK), поэтому ответственность за отображение архивного банка лежит на потребителе.

Идемпотентность записи. Создание естественно идемпотентно по имени: повторный сабмит формы с тем же названием активной записи → CHESSBOARD_BANK_NAME_TAKEN / CHESSBOARD_HOTELIER_NAME_TAKEN (INV-BNK-1 / INV-HTL-1; гонку страхует unique-индекс), задвоения не будет. Правка / архив / восстановление защищены RowVersion (устаревшая версия → 409, A4): побеждает первый коммит, проигравший перечитывает карточку и повторяет.

Наблюдаемость / НФТ. Записей — десятки, изменения редки, спец-нагрузки нет. Логировать создание/правку/архив/восстановление с id администратора (actor-stamping) — без PII; аудит обеспечивают Version + CreatedBy/UpdatedBy/DeletedByUserId. Метрика конфликтов имени и отказов архива (INV-HTL-2) помогает ловить попытки нарушить целостность справочника.

Постусловие

  • Справочник актуален: активные записи уникальны по имени, архивные сохранены со ссылками (Project.Financing.*BankIds, projects.hotelier_id, сделки crm не ломаются).
  • Lookup-списки developers/agents отдают только активные записи; public-проекция карточки проекта (название+лого банка/отельера) — тоже.
  • Audit: CreatedBy/UpdatedBy/DeletedByUserId = id администратора, Version инкрементирован.
  • Kafka-событий не публиковалось (справочники читаются синхронно).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
CHESSBOARD_BANK_NAME_TAKEN / CHESSBOARD_HOTELIER_NAME_TAKEN409INV-BNK-1 / INV-HTL-1: имя занято среди активных (A1)Показать конфликт, предложить другое имя
CHESSBOARD_HOTELIER_HAS_PROJECTS409INV-HTL-2: архив отельера с активными проектами (A2)Показать список проектов, требующих переназначения
CHESSBOARD_BANK_NOT_FOUND / CHESSBOARD_HOTELIER_NOT_FOUND404Записи нетОбновить список
— (validation)400Name пустое/длиннее 256; LogoFileId не GuidПодсветить поля
— (forbidden)403m2m-токен без chessboard.*.manage; lookup без chessboard.references.lookupОшибка доступа; не ретраить
— (concurrency)409RowVersion устарел (A4)Перечитать карточку, повторить

Доменные События

Не поднимаются — anemic-справочники без DDD-обвязки (bank.md, hotelier.md); аудит обеспечивают Version + actor-stamping.

Kafka Интеграционные События

Не публикуются (simplicity-first, ADR-0056): банков/отельеров десятки, изменения редки; потребители (catalog — карточка проекта, crm — банк сделки) резолвят название/лого синхронно через lookup-API. Если потребуется реплика — BankUpsertedKafkaEvent добавляется отдельным изменением контракта (bank.md «Доменные и интеграционные события»).

Acceptance Criteria

#КритерийПроверка
AC1Create/Update/Archive/Restore/Get/Search работают для обоих справочниковCRUD-интеграционные тесты
AC2Дубль имени среди активных отклоняется и под гонкой; с архивным именем — создаётсяпараллельный тест + тест архива (A1)
AC3Архив банка не ломает проекты с его id в Financing; lookup его больше не отдаёттест INV-BNK-2
AC4Архив отельера с активными проектами отклоняется; после переназначения — проходиттест INV-HTL-2
AC5«Delete» — soft: запись остаётся в БД с DeletedAt, GET by id для админа доступентест soft-delete
AC6Lookup доступен developers/agents c chessboard.references.lookup, недоступен гостютест областей и permissions
AC7CRUD недоступен без m2m-permission chessboard.banks.manage/hoteliers.manageтест 403
AC8Actor-stamping фиксирует id администраторатест audit-полей
AC9Архивная запись исчезает из активного lookup и карточки проекта; admin-путь по id всё ещё её возвращает, ссылки не ломаютсятест архивной видимости и разрешения устаревания
AC10Повторный сабмит создания с тем же именем не задваивает запись (idempotent через INV-BNK-1/INV-HTL-1)тест повторного создания

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы