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 |
| Kind | AggregateRoot |
| Source of truth | profile |
| Таблица | profile.persons |
| Tenant isolation | Не tenanted (ADR-0052 п.2: данные инвестора — вне tenant); изоляция record-level по OwnerUserId |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор персоны. |
OwnerUserId | Guid | Да | index ix_persons_owner_user_id; логическая ссылка на identity.users.id (без FK — cross-context) | app (actor) | Владелец набора данных. Все операции — только от имени владельца (record-level). |
Relation | PersonRelationType | Да | varchar(32) (enum как текст) | app | Тип связи персоны с аккаунтом (см. enum ниже). |
Status | PersonStatus | Да | varchar(32) (enum как текст), index ix_persons_status | app | Active / Archived. |
Name | EncryptedPersonName (owned) | Да | см. VO ниже | app (шифрование PII) | ФИО персоны: три шифрованные колонки. Расшифровывается только в self-операциях и internal-снапшотах для CRM. |
BirthDateCipher | byte[] | Нет (обязателен для Submit верификации — инвариант INV-VRF-2 в verification-request.md) | bytea | app, шифруется | Дата рождения (DateOnly, ISO yyyy-MM-dd), зашифрованная PersonalDataCrypto.Encrypt. |
CitizenshipCountryId | short? | Нет | ссылка на справочник стран handbook (country.md) | app | Гражданство (Россия, Беларусь, Казахстан, Армения, Грузия, ОАЭ и др.). На write-path резолвится через internal resolve справочника (resolve-references.md); неизвестная/архивная страна → PROFILE_COUNTRY_UNKNOWN. Хранится открыто: нужно для выбора допустимых типов документов и не идентифицирует персону само по себе. |
Gender | PersonGender? | Нет | varchar(16) (enum как текст) | app | Male / Female. Открыто (не идентифицирует персону). |
KeyVersion | int | Да | >= 1 | app (шифрование PII) | Версия ключа шифрования, которым зашифрованы все cipher-поля записи (ротация ключей — фоновая перешифровка). |
DocumentsCount | int | Да | >= 0, default 0 | app (денормализация) | Число активных документов персоны — для списков без загрузки child-коллекции. Пересчитывается методами агрегата AddDocument/ArchiveDocument. |
CreatedAt | DateTimeOffset | Да | index ix_persons_created_at | app | Создание персоны. |
CreatedByUserId | Guid? | Нет | — | app | = OwnerUserId (self-service) либо сотрудник заботы (арбитраж, с аудитом). |
UpdatedAt | DateTimeOffset | Да | — | app | Время последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Soft-delete (архив персоны). |
DeletedByUserId | Guid? | Нет | — | app | Кто архивировал. |
Version | long | Да | >= 1 | app | Версия записи (оптимистическая блокировка). |
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 записи).
| Поле | Тип | Обязательность | Ограничения | Колонка |
|---|---|---|---|---|
FirstNameCipher | byte[] | Да | bytea; plaintext до шифрования: 1–128 символов | name_first_name_cipher |
LastNameCipher | byte[] | Да | bytea; plaintext 1–128 символов | name_last_name_cipher |
MiddleNameCipher | byte[]? | Нет | bytea; plaintext 1–128 символов; отчество опционально (форма бронирования прототипа — «опциональное отчество») | name_middle_name_cipher |
Ограничения длины проверяются до шифрования. Поиск/сортировка по ФИО в контексте profile не
выполняются (списки персон пользователя малы — расшифровка в памяти при обработке операции).
Модели JSONB (versioned)
JSONB-модели на уровне PersonEntity не используются. Typed jsonb документов персоны (модели по типу
документа + сканы) — person-document.md.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
PersonDocumentEntity | child (в агрегате) | person_documents.person_id → persons.id (FK) | документ загружается включённым в агрегат персоны (документов ≤ 20) | cascade (архив персоны архивирует документы) |
ProfileEntity | reference (владелец) | OwnerUserId → profiles.id | профиль загружается в guard-ах операций (тип пользователя, ContactsManagedBy) | restrict |
RequisiteTemplateEntity | reference (обратная) | requisite_templates.person_id | проверка ссылок по PersonId в guard INV-PRS-4 | restrict (архив персоны блокируется активными шаблонами) |
VerificationRequestEntity | reference (обратная) | verification_requests.person_id | проверка ссылок по PersonId в guard INV-PRS-5 | restrict (при активной заявке) |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-PRS-1 SingleSelf | На OwnerUserId — не более одной персоны Relation = Self со Status = Active | unique partial index ux_persons_owner_self + создание персоны Self | PROFILE_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 ArchiveNotReferenced | Active → Archived разрешён, только если нет активных RequisiteTemplateEntity с этим PersonId | при архиве персоны (проверка активных шаблонов) | PROFILE_PERSON_USED_IN_TEMPLATE |
| INV-PRS-5 ArchiveNoActiveVerification | Active → 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 ↔ Archived | Id, 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 |
| Партиционирование / retention | none; персоны — бессрочно (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) |
| Audit | Version + actor-stamping; каждое изменение данных — PersonDataChangedEvent (лента аудита без значений) |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (4): Entity / Aggregate: ProfileEntity, Entity / Aggregate: RequisiteTemplateEntity, Entity / Aggregate: VerificationRequestEntity, Entity: PersonDocumentEntity
- API (17): GET /api/v1/profile/persons — Список персон аккаунта (listPersons), GET /api/v1/profile/persons/{personId} — Получение персоны (getPerson), POST /api/v1/internal/profile/requisite-snapshot — Снапшот реквизитов для CRM (getRequisiteSnapshot + RegisterUsage), POST /api/v1/profile/persons — Создание персоны (createPerson), POST /api/v1/profile/persons/{personId}/archive — Архивация персоны (archivePerson), POST /api/v1/profile/persons/{personId}/documents — Добавление документа персоны (addPersonDocument), POST /api/v1/profile/persons/{personId}/documents/{documentId}/archive — Архивация документа персоны (archivePersonDocument), POST /api/v1/profile/persons/{personId}/restore — Восстановление персоны (restorePerson), POST /api/v1/profile/requisite-templates — Создание шаблона реквизитов (createRequisiteTemplate), POST /api/v1/profile/requisite-templates/{templateId}/archive — Архивация шаблона реквизитов (archiveRequisiteTemplate), POST /api/v1/profile/verification — Создание черновика заявки KYC (createVerificationRequest), POST /api/v1/profile/verification/{requestId}/submit — Отправка заявки KYC на ревью (submitVerification) (+5)
- Use Cases (5): UC-PRF-001. Заполнение профиля и персоны «Сам» (первый вход инвестора в ЛК), UC-PRF-002. Добавление персоны «Супруг(а)» с паспортом и сканами, UC-PRF-003. Подача заявки на верификацию инвестора и её одобрение (KYC), UC-PRF-004. Создание шаблона реквизитов и его использование в бронировании (CRM), UC-PRF-005. Запрет редактирования профиля для менеджера застройщика (TalentSync)
- События (1): Event Contract: pin.profile.person-upserted (PersonUpsertedKafkaEvent)
- Консюмеры (1): Consumer: identity-users-sync (ProfileUserUpsertedConsumer, ProfileUserBlockedConsumer)
- Фоновые задачи (2): Background Job: DocumentExpiryReminderJob, Background Job: ReEncryptRotationJob
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (1): Profile Service (Pin.Profile)
Связанные документы
- person-document.md — документы персоны (child агрегата).
- requisite-template.md, verification-request.md, profile.md.
- Identity: user.md (INV-4 ManagedContacts),
permission.md (permissions
profile.*). - Справочник стран: country.md,
resolve-references.md — валидация
CitizenshipCountryIdна write-path. - ADR-0052 (tenancy: инвестор вне tenant), ADR-0056 (simplicity-first),
prototype-mapping.md (
/profile/personal, persons-store).