DELETE /api/v1/favorites/{propertyObjectId} — Удалить объект недвижимости из избранного
Физически удаляет запись Favorite текущего владельца по PropertyObjectId (soft delete не применяется —
ADR-0056, ../../domain/favorite.md: история действий живёт в user-activity).
Операция идемпотентна: удаление отсутствующей записи — успех 204 без побочных эффектов
(«уже удалено» — конечное состояние достигнуто).
Назначение
Снятие «сердечка» с карточки объекта недвижимости и кнопка удаления на странице избранного. Ключ операции —
PropertyObjectId, а не FavoriteId: на карточке объекта недвижимости клиент знает только объект недвижимости.
| Поле | Значение |
|---|
| Сервис/контекст | favorites |
| API area | public |
| Feature group | favorites |
| Статус | draft |
| Документ | docs/06-services/favorites/api/public/remove-favorite.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Anonymous (гость по X-Anonymous-Id) или User (JWT) |
| Auth policy / Permissions | none (self-scope) |
| Scope (RBAC) | не применяется |
| Record-level | предикат удаления жёстко включает владельца — чужую запись удалить невозможно (INV-FAV-005); существование чужой записи не раскрывается (идемпотентный 204) |
| Tenant isolation | не применяется — сущности вне tenant (ADR-0052 п.2) |
| Audit | delete: факт remove уходит в user-activity событием pin.favorites.favorite-removed.v1 |
HTTP-контракт
| Поле | Значение |
|---|
| Method | DELETE |
| Route | /api/v1/favorites/{propertyObjectId} |
| Success status | 204 (в т.ч. если записи уже не было) |
| Idempotency header | не применяется — естественная идемпотентность DELETE |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity (JWT claim sub) | Guid? | Условно | заполнен при авторизованном запросе; приоритет над AnonymousId | владелец — только сам актор |
AnonymousId | header X-Anonymous-Id | Guid? | Условно | валидный Guid ≠ Guid.Empty; используется при отсутствии JWT | недоверенный вход |
PropertyObjectId | route {propertyObjectId} | Guid | Да | != Guid.Empty | недоверенный вход; несуществующий объект недвижимости → идемпотентный 204 |
Модель ответа
Тело ответа отсутствует (204 No Content). Ошибки — ProblemDetails.
Алгоритм
- Контекст и доступ. Разрешить владельца:
UserId из JWT, иначе гостевая сессия AnonymousId из
заголовка. Не разрешён ни тот, ни другой — отказ FAVORITES_OWNER_UNRESOLVED.
- Проверка входа.
PropertyObjectId != Guid.Empty. Существование записи проверяется мягко на шаге 3
(отсутствие — не ошибка); статуса у Favorite нет.
- Что читаем. Запись владельца по
PropertyObjectId — предикат жёстко включает владельца, поэтому
чужая запись невидима (INV-FAV-005). Записи нет → сразу успех (шаг 6) без мутаций и событий.
- Бизнес-правила. Guard-ов перехода нет — удаление разрешено из любого состояния записи. Перед
удалением поднимается доменное событие
FavoriteRemovedEvent (FavoriteId, UserId?, AnonymousId?,
PropertyObjectId).
- Что меняется. Запись физически удаляется (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 «удалил из избранного»). Удаление записи и публикация события фиксируются в одном
согласованном изменении. Для идемпотентной ветки (записи не было) изменений нет — событие не публикуется.
- Результат. HTTP
204 No Content в обеих ветках (удалено сейчас / уже не было).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | FavoriteRemovedKafkaEvent / pin.favorites.favorite-removed.v1 / key = FavoriteId; только если запись существовала |
| Проекции / поиск | none (ADR-0056) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
FAVORITES_OWNER_UNRESOLVED | 400 | Нет ни JWT, ни валидного X-Anonymous-Id | «Не удалось определить владельца избранного» | no retry |
ValidationError | 400 | PropertyObjectId = Guid.Empty / невалидный Guid в route | детали валидации / model binding | no retry |
InternalError | 500 | Отказ хранилища | generic message | retry — операция идемпотентна |
Замечание: 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-параметр); смена ключа операции (
PropertyObjectId → FavoriteId) — 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы