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

POST /api/v1/profile/requisite-templates/{templateId}/set-default — Шаблон по умолчанию (setDefaultTemplate)

Назначение

Пометить активный шаблон реквизитов инвестора как «по умолчанию» (IsDefault = true) — для быстрого предзаполнения формы бронирования выбранными реквизитами (requisite-template.md, INV-RQT-5). Операция снимает флаг с прежнего default-шаблона и синхронизирует Profile.DefaultRequisiteTemplateId (profile.md, INV-PRF-5) — в одной транзакции, чтобы у владельца всегда был не более чем один default и проекция в профиле оставалась согласованной. Вызывает инвестор из ЛК.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.requisites.manage-self
Scope (RBAC)self
Record-levelOwnerUserId = UserId (sub); чужой templateId → 404
Tenant isolationне применяется (ADR-0052; инвестор вне tenant)
Auditupdate: IsDefault (прежний → false, текущий → true) + Profile.DefaultRequisiteTemplateId, Version++ на всех, UpdatedByUserId = UserId, событие RequisiteTemplateChangedEvent

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/profile/requisite-templates/{templateId}/set-default
Success status200
Idempotency headerне применяется (повтор для уже-default шаблона — идемпотентный no-op 200)
Correlationсквозной trace через заголовок traceparent

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

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

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

Дом модели — list-requisite-templates.md; поля-в-поле — из requisite-template.md, контакты — только маски. Возвращается целевой шаблон с isDefault = true и status = Active.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена); tenant не применяется; требуется право profile.requisites.manage-self.
  2. Проверка входа. templateId задан. Нарушение → ValidationError.
  3. Проверка предусловий. Профиль владельца существует; иначе PROFILE_NOT_FOUND (реплика identity ещё догоняется — retry). Профиль в режиме самоуправления (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). Шаблон templateId существует и принадлежит владельцу; иначе PROFILE_TEMPLATE_NOT_FOUND; шаблон активен (Status = Active), иначе PROFILE_TEMPLATE_ARCHIVED (default назначается только активному, INV-RQT-5). Определяется прежний default-шаблон владельца (может отсутствовать или совпадать с целевым).
  4. Бизнес-правила и переходы. Если целевой шаблон уже default — идемпотентный no-op (шаг 6, без изменений). Иначе (INV-RQT-5, SingleDefault): снять флаг с прежнего default (IsDefault = false, Version++), пометить целевой (IsDefault = true, Version++); кросс-агрегатная синхронизация — Profile.DefaultRequisiteTemplateId = templateId (Version++, INV-PRF-5); фиксируется факт RequisiteTemplateChangedEvent(changedFields: ["IsDefault"]).
  5. Согласованность. В одном согласованном изменении сохраняются оба шаблона и профиль; порядок — снять прежний флаг до установки нового, чтобы уложиться в требование единственного default. Гонка с параллельным set-default другого шаблона → PROFILE_TEMPLATE_DEFAULT_CONFLICT (retry); гонка по версии агрегата → PROFILE_CONCURRENCY_CONFLICT (retry).
  6. Результат. Модель RequisiteTemplateModel (целевой шаблон, isDefault = true, status = Active); HTTP 200.

Диаграмма

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

ТипДетали
ПроекцииProfile.DefaultRequisiteTemplateId обновляется в той же транзакции (INV-PRF-5)

Интеграционные события в шину не публикуются (ADR-0056; DefaultRequisiteTemplateId читается формой брони синхронно, PII в шину не выносится).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустой templateIdперечень невалидных полейno retry
PROFILE_NOT_FOUND404Профиль не создан (реплика identity отстаёт)«Профиль ещё создаётся»retry
PROFILE_USER_BLOCKED400Status != Active (INV-PRF-3)«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403ContactsManagedBy = TalentSync (INV-PRF-2)«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor (INV-PRF-4)«Доступно только инвесторам»no retry
PROFILE_TEMPLATE_NOT_FOUND404Нет шаблона / чужой«Шаблон реквизитов не найден»no retry
PROFILE_TEMPLATE_ARCHIVED409Status = Archived (INV-RQT-5)«Архивный шаблон нельзя сделать основным»после восстановления
PROFILE_TEMPLATE_DEFAULT_CONFLICT409Гонка default (INV-RQT-5/INV-PRF-5)«Данные изменены, повторите»retry
PROFILE_CONCURRENCY_CONFLICT409Гонка версии агрегата«Данные изменены, повторите»retry

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

  • Семантика стабильна; расширения RequisiteTemplateModel — additive (optional-поля без breaking change); удаление/переименование/смена типа — breaking (migration + rollout).
  • Logs: UserId, templateId, previousDefaultId; metrics: profile_api_operations_total{operation="setDefaultTemplate",result} (в т.ч. result=default_conflict), latency p99; traces — OTel; dashboard profile-overview.

Acceptance criteria

  • Дано активный шаблон владельца, не являющийся default, и другой шаблон-default; когда владелец делает целевой основным; тогда прежний default → IsDefault = false, целевой → true, Profile.DefaultRequisiteTemplateId = templateId — всё в одной транзакции; у владельца остаётся ровно один default; HTTP 200.
  • Дано у владельца нет ни одного default; когда делает шаблон основным; тогда целевой становится единственным default, Profile.DefaultRequisiteTemplateId заполняется (INV-PRF-5).
  • Дано целевой шаблон уже default; когда повтор set-default; тогда идемпотентный no-op 200, версии не растут.
  • Дано шаблон в Archived; когда set-default; тогда PROFILE_TEMPLATE_ARCHIVED (409) — default назначается только активному (INV-RQT-5).
  • Дано templateId чужой или не существует; когда set-default; тогда PROFILE_TEMPLATE_NOT_FOUND (404).
  • Дано два параллельных set-default разных шаблонов владельца; когда оба коммитятся; тогда один выигрывает, второй получает PROFILE_TEMPLATE_DEFAULT_CONFLICT / PROFILE_CONCURRENCY_CONFLICT (409) и повторяет — инвариант «≤ 1 default» сохраняется.
  • Дано профиль заблокирован / TalentSync / не инвестор; когда set-default; тогда соответствующий guard-отказ.

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

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

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