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

GET /api/v1/admin/owner-mappings/conflicts — Очередь конфликтов связи Собственник ↔ Инвестор (listOwnerMappingConflicts)

Назначение

Admin-операция чтения очереди неразрешённых конфликтов сопоставления Собственник ↔ Инвестор для Службы заботы: постраничный список связей OwnerMappingEntity со Status = Conflict, которые автосвязка синхронизации Talent Owners установить не смогла (дубль телефона, невалидный номер, найденный пользователь — не инвестор, коллизия id). Это входная точка ручного разбора: из каждой строки сотрудник переходит в разрешение resolve-owner-mapping-conflict.md. Источник конфликтов — consumer ../../consumers/talent-owners-sync.md (доменное событие OwnerMappingConflictDetected). Дом полей связи — ../../domain/owner-mapping.md.

Metadata

ПолеЗначение
Сервис/контекстinvestor-owner
API areaadmin (Служба заботы)
Feature groupowner-mappings
Статусdraft
Документdocs/06-services/investor-owner/api/admin/list-owner-mapping-conflicts.md

Актор и доступ

ПроверкаЗначение
Actor typeOperator(Admin) (сотрудник Службы заботы, Bearer JWT)
Auth policy / Permissionsauthenticated; permission investor-owner.mappings.manage (../../../identity/domain/permission.md)
Scope (RBAC)платформенный (не self, не tenant); record-level фильтр по InvestorUserId не применяется — платформенный просмотр очереди
Record-levelне применяется (платформенная роль); ключ выборки — Status = Conflict
Tenant isolationне применяется — данные инвестора вне tenant (ADR-0052)
Auditread-sensitive (deep audit, ADR-0053): платформенное чтение чужих PII (ФИО собственников) — запись доступа (actor sub, фильтры, traceparent)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/admin/owner-mappings/conflicts
Success status200 (в т.ч. пустая очередь — пустая страница, не ошибка)
Idempotency headerне применяется (safe GET)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
RequestedByUserIdidentity claims (sub)GuidДавалидный JWT — 401 до операции; permission — 403пишется в deep-audit (кто читал очередь)
ConflictReasonqueryOwnerLinkConflictReason?Нетenum ∈ {PhoneAlreadyMappedToAnotherOwner, PhoneInvalid, MatchedUserNotInvestor, IdentityIdCollision}; неизвестное значение → ValidationError
Searchquerystring?Нет2..64 после trim; поиск по видимому хвосту телефона (PhoneNumber) и подстроке OwnerFullName (case-insensitive)сам запрос — не PII; в логи пишется маской
PagequeryintНет>= 1, default 1
PageSizequeryintНет1..100, default 20

Модель ответа (постраничный список OwnerMappingConflictModel)

Постраничный контейнер (items, page, pageSize, totalCount); totalCount = число конфликтных связей под фильтром. Финансовых данных в модели нет.

ПолеТипОбязательностьИсточникMaskingОписание
itemsIReadOnlyList<OwnerMappingConflictModel>ДапроекцияСтраница конфликтов (упорядочены CreatedAt asc — старейшие первыми, FIFO-очередь).
pageintДаecho запросаТекущая страница.
pageSizeintДаecho запросаРазмер страницы.
totalCountlongДаподсчёт по фильтруВсего конфликтов под фильтром (0 при пустой очереди).

OwnerMappingConflictModel (проекция агрегата — дом полей ../../domain/owner-mapping.md; здесь только дельта)

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаOwnerMapping.Id (= OwnerId)Идентификатор связи; ключ операции resolve-owner-mapping-conflict.md.
phoneNumberMaskedstringДаOwnerMapping.PhoneNumber+7***1234Телефон собственника (PII — маскируется, в открытом виде не отдаётся).
ownerFullNameOwnerFullNameModelДаVO FullNameФИО собственника (снапшот Talent Owners) — нужно заботе для идентификации при разборе; доступ deep-audited.
conflictReasonstring (enum OwnerLinkConflictReason)ДаOwnerMapping.ConflictReasonМашиночитаемая причина конфликта (INV-3 — при Conflict обязателен).
conflictDetailsstring?НетOwnerMapping.ConflictDetailsтелефон третьих лиц — уже маскойЧеловекочитаемая детализация (напр. id конкурирующей связи).
createdAtstring (ISO 8601)ДаOwnerMapping.CreatedAtКогда связь появилась (первое событие синхронизации).
lastSyncedAtstring (ISO 8601)ДаOwnerMapping.LastSyncedAtВремя последнего применённого события Talent Owners.

