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

GET /api/v1/profile/persons/{personId}/documents — Список документов персоны (listPersonDocuments)

Назначение

Вернуть документы персоны: паспорта РФ/РБ/КЗ/СНГ/иностранные, ИНН, кастомные документы. Значения полей хранятся шифрованными целиком в PayloadCipher (person-document.md); для владельца typed payload расшифровывается в памяти (документы ≤ 20, формы редактирования требуют значений). Файлы сканов не возвращаются — только метаданные ScanList; скачивание — presigned-ссылка (get-document-scan-link.md). Вызывает инвестор-владелец персоны.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.documents.view-self
Scope (RBAC)self
Record-levelPersonEntity.OwnerUserId = UserId; чужой personId → 404
Tenant isolationне применяется (ADR-0052)
Auditread-sensitive: self-расшифровка payload не аудируется отдельно; значения в логи не пишутся

HTTP-контракт

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

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

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

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

ПолеТипОбязательностьИсточникMaskingОписание
itemsList<PersonDocumentModel>Да (может быть пустым)child-коллекция PersonEntityПорядок: Type (паспорта → ИНН → кастомные), затем CreatedAt asc.
totalCountintДаcomputedВсего в выборке.
activeCountintДаcomputedДля UI-лимита (20, INV-DOC-8).

Модель PersonDocumentModel

Дом полей и typed-моделей payload — person-document.md; здесь проекция.

ПолеТипОбязательностьИсточникMaskingОписание
idGuidДаPersonDocumentEntity.Id
personIdGuidДаPersonDocumentEntity.PersonId
typePersonDocumentTypeДаPersonDocumentEntity.TypeRfPassport/RbPassport/KzPassport/CisPassport/ForeignPassport/Inn/Custom.
statusPersonDocumentStatusДаPersonDocumentEntity.StatusActive/Archived.
countryIdshortДаPersonDocumentEntity.CountryIdСсылка на справочник стран handbook.
countryIsoCode2stringДаPersonDocumentEntity.CountryIsoCode2char(2) — денормализованный код страны, снятый при записи документа.
titlestringДаPersonDocumentEntity.Title«Паспорт РФ •• 1234» / пользовательский для Custom.
numberMaskstringДаPersonDocumentEntity.NumberMaskэто и есть маскаДля списков; полный номер — в payload.
issueDateDateOnly?НетPersonDocumentEntity.IssueDateОткрытая копия.
expiresAtDateOnly?НетPersonDocumentEntity.ExpiresAtДля подсветки «истекает».
payloadContractVersionintДаPersonDocumentEntity.PayloadContractVersionВерсия typed-модели.
payloadtyped-модель по typeДарасшифровка PayloadCipher → десериализациятолько self; в логах — никогдаRfPassportModel/RbPassportModel/KzPassportModel/CisPassportModel/ForeignPassportModel/InnModel/CustomDocumentModel — поля см. дом.
scansList<DocumentScanModel>Да (может быть пустым)ScanList.ItemsfileId (Guid), fileName (string ≤256), contentType (whitelist), sizeBytes (long), uploadedAt (DateTimeOffset) — дом DocumentScan.
createdAt / updatedAtDateTimeOffsetДасущность
versionlongДаPersonDocumentEntity.VersionДля optimistic-обновлений.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена); tenant не применяется; требуется право profile.documents.view-self.
  2. Проверка входа. personId задан. Нарушение → ValidationError.
  3. Проверка предусловий и выборка. Профиль владельца существует; иначе PROFILE_NOT_FOUND. Тип пользователя — Investor (иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE, INV-PRF-4). Персона personId существует и принадлежит владельцу; иначе PROFILE_PERSON_NOT_FOUND. Из документов персоны отбираются активные, а при includeArchived = true — и архивные.
  4. Маппинг с расшифровкой. Для каждого документа: расшифровать PayloadCipher и десериализовать строго в typed-модель по Type (INV-DOC-1); при PayloadContractVersion < актуальной — lazy-миграция модели в памяти (additive-правило дома). Неизвестный Type — fail-fast ошибка данных (не бизнес-ошибка). Недоступность ключей — fail-closed PROFILE_CRYPTO_UNAVAILABLE. Спроецировать по таблице PersonDocumentModel; сортировка Type, затем CreatedAt asc.
  5. Результат. Модель PersonDocumentListModel; полный payload — только владельцу (record-level). HTTP 200.

Диаграмма

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

ТипДетали
Внешние вызовыпровайдер ключей шифрования для расшифровки payload документов; недоступность → PROFILE_CRYPTO_UNAVAILABLE

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустой personIdперечень невалидных полейno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor«Доступно только инвесторам»no retry
PROFILE_PERSON_NOT_FOUND404Нет персоны / чужая«Персона не найдена»no retry
PROFILE_CRYPTO_UNAVAILABLE503Ключи недоступны«Сервис временно недоступен»retry

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

  • Payload-модели меняются только additive (рост PayloadContractVersion, дом person-document.md); новые type — additive для списка (клиент скрывает неизвестные типы).
  • Logs: UserId, PersonId, totalCount, DocumentId+Type (без значений payload); metrics: profile_api_operations_total{operation="listPersonDocuments",result}, profile_crypto_errors_total; traces — OTel.

Acceptance Criteria

  • Given персона владельца с 3 активными и 1 архивным документами, When GET includeArchived=false, Then 200; items — 3 активных, порядок Type (паспорта → ИНН → кастомные) затем CreatedAt asc; payload каждого расшифрован в typed-модель по Type; сканы — только метаданные (без содержимого/ссылок); totalCount/activeCount вычислены.
  • Given includeArchived = true, When GET, Then в выборку включены и архивные документы.
  • Given у персоны нет документов, When GET, Then 200 с пустым items.
  • Given PayloadContractVersion < актуальной, When GET, Then lazy-миграция модели в памяти (additive), без ошибки.
  • Given неизвестный Type в хранилище, When GET, Then fail-fast ошибка данных (не бизнес-ошибка).
  • Given ключи шифрования недоступны, When GET, Then PROFILE_CRYPTO_UNAVAILABLE (503) без частичного ответа.
  • Given personId чужой, When GET, Then PROFILE_PERSON_NOT_FOUND (404).
  • Given UserType != Investor / профиль не создан, When GET, Then PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403) / PROFILE_NOT_FOUND (404) соответственно.
  • Given любой успешный ответ, Then полный payload виден только владельцу (record-level) и никогда не логируется.

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

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

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