GET /api/v1/profile/persons — Список персон аккаунта (listPersons)
Назначение
Вернуть все персоны текущего инвестора: несколько наборов персональных данных на аккаунт (сам/супруг/
ребёнок/иное — сделку оформляют на другого человека). ФИО и дата рождения хранятся шифрованными и
расшифровываются в памяти для владельца — список мал (≤ 20 активных, INV-PRS-7), поиск по шифрованным
полям не выполняется (person.md). Вызывает инвестор из ЛК.
| Поле | Значение |
|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | persons |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/list-persons.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT), UserType = Investor (INV-PRF-4) |
| Auth policy / Permissions | authenticated; permission profile.personal.view-self |
| Scope (RBAC) | self |
| Record-level | PersonEntity.OwnerUserId = UserId из sub |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | read-sensitive: расшифровка ФИО/дат рождения self-владельцем — не аудируется отдельно (собственные данные); в логах значения не пишутся |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/profile/persons |
| Success status | 200 |
| Idempotency header | не применяется |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
includeArchived | query | bool | Нет (default false) | true/false | — |
Модель ответа PersonListModel (container)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
items | List<PersonModel> | Да (может быть пустым только при includeArchived=false и отсутствии Self — до догона консюмера) | проекция PersonEntity | — | Порядок: Self первой, далее по CreatedAt asc. |
totalCount | int | Да | computed | — | Всего записей в выборке. |
activeCount | int | Да | computed | — | Активных персон (для UI-лимита: 20, INV-PRS-7). |
Модель PersonModel
Дом полей — person.md; проекция с расшифровкой для владельца.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
id | Guid | Да | PersonEntity.Id | — | — |
relation | PersonRelationType | Да | PersonEntity.Relation | — | Self/Spouse/Child/Parent/Other. |
status | PersonStatus | Да | PersonEntity.Status | — | Active/Archived. |
firstName | string | Да | расшифровка Name.FirstNameCipher | только self; в логах — никогда | Расшифрованное имя. |
lastName | string | Да | расшифровка Name.LastNameCipher | только self | Расшифрованная фамилия. |
middleName | string? | Нет | расшифровка Name.MiddleNameCipher | только self | Отчество (опционально). |
birthDate | DateOnly? | Нет | расшифровка BirthDateCipher | только self | ISO yyyy-MM-dd. |
citizenshipCountryId | short? | Нет | PersonEntity.CitizenshipCountryId | — | Ссылка на справочник стран handbook (открытое поле); display-имя страны клиент берёт из публичного lookup справочника. |
gender | PersonGender? | Нет | PersonEntity.Gender | — | Male/Female. |
documentsCount | int | Да | PersonEntity.DocumentsCount | — | Денормализация — без загрузки child-коллекции. |
createdAt | DateTimeOffset | Да | PersonEntity.CreatedAt | — | — |
updatedAt | DateTimeOffset | Да | PersonEntity.UpdatedAt | — | — |
version | long | Да | PersonEntity.Version | — | Для optimistic-обновлений. |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (
UserId из токена); tenant не применяется;
требуется право profile.personal.view-self.
- Проверка входа.
UserId присутствует в claims. Существование профиля и допустимость фичи — шаг 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 денормализован).
- Маппинг с расшифровкой. Для каждой персоны расшифровать в памяти
firstName/lastName/middleName
и (при наличии) birthDate ключом версии записи. Недоступность ключа — fail-closed
PROFILE_CRYPTO_UNAVAILABLE без частичного ответа. Сортировка: Relation = Self первой, далее
CreatedAt asc; activeCount = количество персон со Status = Active.
- Результат. Модель
PersonListModel; расшифрованные значения возвращаются только владельцу
(record-level гарантирован фильтром OwnerUserId). HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Внешние вызовы | провайдер ключей шифрования (in-process кэш) для расшифровки ФИО/дат рождения; недоступность → PROFILE_CRYPTO_UNAVAILABLE |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | Пустой UserId | перечень невалидных полей | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан консюмером | «Профиль ещё создаётся» | retry с backoff |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | UserType != Investor (INV-PRF-4) | «Раздел доступен только инвесторам» | no retry |
PROFILE_CRYPTO_UNAVAILABLE | 503 | Ключи шифрования недоступны (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) и никогда не логируются.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы