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

DELETE /api/v1/profile/persons/{personId}/documents/{documentId}/scans/{fileId} — Удаление скана документа (removeDocumentScan)

Назначение

Отвязать скан от документа персоны: элемент удаляется из открытого списка сканов ScanList (person-document.md). Сам файл в storage немедленно не удаляется — он может фигурировать в иммутабельных снапшотах (сделки CRM, KYC-заявки); очистку нессылаемых файлов выполняет retention-политика storage. Вызывает инвестор-владелец документа.

Metadata

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

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

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

HTTP-контракт

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

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

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

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

Дом модели — list-person-documents.md; документ с обновлённым scans.

Алгоритм

  1. Контекст и доступ. Операцию выполняет инвестор (UserId из токена); tenant не применяется; требуется право profile.documents.manage-self.
  2. Проверка входа. personId/documentId/fileId заданы. Нарушение → 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). Персона принадлежит владельцу; иначе PROFILE_PERSON_NOT_FOUND. Документ documentId принадлежит персоне; иначе PROFILE_DOCUMENT_NOT_FOUND; документ активен (Status = Active), иначе PROFILE_DOCUMENT_ARCHIVED. Если fileId отсутствует в ScanList — идемпотентный успех без изменений (шаг 5).
  4. Бизнес-правила и переходы. Скан fileId удаляется из ScanList документа; у документа UpdatedByUserId = UserId, Version++; у персоны Version++; фиксируется факт PersonDocumentUpdatedEvent(PersonId, DocumentId, ["ScanList"]). В одном согласованном изменении сохраняются изменение документа и интеграционное событие. Конкурентная правка (конфликт версий) → PROFILE_CONCURRENCY_CONFLICT.
  5. Результат. Модель PersonDocumentModel с обновлённым scans; HTTP 200. Storage-вызовов нет (файл остаётся до retention).

Диаграмма

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

ТипДетали
Интеграционные событияPersonUpsertedKafkaEvent / pin.profile.persons.v1 / key = PersonId (без PII)

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен JWTбез телаre-login
ValidationError400Пустые route-idперечень невалидных полей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_DOCUMENT_NOT_FOUND404Нет документа«Документ не найден»no retry
PROFILE_DOCUMENT_ARCHIVED400Документ в архиве«Документ в архиве»no retry
PROFILE_CONCURRENCY_CONFLICT409Конкурентная правка (конфликт версий)«Данные изменены, повторите»retry

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

  • Семантика стабильна; расширения — additive.
  • Logs: UserId, DocumentId, FileId; metrics: profile_api_operations_total{operation="removeDocumentScan",result}; traces — OTel.

Acceptance Criteria

  • Given активный документ владельца, содержащий скан fileId, When DELETE, Then 200; скан удалён из ScanList, у документа и персоны Version++; опубликован PersonUpsertedKafkaEvent (pin.profile.persons.v1, без PII); файл в storage не удаляется (retention).
  • Given fileId отсутствует в ScanList, When DELETE, Then 200 идемпотентный no-op; без изменений и без события.
  • Given документ в архиве, When DELETE, Then PROFILE_DOCUMENT_ARCHIVED (400).
  • Given personId/documentId чужие, When DELETE, Then PROFILE_PERSON_NOT_FOUND / PROFILE_DOCUMENT_NOT_FOUND (404).
  • Given профиль заблокирован / TalentSync / UserType != Investor, When DELETE, Then PROFILE_USER_BLOCKED (400) / PROFILE_MANAGED_EXTERNALLY (403) / PROFILE_PERSONS_NOT_ALLOWED_FOR_USER_TYPE (403) соответственно.
  • Given конкурентная правка документа (конфликт версий), When DELETE, Then PROFILE_CONCURRENCY_CONFLICT (409), retry.

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

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

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