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

POST /api/v1/profile/persons — Создание персоны (createPerson)

Назначение

Создать новый набор персональных данных (персону) на аккаунте инвестора: аккаунт может принадлежать мужу/жене, а сделка оформляется на супруга/ребёнка/иное лицо — поэтому персон несколько. ФИО и дата рождения шифруются на уровне приложения (AES-256-GCM) до записи в БД (person.md). Персона Relation = Self создаётся автоматически консюмером identity (../consumers/identity-users-sync.md); повторная активная Self запрещена (INV-PRS-1).

Metadata

ПолеЗначение
Сервис/контекстprofile
API areapublic
Feature grouppersons
Статусdraft
Документdocs/06-services/profile/api/public/create-person.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor (INV-PRS-2)
Auth policy / Permissionsauthenticated; permission profile.personal.manage-self
Scope (RBAC)self
Record-levelOwnerUserId = UserId из sub (проставляется handler-ом, из body не принимается)
Tenant isolationне применяется (ADR-0052)
Auditcreate: CreatedByUserId = UserId, Version = 1, доменное событие PersonCreatedEvent

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/profile/persons
Success status201
Idempotency headerIdempotency-Key (опционально; защита от двойного сабмита формы)
CorrelationOpenTelemetry trace (traceparent)

Входные данные / параметры запроса (body CreatePersonForm)

Проверка входа выполняется до шифрования на plaintext (правила полей — person.md).

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
relationbodyPersonRelationTypeДаenum Self/Spouse/Child/Parent/Other; Self — только при отсутствии активной Self (INV-PRS-1)
firstNamebodystringДа1–128 символовPII → шифруется, не логируется
lastNamebodystringДа1–128 символовPII → шифруется
middleNamebodystring?Нет1–128 символов (опциональное отчество — прототип)PII → шифруется
birthDatebodyDateOnly?НетISO yyyy-MM-dd; ≤ сегодня; ≥ 1900-01-01PII → шифруется
citizenshipCountryIdbodyshort?Нетссылка на справочник стран handbook; резолв через internal resolve (resolve-references.md), неизвестная/архивная → PROFILE_COUNTRY_UNKNOWNоткрытое поле
genderbodyPersonGender?Нетenum Male/Femaleоткрытое поле

Модель ответа PersonModel

Дом модели — list-persons.md; возвращается созданная персона (documentsCount = 0, version = 1), значения ФИО/даты — из входного plaintext (повторная расшифровка не выполняется).

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId = sub, INV-PRS-2) с permission profile.personal.manage-self; владелец берётся из токена, из body не принимается. Tenant не применяется (ADR-0052). При повторе с тем же Idempotency-Key возвращается сохранённый результат без создания дубля.
  2. Проверка входа. enum relation; длины ФИО (1–128); birthDate ≤ сегодня и ≥ 1900-01-01; gender — enum — иначе ValidationError (на plaintext до шифрования). Если задан citizenshipCountryId — резолв страны через internal resolve справочника handbook; страна не найдена или архивна → PROFILE_COUNTRY_UNKNOWN (422); справочник недоступен → fail-closed PROFILE_HANDBOOK_UNAVAILABLE (503).
  3. Проверка профиля и лимитов. Профиль self-service: отсутствие → PROFILE_NOT_FOUND; Status = Active (INV-PRF-3 → PROFILE_USER_BLOCKED); ContactsManagedBy = SelfService (INV-PRF-2/INV-PRS-3 → PROFILE_MANAGED_EXTERNALLY; менеджеры застройщика — источник identity); UserType = Investor (INV-PRS-2 → PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE). Лимит: ≥ 20 активных персон → PROFILE_PERSONS_LIMIT_EXCEEDED (INV-PRS-7). При relation = Self: активная Self уже есть → PROFILE_PERSON_SELF_ALREADY_EXISTS (INV-PRS-1).
  4. Шифрование и создание. ФИО (first/last/middle) и birthDate шифруются единым KeyVersion записи (INV-PRS-8); недоступность ключей → PROFILE_CRYPTO_UNAVAILABLE (503). Создаётся персона: Status = Active, DocumentsCount = 0, CreatedByUserId = UserId, Version = 1; citizenshipCountryId/gender — открытые поля.
  5. События. PersonUpsertedKafkaEvent (топик pin.profile.persons.v1, key PersonId; payload без PII: Id, OwnerUserId, Relation, Status = Active, DocumentsCount = 0, Version = 1). Контракт — ../events/person-upserted.md.
  6. Согласованность. Персона, публикация события и idempotency-запись фиксируются атомарно; гонку двойного Self страхует уникальный partial-индекс ux_persons_owner_self (конфликт → PROFILE_PERSON_SELF_ALREADY_EXISTS).
  7. Результат. PersonModel (документов 0, версия 1; значения ФИО/даты — из входного plaintext). HTTP 201 + Location: /api/v1/profile/persons/{id}.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsPersonUpsertedKafkaEvent / pin.profile.persons.v1 / key = PersonId (без PII)
