POST /api/v1/profile/requisite-templates — Создание шаблона реквизитов (createRequisiteTemplate)
Назначение
Создать именованный переиспользуемый набор реквизитов для оформления брони и сделки CRM: «контакты,
реквизиты, документы как шаблон переиспользовать в бронировании, в оформлении сделок». Шаблон связывает
персону (person.md) с конкретными её документами
(person-document.md) — удостоверение личности, опц. ИНН, доп. документы —
и опциональными контактами-оверрайдами для документов сделки. Состав (Composition) хранится открыто
(только id — не PII), контакты-оверрайды хранятся зашифрованными. Операцию вызывает инвестор из ЛК.
Позже crm синхронно получает иммутабельный расшифрованный снапшот через internal API
(get-requisite-snapshot.md); события шаблонов в шину не
публикуются (ADR-0056, PII не выносится). Сценарий — UC-PRF-004.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | requisites |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/create-requisite-template.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT), UserType = Investor (INV-RQT-6) |
| Auth policy / Permissions | authenticated; permission profile.requisites.manage-self |
| Scope (RBAC) | self |
| Record-level | OwnerUserId = UserId из sub (из body не принимается); чужие personId/документы → PROFILE_TEMPLATE_FOREIGN_REFERENCE без раскрытия существования |
| Tenant isolation | не применяется (ADR-0052: инвестор вне tenant) |
| Audit | create: CreatedByUserId = UserId, Version = 1, событие RequisiteTemplateCreatedEvent (+ RequisiteTemplateChangedEvent при SetDefault) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/profile/requisite-templates |
| Success status | 201 + Location: /api/v1/profile/requisite-templates/{id} |
| Idempotency header | Idempotency-Key (обязателен; двойной сабмит формы дополнительно гасится уникальностью имени INV-RQT-4) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса (body CreateRequisiteTemplateForm)
Валидация формата/лимитов выполняется до шифрования, на исходных значениях. Модели полей документов
и типы — дом person-document.md; состав — RequisiteTemplateComposition
(requisite-template.md), здесь не переописываются.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
name | body | string | Да | 1–128 символов, не пустое после trim; уникально среди активных шаблонов владельца, case-insensitive (INV-RQT-4) | не PII (имя в UI) |
personId | body | Guid | Да | NotEmpty; активная персона владельца (INV-RQT-1/3) | чужая → PROFILE_TEMPLATE_FOREIGN_REFERENCE |
composition.identityDocumentId | body | Guid | Да | NotEmpty; активный документ персоны паспортного типа {RfPassport, RbPassport, KzPassport, CisPassport, ForeignPassport} (INV-RQT-2) | — |
composition.innDocumentId | body | Guid? | Нет | если задан — активный документ персоны Type = Inn (INV-RQT-2) | — |
composition.additionalDocumentIds | body | List<Guid> | Да (может быть пустым) | ≤ 10 элементов; без дублей; без повтора identityDocumentId/innDocumentId; каждый — активный документ этой персоны | — |
contactEmail | body | string? | Нет | формат email, ≤ 256 символов (валидация до шифрования) | PII → ContactEmailCipher, не логируется, наружу не отдаётся |
contactPhone | body | string? | Нет | E.164 (^\+[1-9]\d{9,14}$), ≤ 32 символов | PII → ContactPhoneCipher, не отдаётся |
isDefault | body | bool? | Нет | default false; true — шаблон по умолчанию (SetDefault-логика, INV-RQT-5) | — |
Модель ответа RequisiteTemplateModel
Дом проекции — list-requisite-templates.md; значения контактов наружу не отдаются — только признак наличия. Проекция агрегата requisite-template.md.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
id | Guid | Да | RequisiteTemplateEntity.Id | — | Идентификатор шаблона. |
name | string | Да | Name | — | Имя шаблона в UI. |
personId | Guid | Да | PersonId | — | Персона, на которую оформляются документы сделки. |
status | RequisiteTemplateStatus | Да | Status | — | Enum строкой; при создании Active. |
isDefault | bool | Да | IsDefault | — | Шаблон по умолчанию для формы бронирования. |
composition | RequisiteTemplateComposition | Да | Composition | — | Container-модель: identityDocumentId, innDocumentId?, additionalDocumentIds — только id (не PII, отдаётся открыто). |
hasContactEmail | bool | Да | computed (ContactEmailCipher != null) | значение исключено | Признак заданного контактного email-оверрайда. |
hasContactPhone | bool | Да | computed (ContactPhoneCipher != null) | значение исключено | Признак заданного контактного телефона-оверрайда. |
lastUsedAt | DateTimeOffset? | Нет | LastUsedAt | — | При создании null. |
usageCount | int | Да | UsageCount | — | При создании 0. |
createdAt | DateTimeOffset | Да | CreatedAt | — | Момент создания. |
updatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент последнего изменения. |
version | long | Да | Version | — | При создании 1. |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (актор
User,UserIdиз токена); tenant не применяется (инвестор вне tenant, ADR-0052); требуется правоprofile.requisites.manage-self. Повторный запрос с тем жеIdempotency-Keyвозвращает ранее сохранённый результат без повторного создания. - Проверка входа.
name— 1–128 символов, не пустое после trim;personId/identityDocumentIdзаданы;additionalDocumentIds— ≤ 10, без дублей и без пересечения сidentityDocumentId/innDocumentId; форматcontactEmail/contactPhone(E.164) — на исходных значениях до шифрования. Нарушение →ValidationError. - Проверка предусловий.
- Профиль владельца существует; иначе
PROFILE_NOT_FOUND. Профиль активен (Status = Active, иначеPROFILE_USER_BLOCKED, INV-PRF-3) и в режиме самоуправления (ContactsManagedBy = SelfService, иначеPROFILE_MANAGED_EXTERNALLY, INV-PRF-2 — менеджеры застройщика управляются внешней системой); тип пользователя —Investor(иначеPROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE, INV-RQT-6/INV-PRF-4). - Лимит: активных шаблонов владельца < 20; иначе
PROFILE_TEMPLATES_LIMIT_EXCEEDED(INV-RQT-7). - Уникальность имени: среди активных шаблонов владельца нет шаблона с тем же именем (case-insensitive);
иначе
PROFILE_TEMPLATE_NAME_TAKEN(INV-RQT-4). - Персона
personIdпринадлежит владельцу; чужая →PROFILE_TEMPLATE_FOREIGN_REFERENCE(INV-RQT-1, без подтверждения существования чужой персоны); персона активна, иначеPROFILE_TEMPLATE_REFERENCE_ARCHIVED(INV-RQT-3).
- Профиль владельца существует; иначе
- Бизнес-правила и маппинг. Сверить состав по документам персоны: каждый
*DocumentId(identity + inn? + additional) принадлежит персоне — иначеPROFILE_TEMPLATE_FOREIGN_REFERENCE(INV-RQT-1);identityDocumentId— паспортного типа,innDocumentId— типаInn— иначеPROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE(INV-RQT-2); все документы состава активны — иначеPROFILE_TEMPLATE_REFERENCE_ARCHIVED(INV-RQT-3). Контакты-оверрайды шифруются единымKeyVersionзаписи; в состав пишутся только id (не PII). Создаётся шаблон:Status = Active,IsDefault = false,UsageCount = 0,Version = 1; фиксируется фактRequisiteTemplateCreatedEvent. ПриisDefault = trueприменяется логика SetDefault: снять флаг с прежнего default-шаблона, пометить новый как default, синхронизироватьProfile.DefaultRequisiteTemplateId; фиксируетсяRequisiteTemplateChangedEvent(INV-RQT-5 + INV-PRF-5). - Согласованность. В одном согласованном изменении сохраняются шаблон и (при
isDefault = true) прежний default и профиль. Гонку имени страхует уникальность имени владельца (конфликт →PROFILE_TEMPLATE_NAME_TAKEN); гонку default — уникальность default владельца (конфликт →PROFILE_TEMPLATE_DEFAULT_CONFLICT, retry). - Результат. Модель шаблона
RequisiteTemplateModel; контакты наружу не отдаются — толькоhasContactEmail/hasContactPhone;composition— только id. HTTP201+Location: /api/v1/profile/requisite-templates/{id}.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Внешние вызовы | провайдер ключей шифрования (хранилище секретов) для шифрования контактов-оверрайдов; недоступность — fail-closed PROFILE_CRYPTO_UNAVAILABLE |
| Проекции | ProfileEntity.DefaultRequisiteTemplateId при isDefault = true (та же транзакция) |
Интеграционные события в шину не публикуются (ADR-0056, PII не выносится); CRM получает снапшот синхронно через internal API.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | без тела | re-login |
ValidationError | 400 | Формат/длины/лимиты списков/email/E.164 | перечень невалидных полей | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан консюмером identity | «Профиль ещё создаётся» | retry с backoff |
PROFILE_USER_BLOCKED | 400 | Status != Active (INV-PRF-3) | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | ContactsManagedBy = TalentSync (INV-PRF-2) | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | UserType != Investor (INV-RQT-6) | «Доступно только инвесторам» | no retry |
PROFILE_TEMPLATE_FOREIGN_REFERENCE | 400 | Персона/документ не принадлежат владельцу (INV-RQT-1) | «Проверьте выбор персоны и документов» | no retry |
PROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE | 400 | Identity-документ не паспортного типа / innDocumentId не 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_TEMPLATES_LIMIT_EXCEEDED | 409 | ≥ 20 активных шаблонов (INV-RQT-7) | «Достигнут лимит шаблонов (20)» | no retry |
PROFILE_TEMPLATE_DEFAULT_CONFLICT | 409 | Гонка SetDefault (INV-RQT-5, конфликт уникальности default) | «Повторите попытку» | retry |
PROFILE_CRYPTO_UNAVAILABLE | 503 | Ключи шифрования недоступны | «Сервис временно недоступен» | retry |
Совместимость и наблюдаемость
- Форма расширяется только optional-полями (additive);
composition— versioned (ContractVersion), рост версии additive; удаление/переименование/смена типа поля — breaking (migration + rollout). Значения контактов-оверрайдов никогда не появляются в логах/метриках/ответе (толькоhasContact*). - Logs:
UserId,TemplateId,PersonId,IsDefault(без значений контактов); metrics:profile_api_operations_total{operation="createRequisiteTemplate",result},profile_requisite_templates_created_total,profile_crypto_errors_total; traces — OTel; dashboardprofile-overview.
Acceptance Criteria
- Given инвестор с активной персоной и валидным удостоверением, When
POSTс корректнымиname/composition, Then201+Location; шаблонActive,IsDefault = false,UsageCount = 0,Version = 1; контакты-оверрайды зашифрованы;compositionхранит только id; событие в шину не публикуется (PII не выносится, ADR-0056). - Given
isDefault = true, WhenPOST, Then новый шаблон — default; флаг снят с прежнего default;Profile.DefaultRequisiteTemplateIdсинхронизирован в той же транзакции (INV-RQT-5, INV-PRF-5). - Given имя дублирует активный шаблон владельца (case-insensitive), When
POST, ThenPROFILE_TEMPLATE_NAME_TAKEN(409) — INV-RQT-4. - Given 20 активных шаблонов, When
POST, ThenPROFILE_TEMPLATES_LIMIT_EXCEEDED(409) — INV-RQT-7. - Given
personIdили документ состава принадлежат другому владельцу, WhenPOST, ThenPROFILE_TEMPLATE_FOREIGN_REFERENCE(400) без раскрытия существования — INV-RQT-1. - Given
identityDocumentIdне паспортного типа илиinnDocumentIdнеInn, WhenPOST, ThenPROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE(400) — INV-RQT-2. - Given персона или документ состава в архиве, When
POST, ThenPROFILE_TEMPLATE_REFERENCE_ARCHIVED(409) — INV-RQT-3. - Given
additionalDocumentIdsс дублями / > 10 / пересечением с identity или inn / некорректныйcontactEmail/contactPhoneне E.164, WhenPOST, ThenValidationError(400) на исходных значениях до шифрования. - Given ключи шифрования недоступны, When
POST, ThenPROFILE_CRYPTO_UNAVAILABLE(503), fail-closed. - Given конкурентный
SetDefault, When дваPOSTсisDefault = true, Then один побеждает, второй —PROFILE_TEMPLATE_DEFAULT_CONFLICT(409), retry. - Given повтор с тем же
Idempotency-Key, WhenPOST, Then возвращается ранее сохранённый результат без дубля. - Given
UserType != Investor/ заблокирован /TalentSync, WhenPOST, ThenPROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE(403) /PROFILE_USER_BLOCKED(400) /PROFILE_MANAGED_EXTERNALLY(403) соответственно. - Given любой успешный ответ, Then значения контактов наружу не отдаются — только
hasContactEmail/hasContactPhone;composition— только id.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): GET /api/v1/profile/requisite-templates — Список шаблонов реквизитов (listRequisiteTemplates), POST /api/v1/profile/requisite-templates/{templateId}/set-default — Шаблон по умолчанию (setDefaultTemplate), Profile API — реестр операций областей public / internal / admin, PUT /api/v1/profile/requisite-templates/{templateId} — Изменение шаблона реквизитов (updateRequisiteTemplate)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: requisite-template.md (INV-RQT-1..8,
RequisiteTemplateComposition,SetDefault); person.md, person-document.md (типы документов); guard-ы — profile.md (INV-PRF-2..5,DefaultRequisiteTemplateId). - Сценарий: UC-PRF-004; потребитель снапшота — get-requisite-snapshot.md.
- Соседние операции: update-requisite-template.md, set-default-template.md, archive-requisite-template.md.