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

Entity / Aggregate: PersonEntity

Дом полей агрегата «Персона» контекста profile. Все проекции (модели ответов API профиля, снапшоты реквизитов для CRM, снапшот заявки на верификацию, payload событий) ссылаются сюда и описывают только дельту.

Назначение

PersonEntity — один набор персональных данных на аккаунте пользователя: аккаунт может принадлежать мужу/жене, а сделку оформляют на супруга/детей, поэтому у одного UserId может быть несколько персон (сам владелец, супруг(а), ребёнок, иное лицо). Персона хранит ФИО, дату рождения, гражданство — в зашифрованном виде (значения шифруются на уровне приложения) — и владеет своими документами (person-document.md) как частью агрегата.

Границы агрегата: PersonEntity (root) + коллекция PersonDocumentEntity (child). Шаблоны реквизитов (requisite-template.md) и заявки на верификацию (verification-request.md) — отдельные агрегаты, ссылающиеся на персону по Id.

Раздел ЛК — /profile/personal (несколько персон); карта — prototype-mapping.md.

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

ПолеЗначение
Контекстprofile
KindAggregateRoot
Source of truthprofile
Таблицаprofile.persons
Tenant isolationНе tenanted (ADR-0052 п.2: данные инвестора — вне tenant); изоляция record-level по OwnerUserId

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор персоны.
OwnerUserIdGuidДаindex ix_persons_owner_user_id; логическая ссылка на identity.users.id (без FK — cross-context)app (actor)Владелец набора данных. Все операции — только от имени владельца (record-level).
RelationPersonRelationTypeДаvarchar(32) (enum как текст)appТип связи персоны с аккаунтом (см. enum ниже).
StatusPersonStatusДаvarchar(32) (enum как текст), index ix_persons_statusappActive / Archived.
NameEncryptedPersonName (owned)Дасм. VO нижеapp (шифрование PII)ФИО персоны: три шифрованные колонки. Расшифровывается только в self-операциях и internal-снапшотах для CRM.
BirthDateCipherbyte[]Нет (обязателен для Submit верификации — инвариант INV-VRF-2 в verification-request.md)byteaapp, шифруетсяДата рождения (DateOnly, ISO yyyy-MM-dd), зашифрованная PersonalDataCrypto.Encrypt.
CitizenshipCountryIdshort?Нетссылка на справочник стран handbook (country.md)appГражданство (Россия, Беларусь, Казахстан, Армения, Грузия, ОАЭ и др.). На write-path резолвится через internal resolve справочника (resolve-references.md); неизвестная/архивная страна → PROFILE_COUNTRY_UNKNOWN. Хранится открыто: нужно для выбора допустимых типов документов и не идентифицирует персону само по себе.
GenderPersonGender?Нетvarchar(16) (enum как текст)appMale / Female. Открыто (не идентифицирует персону).
KeyVersionintДа>= 1app (шифрование PII)Версия ключа шифрования, которым зашифрованы все cipher-поля записи (ротация ключей — фоновая перешифровка).
DocumentsCountintДа>= 0, default 0app (денормализация)Число активных документов персоны — для списков без загрузки child-коллекции. Пересчитывается методами агрегата AddDocument/ArchiveDocument.
CreatedAtDateTimeOffsetДаindex ix_persons_created_atappСоздание персоны.
CreatedByUserIdGuid?Нетapp= OwnerUserId (self-service) либо сотрудник заботы (арбитраж, с аудитом).
UpdatedAtDateTimeOffsetДаappВремя последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
DeletedAtDateTimeOffset?НетappSoft-delete (архив персоны).
DeletedByUserIdGuid?НетappКто архивировал.
VersionlongДа>= 1appВерсия записи (оптимистическая блокировка).

Enum PersonRelationType

ЗначениеОписание
SelfСам владелец аккаунта. Создаётся автоматически при первом входе в ЛК (одна активная на аккаунт — INV-PRS-1).
SpouseСупруг / супруга.
ChildРебёнок.
ParentРодитель.
OtherИное лицо (например, доверенное лицо), на которое оформляется сделка.

Enum PersonStatus

ЗначениеОписание
ActiveПерсона доступна для выбора в шаблонах реквизитов, бронировании, верификации.
ArchivedСкрыта из списков; исторические ссылки (сделки CRM, завершённые верификации) остаются валидными.

Enum PersonGender

ЗначениеОписание
MaleМужской.
FemaleЖенский.

Value objects

EncryptedPersonName (owned)

ФИО хранится тремя раздельно зашифрованными значениями (шифрование на уровне приложения, AES-256-GCM с nonce внутри ciphertext, единый KeyVersion записи).

ПолеТипОбязательностьОграниченияКолонка
FirstNameCipherbyte[]Даbytea; plaintext до шифрования: 1–128 символовname_first_name_cipher
LastNameCipherbyte[]Даbytea; plaintext 1–128 символовname_last_name_cipher
MiddleNameCipherbyte[]?Нетbytea; plaintext 1–128 символов; отчество опционально (форма бронирования прототипа — «опциональное отчество»)name_middle_name_cipher

Ограничения длины проверяются до шифрования. Поиск/сортировка по ФИО в контексте profile не выполняются (списки персон пользователя малы — расшифровка в памяти при обработке операции).

Модели JSONB (versioned)

JSONB-модели на уровне PersonEntity не используются. Typed jsonb документов персоны (модели по типу документа + сканы) — person-document.md.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
PersonDocumentEntitychild (в агрегате)person_documents.person_id → persons.id (FK)документ загружается включённым в агрегат персоны (документов ≤ 20)cascade (архив персоны архивирует документы)
ProfileEntityreference (владелец)OwnerUserId → profiles.idпрофиль загружается в guard-ах операций (тип пользователя, ContactsManagedBy)restrict
RequisiteTemplateEntityreference (обратная)requisite_templates.person_idпроверка ссылок по PersonId в guard INV-PRS-4restrict (архив персоны блокируется активными шаблонами)
VerificationRequestEntityreference (обратная)verification_requests.person_idпроверка ссылок по PersonId в guard INV-PRS-5restrict (при активной заявке)

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-PRS-1 SingleSelfНа OwnerUserId — не более одной персоны Relation = Self со Status = Activeunique partial index ux_persons_owner_self + создание персоны SelfPROFILE_PERSON_SELF_ALREADY_EXISTS
INV-PRS-2 InvestorOnlyПерсоны создаёт/меняет только пользователь с Profile.UserType = Investor (только инвестор управляет реквизитами/документами); permission profile.documents.manage-selfпри создании/правке персоны (проверка типа профиля)PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE
INV-PRS-3 NotManagedExternallyПри Profile.ContactsManagedBy = TalentSync (менеджер застройщика) любые правки персональных данных запрещены — источник знаний внешняя система (требование INV-IDN-001; в identity — инвариант INV-4 ManagedContacts, user.md)при правке персоныPROFILE_MANAGED_EXTERNALLY
INV-PRS-4 ArchiveNotReferencedActive → Archived разрешён, только если нет активных RequisiteTemplateEntity с этим PersonIdпри архиве персоны (проверка активных шаблонов)PROFILE_PERSON_USED_IN_TEMPLATE
INV-PRS-5 ArchiveNoActiveVerificationActive → Archived запрещён при заявке верификации в статусах Draft/Submitted/InReview на эту персонупри архиве персоны (проверка активных заявок)PROFILE_PERSON_HAS_ACTIVE_VERIFICATION
INV-PRS-6 SelfNotArchivableПерсона Relation = Self не архивируется (базовый набор данных аккаунта)при архиве персоныPROFILE_SELF_PERSON_NOT_ARCHIVABLE
INV-PRS-7 PersonsLimitНе более 20 активных персон на аккаунт (simplicity-first, ADR-0056; агрегат загружается целиком)при создании персоны (проверка счётчика)PROFILE_PERSONS_LIMIT_EXCEEDED
INV-PRS-8 CipherKeyConsistencyВсе cipher-поля записи зашифрованы одним KeyVersion; смена любого поля перешифровывает запись текущим ключомпри правке персональных данных (единая точка шифрования)инфраструктурная ошибка (не бизнес)

Архив персоны (INV-PRS-4/5) блокируют только живые ссылки — активный шаблон реквизитов и незавершённая заявка верификации (Draft/Submitted/InReview). Персона, попавшая лишь в иммутабельный снапшот (завершённая заявка KYC, снапшот реквизитов сделки CRM), архивируется свободно — снапшот это копия и от статуса персоны не зависит.

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

СобытиеТипКогдаSnapshot/поля
PersonCreatedEventдоменноеСоздание персоны (в т.ч. автосоздание Self)Id, OwnerUserId, Relation
PersonDataChangedEventдоменноеПравка ФИО / даты рождения / гражданстваId, OwnerUserId, список изменённых полей (имена полей, без значений)
PersonArchivedEvent / PersonRestoredEventдоменноеПереходы Active ↔ ArchivedId, OwnerUserId
PersonUpsertedKafkaEventинтеграционное (ContractVersion = 1, topic pin.profile.persons.v1, key = Id)Создание/изменение/архив персоны, а также добавление/архив документа персоны (меняется DocumentsCount)Без PII: Id, OwnerUserId, Relation, Status, DocumentsCount, Version. key = Id даёт потребителям строгий порядок upsert по персоне; Version — монотонный guard. Расшифрованные данные никогда не публикуются в Kafka; CRM получает снапшот реквизитов через internal API (requisite-template.md).

Доменные события поднимаются при изменении агрегата; интеграционное событие публикуется через транзакционный outbox атомарно со сменой состояния. Контракт события — будущий документ ../events/person-upserted.md.

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

АспектЗначение
ИндексыPK (Id); ix_persons_owner_user_id btree; ux_persons_owner_self unique (owner_user_id) where relation = 'Self' and status = 'Active' and deleted_at is null; ix_persons_status; ix_persons_created_at
Партиционирование / retentionnone; персоны — бессрочно (soft-delete); полное удаление PII по запросу субъекта — регламент администратора платформы (ADR-0053) + перешифровка/crypto-erase ключа
Concurrencyоптимистическая блокировка root (правка не должна затирать чужую); правки child-документов идут через root (инкремент Version)
Permissions / PIIФИО/дата рождения — PII, хранятся только шифротекстом (PersonalDataCrypto, AES-256-GCM, ключи в Vault/k8s secrets, KeyVersion для ротации); в логах значения не пишутся вообще (даже маскированные), логируются только PersonId/имена полей; self-доступ — profile.personal.view-self/profile.personal.manage-self, документы — profile.documents.*-self; чтение чужой персоны — только profile.personal.view-any (забота/админ) с аудитом каждого доступа и маскированием значений документов (permission.md)
AuditVersion + actor-stamping; каждое изменение данных — PersonDataChangedEvent (лента аудита без значений)

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

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

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