← Все проекты уровня 7
Уровень 7 · Сервисы с базой данныхВариант B

Учёт склада

Остатки строятся из журнала движений, а повторные команды не создают повторного списания или поступления.

Техническое задание

Модель данных и назначение

Реализуйте REST API складского учёта на Go и PostgreSQL с миграциями и ручным параметризованным SQL, без ORM. Product содержит id, sku, name, integer stock, active и timestamps; sku уникален после trim с нечувствительным к регистру сравнением. StockMovement — неизменяемая запись с id, product_id, kind (receipt или issue), positive integer quantity, request_key, note и created_at. Текущий остаток должен согласованно отражать журнал. Количество не дробное и не может быть отрицательным; арифметика проверяет переполнение допустимого целого диапазона до записи.

API, проверки и эксплуатация

POST /api/products создаёт продукт с остатком 0 и отвечает 201; sku и name обязательны, name длиной 1–160 символов после trim, повтор SKU возвращает 409. GET /api/products принимает limit (default 50, максимум 200), offset и необязательный q для поиска по SKU или имени без учёта регистра; ответ включает items и total и сортирует по sku,id. GET /api/products/{id} возвращает продукт или 404. POST /api/products/{id}/movements принимает kind, quantity, requestKey и note. quantity от 1 до 1,000,000,000, requestKey обязателен и ограничен 128 символами. Один requestKey уникален глобально в пределах сервиса: повтор того же запроса возвращает прежний результат с признаком replay, а использование ключа с другим product/kind/quantity даёт 409 idempotency_conflict.

Изменение журнала и остатка выполняется в одной транзакции. При issue недостаточный остаток даёт 409 insufficient_stock, не добавляя движение. При конкурентных списаниях итоговый остаток никогда не становится отрицательным, а для каждой принятой операции создаётся ровно одно движение. При receipt превышение верхнего лимита возвращает 409 stock_limit. GET /api/products/{id}/movements поддерживает временной диапазон и cursor, возвращая журнал в стабильном порядке created_at,id. Все пользовательские ошибки имеют стабильные коды и статусы: 400 для формы, 404 для отсутствующей записи, 409 для конфликта состояния или уникальности. Ввод неизвестных полей и слишком большое тело отклоняется. Контекст SQL ограничен по времени и отмена запроса прекращает работу; операции с несколькими записями атомарны. Индексы обеспечивают уникальность SKU/requestKey и чтение истории без N+1. Секреты и персональные данные в логах отсутствуют; note не выводится в операционные логи.

Структура программы

Код разделён на небольшие internal-пакеты: internal/httpapi содержит обработчики и DTO, internal/stock — арифметику остатков и идемпотентность движений, internal/postgres — транзакции и SQL. main.go связывает приложение и управляет запуском. DTO не входят в домен; правила тестируются без HTTP, SQL-сценарии отдельно.

Критерии готовности

Ожидаемый результат

Поставка

  • Запускаемый Go REST API, PostgreSQL-миграции и ручные SQL-запросы обеспечивают каталог продуктов, журнал движений и остатки.
  • README документирует ключи идемпотентности, поиск, пагинацию и ограничения количества; тесты используют локальную временную БД.

Приёмка

  • Поступление 12 единиц создаёт одну запись движения и увеличивает остаток ровно на 12.
  • Повтор requestKey с теми же полями не меняет остаток; повтор ключа с другим количеством возвращает 409.
  • Два одновременных списания сверх остатка оставляют остаток неотрицательным и принимают только допустимое количество.
  • Количество 0, отрицательное или дробное даёт 400; недостаточный остаток возвращает 409 без движения.
  • Сбой второй записи транзакции откатывает и журнал, и остаток; тесты не обращаются к сети.
  • README показывает связи пакетов; правила остатка тестируются без HTTP/БД, атомарность SQL — отдельными сценариями.