0028. Отправка ответов автоответчика (статусы, авто-режим, идемпотентность)
Копия ADR из репозитория:
backend/docs/adr/0028-autoreply-answer-send.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-06-26
- Связанные задачи: OP-74 (5.5 · Автоответы на отзывы и вопросы)
Контекст и проблема
ТЗ 5.5: ответы на отзывы/вопросы публикуются на Wildberries в ручном и автоматическом режимах, с фиксацией статуса, ретраями и аудитом. Публикация ответа на WB — необратимое публичное действие (повторная отправка создаёт дубль/конфликт). Вопросы:
- Где хранить статус отправки и как им управлять?
- Как реализовать авто-режим, не создавая двойных публикаций?
- Как обеспечить ретраи и идемпотентность при at-least-once доставке джоб?
Решение
Статусная модель — на черновике
Жизненный цикл хранится на autoreply_drafts (status: draft → pending →
sent/error), а не в отдельной сущности: черновик и его отправка — один
объект (1:1 с обращением). Регенерация (OP-73 upsert) сбрасывает статус в
draft и очищает sent_at/send_error/send_attempts — новый текст считается
неотправленным.
Очередь + ретраи
Отправка — через BullMQ-очередь autoreply-answer-send (attempts=3,
exponential backoff). Воркер постит ответ (отзыв — POST /feedbacks/answer,
вопрос — PATCH /questions), при ошибке пробрасывает исключение для ретрая.
Статус error ставится только на финальной попытке (finalAttempt из
job.attemptsMade), между попытками — pending (чтобы UI не показывал ложный
финал).
Идемпотентность (против двойной публикации)
enqueue:sent→409 Conflict(ручной путь) / пропуск (авто);pending→ без дублирующей джобы.sendDraft(воркер): если статус ужеsent— выходит без вызова WB (защита от повторной доставки джобы).
Авто-режим
Режим берётся из сценария (ReplyMode). При auto генерация (OP-73) после
сохранения черновика ставит его в очередь отправки; при manual — ждёт
явного POST /autoreply/send.
Последствия
- Один текущий черновик/ответ на обращение; история отправок — отдельная задача (OP-76).
- Авто-публикация необратима: идемпотентные гарды критичны и покрыты тестами.
- Аудит (F6) пишется на каждую успешную/неуспешную отправку.
errorмежду ретраями не финализируется — потребитель отличает «упало, будет ретрай» (pending) от «упало окончательно» (error).