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

0027. Единая модель отзывов и вопросов (autoreply)

Копия ADR из репозитория: backend/docs/adr/0027-autoreply-feedback-model.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-06-26
  • Связанные задачи: OP-71 (5.5 · Автоответы на отзывы и вопросы)

Контекст и проблема​

ТЗ 5.5: импортировать из Wildberries и хранить отзывы и вопросы покупателей. Описание тикета OP-71 называет «Сущности Review/Question». WB отдаёт их разными эндпоинтами (GET /api/v1/feedbacks, GET /api/v1/questions), но структура элементов почти идентична: id, text, createdDate, state, answer, productDetails (артикул/бренд/товар), wasViewed. Отличие — только рейтинг (productValuation) есть у отзывов и отсутствует у вопросов.

Вопрос: моделировать двумя таблицами (Review + Question) или одной с дискриминатором?

Рассмотренные варианты​

ВариантОписаниеОценка
Единая FeedbackEntity + kind (выбрано)Одна таблица autoreply_feedbacks, поле kind = review | question, rating nullable (только отзывы)Нет дублирования таблицы/миграции/репозитория; общий импорт и upsert; downstream-модули (сценарии, генерация, отправка) работают с одной сущностью
Две сущности Review/QuestionОтдельные таблицы и репозиторииДублирование ~90% полей, импорта и индексов; два пути upsert; усложняет последующие фичи (выборка «всей обратной связи по артикулу»)
Единая таблица + JSON-payloadХранить сырой ответ WB в jsonbТеряется типизация и индексы по полям (артикул, рейтинг, статус)

Решение​

  • Одна таблица autoreply_feedbacks с kind (FeedbackKind), идемпотентность — UNIQUE (wb_account_id, kind, wb_id).
  • Общие поля для обоих типов; rating nullable заполняется только для отзывов.
  • Импорт обоих видов — одной generic-функцией (importKind) с разным путём/экстрактором/маппером.

Последствия​

  • Downstream-тикеты автоответов (OP-72..OP-75) строятся на единой сущности.
  • Known limitation: OP-71 импортирует только неотвеченные (isAnswered=false) отзывы/вопросы, поэтому answered/answer_text при текущем единственном пути импорта всегда false/null. Эти поля — задел под отправку ответов (OP-74), которая будет проставлять статус ответа; импорт уже отвеченной обратной связи — отдельная задача.
  • При расхождении жизненного цикла отзывов и вопросов в будущем (разные поля или переходы статусов) модель можно разнести — стоимость миграции учтена осознанно.