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

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 areapublic
Feature grouprequisites
Статусdraft
Документdocs/06-services/profile/api/public/update-requisite-template.md

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.requisites.manage-self
Scope (RBAC)self
Record-levelRequisiteTemplateEntity.OwnerUserId = UserId (sub); чужой templateId → 404
Tenant isolationне применяется (ADR-0052)
Auditupdate: фиксируется автор изменения, растёт версия шаблона; доменное событие RequisiteTemplateChangedEvent (имена изменённых полей, без значений)

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/profile/requisite-templates/{templateId}
Success status200
Idempotency headerне применяется (PUT идемпотентен по значению)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса (body UpdateRequisiteTemplateForm)

Partial-PUT: поле, отсутствующее в body, не меняется (name?, composition?); соответствующее изменение (переименование / смена состава / установка контактов) применяется только для присутствующих полей. Контакты меняются либо явными contactEmail/contactPhone (перешифровка), либо clearContacts = true (обнуление); эти варианты взаимоисключающие.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
templateIdrouteGuidДаNotEmpty; активный шаблон владельцачужой → 404
namebodystring?Нет1–128; при отличии от текущего — Rename (INV-RQT-4); отсутствует → имя не меняетсяне PII
compositionbodyRequisiteTemplateComposition? (typed)Нетмодель — дом requisite-template.md; отсутствует → состав не меняетсяid (не PII)
composition.identityDocumentIdbodyGuidДа (если composition задан)NotEmpty; активный документ паспортного типа персоны (INV-RQT-2)
composition.innDocumentIdbodyGuid?Нетактивный документ Type = Inn персоны (INV-RQT-2)
composition.additionalDocumentIdsbodyList<Guid>Да (может быть пустым)≤ 10; без дублей и без повтора identity/inn id
contactEmailbodystring?Нетформат email, ≤ 256 (plaintext до шифрования); запрещён при clearContacts = truePII → шифруется
contactPhonebodystring?НетE.164, ≤ 32 (plaintext до шифрования); запрещён при clearContacts = truePII → шифруется
clearContactsbodybool?Нетtrue → обнулить оба cipher-поля; несовместим с contactEmail/contactPhone
versionbodylongДа= текущей RequisiteTemplateEntity.Version (optimistic-предикат UI)

Модель ответа RequisiteTemplateModel

Дом модели — list-requisite-templates.md; возвращается обновлённый шаблон (version — новая; контакты наружу — маской, состав — id).

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный инвестор (владелец шаблона, UserId из токена); вне tenant; требуется permission profile.requisites.manage-self.
  2. Проверка входа (plaintext до шифрования): templateId/version не пусты; name — длина; compositionidentityDocumentId не пуст, additionalDocumentIds ≤ 10 без дублей; contactEmail/contactPhone — формат; варианты «явные контакты» и clearContacts = true взаимоисключающи. При нарушении — ValidationError. Существование/типы/состояние — шаги 3–4.
  3. Загрузка и предусловия. Найти профиль владельца; при отсутствии — 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 — загрузить персону шаблона с её документами (для сверки владения, типов и статусов документов).
  4. Бизнес-правила и маппинг. Собрать список изменённых полей:
    • Переименование (если name задан и отличается от текущего без учёта регистра): проверить уникальность среди активных шаблонов владельца; если имя занято — PROFILE_TEMPLATE_NAME_TAKEN (INV-RQT-4).
    • Смена состава (если composition задан), по документам персоны в памяти: все документы состава принадлежат персоне владельца (INV-RQT-1) — иначе PROFILE_TEMPLATE_FOREIGN_REFERENCE; identityDocumentId — паспортного типа, innDocumentIdInn (INV-RQT-2) — иначе PROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE; все документы состава и сама персона активны (INV-RQT-3) — иначе PROFILE_TEMPLATE_REFERENCE_ARCHIVED. Новый состав — типизированная модель RequisiteTemplateComposition (ContractVersion = 1).
    • Контакты: при clearContacts = true оба cipher-поля обнуляются; иначе при заданных contactEmail/contactPhone оба контакта перешифровываются текущим ключом (единый ключ записи).
    • Флаг «по умолчанию» не меняется. Растёт версия шаблона, фиксируется автор правки; доменное событие RequisiteTemplateChangedEvent (только имена изменённых полей, без значений).
  5. Что меняется. Шаблон обновляется в одном согласованном изменении; уникальный индекс имени страхует гонку переименования. Если версия из запроса не совпадает с текущей — PROFILE_CONCURRENCY_CONFLICT.
  6. События. Только доменное RequisiteTemplateChangedEvent (в процессе при коммите). Kafka-события не публикуются (simplicity-first, ADR-0056; PII контактов в Kafka не выносится — CRM берёт снапшот синхронно через internal API).
  7. Результат. Обновлённый шаблон (контакты — маской, version — новый), HTTP 200. Ранее выданные CRM снапшоты реквизитов не меняются (иммутабельные копии, владение crm).

Диаграмма

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

ТипДетали
Интеграционные событиянет — Kafka не публикуется (ADR-0056); только доменное RequisiteTemplateChangedEvent (в процессе)
Внешние вызовыпровайдер ключей шифрования PII (только при смене контактов)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Длины/формат/дубли id/взаимоисключение контактовRestApiValidationErrorResponseno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry с backoff
PROFILE_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403Контакты управляются внешней системой (INV-PRF-2)«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor«Доступно только инвесторам»no retry
PROFILE_TEMPLATE_NOT_FOUND404Нет шаблона / чужой«Шаблон не найден»no retry
PROFILE_TEMPLATE_ARCHIVED409Status = Archived«Шаблон в архиве — сначала восстановите»no retry
PROFILE_TEMPLATE_FOREIGN_REFERENCE400Документ состава не принадлежит персоне владельца (INV-RQT-1)«Документ недоступен»no retry
PROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE400identity — не паспорт / inn — не Inn (INV-RQT-2)«Неверный тип документа»no retry
PROFILE_TEMPLATE_REFERENCE_ARCHIVED409Документ/персона состава в архиве (INV-RQT-3)«Документ в архиве»no retry
PROFILE_TEMPLATE_NAME_TAKEN409Имя занято активным шаблоном (INV-RQT-4)«Шаблон с таким именем уже есть»no retry
PROFILE_CONCURRENCY_CONFLICT409Гонка версий (version)«Данные изменены, повторите»retry с новыми данными
PROFILE_CRYPTO_UNAVAILABLE503Ключи PersonalDataCrypto недоступны«Сервис временно недоступен»retry

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

  • Additive-расширение формы; composition — typed jsonb, versioned ContractVersion (дом 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 не публикуется; HTTP 200.
  • Дано одновременно заданы 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).

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

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

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