OwnerFullNameModel (проекция VO FullName — дельта: поле-в-поле)

ПолеТипОбязательностьИсточникОписание
firstNamestringДаFullName.FirstNameИмя.
lastNamestringДа (пустая строка допустима)FullName.LastNameФамилия.
middleNamestring?НетFullName.MiddleNameОтчество.

Алгоритм

  1. Контекст и доступ. Операцию выполняет оператор Службы заботы (RequestedByUserId — из claim sub); требуется permission investor-owner.mappings.manage (иначе 403); scope платформенный (не tenant). Чтение чужих PII фиксируется в deep-audit (ADR-0053).
  2. Проверка входа. Page >= 1, PageSize ∈ 1..100, ConflictReason ∈ enum (если задан), Search2..64 после trim (если задан) → иначе ValidationError (400). Пустая очередь — валидный ответ с totalCount = 0.
  3. Что читаем. Постраничная выборка связей со Status = Conflict (не удалённых); при заданном ConflictReason — фильтр по причине; при заданном Search — поиск по подстроке телефона и по ФИО (OwnerFullName); сортировка CreatedAt asc (FIFO — старейшие первыми). Тем же запросом возвращается totalCount. Телефон из хранилища используется только для предиката поиска, наружу отдаётся маской.
  4. Маппинг и маскирование. Проекция — таблица OwnerMappingConflictModel; phoneNumberMasked = Mask(PhoneNumber) (+7***1234); ownerFullName — как есть (сотрудник должен опознать собственника, доступ покрыт deep-audit); createdAt/lastSyncedAt — ISO 8601.
  5. Результат. 200 (пусто → items = [], totalCount = 0). Консистентность: read-your-writes — конфликт, только что зафиксированный синхронизацией, виден сразу.

Диаграмма

Побочные эффекты

ТипДетали
Внешние вызовыnone
Проекции / поискnone (btree ix_owner_mappings_status, ix_owner_mappings_created_at../../domain/owner-mapping.md; ADR-0056 — без Redis/поисковых индексов)
Аудитзапись доступа (deep audit) при чтении чужих PII

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен/невалиден JWTбез тела схемы (фронт очищает сессию)re-login
FORBIDDEN403Нет investor-owner.mappings.manage«Недостаточно прав»no retry
ValidationError400Некорректные Page/PageSize/ConflictReason/SearchСписок ошибок полейno retry
— (пустая очередь, не ошибка)200Конфликтов под фильтром нетitems = [], totalCount = 0

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

  • OwnerMappingConflictModel — проекция агрегата; новые optional поля additive; удаление/переименование/смена типа — breaking (migration + rollout admin-UI). Enum OwnerLinkConflictReason сериализуется строкой; клиент обязан игнорировать неизвестные значения (unknown enum handling — показать строку без бейджа).
  • Logs: owner-mapping-conflicts-listed (RequestedByUserId, ConflictReason, размер страницы, totalCount; телефон/ФИО/Search — маской, суммы отсутствуют); metrics: investor_owner_conflicts_listed_total{result}, latency histogram, gauge investor_owner_conflicts_open (алерт роста > 10/день — ../../README.md); traces — сквозной trace; dashboard investor-owner-overview; runbook — «разбор конфликтов связи» (../../README.md).

Acceptance Criteria

  • Given оператор заботы и очередь с конфликтными связями, When GET /admin/owner-mappings/conflicts без фильтров, Then 200 со страницей Status = Conflict в порядке CreatedAt asc (FIFO), totalCount = число конфликтов под фильтром; телефон отдан маской +7***1234, ФИО — как есть; чтение зафиксировано в deep-audit.
  • Given пустая очередь (нет конфликтов под фильтром), When запрос, Then 200 с items = [], totalCount = 0.
  • Given ConflictReason = PhoneAlreadyMappedToAnotherOwner, When запрос, Then возвращаются только связи с этой причиной; Given Search по хвосту телефона/подстроке ФИО, Then предикат применяется к PhoneNumber/OwnerFullName, но телефон в ответе остаётся маской.
  • Given конфликт, только что зафиксированный синхронизацией, When оператор открывает очередь, Then он виден сразу (read-your-writes).
  • Given Page < 1, PageSize ∉ 1..100, неизвестный ConflictReason или Search вне 2..64, When запрос, Then ValidationError (400).
  • Given нет investor-owner.mappings.manage, When запрос, Then 403; невалидный JWT — 401.

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

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

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