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 area | admin (администратор платформы, ADR-0053) |
| Feature group | banks |
| Статус | draft |
| Документ | docs/06-services/chessboard/api/admin/banks-crud.md |
Актор и доступ (общее для op#32–37)
| Проверка | Значение |
|---|---|
| Actor type | Администратор платформы через admin API (m2m-токен, audience pin.chessboard.admin) |
| Auth policy / Permissions | chessboard.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.
| # | Операция | Method | Route (/api/v1/admin) | Успех |
|---|---|---|---|---|
| 32 | create | POST | /banks | 201 + Location |
| 33 | search | GET | /banks | 200 |
| 34 | get | GET | /banks/{bankId} | 200 |
| 35 | update | PUT | /banks/{bankId} | 200 |
| 36 | archive | DELETE | /banks/{bankId} | 204 |
| 37 | lookup | GET | /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.
| Поле | Источник | Тип | Обяз. | Валидация / Ограничения | Операции |
|---|---|---|---|---|---|
AdminUserId | header X-Admin-User-Id | Guid | Да (команды) | непустой; используется для аудита | 32,35,36 |
bankId | route | Guid | Да | непустой; существующий (иначе …NOT_FOUND) | 34,35,36,37(by-id) |
Name | body (BankWriteModel) | string | Да | trim, 1..256; уникально среди активных (INV-BNK-1) | 32,35 |
LogoFileId | body (BankWriteModel) | Guid? | Нет | FileId storage/CDN, без FK | 32,35 |
Version | body (BankWriteModel) | long | Да (только update) | >= 1; сверка с текущей версией банка (optimistic) | 35 |
Search | query | string? | Нет | <= 256; регистронезависимый поиск подстроки по названию | 33,37 |
IncludeArchived | query | bool | Нет (default false) | false → только активные (не архивные) | 33 |
SortBy | query | BankSortBy | Нет (default NameAsc) | enum-string: NameAsc / CreatedAtDesc | 33 |
Page / PageSize | query | int | Нет (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)
| Поле | Тип | Обяз. | Источник | Описание |
|---|---|---|---|---|
Id | Guid | Да | Id | Идентификатор банка. |
Name | string | Да | Name | Название. |
LogoFileId | Guid? | Нет | LogoFileId | Лого (storage/CDN, пресайн — на фронте). |
IsArchived | bool | Да | computed (DeletedAt != null) | Архивный флаг. |
CreatedAt | DateTimeOffset | Да | CreatedAt | Создан. |
CreatedByUserId | Guid? | Нет | CreatedByUserId | Админ-создатель. |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | Изменён. |
UpdatedByUserId | Guid? | Нет | UpdatedByUserId | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | DeletedAt | != null → архив. |
Version | long | Да | 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
- Проверить вход:
Nameпосле trim в диапазоне1..256,LogoFileId— необязательный,AdminUserIdзадан. Нарушение →ValidationError. - Проверить занятость имени среди активных банков (INV-BNK-1). Если название уже занято активным банком →
CHESSBOARD_BANK_NAME_TAKEN. - Создать банк с новым идентификатором, названием (trim) и логотипом; версия = 1, банк не архивный;
проставить аудит (создатель и последний изменивший =
AdminUserId). При гонке на уникальном имени →CHESSBOARD_BANK_NAME_TAKEN. - Результат: карточка
BankModel,201+Location: /api/v1/admin/banks/{Id}.
op#35 update
- Проверить вход:
Name1..256,Version >= 1,bankIdзадан. Нарушение →ValidationError. - Найти банк по
bankId. Не найден либо архивный (DeletedAt != null— архивный не редактируется) →CHESSBOARD_BANK_NOT_FOUND(404). При смене имени — проверить уникальность среди активных, исключая сам банк (INV-BNK-1); занято →CHESSBOARD_BANK_NAME_TAKEN. Если переданнаяVersionне совпадает с текущей версией банка →CHESSBOARD_CONCURRENCY_CONFLICT(409). - Применить новое название (trim) и лого; последний изменивший =
AdminUserId; версия увеличивается. При конкурентной правке →CHESSBOARD_CONCURRENCY_CONFLICT; при гонке уникального имени →CHESSBOARD_BANK_NAME_TAKEN. - Результат:
BankModel,200.
op#36 archive (soft-delete)
- Проверить вход:
bankIdзадан. - Найти банк. Не найден →
CHESSBOARD_BANK_NOT_FOUND(404). Уже архивный (DeletedAt != null) → идемпотентно204(изменений нет). - INV-BNK-2: архив не каскадит — ссылки
Project.Financing/crmне проверяются и остаются валидными; из lookup-списков банк исчезает (остаются только активные). Пометить банк архивным (DeletedAt = now,DeletedByUserId = AdminUserId) — строка физически не удаляется (soft-delete). При конкурентной правке →CHESSBOARD_CONCURRENCY_CONFLICT. - Результат:
204.
op#33 search / op#34 get / op#37 lookup (read-only)
- Проверить вход: paging/
Search/SortBy(op#33/op#37); для op#34 и op#37(by-id) —bankIdзадан. Нарушение →ValidationError. - Загрузка и выдача:
- op#33 search — постраничный список банков; при
IncludeArchived == false— только активные;Search— регистронезависимый поиск подстроки по названию; сортировка поSortBy(NameAsc— по названию;CreatedAtDesc— по дате создания). ВозвращаетBankListItemcTotalCount. - 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.
- op#33 search — постраничный список банков; при
- Результат: 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_TAKEN | 409 | Name занят активным банком (INV-BNK-1) / гонка уникального имени; op#32/op#35 | «Банк с таким названием уже существует» | no retry (сменить имя) |
CHESSBOARD_BANK_NOT_FOUND | 404 | bankId не найден либо архивный при update; op#34/op#35/op#36/op#37(by-id) | «Банк не найден» | no retry |
CHESSBOARD_CONCURRENCY_CONFLICT | 409 | Доменная версия не совпала; op#35/op#36 | «Данные изменены параллельно» | retry после перечитывания |
Совместимость и наблюдаемость
- Новые optional поля
BankWriteModel/проекций — additive; переименование/смена типа/удаление — breaking (migration + rollout). EnumBankSortBy— строкой; неизвестное значение →ValidationError. Событий справочник не публикует;catalog/crmрезолвят имя и лого через lookup-API синхронно. Появление потребности в реплике уcatalog→BankUpsertedKafkaEventотдельным изменением (bank.md). - Logs:
bank created/updated/archived(BankId,AdminUserId; без значенийSearch); metricschessboard_admin_banks_write_total{op,result}, latency p99; traces — сквозной trace черезtraceparent; dashboardchessboard-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заполнен.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: BankEntity
- API (3): Admin CRUD отельеров (создание / список / карточка / правка / архив / lookup) — область admin, Chessboard API — реестр операций областей Developers и Admin, GET /api/v1/developers/lookup/{banks,hoteliers} — Lookup справочников банков/отельеров
- Use Cases (1): UC-CHS-007. Администратор ведёт справочники банков и отельеров
- registries (1): Реестр API-методов PIN
Связанные документы
- Дом полей: ../domain/bank.md (INV-BNK-1/2, поля, индексы); использование ссылок:
../domain/project.md (
ProjectFinancingDocument, INV-PRJ-7); парный справочник: admin-hoteliers-crud.md (та же форма). - Реестр операций: README.md; lookup для developers/agents: developers-references-lookup.md.
- ADR: ADR-0052 (tenancy), ADR-0053 (админ-доступ), ADR-0056 (simplicity-first).