Skip to content

MaxSkr0210/tola-test-task

Repository files navigation

Backend Test Task

Полная постановка задания находится в TASK.md.

Быстрый старт

docker compose up --build

API: http://localhost:3000

Полезные команды

docker compose exec api npm run migrate
docker compose exec api npm test
docker compose down -v

Что сдавать

  • ссылка на репозиторий или ветку с изменениями;
  • обновленный README.md с разделом Анализ и рефакторинг (ответы на 7 вопросов из TASK.md);
  • инструкция запуска тестов и подтверждение, что обязательные сценарии проходят.

Анализ и рефакторинг

1. Какие проблемы были в исходной реализации списания?

  • Учёт баланса: Считалась сумма всех начислений (accrual) без учёта expires_at и без вычета списаний (spend), то есть доступный баланс был неверным.
  • Гонки: Не было транзакции и блокировки строк — при параллельных запросах баланс мог уйти в минус или произойти двойное списание.
  • Идемпотентность: Не было идентификатора запроса (requestId / Idempotency-Key), повтор запроса создавал второе списание.
  • Просроченные начисления: В баланс не закладывалось правило «просроченное начисление не участвует в балансе».

2. Какие проблемы при одновременных запросах вы устранили и как именно?

  • Отрицательный баланс: В одной транзакции делается SELECT ... FOR UPDATE по строке пользователя в users, затем пересчитывается доступный баланс (только не просроченные начисления минус списания) и создаётся списание. Параллельные запросы по одному пользователю выполняются последовательно на уровне блокировки строки, поэтому баланс не уходит в минус.
  • Двойное списание по одному и тому же запросу: Перед списанием проверяется наличие уже существующего списания с тем же (user_id, request_id). Если есть — возвращается успех с duplicated: true без нового списания. Уникальный индекс по (user_id, request_id) в БД страхует от дубликатов при гонке.

3. Какие обязательные правила корректности вы обеспечили на уровне БД и на уровне кода?

  • БД: Частичный уникальный индекс (user_id, request_id) WHERE request_id IS NOT NULL — не допускает двух списаний с одной парой (user, requestId).
  • Код: Расчёт доступного баланса только по начислениям с expires_at IS NULL OR expires_at > NOW() минус сумма списаний; в одной транзакции блокировка пользователя → проверка идемпотентности → проверка баланса → вставка списания; обязательный requestId (из тела или заголовка Idempotency-Key); при том же requestId и другом amount — ответ 409.

4. Как реализована защита от повторного одинакового запроса (duplicate request)?

  • Идентификатор берётся из body.requestId или заголовка Idempotency-Key (заголовок приоритетнее). Область идемпотентности — пара (user_id, requestId).
  • Перед списанием ищется существующее списание с такой парой. Если найдено и amount совпадает — возвращается 200 { success: true, duplicated: true }. Если amount другой — 409. Если не найдено — выполняется списание и возвращается 200 { success: true, duplicated: false }.
  • Уникальный индекс по (user_id, request_id) в БД страхует от дубликатов при гонке.

5. Как обеспечено, что просроченное начисление (expired accrual) не участвует в балансе?

  • При расчёте доступного баланса учитываются только начисления с expires_at IS NULL OR expires_at > NOW(). Та же логика используется внутри транзакции списания при проверке достаточности баланса.

6. Как реализована надежная обработка очереди (повторные попытки, паузы, защита от дублей)?

  • Повторные попытки: При постановке задачи заданы attempts: 3 и backoff: { type: 'exponential', delay: 1000 } (не менее 1000 мс).
  • Предсказуемый jobId: Задача ставится с jobId: 'expire-accruals'.
  • Защита от дублей: Для каждого просроченного начисления создаётся списание с request_id = "expire:<accrual_id>". Перед созданием проверяется наличие такой записи; при наличии списание не создаётся. Повторный запуск джобы не даёт повторного списания по одному начислению.

7. Какие компромиссы вы приняли в рамках ограничения 4–6 часов?

  • Добавлена только новая миграция 003; в down не восстанавливается старый глобальный уникальный индекс по request_id.
  • Защита от дублей очереди проверена на уровне бизнес-логики (идемпотентное создание списания); в тестах не поднимается полный воркер BullMQ.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors