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

POST /api/v1/admin/portfolio/properties/{propertyId}/archive — Архивирование записи портфеля (admin, archiveInvestorProperty)

Назначение

Компенсирующая admin-операция Службы заботы: мягкое архивирование (soft-delete) ошибочной записи портфеля инвестора. Основной сценарий — разбор дублей INV-1 SingleActivePerLot: у инвестора оказались две Active-записи на один объект недвижимости (одна из синхронизации Talent Owners, вторая из завершённой сделки PIN); оператор архивирует лишнюю, после чего событие переигрывается из DLQ (см. оба consumer-а: ../../consumers/crm-deal-completed.md, ../../consumers/talent-owners-sync.md). Также применяется к исправлению любых ошибочных записей (INV-9 ImmutablePurchase: цены/даты покупки иммутабельны — правка только через архивирование + новая запись create-investor-property.md).

Физическое удаление запрещено: запись помечается DeletedAt и остаётся в БД (история заработка, аудит). Дом полей — ../../domain/investor-property.md.

Metadata

ПолеЗначение
Сервис/контекстinvestor-owner
API areaadmin (Служба заботы)
Feature groupportfolio
Статусdraft
Документdocs/06-services/investor-owner/api/admin/archive-investor-property.md

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

ПроверкаЗначение
Actor typeOperator(Admin) (Служба заботы, Bearer JWT)
Auth policy / Permissionsauthenticated; permission investor-owner.portfolio.manage (../../../identity/domain/permission.md)
Scope (RBAC)платформенный (не self): оператор архивирует запись любого инвестора
Record-levelне применяется: доступ гейтит permission portfolio.manage; запись читается по Id без фильтра владельца
Tenant isolationне применяется — инвестор вне tenant (ADR-0052)
Auditdelete (привилегированная правка): DeletedByUserId = actor + Reason + доменное событие InvestorPropertyArchived; deep-audit доступа к чужим финансовым данным — ADR-0053

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/admin/portfolio/properties/{propertyId}/archive
Success status204 (No Content); идемпотентный повтор на уже архивированной — тоже 204
Idempotency headerне применяется; доменная идемпотентность — повторный архив = no-op
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДавалидный JWT — 401 до операции; → DeletedByUserIdid оператора заботы
PropertyIdroute {propertyId}GuidДа!= Guid.Empty (иначе 404 route-binding / ValidationError)id записи портфеля, не PropertyObjectId
ReasonbodystringДанепустой после trim, 1..512; фиксируется в аудит (доменное событие)операционный текст, не PII

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

Полезной нагрузки нет: успех отдаёт 204 No Content. Признак «уже была архивирована» наружу не выводится (идемпотентность прозрачна для оператора).

Алгоритм

  1. Контекст и доступ. Операцию выполняет оператор Службы заботы (ActorUserId — из claim sub); требуется permission investor-owner.portfolio.manage (иначе 403); scope платформенный (не self).
  2. Проверка входа и предусловий. PropertyId != Guid.Empty; Reason непустой после trim, длина 1..512 (обязателен для аудита) — иначе ValidationError. Найти запись портфеля по PropertyId, включая уже архивированные (для идемпотентности повторного архива); если записи нет → INVESTOR_OWNER_PROPERTY_NOT_FOUND. Статус не проверяется — архивируется запись в любом статусе (Active и Sold; переход soft-delete из обоих — ../../domain/investor-property.md).
  3. Идемпотентность. Если запись уже архивирована (DeletedAt задан) → идемпотентный no-op: лог skipped-already-archived, метрика investor_owner_property_archive_noop_total, операция завершается 204 без мутации и события.
  4. Архивирование. Мягкое удаление: DeletedAt = now(), DeletedByUserId = ActorUserId, Reason фиксируется, версия записи растёт. Физического удаления нет. Фиксируется доменное событие InvestorPropertyArchived (Id, InvestorUserId, PropertyObjectId, Reason, DeletedByUserId) — только внутренний аудит. Публичного Kafka-события об удалении НЕТ (property-removed не объявлен) — осознанное решение минимальности (ADR-0056): архив — редкая компенсирующая правка заботы; потребители портфеля (notifications, user-activity) рассылку об удалении не получают, реконсиляция — через replay источника.
  5. Результат. 204 (включая идемпотентный no-op). Ошибки — таблица ниже.

