UC-FAV-003. Заметка к объекту недвижимости в избранном (установка/правка/очистка)
Назначение
Описывает сценарий работы с личной заметкой-комментарием к объекту недвижимости в избранном:
nullable-поле до 4000 символов. Владелец (пользователь или гость) устанавливает, редактирует и очищает
заметку; заметка приватна — видна только владельцу, не попадает в события и логи. Связан с контекстом
favorites.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-FAV-003 |
| Контекст | favorites (см. docs/06-services/favorites/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор / Агент (JWT) или Гость (X-Anonymous-Id) — владелец записи избранного |
| Permissions / scope | self-scope без явных permissions; заметка недоступна агентам/менеджерам/службе заботы (../domain/favorite.md «Permissions / PII») |
| Tenant | не tenanted (ADR-0052 п.2) |
| Record-level | UserId = actor.UserId либо AnonymousId = X-Anonymous-Id — INV-FAV-005; чужая запись неотличима от несуществующей (404) |
Предусловие
- Объект недвижимости уже добавлен в избранное владельца (запись пары «владелец + объект недвижимости» существует —
UC-FAV-001); иначе заметка ставится сразу при добавлении
(
POST /favoritescnote— тот же UC-FAV-001). - Интерфейс показывает страницу
/favoritesсо списком записей (noteуже в ответеGET /favorites).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI (форма заметки) → API call |
| Триггерящий элемент | Страница /favorites → карточка объекта недвижимости → поле «Моя заметка» (сохранение по кнопке/blur) |
Основной Поток
- Владелец открывает
/favorites, раскрывает заметку у объекта недвижимости, вводит/правит текст (интерфейс показывает счётчикN/4000) и сохраняет. - Интерфейс отправляет
PUT /favorites/{propertyObjectId}/noteс теломUpdateFavoriteNoteForm { note: string? }(nullили пустая строка = очистить заметку) + JWT либоX-Anonymous-Id. - Система определяет владельца (JWT приоритетнее заголовка); если нет ни JWT, ни заголовка →
ValidationError. - Проверка входа:
propertyObjectId != Guid.Empty(иначеValidationError); заметка нормализуется (trim, пустая строка →null), длина после trim ≤ 4000 (INV-FAV-003, иначеFAVORITES_NOTE_TOO_LONG). Существование объекта недвижимости не проверяется (INV-FAV-004): заметка живёт на записи избранного, не на объекте. - Система ищет запись избранного владельца по паре (владелец,
PropertyObjectId). Записи нет (или запись чужая) →FAVORITES_NOT_FOUND(404 — заметка без лайка не существует; чужая запись не раскрывается, INV-FAV-005). - Устанавливается заметка: если новое значение равно текущему (включая оба
null) — изменений и событий нет; иначеNote ← нормализованное значение,UpdatedAt ← now(участвует в LastWriterWins при будущем слиянии — ../domain/merge-strategy.md), поднимается доменный фактFavoriteNoteUpdatedEvent(FavoriteId, HasNote: note != null)— без текста заметки (приватные данные). - Конкурентная правка с двух вкладок/устройств разрешается оптимистической блокировкой (поток A2).
- Интеграционные Kafka-события не публикуются: заметка приватна, факт её изменения потребителям не
нужен (
user-activityполучает только add/remove/merge — ../domain/favorite.md «События»). - Возвращается
FavoriteResult:FavoriteId,PropertyObjectId,HasNote ← favorite.Note != null,UpdatedAt. Полный текст заметки отдаёт толькоGET /favoritesвладельцу; в логи текст не пишется (masking-правило README «Безопасность»). - Интерфейс помечает заметку сохранённой и обновляет локальный кэш списка.
Правила нормализации и валидации note
| Вход формы | Нормализация | Результат |
|---|---|---|
null | — | Note = null (очистка, поток A1) |
| Пустая или только пробельная строка | trim → "" → null | Note = null (очистка, поток A1) |
| 1..4000 символов (после trim) | trim | Note = <текст> |
| > 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. Очистка заметки
- Форма передала
note = nullили пустую/пробельную строку → нормализация вnull. - Заметка очищается:
Note ← null,UpdatedAt ← now; запись избранного (лайк) сохраняется — удаляется только заметка. Возврат к основному потоку, шаг 6.
A2. Конкурентная правка с двух вкладок/устройств
- Обе вкладки загрузили запись; первая сохранила изменение; правка второй наталкивается на конфликт оптимистической блокировки.
- Система возвращает 409; интерфейс перечитывает
GET /favorites, показывает актуальную заметку и предлагает повторить правку поверх неё. Завершается (авто-merge текста не выполняется — лимит 4000, INV-FAV-003).
A3. Заметка сразу при добавлении в избранное
- Объект недвижимости ещё не в избранном: интерфейс вызывает
POST /favorites { propertyObjectId, note }— заметка устанавливается вместе с созданием записи (UC-FAV-001, шаг 6). Завершается.
A4. Объект недвижимости удалён из избранного до сохранения заметки
- Запись удалена в другой вкладке (или retention-job для гостя) → шаг 5 не нашёл запись →
FAVORITES_NOT_FOUND(404). - Интерфейс сообщает «объект недвижимости больше не в избранном» и предлагает добавить снова (с текстом из формы — через A3). Завершается.
A5. Судьба заметки при слиянии после входа
- Гостевые заметки участвуют в слиянии по правилам таблицы конфликтов заметок
(../domain/merge-strategy.md): при
Mergeконфликт двух непустых различающихся заметок решается LastWriterWins поUpdatedAtэтой записи. См. UC-FAV-002. Завершается.
Постусловие
Noteзаписи равна нормализованному значению формы (илиnullпри очистке);UpdatedAtобновлён; сам лайк не затронут.- Интеграционные события не публиковались; история действий
user-activityизменения заметки не содержит (приватность). - Заметка доступна только владельцу через
GET /favorites.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | propertyObjectId == Guid.Empty; нет ни JWT, ни X-Anonymous-Id | Исправить запрос / восстановить сессию |
FAVORITES_NOTE_TOO_LONG | 400 | Текст длиннее 4000 символов после trim (INV-FAV-003) | Показать счётчик, не отправлять |
FAVORITES_NOT_FOUND | 404 | Записи пары «владелец + объект недвижимости» нет, либо запись чужая (INV-FAV-005, A4) | Предложить добавить объект недвижимости в избранное с заметкой (A3) |
409 Conflict | 409 | Конкурентная правка, оптимистическая блокировка (A2) | Перечитать список, повторить правку |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
FavoriteNoteUpdatedEvent (FavoriteId, HasNote) | Установка заметки изменила значение | Внутренние (метрики); текст заметки в событие не входит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
| — | — | Не публикуются: заметка — приватные данные владельца, факт правки потребителям не нужен | — |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Заметка 1..4000 символов сохраняется и возвращается владельцу в GET /favorites | интеграционный тест |
| AC2 | 4001 символ → FAVORITES_NOTE_TOO_LONG; ровно 4000 — успех; пробельная строка → null | параметризованный тест нормализации/границы |
| AC3 | Очистка (note = null) удаляет заметку, лайк остаётся (A1) | интеграционный тест |
| AC4 | Правка чужой записи (другой UserId/AnonymousId) → 404, существование не раскрывается | тест record-level изоляции |
| AC5 | Конкурентная правка: вторая вкладка получает 409 по оптимистической блокировке (A2) | конкурентный тест |
| AC6 | Kafka-события при правке заметки не публикуются; текст не появляется в логах | проверка outbox + анализ логов теста |
| AC7 | Повторный PUT с тем же текстом — успех без мутаций (no-op шага 6) | тест идемпотентности |
| AC8 | Гость правит заметку своей сессии по X-Anonymous-Id наравне с пользователем | интеграционный тест без JWT |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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 (простота нагрузки).