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

Entity / Aggregate: RequisiteTemplateEntity

Дом полей агрегата «Шаблон реквизитов» контекста profile. Снапшот реквизитов, который CRM копирует в сделку/бронирование, ссылается сюда и на person.md/person-document.md.

Назначение

RequisiteTemplateEntity — именованный переиспользуемый набор реквизитов для оформления бронирования и сделки: только инвестор управляет своими контактами, реквизитами и документами, чтобы затем переиспользовать их как шаблон при бронировании и оформлении сделок. Шаблон связывает персону (person.md) с конкретными её документами (какой паспорт — удостоверение личности, какой ИНН, какие дополнительные документы) и опциональными контактами для документов сделки.

Потребитель — контекст crm: при создании брони/сделки инвестор выбирает шаблон, CRM запрашивает иммутабельный снапшот расшифрованных реквизитов через internal API profile (будущий документ ../api/internal/get-requisite-snapshot.md) и хранит копию у себя (документы сделки — владение crm). profile события с PII не публикует.

Выдача снапшота и согласованность (кросс-сервисный контракт). Снапшот выдаётся crm синхронно и только по активному шаблону (INV-RQT-8); после выдачи копия иммутабельна и принадлежит crm — последующие правка/архив шаблона или изменение персоны на неё не влияют. Выдача не идемпотентна и намеренно повторяема: каждый вызов заново собирает снапшот, инкрементирует UsageCount, обновляет LastUsedAt и порождает обязательный аудит RequisiteTemplateUsedEvent (повтор — это новая выдача, не дубль). Конкурентные выдачи одного шаблона учитываются как увеличение счётчика; возможная потеря части инкрементов некритична — UsageCount/LastUsedAt статистические, не финансовые. Об изменении данных персоны, на которую ссылается шаблон, crm узнаёт из PersonUpserted (person-upserted.md) и сам решает, предлагать ли повторный запрос снапшота для незавершённой брони; шаблон при этом не переписывается, а ранее выданные копии остаются валидными.

Классификация

ПолеЗначение
Контекстprofile
KindAggregateRoot
Source of truthprofile (снапшоты в сделках CRM — копии на момент использования, source of truth снапшота — crm)
Таблицаprofile.requisite_templates
Tenant isolationНе tenanted (ADR-0052); record-level по OwnerUserId

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор шаблона.
OwnerUserIdGuidДаindex ix_requisite_templates_owner_user_idapp (actor)Владелец (инвестор). Record-level фильтр.
NamestringДаvarchar(128), min 1; unique ux_requisite_templates_owner_name (owner_user_id, lower(name)) where status='Active' and deleted_at is nullappИмя шаблона в UI (INV-RQT-4).
PersonIdGuidДаFK fk_requisite_templates_person_id → profile.persons(id), index ix_requisite_templates_person_idappПерсона, на которую оформляются документы сделки.
StatusRequisiteTemplateStatusДаvarchar(32) (enum как текст)appActive / Archived.
IsDefaultboolДаdefault false; unique ux_requisite_templates_owner_default (owner_user_id) where is_default and status='Active' and deleted_at is nullappШаблон по умолчанию, подставляемый в форму бронирования (INV-RQT-5).
CompositionRequisiteTemplateComposition (jsonb)Даtyped jsonb, модель нижеappСостав шаблона: ссылки на документы персоны (только id — не PII, хранится открыто).
ContactEmailCipherbyte[]?Нетbyteaapp (шифрование PII)Контактный email для документов сделки, если отличается от контактов аккаунта (например, email супруги). Plaintext: формат email, ≤ 256 символов (валидация до шифрования).
ContactPhoneCipherbyte[]?Нетbyteaapp (шифрование PII)Контактный телефон для документов сделки (E.164, ≤ 32 символов до шифрования).
KeyVersionintДа>= 1appВерсия ключа шифрования cipher-полей.
LastUsedAtDateTimeOffset?Нетindex ix_requisite_templates_last_used_atapp (internal API снапшота)Момент последнего использования в брони/сделке — сортировка «недавние» в UI.
UsageCountintДа>= 0, default 0appСколько раз запрошен снапшот для брони/сделки.
CreatedAtDateTimeOffsetДаappСоздание.
CreatedByUserIdGuid?НетappКто создал.
UpdatedAtDateTimeOffsetДаappВремя последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete.
DeletedByUserIdGuid?НетappКто архивировал.
VersionlongДа>= 1appВерсия записи (оптимистическая блокировка).

Enum RequisiteTemplateStatus

ЗначениеОписание
ActiveДоступен для выбора в бронировании/сделке.
ArchivedСкрыт; ранее созданные снапшоты в CRM не затрагиваются.

Value objects

VO не используются: контакты — отдельные шифрованные колонки (не структурные), состав — typed jsonb.

Модели JSONB (versioned)

RequisiteTemplateComposition (jsonb, ContractVersion = 1)

Хранится открыто (plaintext jsonb): содержит только идентификаторы документов, не значения.

ПолеТипОбязательностьОграниченияSince version
ContractVersionintДа= 1v1
IdentityDocumentIdGuidДаid PersonDocumentEntity c Type ∈ {RfPassport, RbPassport, KzPassport, CisPassport, ForeignPassport} (INV-RQT-2)v1
InnDocumentIdGuid?Нетid PersonDocumentEntity c Type = Innv1
AdditionalDocumentIdsList<Guid>Да (может быть пустым)≤ 10 элементов; каждый — id документа персоны любого типа; без дублей и без повторов IdentityDocumentId/InnDocumentIdv1

Совместимость: additive-изменения с ростом ContractVersion; чтение старых версий — lazy-миграция в памяти.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
PersonEntityreferencePersonId (FK)персона с документами загружается при валидации состава и построении снапшотаrestrict (персона не архивируется при активном шаблоне — INV-PRS-4 в person.md)
PersonDocumentEntityreference (по id в Composition)Composition.*DocumentIdдокументы приходят включёнными в агрегат персоны; принадлежность и статус проверяются в памятиrestrict (INV-DOC-6 в person-document.md)
ProfileEntityreferenceOwnerUserId → profiles.id; обратная — profiles.default_requisite_template_idпрофиль загружается в guard-ахrestrict
Снапшот в CRMкопия (вне контекста)RequisiteSnapshot в сделке crminternal API по запросу CRMне связаны после копирования

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-RQT-1 SameOwnerPersonId и все документы из Composition принадлежат OwnerUserId (person.OwnerUserId == actor; каждый documentId ∈ документам этой персоны)при создании/изменении шаблона (сверка в памяти)PROFILE_TEMPLATE_FOREIGN_REFERENCE
INV-RQT-2 IdentityDocTypeIdentityDocumentId ссылается на активный документ паспортного типа; InnDocumentId (если задан) — на активный Innпри создании/изменении шаблонаPROFILE_TEMPLATE_INVALID_DOCUMENT_TYPE
INV-RQT-3 ActiveReferencesВсе документы состава и персона — Status = Active на момент создания/изменения шаблонапри создании/изменении шаблонаPROFILE_TEMPLATE_REFERENCE_ARCHIVED
INV-RQT-4 UniqueNameИмя шаблона уникально среди активных шаблонов владельца (case-insensitive)unique partial index + предварительная проверка имениPROFILE_TEMPLATE_NAME_TAKEN
INV-RQT-5 SingleDefaultНе более одного IsDefault = true среди активных шаблонов владельца; установка по умолчанию снимает флаг с предыдущего в той же транзакцииunique partial index + установка шаблона по умолчанию (два обновления в одной транзакции)PROFILE_TEMPLATE_DEFAULT_CONFLICT (конфликт индекса — retry)
INV-RQT-6 InvestorOnlyУправление шаблонами — только Profile.UserType = Investor, permission profile.requisites.manage-selfguard investor-функцийPROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE
INV-RQT-7 TemplatesLimit≤ 20 активных шаблонов на владельцапри создании шаблона (проверка счётчика)PROFILE_TEMPLATES_LIMIT_EXCEEDED
INV-RQT-8 SnapshotFromActiveСнапшот для CRM выдаётся только по Status = Active (архивный шаблон в новые сделки не попадает)при выдаче снапшота (internal API)PROFILE_TEMPLATE_ARCHIVED

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
RequisiteTemplateCreatedEventдоменноеСозданиеId, OwnerUserId, PersonId
RequisiteTemplateChangedEventдоменноеИзменение состава/имени/контактов/defaultId, OwnerUserId, список изменённых полей (имена)
RequisiteTemplateUsedEventдоменноеВыдача снапшота CRMId, OwnerUserId, RequestedByService
RequisiteTemplateArchivedEventдоменноеАрхивId, OwnerUserId
интеграционноеНе публикуютсяSimplicity-first (ADR-0056): CRM тянет снапшот синхронно через internal API в момент оформления брони/сделки; событийная репликация шаблонов не нужна и означала бы вынос PII в Kafka

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK (Id); ix_requisite_templates_owner_user_id; ix_requisite_templates_person_id; ux_requisite_templates_owner_name unique (owner_user_id, lower(name)) where status='Active' and deleted_at is null; ux_requisite_templates_owner_default unique (owner_user_id) where is_default and status='Active' and deleted_at is null; ix_requisite_templates_last_used_at
Партиционирование / retentionnone; бессрочно (soft-delete)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую); установка шаблона по умолчанию — два обновления в одной транзакции под unique-индексом
Permissions / PIIComposition — только id (не PII, открыто); контакты-оверрайды — шифрованные (PersonalDataCrypto); self — profile.requisites.manage-self; выдача расшифрованного снапшота — только internal API для m2m-actor crm (запрос обязан содержать OwnerUserId инициатора сделки; сверка владения) + аудит выдачи (RequisiteTemplateUsedEvent); в логах — только TemplateId/PersonId
AuditVersion + actor-stamping + UsageCount/LastUsedAt + RequisiteTemplateUsedEvent на каждую выдачу

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

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

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