Диаграмма

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

ТипДетали
Интеграционные событияnone — Kafka-событие об удалении не публикуется (осознанно, ADR-0056; property-removed не существует)
Внешние вызовыnone
Проекции / поискnone (частичный индекс ux_investor_properties_investor_property_object_active where deleted_at is null — архив освобождает объект недвижимости под INV-1)
Уведомленияnone
Реконсиляцияпосле архива дубля INV-1 — точечный replay из DLQ соответствующего consumer-а (архивированная по DealId/ExternalOwnershipId запись синхронизацией/сделкой не пересоздаётся); runbook «компенсирующее исправление записи портфеля»

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен/невалиден JWTбез тела схемыre-login
ValidationError400PropertyId пустой/не Guid; Reason пустой/длиннее 512Список ошибок полейno retry
FORBIDDEN403Нет permission investor-owner.portfolio.manage«Недостаточно прав»no retry
INVESTOR_OWNER_PROPERTY_NOT_FOUND404Записи с PropertyId нет (в т.ч. уже архивированной)«Объект портфеля не найден»no retry
— (идемпотентный no-op, не ошибка)204DeletedAt задан (уже архивирована)без тела
— (concurrency)409Гонка с параллельной правкой записи«Повторите попытку»retry (перечитать → повтор)

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

  • Reason обязателен с v1; добавление optional-полей body — additive; удаление/смена типа — breaking (rollout admin-UI). Отсутствие property-removed-события — сознательный контрактный минимум; его введение (если понадобится реконсиляция без replay) — additive новый топик pin.investor-owner.*.
  • Logs: archived / skipped-already-archived / not-found с полями PropertyId, InvestorUserId, PropertyObjectId, DeletedByUserId, Reason (операционный текст) — без сумм (финансовые данные, правило дома ../../domain/investor-property.md); metrics: investor_owner_property_archived_total, investor_owner_property_archive_noop_total, latency histogram; traces — сквозной trace через traceparent; dashboard investor-owner-overview; runbook «компенсирующее исправление записи портфеля».

Acceptance Criteria

  • Given оператор заботы и существующая не-архивированная запись (в любом статусе Active/Sold), When POST /archive c непустым Reason, Then 204, запись помечена DeletedAt/DeletedByUserId, Reason зафиксирован, версия выросла, публикуется доменное InvestorPropertyArchived; Kafka-события об удалении НЕ уходит.
  • Given запись уже архивирована (DeletedAt задан), When повторный POST /archive, Then 204 (идемпотентный no-op): без мутации, без события, лог skipped-already-archived, инкремент investor_owner_property_archive_noop_total.
  • Given архивирование одной из двух Active-записей по объекту (дубль INV-1), When операция успешна, Then частичный индекс ux_investor_properties_investor_property_object_active освобождает объект под INV-1, и точечный replay из DLQ соответствующего consumer-а проходит (архивированная по DealId/ExternalOwnershipId запись синхронизацией/сделкой НЕ пересоздаётся).
  • Given propertyId не существует, When POST /archive, Then INVESTOR_OWNER_PROPERTY_NOT_FOUND (404).
  • Given пустой/невалидный propertyId или Reason пустой/длиннее 512, When запрос, Then ValidationError (400).
  • Given отсутствует investor-owner.portfolio.manage, When запрос, Then 403; невалидный JWT — 401. Доступ к чужим финансовым данным фиксируется deep-audit (ADR-0053).
  • Given параллельная правка той же записи, When архивирование, Then проигравший получает 409 (concurrency) и повторяет после перечитки.

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

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

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