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

GET /api/v1/profile/persons — Список персон аккаунта (listPersons)

Назначение

Вернуть все персоны текущего инвестора: несколько наборов персональных данных на аккаунт (сам/супруг/ ребёнок/иное — сделку оформляют на другого человека). ФИО и дата рождения хранятся шифрованными и расшифровываются в памяти для владельца — список мал (≤ 20 активных, INV-PRS-7), поиск по шифрованным полям не выполняется (person.md). Вызывает инвестор из ЛК.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areapublic
Feature grouppersons
Статусdraft
Документdocs/06-services/profile/api/public/list-persons.md

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor (INV-PRF-4)
Auth policy / Permissionsauthenticated; permission profile.personal.view-self
Scope (RBAC)self
Record-levelPersonEntity.OwnerUserId = UserId из sub
Tenant isolationне применяется (ADR-0052)
Auditread-sensitive: расшифровка ФИО/дат рождения self-владельцем — не аудируется отдельно (собственные данные); в логах значения не пишутся

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/profile/persons
Success status200
Idempotency headerне применяется
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
includeArchivedqueryboolНет (default false)true/false

Модель ответа PersonListModel (container)

ПолеТипОбязательностьИсточникMaskingОписание
itemsList<PersonModel>Да (может быть пустым только при includeArchived=false и отсутствии Self — до догона консюмера)проекция PersonEntityПорядок: Self первой, далее по CreatedAt asc.
totalCountintДаcomputedВсего записей в выборке.
activeCountintДаcomputedАктивных персон (для UI-лимита: 20, INV-PRS-7).

Модель PersonModel

Дом полей — person.md; проекция с расшифровкой для владельца.

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаPersonEntity.Id
relationPersonRelationTypeДаPersonEntity.RelationSelf/Spouse/Child/Parent/Other.
statusPersonStatusДаPersonEntity.StatusActive/Archived.
firstNamestringДарасшифровка Name.FirstNameCipherтолько self; в логах — никогдаРасшифрованное имя.
lastNamestringДарасшифровка Name.LastNameCipherтолько selfРасшифрованная фамилия.
middleNamestring?Нетрасшифровка Name.MiddleNameCipherтолько selfОтчество (опционально).
birthDateDateOnly?Нетрасшифровка BirthDateCipherтолько selfISO yyyy-MM-dd.
citizenshipCountryIdshort?НетPersonEntity.CitizenshipCountryIdСсылка на справочник стран handbook (открытое поле); display-имя страны клиент берёт из публичного lookup справочника.
genderPersonGender?НетPersonEntity.GenderMale/Female.
documentsCountintДаPersonEntity.DocumentsCountДенормализация — без загрузки child-коллекции.
createdAtDateTimeOffsetДаPersonEntity.CreatedAt
updatedAtDateTimeOffsetДаPersonEntity.UpdatedAt
versionlongДаPersonEntity.VersionДля optimistic-обновлений.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена); tenant не применяется; требуется право profile.personal.view-self.
  2. Проверка входа. UserId присутствует в claims. Существование профиля и допустимость фичи — шаг 3.
  3. Проверка предусловий и выборка. Профиль владельца существует; иначе PROFILE_NOT_FOUND. Тип пользователя — Investor; иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (INV-PRF-4; чтение для TalentSync-инвестора допустимо — запрещены только правки, INV-PRF-2). Отобрать персоны владельца (активные, а при includeArchived = true — и архивные), сортировка CreatedAt asc; без paging (≤ 20 активных, INV-PRS-7; child-документы не включаются — DocumentsCount денормализован).
  4. Маппинг с расшифровкой. Для каждой персоны расшифровать в памяти firstName/lastName/middleName и (при наличии) birthDate ключом версии записи. Недоступность ключа — fail-closed PROFILE_CRYPTO_UNAVAILABLE без частичного ответа. Сортировка: Relation = Self первой, далее CreatedAt asc; activeCount = количество персон со Status = Active.
  5. Результат. Модель PersonListModel; расшифрованные значения возвращаются только владельцу (record-level гарантирован фильтром OwnerUserId). HTTP 200.

Диаграмма

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

ТипДетали
Внешние вызовыпровайдер ключей шифрования (in-process кэш) для расшифровки ФИО/дат рождения; недоступность → PROFILE_CRYPTO_UNAVAILABLE

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустой UserIdперечень невалидных полейno retry
PROFILE_NOT_FOUND404Профиль не создан консюмером«Профиль ещё создаётся»retry с backoff
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403UserType != Investor (INV-PRF-4)«Раздел доступен только инвесторам»no retry
PROFILE_CRYPTO_UNAVAILABLE503Ключи шифрования недоступны (fail-closed)«Сервис временно недоступен»retry (alert по README)

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

  • Новые optional поля PersonModel — additive; unknown relation/status у клиента — показывать как «Иное»/скрыть действия.
  • Logs: UserId, totalCount (без ФИО/дат — правило person.md); metrics: profile_api_operations_total{operation="listPersons",result}, profile_crypto_errors_total (alert > 0); traces — OTel.

Acceptance Criteria

  • Given инвестор с активными Self и Spouse, When GET includeArchived=false, Then 200; порядок Self первой, далее CreatedAt asc; ФИО/birthDate расшифрованы; documentsCount денормализован; activeCount вычислен.
  • Given includeArchived = true, When GET, Then в выборку включены и архивные персоны.
  • Given Self ещё не создана консюмером identity (лаг реплики), When GET, Then 200 с пустым/частичным items до догона консюмера.
  • Given ключи шифрования недоступны, When GET, Then PROFILE_CRYPTO_UNAVAILABLE (503) без частичного ответа (fail-closed).
  • Given TalentSync-инвестор, When GET, Then 200 (чтение допустимо; запрещены только правки, INV-PRF-2).
  • Given UserType != Investor, When GET, Then PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403) — INV-PRF-4.
  • Given профиль ещё не создан консюмером identity, When GET, Then PROFILE_NOT_FOUND (404), retry с backoff.
  • Given любой успешный ответ, Then расшифрованные значения возвращаются только владельцу (record-level по OwnerUserId) и никогда не логируются.

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

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

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