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

UC-FAV-003. Заметка к объекту недвижимости в избранном (установка/правка/очистка)

Назначение

Описывает сценарий работы с личной заметкой-комментарием к объекту недвижимости в избранном: nullable-поле до 4000 символов. Владелец (пользователь или гость) устанавливает, редактирует и очищает заметку; заметка приватна — видна только владельцу, не попадает в события и логи. Связан с контекстом favorites.

Metadata

ПолеЗначение
IDUC-FAV-003
Контекстfavorites (см. docs/06-services/favorites/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор / Агент (JWT) или Гость (X-Anonymous-Id) — владелец записи избранного
Permissions / scopeself-scope без явных permissions; заметка недоступна агентам/менеджерам/службе заботы (../domain/favorite.md «Permissions / PII»)
Tenantне tenanted (ADR-0052 п.2)
Record-levelUserId = actor.UserId либо AnonymousId = X-Anonymous-Id — INV-FAV-005; чужая запись неотличима от несуществующей (404)

Предусловие

  • Объект недвижимости уже добавлен в избранное владельца (запись пары «владелец + объект недвижимости» существует — UC-FAV-001); иначе заметка ставится сразу при добавлении (POST /favorites c note — тот же UC-FAV-001).
  • Интерфейс показывает страницу /favorites со списком записей (note уже в ответе GET /favorites).

Триггер

ПолеЗначение
ИсточникUI (форма заметки) → API call
Триггерящий элементСтраница /favorites → карточка объекта недвижимости → поле «Моя заметка» (сохранение по кнопке/blur)

Основной Поток

  1. Владелец открывает /favorites, раскрывает заметку у объекта недвижимости, вводит/правит текст (интерфейс показывает счётчик N/4000) и сохраняет.
  2. Интерфейс отправляет PUT /favorites/{propertyObjectId}/note с телом UpdateFavoriteNoteForm { note: string? } (null или пустая строка = очистить заметку) + JWT либо X-Anonymous-Id.
  3. Система определяет владельца (JWT приоритетнее заголовка); если нет ни JWT, ни заголовка → ValidationError.
  4. Проверка входа: propertyObjectId != Guid.Empty (иначе ValidationError); заметка нормализуется (trim, пустая строка → null), длина после trim ≤ 4000 (INV-FAV-003, иначе FAVORITES_NOTE_TOO_LONG). Существование объекта недвижимости не проверяется (INV-FAV-004): заметка живёт на записи избранного, не на объекте.
  5. Система ищет запись избранного владельца по паре (владелец, PropertyObjectId). Записи нет (или запись чужая) → FAVORITES_NOT_FOUND (404 — заметка без лайка не существует; чужая запись не раскрывается, INV-FAV-005).
  6. Устанавливается заметка: если новое значение равно текущему (включая оба null) — изменений и событий нет; иначе Note ← нормализованное значение, UpdatedAt ← now (участвует в LastWriterWins при будущем слиянии — ../domain/merge-strategy.md), поднимается доменный факт FavoriteNoteUpdatedEvent(FavoriteId, HasNote: note != null)без текста заметки (приватные данные).
  7. Конкурентная правка с двух вкладок/устройств разрешается оптимистической блокировкой (поток A2).
  8. Интеграционные Kafka-события не публикуются: заметка приватна, факт её изменения потребителям не нужен (user-activity получает только add/remove/merge — ../domain/favorite.md «События»).
  9. Возвращается FavoriteResult: FavoriteId, PropertyObjectId, HasNote ← favorite.Note != null, UpdatedAt. Полный текст заметки отдаёт только GET /favorites владельцу; в логи текст не пишется (masking-правило README «Безопасность»).
  10. Интерфейс помечает заметку сохранённой и обновляет локальный кэш списка.

Правила нормализации и валидации note

Вход формыНормализацияРезультат
nullNote = null (очистка, поток A1)
Пустая или только пробельная строкаtrim → ""nullNote = null (очистка, поток A1)
1..4000 символов (после trim)trimNote = <текст>
> 4000 символов (после trim)trim, проверка длиныFAVORITES_NOTE_TOO_LONG (400, INV-FAV-003)
  • Длина считается в символах Unicode (char_length), не в байтах (граница ровно 4000 — успех, 4001 — отказ).
  • Повтор PUT с уже сохранённым значением (включая оба null) — no-op (шаг 6): без мутаций, без обновления UpdatedAt, без доменного события.
  • note — единственное правимое поле; сам лайк (PropertyObjectId, владелец, CreatedAt) через этот метод не меняется. UpdatedAt после реальной правки участвует в LastWriterWins при будущем слиянии (../domain/merge-strategy.md, таблица конфликтов заметок).

Альтернативные Потоки

A1. Очистка заметки

  1. Форма передала note = null или пустую/пробельную строку → нормализация в null.
  2. Заметка очищается: Note ← null, UpdatedAt ← now; запись избранного (лайк) сохраняется — удаляется только заметка. Возврат к основному потоку, шаг 6.

A2. Конкурентная правка с двух вкладок/устройств

  1. Обе вкладки загрузили запись; первая сохранила изменение; правка второй наталкивается на конфликт оптимистической блокировки.
  2. Система возвращает 409; интерфейс перечитывает GET /favorites, показывает актуальную заметку и предлагает повторить правку поверх неё. Завершается (авто-merge текста не выполняется — лимит 4000, INV-FAV-003).

A3. Заметка сразу при добавлении в избранное

  1. Объект недвижимости ещё не в избранном: интерфейс вызывает POST /favorites { propertyObjectId, note } — заметка устанавливается вместе с созданием записи (UC-FAV-001, шаг 6). Завершается.

A4. Объект недвижимости удалён из избранного до сохранения заметки

  1. Запись удалена в другой вкладке (или retention-job для гостя) → шаг 5 не нашёл запись → FAVORITES_NOT_FOUND (404).
  2. Интерфейс сообщает «объект недвижимости больше не в избранном» и предлагает добавить снова (с текстом из формы — через A3). Завершается.

A5. Судьба заметки при слиянии после входа

  1. Гостевые заметки участвуют в слиянии по правилам таблицы конфликтов заметок (../domain/merge-strategy.md): при Merge конфликт двух непустых различающихся заметок решается LastWriterWins по UpdatedAt этой записи. См. UC-FAV-002. Завершается.

Постусловие

  • Note записи равна нормализованному значению формы (или null при очистке); UpdatedAt обновлён; сам лайк не затронут.
  • Интеграционные события не публиковались; история действий user-activity изменения заметки не содержит (приватность).
  • Заметка доступна только владельцу через GET /favorites.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400propertyObjectId == Guid.Empty; нет ни JWT, ни X-Anonymous-IdИсправить запрос / восстановить сессию
FAVORITES_NOTE_TOO_LONG400Текст длиннее 4000 символов после trim (INV-FAV-003)Показать счётчик, не отправлять
FAVORITES_NOT_FOUND404Записи пары «владелец + объект недвижимости» нет, либо запись чужая (INV-FAV-005, A4)Предложить добавить объект недвижимости в избранное с заметкой (A3)
409 Conflict409Конкурентная правка, оптимистическая блокировка (A2)Перечитать список, повторить правку

Доменные События

СобытиеКогда поднимаетсяSubscribers
FavoriteNoteUpdatedEvent (FavoriteId, HasNote)Установка заметки изменила значениеВнутренние (метрики); текст заметки в событие не входит

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
Не публикуются: заметка — приватные данные владельца, факт правки потребителям не нужен

Acceptance Criteria

#КритерийПроверка
AC1Заметка 1..4000 символов сохраняется и возвращается владельцу в GET /favoritesинтеграционный тест
AC24001 символ → FAVORITES_NOTE_TOO_LONG; ровно 4000 — успех; пробельная строка → nullпараметризованный тест нормализации/границы
AC3Очистка (note = null) удаляет заметку, лайк остаётся (A1)интеграционный тест
AC4Правка чужой записи (другой UserId/AnonymousId) → 404, существование не раскрываетсятест record-level изоляции
AC5Конкурентная правка: вторая вкладка получает 409 по оптимистической блокировке (A2)конкурентный тест
AC6Kafka-события при правке заметки не публикуются; текст не появляется в логахпроверка outbox + анализ логов теста
AC7Повторный PUT с тем же текстом — успех без мутаций (no-op шага 6)тест идемпотентности
AC8Гость правит заметку своей сессии по X-Anonymous-Id наравне с пользователеминтеграционный тест без JWT

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

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

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

  • API метод: ../api/public/update-favorite-note.md.
  • Domain entity: ../domain/favorite.md — поле Note, INV-FAV-003/005, приватность заметки.
  • Слияние заметок: ../domain/merge-strategy.md (таблица конфликтов), UC-FAV-002.
  • Добавление с заметкой: UC-FAV-001.
  • Архитектурные решения: ADR-0052 (tenant), ADR-0056 (простота нагрузки).