External callsкриптосервис (ключи Vault) — шифрование ФИО/даты рождения; недоступность — fail-closed PROFILE_CRYPTO_UNAVAILABLE. Справочник handbook (internal resolve) — валидация citizenshipCountryId, только если поле задано; недоступность — fail-closed PROFILE_HANDBOOK_UNAVAILABLE
Cache / projections / searchnone (поиск по шифрованным полям не ведётся)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Формат/длины/enum/датыRestApiValidationErrorResponseno retry
PROFILE_NOT_FOUND404Профиль не создан консюмером«Профиль ещё создаётся»retry с backoff
PROFILE_USER_BLOCKED400Status != Active (INV-PRF-3)«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403ContactsManagedBy = TalentSync (INV-PRS-3)«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403UserType != Investor (INV-PRS-2)«Доступно только инвесторам»no retry
PROFILE_PERSONS_LIMIT_EXCEEDED400≥ 20 активных персон (INV-PRS-7)«Достигнут лимит персон (20)»no retry
PROFILE_PERSON_SELF_ALREADY_EXISTS409Активная Self уже есть (INV-PRS-1)«Основная персона уже существует»no retry
PROFILE_COUNTRY_UNKNOWN422citizenshipCountryId не найден в справочнике стран или архивен«Страна не найдена в справочнике»no retry (исправить страну)
PROFILE_HANDBOOK_UNAVAILABLE503Справочник недоступен при валидации citizenshipCountryId (fail-closed)«Сервис временно недоступен»retry
PROFILE_CRYPTO_UNAVAILABLE503Ключи шифрования недоступны«Сервис временно недоступен»retry

Совместимость и наблюдаемость

  • Форма расширяется только optional-полями (additive); plaintext-значения PII никогда не появляются в логах/метриках/событиях.
  • Logs: UserId, PersonId, Relation (без значений ФИО/дат); metrics: profile_api_operations_total{operation="createPerson",result}, profile_persons_created_total{relation}, profile_crypto_errors_total; traces — OTel.

Acceptance Criteria

  • Given инвестор с активным self-service-профилем и без активной Self, When POST relation = Self с корректными ФИО и birthDate, Then 201 + Location; персона Active, DocumentsCount = 0, Version = 1; ФИО и дата рождения зашифрованы единым KeyVersion; опубликован PersonUpsertedKafkaEvent (pin.profile.persons.v1, key PersonId, без PII).
  • Given уже есть активная Self, When POST relation = Self, Then PROFILE_PERSON_SELF_ALREADY_EXISTS (409) — INV-PRS-1; персона не создана, событие не публикуется.
  • Given корректный relation = Spouse, When POST, Then 201; вторая персона создана (несколько персон на аккаунт допустимо).
  • Given 20 активных персон, When POST, Then PROFILE_PERSONS_LIMIT_EXCEEDED (400) — INV-PRS-7.
  • Given firstName длиной 0 или > 128 / birthDate вне [1900-01-01 .. сегодня] / неизвестное значение relation, When POST, Then ValidationError (400) — проверка на plaintext до шифрования; шифротекст и PII в ответ/лог не попадают.
  • Given citizenshipCountryId, которого нет в справочнике стран (или страна архивна), When POST, Then PROFILE_COUNTRY_UNKNOWN (422); персона не создана.
  • Given справочник handbook недоступен и citizenshipCountryId задан, When POST, Then PROFILE_HANDBOOK_UNAVAILABLE (503), fail-closed; без citizenshipCountryId операция резолв не вызывает и проходит.
  • Given профиль под управлением внешней системы (ContactsManagedBy = TalentSync), When POST, Then PROFILE_MANAGED_EXTERNALLY (403) — INV-PRS-3.
  • Given UserType != Investor, When POST, Then PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403) — INV-PRS-2.
  • Given профиль заблокирован (Status != Active), When POST, Then PROFILE_USER_BLOCKED (400) — INV-PRF-3.
  • Given профиль ещё не создан консюмером identity, When POST, Then PROFILE_NOT_FOUND (404); клиент повторяет с backoff.
  • Given ключи шифрования недоступны, When POST, Then PROFILE_CRYPTO_UNAVAILABLE (503), fail-closed; персона не создана, retry.
  • Given повтор запроса с тем же Idempotency-Key, When POST, Then возвращается ранее сохранённый результат без создания дубля.
  • Given два конкурентных создания Self, When оба POST, Then одно 201, второе — PROFILE_PERSON_SELF_ALREADY_EXISTS (409) (страховка уникальным partial-индексом ux_persons_owner_self).

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

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

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