← Все проекты уровня 12
Уровень 12 · Поток данныхВариант C

Изменения цен

Нормализованный поток цен с историей, правилами изменения и дедупликацией уведомлений.

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

Модель данных

Создайте сервис мониторинга цен с локальными имитациями поставщиков. Наблюдение содержит provider_id, source_product_id, product_id, currency, amount_minor, source_time, received_at и event_id. Валюта — RUB, USD или EUR; сумма — неотрицательное целое число в копейках, центах или евроцентах соответственно. Отсутствующая, null или некорректная цена не создаёт наблюдение и не заменяется нулём. Нулевое значение допустимо только если поставщик передал числовой ноль явно. Локальный seed создаёт price_admin с фиксированным токеном в Authorization header. Только эта роль сопоставляет source_product_id с product_id через admin endpoint и создаёт правила; по названию товаров автоматическое связывание запрещено. Сравнение истории и порогов выполняется только для одного продукта и валюты.

API и правила

POST /api/v1/providers/{provider_id}/observations принимает пакет до 500 строк. GET /api/v1/products/{product_id}/prices?currency=RUB&from=...&to=... возвращает историю, а GET /api/v1/products/{product_id}/price?currency=RUB возвращает последнюю известную цену с source_time и состоянием freshness. Максимальный диапазон истории — 365 дней, интервалы полуоткрытые. Цена считается stale через 24 часа после source_time; stale запись остаётся последней известной и не исчезает. POST /api/v1/price-rules задаёт product_id, currency, direction, threshold_minor, threshold_percent и cooldown_seconds от 60 до 86400. Достаточно одного порога; percent рассчитывается относительно последней цены в той же валюте и не применяется при базовой цене ноль. Правило не срабатывает на первое наблюдение.

Обработка и доставка

Имитация поставщика читает локальную JSON-фикстуру и умеет задерживать ответ, вернуть 500 или повторить партию. Poller имеет максимум 4 параллельных запроса на процесс, deadline 2 секунды и экспоненциальный backoff 1, 2, 4, 8 секунд с пределом 60 секунд. Каждое наблюдение уникально по provider_id/event_id. Если поставщик не передаёт ID, event_id строится как SHA-256 от канонических provider, source product, валюты, суммы и source_time; две одинаковые реальные покупки в одну секунду считаются одним наблюдением, и это ограничение описано. Переставленное старое событие сохраняется в историю, но не меняет текущую цену. Срабатывание правила и запись outbox атомарно фиксируются с уникальным ключом rule_id/product_id/event_id. Имитация получателя принимает idempotency key; он может упасть, после чего доставщик повторяет запись до 5 раз с интервалами 1, 2, 4, 8 секунд и ставит состояние dead для ручного повтора. Гарантия ровно одной внешней доставки не заявляется.

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

Приём нормализует цены и устраняет повторы; предметный пакет применяет пороги; хранилище ведёт историю, отдельный цикл опрашивает имитации поставщиков и отправляет уведомления. main.go только загружает конфигурацию, связывает зависимости и управляет запуском. Пакеты без циклов, общих utils и интерфейсов без потребителя. README показывает зависимости; тесты ядра без HTTP/БД, интеграционные отдельно.

Хранение и приёмка

PostgreSQL, миграции и ручной SQL обязательны. Очередь обработки ограничена 5000 наблюдениями; при заполнении новая партия не подтверждается. Контрольная позиция источника сохраняется только после фиксации наблюдений. Перезапуск повторяет незавершённую партию с дедупликацией. Офлайн-тесты с фиксированными часами проверяют отсутствующую цену, ноль, валютный конфликт, stale, late observation, повтор ID, backoff, заполнение очереди, outbox и восстановление после перезапуска. Метрики показывают отставание poller, количество невалидных цен, глубину очереди, возраст outbox и dead deliveries. Критерии: неизвестная цена не отображается как ноль, событие хранится максимум один раз, валюты не сравниваются, а повтор обработки сохраняет одну outbox-запись. SQL timeout равен 3 секундам; HTTP-ответы не раскрывают ошибки БД.

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

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

Проверяемый результат

  • README показывает границы и направление зависимостей; main.go только связывает конфигурацию и запуск; модульные тесты ядра обходятся без HTTP и БД, интеграционные тесты отделены.
  • Локальная имитация поставщика загружает фикстуры, задерживает запрос, отвечает ошибкой и воспроизводит ту же партию.
  • История и текущая цена доступны через REST; ответ сообщает валюту, время источника и stale после 24 часов.
  • Пустая цена пропускается, явный ноль хранится как ноль, валюты не сравниваются между собой.
  • Уникальная запись наблюдения и outbox подтверждена миграциями; поздние события не заменяют более новое значение.
  • Локальный тестовый получатель демонстрирует до пяти повторов и состояние dead; API позволяет вручную перезапустить доставку.
  • Офлайн-тесты покрывают очереди, контрольные позиции, правило на нулевой базе, повтор партии и перезапуск.
  • Документация фиксирует пределы параллелизма, интервалы повторов, очередь и SQL timeout, команды запуска и фактический результат сценария отказа.