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

Admin CRUD банков — создание / список / карточка / правка / архив / lookup

CRUD-группа платформенного справочника Банк (BankEntity) области admin контекста chessboard — операции реестра op#32–37 (README.md). Банк — участник сделки/эскроу; на него логически ссылаются Project.Financing (эскроу/ипотека, INV-PRJ-7) и сделки crm. Справочник без бизнес-логики и событий (ADR-0056); дом полей — ../domain/bank.md. Шесть операций одной формы описаны в одном файле; полный алгоритм — на каждую операцию ниже.

Назначение

Дать администратору платформы (admin API) вести реестр банков платформы: завести банк, найти в списке, открыть карточку, переименовать или сменить лого, отправить в архив (soft-delete), а также получить lookup-срез для выпадающих списков привязки финансирования проекта. Owning-контекст — chessboard. Архив ссылки не ломает (INV-BNK-2).

Metadata

ПолеЗначение
Сервис/контекстchessboard
API areaadmin (администратор платформы, ADR-0053)
Feature groupbanks
Статусdraft
Документdocs/06-services/chessboard/api/admin/banks-crud.md

Актор и доступ (общее для op#32–37)

ПроверкаЗначение
Actor typeАдминистратор платформы через admin API (m2m-токен, audience pin.chessboard.admin)
Auth policy / Permissionschessboard.banks.manage (../../identity/domain/permission.md)
Scope (RBAC)Платформа; не сужается
Record-levelне применяется
Tenant isolationНе применяется — справочник платформенный, не привязан к застройщику (ADR-0052 п.3)
AuditОбязателен: X-Admin-User-Id (header, Guid) → проставление Created/Updated/DeletedByUserId; на командах header обязателен

HTTP-контракт (шесть операций)

Correlation — сквозной trace через заголовок traceparent.

#ОперацияMethodRoute (/api/v1/admin)Успех
32createPOST/banks201 + Location
33searchGET/banks200
34getGET/banks/{bankId}200
35updatePUT/banks/{bankId}200
36archiveDELETE/banks/{bankId}204
37lookupGET/lookup/banks (+/{bankId})200
Идемпотентность и конкурентность

Idempotency-заголовок не применяется: POST защищён уникальностью имени (INV-BNK-1); PUT/DELETE идемпотентны по природе. Конкурентная правка/архив контролируется сверкой доменной версии сущности; несовпадение → CHESSBOARD_CONCURRENCY_CONFLICT (409, сквозная ошибка области).

Входные данные / параметры

Ограничения полей — из дома ../domain/bank.md (здесь не дублируются). Команды записи (op#32, op#35) принимают BankWriteModel (body); bankId — из route; аудит — из header.

ПолеИсточникТипОбяз.Валидация / ОграниченияОперации
AdminUserIdheader X-Admin-User-IdGuidДа (команды)непустой; используется для аудита32,35,36
bankIdrouteGuidДанепустой; существующий (иначе …NOT_FOUND)34,35,36,37(by-id)
Namebody (BankWriteModel)stringДаtrim, 1..256; уникально среди активных (INV-BNK-1)32,35
LogoFileIdbody (BankWriteModel)Guid?НетFileId storage/CDN, без FK32,35
Versionbody (BankWriteModel)longДа (только update)>= 1; сверка с текущей версией банка (optimistic)35
Searchquerystring?Нет<= 256; регистронезависимый поиск подстроки по названию33,37
IncludeArchivedqueryboolНет (default false)false → только активные (не архивные)33
SortByqueryBankSortByНет (default NameAsc)enum-string: NameAsc / CreatedAtDesc33
Page / PageSizequeryintНет (1 / 20)Page >= 1; PageSize 1..100 (lookup — 1..50)33,37

Модели ответа

Поля — проекция ../domain/bank.md (дом полей); ниже только состав проекций. Списки op#33/op#37 — постраничный результат (поля Items, Page, PageSize, TotalCount), не сырой массив.

BankModel (карточка op#34, результат op#32/op#35)

ПолеТипОбяз.ИсточникОписание
IdGuidДаIdИдентификатор банка.
NamestringДаNameНазвание.
LogoFileIdGuid?НетLogoFileIdЛого (storage/CDN, пресайн — на фронте).
IsArchivedboolДаcomputed (DeletedAt != null)Архивный флаг.
CreatedAtDateTimeOffsetДаCreatedAtСоздан.
CreatedByUserIdGuid?НетCreatedByUserIdАдмин-создатель.
UpdatedAtDateTimeOffsetДаUpdatedAtИзменён.
UpdatedByUserIdGuid?НетUpdatedByUserIdПоследний изменивший.
DeletedAtDateTimeOffset?НетDeletedAt!= null → архив.
VersionlongДаVersionДоменная версия (для последующего PUT/DELETE).

BankListItem (op#33) — BankModel без *ByUserId, плюс IsArchived (bool, = DeletedAt != null).

BankLookupItem (op#37) — { Id: Guid, Name: string, LogoFileId: Guid? } для выпадающих списков.

Алгоритм

Все операции выполняет администратор платформы через admin API (m2m-токен, audience pin.chessboard.admin, permission chessboard.banks.manage); мутации проставляют аудит по AdminUserId из X-Admin-User-Id. Tenant не применяется. Событий справочник не публикует (ADR-0056).

op#32 create

  1. Проверить вход: Name после trim в диапазоне 1..256, LogoFileId — необязательный, AdminUserId задан. Нарушение → ValidationError.
  2. Проверить занятость имени среди активных банков (INV-BNK-1). Если название уже занято активным банком → CHESSBOARD_BANK_NAME_TAKEN.
  3. Создать банк с новым идентификатором, названием (trim) и логотипом; версия = 1, банк не архивный; проставить аудит (создатель и последний изменивший = AdminUserId). При гонке на уникальном имени → CHESSBOARD_BANK_NAME_TAKEN.
  4. Результат: карточка BankModel, 201 + Location: /api/v1/admin/banks/{Id}.

op#35 update

  1. Проверить вход: Name 1..256, Version >= 1, bankId задан. Нарушение → ValidationError.
  2. Найти банк по bankId. Не найден либо архивный (DeletedAt != null — архивный не редактируется) → CHESSBOARD_BANK_NOT_FOUND (404). При смене имени — проверить уникальность среди активных, исключая сам банк (INV-BNK-1); занято → CHESSBOARD_BANK_NAME_TAKEN. Если переданная Version не совпадает с текущей версией банка → CHESSBOARD_CONCURRENCY_CONFLICT (409).
  3. Применить новое название (trim) и лого; последний изменивший = AdminUserId; версия увеличивается. При конкурентной правке → CHESSBOARD_CONCURRENCY_CONFLICT; при гонке уникального имени → CHESSBOARD_BANK_NAME_TAKEN.
  4. Результат: BankModel, 200.

op#36 archive (soft-delete)

  1. Проверить вход: bankId задан.
  2. Найти банк. Не найден → CHESSBOARD_BANK_NOT_FOUND (404). Уже архивный (DeletedAt != null) → идемпотентно 204 (изменений нет).
  3. INV-BNK-2: архив не каскадит — ссылки Project.Financing/crm не проверяются и остаются валидными; из lookup-списков банк исчезает (остаются только активные). Пометить банк архивным (DeletedAt = now, DeletedByUserId = AdminUserId) — строка физически не удаляется (soft-delete). При конкурентной правке → CHESSBOARD_CONCURRENCY_CONFLICT.
  4. Результат: 204.

op#33 search / op#34 get / op#37 lookup (read-only)

  1. Проверить вход: paging/Search/SortBy (op#33/op#37); для op#34 и op#37(by-id) — bankId задан. Нарушение → ValidationError.
  2. Загрузка и выдача:
    • op#33 search — постраничный список банков; при IncludeArchived == false — только активные; Search — регистронезависимый поиск подстроки по названию; сортировка по SortBy (NameAsc — по названию; CreatedAtDesc — по дате создания). Возвращает BankListItem c TotalCount.
    • op#34 get — карточка банка по bankId; не найден → CHESSBOARD_BANK_NOT_FOUND (404); архивные возвращаются (админ видит IsArchived/DeletedAt).
    • op#37 lookup — list: только активные всегда (INV-BNK-2), PageSize 1..50, поиск по названию, сортировка по названию; by-id: только активный банк (архивные исключены, INV-BNK-2) → не найден → CHESSBOARD_BANK_NOT_FOUND. Возвращает BankLookupItem.
  3. Результат: op#33 — постраничный BankListItem; op#34 — BankModel; op#37-list — постраничный BankLookupItem; op#37-by-id — BankLookupItem; 200. Маскирование не требуется (PII нет).

Ошибки и failure modes

Сквозные ValidationError (400), UNAUTHORIZED (401), FORBIDDEN (403), CHESSBOARD_CONCURRENCY_CONFLICT (409) — в реестре области (README.md), здесь не повторяются.

КодHTTPУсловиеСообщениеRetry
CHESSBOARD_BANK_NAME_TAKEN409Name занят активным банком (INV-BNK-1) / гонка уникального имени; op#32/op#35«Банк с таким названием уже существует»no retry (сменить имя)
CHESSBOARD_BANK_NOT_FOUND404bankId не найден либо архивный при update; op#34/op#35/op#36/op#37(by-id)«Банк не найден»no retry
CHESSBOARD_CONCURRENCY_CONFLICT409Доменная версия не совпала; op#35/op#36«Данные изменены параллельно»retry после перечитывания

Совместимость и наблюдаемость

  • Новые optional поля BankWriteModel/проекций — additive; переименование/смена типа/удаление — breaking (migration + rollout). Enum BankSortBy — строкой; неизвестное значение → ValidationError. Событий справочник не публикует; catalog/crm резолвят имя и лого через lookup-API синхронно. Появление потребности в реплике у catalogBankUpsertedKafkaEvent отдельным изменением (bank.md).
  • Logs: bank created/updated/archived (BankId, AdminUserId; без значений Search); metrics chessboard_admin_banks_write_total{op,result}, latency p99; traces — сквозной trace через traceparent; dashboard chessboard-overview; runbook — docs/06-services/chessboard/runbooks/ (будущий путь).

Acceptance Criteria

  • Create happy path (op#32). Дано: администратор с chessboard.banks.manage, X-Admin-User-Id задан, Name свободен среди активных. Когда: POST /banks. Тогда: банк создан (Version = 1, не архивный, аудит = AdminUserId); 201 + Location; событий нет (ADR-0056).
  • Create дубль имени (op#32). Дано: активный банк с тем же Name уже есть (в т.ч. гонка на уникальном имени). Когда: создание. Тогда: CHESSBOARD_BANK_NAME_TAKEN (409, INV-BNK-1).
  • Update concurrency (op#35). Дано: переданный Version не совпал с текущей версией банка. Когда: правка. Тогда: CHESSBOARD_CONCURRENCY_CONFLICT (409); проигравший перечитывает и повторяет.
  • Update архивного (op#35). Дано: bankId указывает на архивный банк. Когда: правка. Тогда: CHESSBOARD_BANK_NOT_FOUND (404, архивный не редактируется).
  • Archive не каскадит (op#36). Дано: на банк ссылаются Project.Financing/сделки crm. Когда: DELETE /banks/{id}. Тогда: банк помечен архивным (soft-delete), ссылки остаются валидными (INV-BNK-2); 204. Повтор архива уже архивного → идемпотентный 204.
  • Lookup исключает архив (op#37). Дано: банк архивирован. Когда: GET /lookup/banks (list) или by-id. Тогда: в list его нет; by-id → CHESSBOARD_BANK_NOT_FOUND (INV-BNK-2). При этом админский GET /banks/{id} (op#34) архивный банк возвращает (с IsArchived = true).
  • Search-фильтры (op#33). Дано: IncludeArchived = false, Search, SortBy. Когда: GET /banks. Тогда: постраничный BankListItem только активных, регистронезависимый поиск подстроки, сортировка по SortBy, TotalCount заполнен.

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

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

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