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

POST /api/v1/profile/persons/{personId}/restore — Восстановление персоны (restorePerson)

Назначение

Восстановить ранее архивированную персону (Archived → Active, person.md). Child-документы, заархивированные каскадом при архиве персоны, восстанавливаются вместе с ней (кроме архивированных вручную до того — их восстанавливает архив/восстановление документа). Вызывает инвестор-владелец персоны.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Bearer JWT), UserType = Investor
Auth policy / Permissionsauthenticated; permission profile.personal.manage-self
Scope (RBAC)self
Record-levelOwnerUserId = UserId; чужой personId → 404
Tenant isolationне применяется (ADR-0052)
Auditupdate: DeletedAt = null, UpdatedByUserId = UserId, Version++, событие PersonRestoredEvent

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/profile/persons/{personId}/restore
Success status200
Idempotency headerне применяется (повтор для активной персоны — no-op 200)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401 без токена
personIdrouteGuidДаNotEmpty; персона владельца (включая архивные)чужая → 404

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

Дом модели — list-persons.md; возвращается персона со status = Active.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена); tenant не применяется; требуется право profile.personal.manage-self.
  2. Проверка входа. personId задан. Нарушение → ValidationError.
  3. Проверка предусловий. Профиль владельца существует; иначе PROFILE_NOT_FOUND. Профиль активен (Status = Active, иначе PROFILE_USER_BLOCKED, INV-PRF-3), в режиме самоуправления (ContactsManagedBy = SelfService, иначе PROFILE_MANAGED_EXTERNALLY, INV-PRF-2), тип пользователя — Investor (иначе PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE, INV-PRF-4). Персона personId (в т.ч. архивная) принадлежит владельцу; иначе PROFILE_PERSON_NOT_FOUND; уже Active — идемпотентный успех. Инварианты восстановления: активных персон владельца < 20, иначе PROFILE_PERSONS_LIMIT_EXCEEDED (INV-PRS-7); при Relation = Self — среди активных персон владельца нет другой Self, иначе PROFILE_PERSON_SELF_ALREADY_EXISTS (INV-PRS-1).
  4. Бизнес-правила и переходы. Персона переходит Archived → Active: DeletedAt = null, DeletedByUserId = null; child-документы, заархивированные каскадом в тот же момент (с DeletedAt == person.DeletedAt), восстанавливаются (Status = Active, DeletedAt = null); DocumentsCount пересчитывается по активным документам; Version++; фиксируется факт PersonRestoredEvent.
  5. Согласованность. В одном согласованном изменении сохраняются персона (root + восстановленные документы) и интеграционное событие. Гонку двойной Self страхует уникальность Self владельца; гонку лимита — повторная проверка конфликтом версий.
  6. Результат. Модель PersonModel (status = Active); HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияPersonUpsertedKafkaEvent / pin.profile.persons.v1 / key = PersonId (Status = Active, актуальный DocumentsCount, Version; без PII)

После коммита событие потребляется асинхронно контекстом crm (SLA < 5 мин, person-upserted.md): персона со Status = Active снова доступна для выбора в форме брони/сделки; незавершённые брони/сделки, чей снапшот ссылается на эту персону, помечаются «реквизиты могли устареть». Иммутабельные снапшоты и завершённые сделки не изменяются.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустой personIdперечень невалидных полейno retry
PROFILE_NOT_FOUND404Профиль не создан«Профиль ещё создаётся»retry
PROFILE_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
PROFILE_MANAGED_EXTERNALLY403TalentSync«Данные управляются внешней системой»no retry
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE403не Investor«Доступно только инвесторам»no retry
PROFILE_PERSON_NOT_FOUND404Нет персоны / чужая«Персона не найдена»no retry
PROFILE_PERSONS_LIMIT_EXCEEDED400≥ 20 активных (INV-PRS-7)«Достигнут лимит персон (20)»после архива другой
PROFILE_PERSON_SELF_ALREADY_EXISTS409Другая активная Self (INV-PRS-1)«Основная персона уже существует»no retry
PROFILE_CONCURRENCY_CONFLICT409Конкурентная правка (конфликт версий)«Данные изменены, повторите»retry

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

  • Семантика стабильна; расширения — additive.
  • Logs: UserId, PersonId, число восстановленных документов; metrics: profile_api_operations_total{operation="restorePerson",result}; traces — OTel.

Acceptance Criteria

  • Given архивная персона владельца (Relation = Spouse) и активных персон < 20, When POST restore, Then 200; персона Archived → Active, DeletedAt = null, DeletedByUserId = null; документы, заархивированные каскадом в тот же момент (DeletedAt == person.DeletedAt), восстановлены; DocumentsCount пересчитан по активным; Version++; опубликован PersonUpsertedKafkaEvent (Status = Active).
  • Given персона уже Active, When POST restore, Then 200 идемпотентный no-op.
  • Given архивная Self и среди активных нет другой Self, When POST restore, Then 200 восстановлена.
  • Given архивная Self, но у владельца уже есть другая активная Self, When POST restore, Then PROFILE_PERSON_SELF_ALREADY_EXISTS (409) — INV-PRS-1.
  • Given у владельца уже 20 активных персон, When POST restore, Then PROFILE_PERSONS_LIMIT_EXCEEDED (400) — INV-PRS-7; восстановление возможно после архива другой.
  • Given документы, заархивированные вручную до архива персоны (DeletedAt != person.DeletedAt), When POST restore, Then они остаются архивными (восстанавливаются отдельной операцией документа).
  • Given personId чужой, When POST restore, Then PROFILE_PERSON_NOT_FOUND (404).
  • Given профиль заблокирован / TalentSync / UserType != Investor, When POST restore, Then PROFILE_USER_BLOCKED (400) / PROFILE_MANAGED_EXTERNALLY (403) / PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403) соответственно.
  • Given конкурентная правка (конфликт версий), When POST restore, Then PROFILE_CONCURRENCY_CONFLICT (409), retry.

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

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

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

  • Сущность: person.md (переход Archived → Active, INV-PRS-1/7).
  • Обратная операция: archive-person.md.