POST /api/v1/profile/persons/{personId}/restore — Восстановление персоны (restorePerson)
Назначение
Восстановить ранее архивированную персону (Archived → Active, person.md).
Child-документы, заархивированные каскадом при архиве персоны, восстанавливаются вместе с ней (кроме
архивированных вручную до того — их восстанавливает
архив/восстановление документа). Вызывает инвестор-владелец персоны.
| Поле | Значение |
|---|
| Сервис/контекст | profile |
| API area | public |
| Feature group | persons |
| Статус | draft |
| Документ | docs/06-services/profile/api/public/restore-person.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT), UserType = Investor |
| Auth policy / Permissions | authenticated; permission profile.personal.manage-self |
| Scope (RBAC) | self |
| Record-level | OwnerUserId = UserId; чужой personId → 404 |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | update: DeletedAt = null, UpdatedByUserId = UserId, Version++, событие PersonRestoredEvent |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/profile/persons/{personId}/restore |
| Success status | 200 |
| Idempotency header | не применяется (повтор для активной персоны — no-op 200) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
personId | route | Guid | Да | NotEmpty; персона владельца (включая архивные) | чужая → 404 |
Модель ответа PersonModel
Дом модели — list-persons.md; возвращается персона со status = Active.
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор (
UserId из токена); tenant не применяется;
требуется право profile.personal.manage-self.
- Проверка входа.
personId задан. Нарушение → ValidationError.
- Проверка предусловий. Профиль владельца существует; иначе
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).
- Бизнес-правила и переходы. Персона переходит
Archived → Active: DeletedAt = null,
DeletedByUserId = null; child-документы, заархивированные каскадом в тот же момент (с
DeletedAt == person.DeletedAt), восстанавливаются (Status = Active, DeletedAt = null);
DocumentsCount пересчитывается по активным документам; Version++; фиксируется факт
PersonRestoredEvent.
- Согласованность. В одном согласованном изменении сохраняются персона (root + восстановленные
документы) и интеграционное событие. Гонку двойной
Self страхует уникальность Self владельца; гонку
лимита — повторная проверка конфликтом версий.
- Результат. Модель
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 |
ValidationError | 400 | Пустой personId | перечень невалидных полей | no retry |
PROFILE_NOT_FOUND | 404 | Профиль не создан | «Профиль ещё создаётся» | retry |
PROFILE_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
PROFILE_MANAGED_EXTERNALLY | 403 | TalentSync | «Данные управляются внешней системой» | no retry |
PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE | 403 | не Investor | «Доступно только инвесторам» | no retry |
PROFILE_PERSON_NOT_FOUND | 404 | Нет персоны / чужая | «Персона не найдена» | no retry |
PROFILE_PERSONS_LIMIT_EXCEEDED | 400 | ≥ 20 активных (INV-PRS-7) | «Достигнут лимит персон (20)» | после архива другой |
PROFILE_PERSON_SELF_ALREADY_EXISTS | 409 | Другая активная Self (INV-PRS-1) | «Основная персона уже существует» | no retry |
PROFILE_CONCURRENCY_CONFLICT | 409 | Конкурентная правка (конфликт версий) | «Данные изменены, повторите» | 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы