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

DELETE /api/v1/favorites/{propertyObjectId} — Удалить объект недвижимости из избранного

Физически удаляет запись Favorite текущего владельца по PropertyObjectId (soft delete не применяется — ADR-0056, ../../domain/favorite.md: история действий живёт в user-activity). Операция идемпотентна: удаление отсутствующей записи — успех 204 без побочных эффектов («уже удалено» — конечное состояние достигнуто).

Назначение

Снятие «сердечка» с карточки объекта недвижимости и кнопка удаления на странице избранного. Ключ операции — PropertyObjectId, а не FavoriteId: на карточке объекта недвижимости клиент знает только объект недвижимости.

Metadata

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

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

ПроверкаЗначение
Actor typeAnonymous (гость по X-Anonymous-Id) или User (JWT)
Auth policy / Permissionsnone (self-scope)
Scope (RBAC)не применяется
Record-levelпредикат удаления жёстко включает владельца — чужую запись удалить невозможно (INV-FAV-005); существование чужой записи не раскрывается (идемпотентный 204)
Tenant isolationне применяется — сущности вне tenant (ADR-0052 п.2)
Auditdelete: факт remove уходит в user-activity событием pin.favorites.favorite-removed.v1

HTTP-контракт

ПолеЗначение
MethodDELETE
Route/api/v1/favorites/{propertyObjectId}
Success status204 (в т.ч. если записи уже не было)
Idempotency headerне применяется — естественная идемпотентность DELETE
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity (JWT claim sub)Guid?Условнозаполнен при авторизованном запросе; приоритет над AnonymousIdвладелец — только сам актор
AnonymousIdheader X-Anonymous-IdGuid?Условновалидный Guid ≠ Guid.Empty; используется при отсутствии JWTнедоверенный вход
PropertyObjectIdroute {propertyObjectId}GuidДа!= Guid.Emptyнедоверенный вход; несуществующий объект недвижимости → идемпотентный 204

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

Тело ответа отсутствует (204 No Content). Ошибки — ProblemDetails.

Алгоритм

  1. Контекст и доступ. Разрешить владельца: UserId из JWT, иначе гостевая сессия AnonymousId из заголовка. Не разрешён ни тот, ни другой — отказ FAVORITES_OWNER_UNRESOLVED.
  2. Проверка входа. PropertyObjectId != Guid.Empty. Существование записи проверяется мягко на шаге 3 (отсутствие — не ошибка); статуса у Favorite нет.
  3. Что читаем. Запись владельца по PropertyObjectId — предикат жёстко включает владельца, поэтому чужая запись невидима (INV-FAV-005). Записи нет → сразу успех (шаг 6) без мутаций и событий.
  4. Бизнес-правила. Guard-ов перехода нет — удаление разрешено из любого состояния записи. Перед удалением поднимается доменное событие FavoriteRemovedEvent (FavoriteId, UserId?, AnonymousId?, PropertyObjectId).
  5. Что меняется. Запись физически удаляется (soft delete не применяется). Конкурентная гонка с параллельным DELETE безопасна: проигравший запрос удалит 0 строк — результат тот же 204. Публикуется FavoriteRemovedKafkaEvent (ContractVersion = 1, topic pin.favorites.favorite-removed.v1, AggregateId = FavoriteId, AggregateType = "Favorite"); payload: FavoriteId, UserId?, AnonymousId?, PropertyObjectId, OccurredAtбез Note. Потребитель — user-activity (ActionEvent «удалил из избранного»). Удаление записи и публикация события фиксируются в одном согласованном изменении. Для идемпотентной ветки (записи не было) изменений нет — событие не публикуется.
  6. Результат. HTTP 204 No Content в обеих ветках (удалено сейчас / уже не было).

Диаграмма

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

ТипДетали
Интеграционные событияFavoriteRemovedKafkaEvent / pin.favorites.favorite-removed.v1 / key = FavoriteId; только если запись существовала
Проекции / поискnone (ADR-0056)

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
FAVORITES_OWNER_UNRESOLVED400Нет ни JWT, ни валидного X-Anonymous-Id«Не удалось определить владельца избранного»no retry
ValidationError400PropertyObjectId = Guid.Empty / невалидный Guid в routeдетали валидации / model bindingno retry
InternalError500Отказ хранилищаgeneric messageretry — операция идемпотентна

Замечание: 404 намеренно не возвращается — отсутствие записи неотличимо от «чужой» записи, чтобы не раскрывать содержимое чужих сессий (Record-level, INV-FAV-005).

Предусловия и постусловия

  • Предусловие. Разрешён владелец (JWT или гость по X-Anonymous-Id); ключ операции — PropertyObjectId (клиент на карточке объекта недвижимости знает только его, не FavoriteId).
  • Постусловие (запись была). Пары «владелец + объект недвижимости» в хранилище больше нет; опубликован факт pin.favorites.favorite-removed.v1; счётчик избранного владельца уменьшился на 1.
  • Постусловие (записи не было). Состояние не изменилось; события не публиковались; конечное состояние «объекта нет в избранном» достигнуто.

Acceptance-критерии

  • Given запись владельца на объект X существует, When DELETE /favorites/{X}, Then запись физически удалена, ответ 204, опубликован FavoriteRemovedKafkaEvent (key = FavoriteId, без Note).
  • Given записи владельца на объект X нет, When DELETE /favorites/{X}, Then ответ 204, событие не публикуется (идемпотентность).
  • Given объект X в избранном другого владельца, When текущий владелец шлёт DELETE /favorites/{X}, Then ответ 204, чужая запись не тронута, событие не публикуется (существование чужой записи не раскрывается, INV-FAV-005).
  • Given запрос без JWT и без валидного X-Anonymous-Id, When DELETE, Then отказ FAVORITES_OWNER_UNRESOLVED (400).
  • Given две параллельные DELETE одной записи, When обе доходят до удаления, Then одна удаляет строку и публикует событие, вторая удаляет 0 строк и возвращает 204 без второго события.

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

  • Контракт минимальный (route-параметр); смена ключа операции (PropertyObjectIdFavoriteId) — breaking.
  • Logs: info FavoriteRemoved {FavoriteId} {owner_type} только при фактическом удалении; no-op — debug.
  • Metrics: favorites_remove_total{owner_type, existed}, latency histogram (SLO p99 < 200 мс).
  • Traces: сквозной correlation; dashboard/alerts/runbook — ../../README.md.

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

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

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