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

UC-PRF-004. Создание шаблона реквизитов и его использование в бронировании (CRM)

Назначение

Создание именованного переиспользуемого набора реквизитов: контакты, реквизиты и документы сохраняются как шаблон и переиспользуются при бронировании и оформлении сделок. Шаблон связывает персону с её документами и контактами-оверрайдами, назначается шаблоном по умолчанию, а при оформлении брони контекст crm синхронно получает иммутабельный расшифрованный снапшот через internal API profile. Связан с контекстами profile (владелец) и crm (потребитель).

Metadata

ПолеЗначение
IDUC-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)

Основной Поток

  1. Инвестор нажимает «Создать шаблон реквизитов», выбирает персону, документ-удостоверение, ИНН, дополнительные документы и (опционально) контакты для документов сделки; вводит имя («Реквизиты жены»). Фронт вызывает 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) }.
  2. Создание шаблона.
    • Проверить вход до шифрования: длины, формат 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 }.
  3. Позже инвестор бронирует объект недвижимости: на форме бронирования (фронт каталога) выбирает шаблон из listRequisiteTemplates (сортировка: default первым, затем по LastUsedAt desc) и отправляет заявку на бронь в crm (createBooking — воронка брони: docs/06-services/crm/, решение №7 technical-assignment.md) с requisiteTemplateId.
  4. crm (booking-обработчик) вызывает internal API profile: getRequisiteSnapshot (POST /profile/internal/requisite-snapshots) с GetRequisiteSnapshotForm { templateId (Guid), ownerUserId (Guid — инициатор сделки), consumerService ("crm"), dealReference? (строка ≤ 64 — id брони/сделки для аудита) }, m2m-аутентификация.
  5. Выдача снапшота реквизитов (операция мутирует 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?/AdditionalDocumentsDocumentSnapshot { 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.
  6. crm копирует снапшот в свою сущность документов брони/сделки (владение копией — crm; source of truth снапшота после выдачи — crm, ../domain/requisite-template.md); дальнейшая жизнь брони — сценарии crm (таймеры резерва/брони, программа бронирования).
  7. Фронт брони показывает предзаполненные реквизиты; изменение шаблона в profile не влияет на уже созданные брони/сделки (иммутабельность копии).

Альтернативные Потоки

A1. Изменение состава шаблона / смена default

  1. updateRequisiteTemplate (PUT /profile/requisite-templates/{id}): перевалидация INV-RQT-1..4 на новый состав; контакты перешифровываются текущим ключом; RequisiteTemplateChangedEvent (имена изменённых полей).
  2. setDefaultTemplate (POST .../{id}/set-default): снятие флага с прежнего default, установка нового и синхронизация Profile.DefaultRequisiteTemplateId — в одном согласованном изменении (INV-RQT-5, INV-PRF-5). Уже выданные снапшоты в CRM не затрагиваются. Возврат к шагу 3.

A2. У инвестора нет шаблона (первая бронь — «форма неполного профиля»)

  1. Форма брони принимает ФИО + телефон (+опциональное отчество) вручную; crm создаёт бронь без снапшота profile (fallback — README profile, зависимость crm).
  2. Фронт предлагает после брони сохранить введённые данные как персону + шаблон (переход в UC-PRF-001/002 → шаг 1 данного UC). Сценарий продолжается на стороне crm.

A3. Архивный шаблон или архивный документ в составе

  1. getRequisiteSnapshot возвращает PROFILE_TEMPLATE_ARCHIVED (INV-RQT-8) либо PROFILE_TEMPLATE_REFERENCE_ARCHIVED (документ состава архивирован после создания шаблона).
  2. crm показывает инвестору просьбу обновить шаблон в ЛК; после правки (A1) — повтор шага 4.

A4. Архив шаблона владельцем

  1. archiveRequisiteTemplate: Status = Archived, DeletedAt = now; если шаблон был default — Profile.DefaultRequisiteTemplateId → null в том же согласованном изменении (правило удаления в ../domain/profile.md).
  2. Ранее выданные снапшоты в CRM не затрагиваются; персона и документы становятся доступными для архива (снимаются блокировки INV-PRS-4/INV-DOC-6). Сценарий завершается.

A5. Недоступность profile при бронировании

  1. Internal API недоступен → crm использует fallback ручного ввода реквизитов (A2) или повторяет запрос снапшота реквизитов; бронь не блокируется жёстко.

A6. Конкурентные выдачи снапшота одного шаблона

  1. Две параллельные брони (или ретрай CRM) запрашивают снапшот одного шаблона: обе получают валидный снапшот — выдача не идемпотентна и намеренно повторяема; каждая порождает RequisiteTemplateUsedEvent и инкремент UsageCount.
  2. LastUsedAt/UsageCount статистические: возможная потеря части конкурентных инкрементов некритична. На корректность выдачи и на копии в crm это не влияет — каждая бронь получает собственную иммутабельную копию снапшота.

Постусловие

  • Создан активный RequisiteTemplateEntity: Composition (plaintext jsonb, только id), контакты-оверрайды — шифротекст; при isDefault — единственный default владельца и Profile.DefaultRequisiteTemplateId синхронизирован.
  • После брони: LastUsedAt/UsageCount обновлены; каждая выдача зафиксирована RequisiteTemplateUsedEvent; crm владеет иммутабельной копией снапшота в документах брони/сделки.
  • PII не публиковалась в Kafka; расшифрованные данные передавались только синхронным internal API.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400Имя/email/E.164/лимиты списковПодсветить поля
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403Не инвестор (INV-RQT-6)Скрыть раздел
PROFILE_MANAGED_EXTERNALLY403TalentSync (INV-PRF-2)UC-PRF-005
PROFILE_TEMPLATE_NAME_TAKEN409Дубль имени (INV-RQT-4)Предложить другое имя
PROFILE_TEMPLATE_FOREIGN_REFERENCE422Персона/документ не принадлежат владельцу (INV-RQT-1)Перечитать списки
PROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE422Не-паспортный identity-документ / не-Inn (INV-RQT-2)Подсветить выбор
PROFILE_TEMPLATE_REFERENCE_ARCHIVED409Архивная персона/документ в составе (INV-RQT-3)Обновить состав (A3)
PROFILE_TEMPLATES_LIMIT_EXCEEDED409≥ 20 активных шаблонов (INV-RQT-7)Предложить архив
PROFILE_TEMPLATE_DEFAULT_CONFLICT409Гонка SetDefault (INV-RQT-5)Retry
PROFILE_TEMPLATE_ARCHIVED409Снапшот по архивному шаблону (INV-RQT-8)CRM: просьба обновить шаблон
404 NOT_FOUND404Чужой/несуществующий шаблон (в т.ч. несовпадение ownerUserId в internal-запросе)Обновить список / CRM fallback

Доменные События

СобытиеКогда поднимаетсяSubscribers
RequisiteTemplateCreatedEventШаг 2in-proc аудит
RequisiteTemplateChangedEventA1 (состав/имя/контакты/default)in-proc аудит
RequisiteTemplateUsedEventКаждая выдача снапшота CRM (шаг 5)in-proc аудит (обязательный)
RequisiteTemplateArchivedEventA4in-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
AC8Kafka не содержит событий с данными шаблоновконтракт-проверка топиков

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

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

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

  • 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/.