PUT /api/v1/profile/requisite-templates/{templateId} — Изменение шаблона реквизитов (updateRequisiteTemplate)
Назначение
Инвестор изменяет активный шаблон реквизитов: переименовывает его, меняет состав документов или задаёт
контакты-оверрайды для документов сделки. Состав хранит только идентификаторы документов персоны
(открыто), контакты — шифрованные оверрайды, при изменении перешифровываются текущим ключом. Флаг «по
умолчанию» (IsDefault) этим методом не меняется — для него отдельная операция
set-default-template.md; персона (PersonId) и статус в этом методе неизменны
(архив — archive-requisite-template.md). Менеджеру застройщика операция
запрещена (INV-PRF-2/INV-IDN-001). На выходе — обновлённый шаблон.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | requisites |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/update-requisite-template.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT), UserType = Investor |
| Auth policy / Permissions | authenticated; permission profile.requisites.manage-self |
| Scope (RBAC) | self |
| Record-level | RequisiteTemplateEntity.OwnerUserId = UserId (sub); чужой templateId → 404 |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | update: фиксируется автор изменения, растёт версия шаблона; доменное событие RequisiteTemplateChangedEvent (имена изменённых полей, без значений) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/profile/requisite-templates/{templateId} |
| Success status | 200 |
| Idempotency header | не применяется (PUT идемпотентен по значению) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса (body UpdateRequisiteTemplateForm)
Partial-PUT: поле, отсутствующее в body, не меняется (name?, composition?); соответствующее
изменение (переименование / смена состава / установка контактов) применяется только для присутствующих
полей. Контакты меняются либо явными contactEmail/contactPhone (перешифровка), либо
clearContacts = true (обнуление); эти варианты взаимоисключающие.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
templateId | route | Guid | Да | NotEmpty; активный шаблон владельца | чужой → 404 |
name | body | string? | Нет | 1–128; при отличии от текущего — Rename (INV-RQT-4); отсутствует → имя не меняется | не PII |
composition | body | RequisiteTemplateComposition? (typed) | Нет | модель — дом requisite-template.md; отсутствует → состав не меняется | id (не PII) |
composition.identityDocumentId | body | Guid | Да (если composition задан) | NotEmpty; активный документ паспортного типа персоны (INV-RQT-2) | — |
composition.innDocumentId | body | Guid? | Нет | активный документ Type = Inn персоны (INV-RQT-2) | — |
composition.additionalDocumentIds | body | List<Guid> | Да (может быть пустым) | ≤ 10; без дублей и без повтора identity/inn id | — |
contactEmail | body | string? | Нет | формат email, ≤ 256 (plaintext до шифрования); запрещён при clearContacts = true | PII → шифруется |
contactPhone | body | string? | Нет | E.164, ≤ 32 (plaintext до шифрования); запрещён при clearContacts = true | PII → шифруется |
clearContacts | body | bool? | Нет | true → обнулить оба cipher-поля; несовместим с contactEmail/contactPhone | — |
version | body | long | Да | = текущей RequisiteTemplateEntity.Version (optimistic-предикат UI) | — |
Модель ответа RequisiteTemplateModel
Дом модели — list-requisite-templates.md; возвращается обновлённый шаблон
(version — новая; контакты наружу — маской, состав — id).
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец шаблона,
UserIdиз токена); вне tenant; требуется permissionprofile.requisites.manage-self. - Проверка входа (plaintext до шифрования):
templateId/versionне пусты;name— длина;composition—identityDocumentIdне пуст,additionalDocumentIds≤ 10 без дублей;contactEmail/contactPhone— формат; варианты «явные контакты» иclearContacts = trueвзаимоисключающи. При нарушении —ValidationError. Существование/типы/состояние — шаги 3–4. - Загрузка и предусловия. Найти профиль владельца; при отсутствии —
PROFILE_NOT_FOUND. Проверить гварды профиля (INV-PRF-2..4): внешнее управление контактами →PROFILE_MANAGED_EXTERNALLY, неактивная учётка →PROFILE_USER_BLOCKED, не инвестор →PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE. Найти шаблон; если его нет или он не принадлежит владельцу —PROFILE_TEMPLATE_NOT_FOUND; если шаблон в архиве —PROFILE_TEMPLATE_ARCHIVED; если версия из запроса не совпадает —PROFILE_CONCURRENCY_CONFLICT. Если заданcomposition— загрузить персону шаблона с её документами (для сверки владения, типов и статусов документов). - Бизнес-правила и маппинг. Собрать список изменённых полей:
- Переименование (если
nameзадан и отличается от текущего без учёта регистра): проверить уникальность среди активных шаблонов владельца; если имя занято —PROFILE_TEMPLATE_NAME_TAKEN(INV-RQT-4). - Смена состава (если
compositionзадан), по документам персоны в памяти: все документы состава принадлежат персоне владельца (INV-RQT-1) — иначеPROFILE_TEMPLATE_FOREIGN_REFERENCE;identityDocumentId— паспортного типа,innDocumentId—Inn(INV-RQT-2) — иначеPROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE; все документы состава и сама персона активны (INV-RQT-3) — иначеPROFILE_TEMPLATE_REFERENCE_ARCHIVED. Новый состав — типизированная модельRequisiteTemplateComposition(ContractVersion = 1). - Контакты: при
clearContacts = trueоба cipher-поля обнуляются; иначе при заданныхcontactEmail/contactPhoneоба контакта перешифровываются текущим ключом (единый ключ записи). - Флаг «по умолчанию» не меняется. Растёт версия шаблона, фиксируется автор правки; доменное событие
RequisiteTemplateChangedEvent(только имена изменённых полей, без значений).
- Переименование (если
- Что меняется. Шаблон обновляется в одном согласованном изменении; уникальный индекс имени страхует
гонку переименования. Если версия из запроса не совпадает с текущей —
PROFILE_CONCURRENCY_CONFLICT. - События. Только доменное
RequisiteTemplateChangedEvent(в процессе при коммите). Kafka-события не публикуются (simplicity-first, ADR-0056; PII контактов в Kafka не выносится — CRM берёт снапшот синхронно через internal API). - Результат. Обновлённый шаблон (контакты — маской,
version— новый), HTTP 200. Ранее выданные CRM снапшоты реквизитов не меняются (иммутабельные копии, владениеcrm).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | нет — Kafka не публикуется (ADR-0056); только доменное RequisiteTemplateChangedEvent (в процессе) |
| Внешние вызовы | провайдер ключей шифрования PII (только при смене контактов) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | Длины/формат/дубли id/взаимоисключение контактов | RestApiValidationErrorResponse | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан | «Профиль ещё создаётся» | retry с backoff |
PROFILE_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | Контакты управляются внешней системой (INV-PRF-2) | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | не Investor | «Доступно только инвесторам» | no retry |
PROFILE_TEMPLATE_NOT_FOUND | 404 | Нет шаблона / чужой | «Шаблон не найден» | no retry |
PROFILE_TEMPLATE_ARCHIVED | 409 | Status = Archived | «Шаблон в архиве — сначала восстановите» | no retry |
PROFILE_TEMPLATE_FOREIGN_REFERENCE | 400 | Документ состава не принадлежит персоне владельца (INV-RQT-1) | «Документ недоступен» | no retry |
PROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE | 400 | identity — не паспорт / inn — не Inn (INV-RQT-2) | «Неверный тип документа» | no retry |
PROFILE_TEMPLATE_REFERENCE_ARCHIVED | 409 | Документ/персона состава в архиве (INV-RQT-3) | «Документ в архиве» | no retry |
PROFILE_TEMPLATE_NAME_TAKEN | 409 | Имя занято активным шаблоном (INV-RQT-4) | «Шаблон с таким именем уже есть» | no retry |
PROFILE_CONCURRENCY_CONFLICT | 409 | Гонка версий (version) | «Данные изменены, повторите» | retry с новыми данными |
PROFILE_CRYPTO_UNAVAILABLE | 503 | Ключи PersonalDataCrypto недоступны | «Сервис временно недоступен» | retry |
Совместимость и наблюдаемость
- Additive-расширение формы;
composition— typed jsonb, versionedContractVersion(дом requisite-template.md);PersonId/isDefaultв форму не входят осознанно (иммутабельны здесь) — их добавление breaking. - Logs:
UserId,TemplateId,PersonId,changedFields(имена, без значений; контакты не логируются); metrics:profile_api_operations_total{operation="updateRequisiteTemplate",result}; traces — OTel.
Acceptance criteria
- Дано активный шаблон владельца с верной
version; когда PUT с частью полей (name/composition/контакты); тогда присутствующие поля меняются (отсутствующие — нет), контакты перешифрованы либо обнулены (clearContacts),IsDefault/PersonId/статус не меняются, растёт версия, поднятоRequisiteTemplateChangedEvent; Kafka не публикуется; HTTP200. - Дано одновременно заданы
contactEmail/contactPhoneиclearContacts = true; когда PUT; тогдаValidationError(400) — варианты взаимоисключающие. - Дано
nameсовпадает (без учёта регистра) с именем другого активного шаблона владельца; когда PUT; тогдаPROFILE_TEMPLATE_NAME_TAKEN(409, INV-RQT-4). - Дано документ состава не принадлежит персоне владельца; когда PUT; тогда
PROFILE_TEMPLATE_FOREIGN_REFERENCE(400, INV-RQT-1). - Дано
identityDocumentIdне паспортного типа илиinnDocumentIdнеInn; когда PUT; тогдаPROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE(400, INV-RQT-2). - Дано документ или персона состава в архиве; когда PUT; тогда
PROFILE_TEMPLATE_REFERENCE_ARCHIVED(409, INV-RQT-3). - Дано шаблон в архиве; когда PUT; тогда
PROFILE_TEMPLATE_ARCHIVED(409). - Дано
versionиз запроса не совпадает с текущей; когда PUT; тогдаPROFILE_CONCURRENCY_CONFLICT(409). - Дано шаблон ранее выдал снапшот реквизитов CRM; когда шаблон изменён; тогда выданные снапшоты не меняются (иммутабельные копии, владение
crm).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): GET /api/v1/profile/requisite-templates — Список шаблонов реквизитов (listRequisiteTemplates), POST /api/v1/profile/persons/{personId}/documents/{documentId}/archive — Архивация документа персоны (archivePersonDocument), POST /api/v1/profile/requisite-templates — Создание шаблона реквизитов (createRequisiteTemplate), Profile API — реестр операций областей public / internal / admin
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: requisite-template.md (переименование / смена состава /
контакты, INV-RQT-1..4,
RequisiteTemplateChangedEvent); документы состава — person-document.md; персона — person.md; guard-ы профиля — profile.md. - Операции: create-requisite-template.md,
set-default-template.md,
archive-requisite-template.md,
list-requisite-templates.md (дом
RequisiteTemplateModel).