UC-PRF-004. Создание шаблона реквизитов и его использование в бронировании (CRM)
Назначение
Создание именованного переиспользуемого набора реквизитов: контакты, реквизиты и документы сохраняются
как шаблон и переиспользуются при бронировании и оформлении сделок. Шаблон связывает персону с её
документами и контактами-оверрайдами, назначается шаблоном по умолчанию, а при оформлении брони контекст
crm синхронно получает иммутабельный расшифрованный снапшот через internal API profile.
Связан с контекстами profile (владелец) и crm (потребитель).
Metadata
| Поле | Значение |
|---|---|
| ID | UC-PRF-004 |
| Контекст | profile (см. docs/06-services/profile/); потребитель — crm (booking) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор (создание/управление шаблонами — INV-RQT-6); m2m-сервис crm (запрос снапшота, internal API) |
| Permissions / scope | Инвестор: profile.requisites.manage-self (self-scope). CRM: m2m-credential internal-area (client-credentials), permission-проверка на стороне profile |
| Tenant | не tenanted (ADR-0052); record-level по OwnerUserId; internal-запрос обязан содержать OwnerUserId инициатора сделки — сверка владения |
| Record-level | только собственные шаблоны/персоны/документы владельца |
Предусловие
- Выполнены UC-PRF-001/002: есть активная персона (например,
Spouse) с активным паспортным документом (дляInnDocumentId— активный документType = Inn). - Активных шаблонов у владельца < 20 (INV-RQT-7).
- Для потока бронирования: инвестор выбрал объект недвижимости и открыл форму брони (вход в воронку — сценарии
crm, будущий документdocs/06-services/crm/use-cases/UC-BKG).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button / API call (m2m) |
| Триггерящий элемент | /profile/personal → «Создать шаблон реквизитов»; форма бронирования объекта недвижимости → выбор шаблона → «Забронировать» (CRM → internal API profile) |
Основной Поток
- Инвестор нажимает «Создать шаблон реквизитов», выбирает персону, документ-удостоверение, ИНН,
дополнительные документы и (опционально) контакты для документов сделки; вводит имя
(«Реквизиты жены»). Фронт вызывает
createRequisiteTemplate(POST /profile/requisite-templates) сCreateRequisiteTemplateForm { name (1..128), personId (Guid), identityDocumentId (Guid), innDocumentId? (Guid), additionalDocumentIds (List<Guid> ≤ 10, без дублей), contactEmail? (email, ≤256), contactPhone? (E.164, ≤32), isDefault (bool) }. - Создание шаблона.
- Проверить вход до шифрования: длины, формат email/E.164, лимит и уникальность id в списках →
ValidationError. - Проверить доступ по профилю (INV-PRF-2/3) и тип пользователя — инвестор (INV-RQT-6, иначе
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE). - Проверить лимит: активных шаблонов владельца < 20 (INV-RQT-7, иначе
PROFILE_TEMPLATES_LIMIT_EXCEEDED). - Проверить уникальность имени среди активных шаблонов владельца (без учёта регистра) — INV-RQT-4,
иначе
PROFILE_TEMPLATE_NAME_TAKEN(финальный арбитр — уникальный индексux_requisite_templates_owner_name). - Проверить состав: персона принадлежит владельцу (
person.OwnerUserId = UserId) и все*DocumentId— её документы (INV-RQT-1, иначеPROFILE_TEMPLATE_FOREIGN_REFERENCE);identityDocumentId— активный документ типа из{RfPassport, RbPassport, KzPassport, CisPassport, ForeignPassport},innDocumentId— активныйInn(INV-RQT-2, иначеPROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE); персона и все документы состава —Status = Active(INV-RQT-3, иначеPROFILE_TEMPLATE_REFERENCE_ARCHIVED). - Создать
RequisiteTemplateEntity(OwnerUserId = UserId,Name← форма,PersonId← форма):Composition = RequisiteTemplateComposition { ContractVersion: 1, IdentityDocumentId, InnDocumentId?, AdditionalDocumentIds }(plaintext jsonb — только id, не PII);ContactEmailCipher/ContactPhoneCipher— шифротекст актуальным ключом (приnull— null);KeyVersion= текущий;Status = Active;UsageCount = 0. Публикуется доменноеRequisiteTemplateCreatedEvent(Id, OwnerUserId, PersonId). - При
isDefault = true— в том же согласованном изменении снять флаг с прежнего default владельца (IsDefault → false), установитьtemplate.IsDefault = trueи синхронизироватьProfile.DefaultRequisiteTemplateId(INV-RQT-5 + INV-PRF-5; уникальный индексux_requisite_templates_owner_default— арбитр гонки →PROFILE_TEMPLATE_DEFAULT_CONFLICT, retry). - Kafka-события шаблонов не публикуются (ADR-0056: CRM тянет снапшот синхронно; событийная
репликация означала бы PII в Kafka). Результат —
RequisiteTemplateResult { id, name, personId, isDefault, composition }.
- Проверить вход до шифрования: длины, формат email/E.164, лимит и уникальность id в списках →
- Позже инвестор бронирует объект недвижимости: на форме бронирования (фронт каталога) выбирает шаблон из
listRequisiteTemplates(сортировка: default первым, затем поLastUsedAtdesc) и отправляет заявку на бронь вcrm(createBooking— воронка брони:docs/06-services/crm/, решение №7 technical-assignment.md) сrequisiteTemplateId. crm(booking-обработчик) вызывает internal APIprofile:getRequisiteSnapshot(POST /profile/internal/requisite-snapshots) сGetRequisiteSnapshotForm { templateId (Guid), ownerUserId (Guid — инициатор сделки), consumerService ("crm"), dealReference? (строка ≤ 64 — id брони/сделки для аудита) }, m2m-аутентификация.- Выдача снапшота реквизитов (операция мутирует
LastUsedAt/UsageCount; повтор запроса допустим —UsageCountстатистический, не финансовый).- Найти шаблон по
templateId; сверить владениеtemplate.OwnerUserId == form.ownerUserId(запрос чужого шаблона → 404 без утечки);Status = Active(INV-RQT-8, иначеPROFILE_TEMPLATE_ARCHIVED); профиль владельцаStatus = Active(INV-PRF-3). - Собрать
RequisiteSnapshotResult(модели полей — ../domain/person-document.md, здесь только состав):PersonId,PersonVersion, ФИО иBirthDate(расшифрованные),CitizenshipCountryId,Gender;IdentityDocument/InnDocument?/AdditionalDocuments—DocumentSnapshot { DocumentId, Type, CountryId, CountryIsoCode2 (юридический след), PayloadContractVersion, Payload (расшифрованный), Scans }по id изComposition(архивный документ в составе →PROFILE_TEMPLATE_REFERENCE_ARCHIVED, 409 — инвестор должен обновить шаблон);ContactEmail/ContactPhone— расшифрованные оверрайды, приnull— контакты учётки из репликиProfileEntity(ContactEmail,ContactPhone);TemplateId,TemplateVersion = template.Version,IssuedAt = now. - Зарегистрировать использование:
LastUsedAt = now,UsageCount + 1. ПубликуетсяRequisiteTemplateUsedEvent(Id, OwnerUserId, RequestedByService = "crm")— обязательный аудит каждой выдачи расшифрованных данных. - Результат —
RequisiteSnapshotResult— единственный канал выдачи расшифрованных реквизитов наружу; ответ по mTLS/внутренней сети, в логах — толькоTemplateId/PersonId.
- Найти шаблон по
crmкопирует снапшот в свою сущность документов брони/сделки (владение копией —crm; source of truth снапшота после выдачи —crm, ../domain/requisite-template.md); дальнейшая жизнь брони — сценарииcrm(таймеры резерва/брони, программа бронирования).- Фронт брони показывает предзаполненные реквизиты; изменение шаблона в profile не влияет на уже созданные брони/сделки (иммутабельность копии).
Альтернативные Потоки
A1. Изменение состава шаблона / смена default
updateRequisiteTemplate(PUT /profile/requisite-templates/{id}): перевалидация INV-RQT-1..4 на новый состав; контакты перешифровываются текущим ключом;RequisiteTemplateChangedEvent(имена изменённых полей).setDefaultTemplate(POST .../{id}/set-default): снятие флага с прежнего default, установка нового и синхронизацияProfile.DefaultRequisiteTemplateId— в одном согласованном изменении (INV-RQT-5, INV-PRF-5). Уже выданные снапшоты в CRM не затрагиваются. Возврат к шагу 3.
A2. У инвестора нет шаблона (первая бронь — «форма неполного профиля»)
- Форма брони принимает ФИО + телефон (+опциональное отчество) вручную;
crmсоздаёт бронь без снапшота profile (fallback — README profile, зависимостьcrm). - Фронт предлагает после брони сохранить введённые данные как персону + шаблон (переход в
UC-PRF-001/002 → шаг 1 данного UC). Сценарий продолжается на стороне
crm.
A3. Архивный шаблон или архивный документ в составе
getRequisiteSnapshotвозвращаетPROFILE_TEMPLATE_ARCHIVED(INV-RQT-8) либоPROFILE_TEMPLATE_REFERENCE_ARCHIVED(документ состава архивирован после создания шаблона).crmпоказывает инвестору просьбу обновить шаблон в ЛК; после правки (A1) — повтор шага 4.
A4. Архив шаблона владельцем
archiveRequisiteTemplate:Status = Archived,DeletedAt = now; если шаблон был default —Profile.DefaultRequisiteTemplateId → nullв том же согласованном изменении (правило удаления в ../domain/profile.md).- Ранее выданные снапшоты в CRM не затрагиваются; персона и документы становятся доступными для архива (снимаются блокировки INV-PRS-4/INV-DOC-6). Сценарий завершается.
A5. Недоступность profile при бронировании
- Internal API недоступен →
crmиспользует fallback ручного ввода реквизитов (A2) или повторяет запрос снапшота реквизитов; бронь не блокируется жёстко.
A6. Конкурентные выдачи снапшота одного шаблона
- Две параллельные брони (или ретрай CRM) запрашивают снапшот одного шаблона: обе получают валидный
снапшот — выдача не идемпотентна и намеренно повторяема; каждая порождает
RequisiteTemplateUsedEventи инкрементUsageCount. LastUsedAt/UsageCountстатистические: возможная потеря части конкурентных инкрементов некритична. На корректность выдачи и на копии вcrmэто не влияет — каждая бронь получает собственную иммутабельную копию снапшота.
Постусловие
- Создан активный
RequisiteTemplateEntity:Composition(plaintext jsonb, только id), контакты-оверрайды — шифротекст; приisDefault— единственный default владельца иProfile.DefaultRequisiteTemplateIdсинхронизирован. - После брони:
LastUsedAt/UsageCountобновлены; каждая выдача зафиксированаRequisiteTemplateUsedEvent;crmвладеет иммутабельной копией снапшота в документах брони/сделки. - PII не публиковалась в Kafka; расшифрованные данные передавались только синхронным internal API.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Имя/email/E.164/лимиты списков | Подсветить поля |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | Не инвестор (INV-RQT-6) | Скрыть раздел |
PROFILE_MANAGED_EXTERNALLY | 403 | TalentSync (INV-PRF-2) | UC-PRF-005 |
PROFILE_TEMPLATE_NAME_TAKEN | 409 | Дубль имени (INV-RQT-4) | Предложить другое имя |
PROFILE_TEMPLATE_FOREIGN_REFERENCE | 422 | Персона/документ не принадлежат владельцу (INV-RQT-1) | Перечитать списки |
PROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE | 422 | Не-паспортный identity-документ / не-Inn (INV-RQT-2) | Подсветить выбор |
PROFILE_TEMPLATE_REFERENCE_ARCHIVED | 409 | Архивная персона/документ в составе (INV-RQT-3) | Обновить состав (A3) |
PROFILE_TEMPLATES_LIMIT_EXCEEDED | 409 | ≥ 20 активных шаблонов (INV-RQT-7) | Предложить архив |
PROFILE_TEMPLATE_DEFAULT_CONFLICT | 409 | Гонка SetDefault (INV-RQT-5) | Retry |
PROFILE_TEMPLATE_ARCHIVED | 409 | Снапшот по архивному шаблону (INV-RQT-8) | CRM: просьба обновить шаблон |
404 NOT_FOUND | 404 | Чужой/несуществующий шаблон (в т.ч. несовпадение ownerUserId в internal-запросе) | Обновить список / CRM fallback |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
RequisiteTemplateCreatedEvent | Шаг 2 | in-proc аудит |
RequisiteTemplateChangedEvent | A1 (состав/имя/контакты/default) | in-proc аудит |
RequisiteTemplateUsedEvent | Каждая выдача снапшота CRM (шаг 5) | in-proc аудит (обязательный) |
RequisiteTemplateArchivedEvent | A4 | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
| — | — | Kafka-события шаблонов не публикуются (ADR-0056: снапшот — синхронный internal API; PII в Kafka запрещена) | — |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Шаблон создаётся только из персоны и документов владельца; чужие id отклоняются (INV-RQT-1) | тест с двумя пользователями |
| AC2 | У владельца не более одного default; SetDefault атомарно переключает флаг и Profile.DefaultRequisiteTemplateId | конкурентный тест INV-RQT-5 |
| AC3 | Снапшот выдаётся только m2m-актору crm с совпадающим ownerUserId; несовпадение → 404 | тест internal API |
| AC4 | Каждая выдача снапшота инкрементирует UsageCount и порождает RequisiteTemplateUsedEvent | тест аудита |
| AC5 | Правка/архив шаблона после выдачи не меняет копию снапшота в CRM | сквозной тест с crm-заглушкой |
| AC6 | Контакты-оверрайды в БД — только шифротекст; при их отсутствии снапшот берёт контакты учётки из реплики | SQL-просмотр + тест fallback |
| AC7 | Снапшот по архивному шаблону/документу отклоняется (INV-RQT-8, INV-RQT-3) | тест A3 |
| AC8 | Kafka не содержит событий с данными шаблонов | контракт-проверка топиков |
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): POST /api/v1/internal/profile/requisite-snapshot — Снапшот реквизитов для CRM (getRequisiteSnapshot + RegisterUsage), POST /api/v1/profile/requisite-templates — Создание шаблона реквизитов (createRequisiteTemplate)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (будущие):
../api/public/create-requisite-template.md,../api/public/update-requisite-template.md,../api/public/set-default-template.md,../api/public/archive-requisite-template.md, ../api/internal/get-requisite-snapshot.md (internal, m2m). - Domain entity: ../domain/requisite-template.md
(INV-RQT-1..8,
RequisiteTemplateComposition), ../domain/person.md, ../domain/person-document.md (моделиDocumentSnapshot/payload), ../domain/profile.md (INV-PRF-5,DefaultRequisiteTemplateId). - CRM-потребитель (будущие ТЗ):
docs/06-services/crm/README.md(booking, программы бронирования),docs/06-services/crm/use-cases/(UC-BKG — бронирование, две точки входа — решение №7). - Frontend: форма брони («Забронировать», опциональное отчество) — prototype-mapping.md.
- Связанные UC: UC-PRF-002 (документы — предусловие), UC-PRF-003 (шаблон как предзаполнение KYC),
сквозной UC-3 (бронь) —
docs/02-architecture/scenarios/.