GET /api/v1/profile/requisite-templates — Список шаблонов реквизитов (listRequisiteTemplates)
Назначение
Вернуть активные (и опционально архивные по флагу includeArchived) шаблоны реквизитов текущего инвестора
— для выбора в форме брони/сделки CRM. Список без PII: Composition хранит только id документов
(открыто), контакты-оверрайды не расшифровываются — отдаётся лишь признак наличия (hasContactOverride).
Расшифрованный снапшот выдаётся отдельно, только по m2m-запросу CRM (internal API снапшота,
requisite-template.md). Сортировка «недавние»: LastUsedAt desc,
затем CreatedAt desc. Вызывает инвестор из формы брони/сделки.
| Поле | Значение |
|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | requisites |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/list-requisite-templates.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT), UserType = Investor (INV-PRF-4) |
| Auth policy / Permissions | authenticated; permission profile.requisites.manage-self (отдельного view нет — управление своими реквизитами) |
| Scope (RBAC) | self |
| Record-level | RequisiteTemplateEntity.OwnerUserId = UserId из sub |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | read: список без PII (id/имена полей); значения контактов не расшифровываются и не аудируются |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/profile/requisite-templates |
| Success status | 200 |
| Idempotency header | не применяется |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
includeArchived | query | bool | Нет (default false) | true/false | — |
Page | query | int | Нет (default 1) | >= 1 | — |
Size | query | int | Нет (default 20) | 1..20 (агрегаты малы, ≤ 20 активных, INV-RQT-7); иначе ValidationError | — |
Модель ответа — постраничный список RequisiteTemplateModel
Ответ — страница элементов (Items) и метаданные страницы (Meta). Дом элемента —
requisite-template.md.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Items | IReadOnlyList<RequisiteTemplateModel> | Да (может быть пустым) | projection RequisiteTemplateEntity | — | Страница шаблонов; порядок — LastUsedAt desc (NULLS LAST), затем CreatedAt desc. |
Meta | PagingMeta | Да | Total, Take = Size, Offset, HasMore | — | Метаданные страницы (Total — всего под фильтром, для UI-лимита 20). |
Модель RequisiteTemplateModel
Проекция агрегата без расшифровки PII (дом полей — requisite-template.md).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
id | Guid | Да | RequisiteTemplateEntity.Id | — | — |
name | string | Да | .Name | — | Имя шаблона (не PII: «Реквизиты жены», «На дочь»). |
personId | Guid | Да | .PersonId | — | Персона документов сделки. |
status | RequisiteTemplateStatus | Да | .Status | — | Active/Archived. |
isDefault | bool | Да | .IsDefault | — | Шаблон по умолчанию для формы брони (INV-RQT-5). |
composition | RequisiteTemplateCompositionModel | Да | .Composition (jsonb) | — | Только id документов — без PII (модель ниже). |
hasContactOverride | bool | Да | computed: ContactEmailCipher != null || ContactPhoneCipher != null | значения не отдаются | Признак контактов-оверрайдов; сами email/phone не расшифровываются и наружу не идут. |
lastUsedAt | DateTimeOffset? | Нет | .LastUsedAt | — | Последнее использование (сортировка «недавние»). |
usageCount | int | Да | .UsageCount | — | Сколько раз запрошен снапшот брони/сделки. |
version | long | Да | .Version | — | Для optimistic-обновлений. |
Модель RequisiteTemplateCompositionModel
Проекция jsonb RequisiteTemplateComposition (дом — requisite-template.md); только id, plaintext.
| Поле | Тип | Обязательность | Источник | Описание |
|---|
identityDocumentId | Guid | Да | Composition.IdentityDocumentId | id документа-удостоверения (паспортный тип, INV-RQT-2). |
innDocumentId | Guid? | Нет | Composition.InnDocumentId | id документа ИНН, если задан. |
additionalDocumentIds | List<Guid> | Да (может быть пустым) | Composition.AdditionalDocumentIds | ≤ 10 id дополнительных документов персоны. |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (
UserId из токена); tenant не применяется
(ADR-0052); требуется право profile.requisites.manage-self.
- Проверка входа.
UserId присутствует; includeArchived — bool; Page >= 1, Size ∈ [1..20] —
иначе ValidationError.
- Проверка предусловий и выборка. Профиль владельца существует; иначе
PROFILE_NOT_FOUND. Профиль в
режиме самоуправления (ContactsManagedBy = SelfService, иначе PROFILE_MANAGED_EXTERNALLY, INV-PRF-2),
активен (Status = Active, иначе PROFILE_USER_BLOCKED, INV-PRF-3), тип пользователя — Investor
(иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE, INV-PRF-4). Отобрать страницу шаблонов владельца
(активные, а при includeArchived = true — и архивные), сортировка LastUsedAt desc (NULLS LAST),
затем CreatedAt desc; постранично по Page/Size. Персона и документы не включаются (composition —
id, признак контактов — по наличию шифрованных значений).
- Маппинг без расшифровки PII. Спроецировать
RequisiteTemplateEntity → RequisiteTemplateModel
поле-в-поле; Composition десериализуется из jsonb в RequisiteTemplateCompositionModel (только id;
при ContractVersion < актуальной — lazy-миграция в памяти, additive-правило дома);
hasContactOverride = ContactEmailCipher != null || ContactPhoneCipher != null (по наличию значений, без
декодирования). Расшифровка не выполняется — список без PII.
- Результат. Страница
Items + Meta { Total, Take = Size, Offset, HasMore }; HTTP 200. Freshness:
source of truth, без кэша; record-level гарантирован фильтром OwnerUserId; пустой набор — 200 с
пустым Items.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | Пустой UserId / неверный paging | перечень невалидных полей | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан консюмером identity | «Профиль ещё создаётся» | retry с backoff |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | UserType != Investor (INV-PRF-4) | «Раздел доступен только инвесторам» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | ContactsManagedBy = TalentSync (INV-PRF-2) | «Данные управляются организацией» | no retry |
PROFILE_USER_BLOCKED | 403 | Status != Active (INV-PRF-3) | «Профиль заблокирован» | no retry |
Совместимость и наблюдаемость
- Новые optional поля
RequisiteTemplateModel — additive; RequisiteTemplateCompositionModel растёт по
ContractVersion дома (lazy-миграция); unknown status у клиента — скрыть действия. Удаление/
переименование/смена типа — breaking (migration + rollout).
- Logs:
UserId, TotalCount, TemplateId/PersonId (без значений контактов и без PII — правило
requisite-template.md); metrics:
profile_api_operations_total{operation="listRequisiteTemplates",result}; traces — OTel;
dashboard profile-overview.
Acceptance Criteria
- Given инвестор с 3 активными шаблонами, When
GET page=1&size=20, Then 200; Items
отсортированы LastUsedAt desc (NULLS LAST) затем CreatedAt desc; composition — только id;
hasContactOverride вычислен по наличию шифрованных значений; контакты не расшифрованы; Meta.Total
отражает всё под фильтром.
- Given
includeArchived = true, When GET, Then в выборку включены и архивные шаблоны.
- Given
Size вне [1..20] или Page < 1, When GET, Then ValidationError (400).
- Given у инвестора нет шаблонов, When
GET, Then 200 с пустым Items.
- Given
ContractVersion состава < актуальной, When GET, Then lazy-миграция Composition в
памяти (additive), без ошибки.
- Given
TalentSync / заблокирован / UserType != Investor, When GET, Then
PROFILE_MANAGED_EXTERNALLY (403) / PROFILE_USER_BLOCKED (403) /
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403) соответственно.
- Given профиль ещё не создан консюмером identity, When
GET, Then PROFILE_NOT_FOUND (404),
retry с backoff.
- Given любой успешный ответ, Then значения контактов-оверрайдов наружу не отдаются и не логируются;
record-level гарантирован фильтром
OwnerUserId.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы