REST-сервис для агрегации данных об онлайн-подписках пользователей.
- CRUDL по подпискам: название сервиса, стоимость месяца в рублях,
user_id(UUID), дата начала и опциональная дата окончания. - Даты передаются с точностью до месяца в формате
MM-YYYY(например,07-2025). - Отдельная ручка считает суммарную стоимость подписок за период с фильтрами по пользователю и названию сервиса.
- Swagger-документация:
http://localhost:8080/swagger/index.html. - Два хранилища на выбор при запуске:
postgresиlocal(in-memory).
cp .env.example .env
make builddocker compose поднимет Postgres, дождётся его готовности, накатит миграции
(сервис migrator на базе goose) и только после этого запустит сервис.
Отдельных шагов для миграций не нужно.
Сервис поднимется на http://localhost:8080, Swagger UI — на
http://localhost:8080/swagger/index.html.
Базовый префикс — /subscriptions. Все ответы в JSON, ошибки имеют вид
{ "error": "описание" }.
{
"service_name": "Yandex Plus",
"price": 400,
"user_id": "60601fee-2bf1-4721-ae6f-7636e79a0cba",
"start_date": "07-2025"
}end_date опционален: если его нет, подписка считается бессрочной.
Ответы:
201 Created— созданная запись400 Bad Request— невалидное тело, невалидный UUID, неверный формат даты, отрицательная цена илиend_dateраньшеstart_date500 Internal Server Error
{
"id": "9b7c2f5e-1c1a-4c2b-9d3e-5f6a7b8c9d0e",
"service_name": "Yandex Plus",
"price": 400,
"user_id": "60601fee-2bf1-4721-ae6f-7636e79a0cba",
"start_date": "07-2025",
"created_at": "2026-07-25T10:45:00Z",
"updated_at": "2026-07-25T10:45:00Z"
}200 OK— запись400 Bad Request—idне UUID404 Not Found—{ "error": "subscription not found" }
Тело совпадает с телом создания и полностью заменяет запись.
200 OK— обновлённая запись400 Bad Request/404 Not Found
204 No Content400 Bad Request/404 Not Found
Query-параметры (все опциональны):
| Параметр | Описание |
|---|---|
user_id |
Фильтр по пользователю (UUID) |
service_name |
Фильтр по названию сервиса, без учёта регистра |
limit |
Размер страницы, 1..500, по умолчанию 50 |
offset |
Смещение, по умолчанию 0 |
{
"items": [ { "id": "...", "service_name": "Yandex Plus", "price": 400, "...": "..." } ],
"count": 1,
"limit": 50,
"offset": 0
}| Параметр | Обязательный | Описание |
|---|---|---|
from |
да | Начало периода, MM-YYYY |
to |
да | Конец периода, MM-YYYY |
user_id |
нет | Фильтр по пользователю |
service_name |
нет | Фильтр по названию сервиса, без учёта регистра |
curl "http://localhost:8080/subscriptions/cost?from=07-2025&to=12-2025&service_name=Yandex%20Plus"{ "total": 2400, "currency": "RUB", "from": "07-2025", "to": "12-2025" }400 Bad Request— отсутствует или неверен форматfrom/to, либоtoраньшеfrom
Проверяет доступность хранилища (для postgres — пинг пула соединений).
200 OK—{ "status": "ok" }503 Service Unavailable— хранилище недоступно
Период [from, to] включает обе границы. Для каждой подписки берётся пересечение
её срока действия с периодом, и стоимость умножается на число месяцев в этом
пересечении:
months = min(end_date, to) - max(start_date, from) + 1
total = Σ price × months
Подписка без end_date считается активной до конца периода, подписки, не
пересекающиеся с периодом, дают 0.
Пример: подписка за 400 ₽ с start_date = 07-2025 без даты окончания при запросе
периода 07-2025 … 12-2025 даёт 400 × 6 = 2400.
В Postgres расчёт выполняется одним запросом (GREATEST/LEAST по границам
периода), в in-memory хранилище — той же формулой в Go, поэтому оба хранилища
дают одинаковый результат.
cp .env.example .env # при необходимости отредактировать, STORAGE=postgres
make buildВ .env выставить STORAGE=local — Postgres в этом режиме не используется,
но контейнер БД всё равно поднимется (можно остановить: docker compose stop db).
export $(cat .env | xargs)
go run ./cmdДля локального запуска с Postgres сначала поднимите базу и накатите миграции:
docker compose up -d db
sh migration/migrations.sh --up| Переменная | Описание |
|---|---|
SERVER_PORT |
Порт HTTP-сервера |
STORAGE |
postgres или local |
LOG_LEVEL |
debug, info, warn или error |
DATABASE_HOST |
Хост Postgres (при STORAGE=postgres) |
DATABASE_PORT |
Порт Postgres |
DATABASE_NAME |
Имя БД |
DATABASE_USER |
Пользователь |
DATABASE_PASSWORD |
Пароль |
Описание команд — в migration/MIGRATION.md.
Документация генерируется из аннотаций в коде через swaggo/swag:
go install github.com/swaggo/swag/cmd/swag@latest
make swaggerГотовые docs/swagger.json и docs/swagger.yaml лежат в репозитории,
UI доступен на /swagger/index.html.
Логи структурированные, log/slog в JSON. Каждому запросу присваивается
X-Request-ID (или переиспользуется из заголовка), по которому связываются
запись о запросе и сообщения об ошибках. Уровень задаётся через LOG_LEVEL.
{"time":"2026-07-25T10:45:00Z","level":"INFO","msg":"request completed","request_id":"...","method":"POST","path":"/subscriptions","status":201,"duration_ms":3}make up # docker compose up -d
make build # docker compose up -d --build
make down # docker compose down
make clean # down -v --rmi all
make test # go test -v ./...
make smoke # сквозная проверка API на поднятом сервисе
make swagger # перегенерировать swagger-документацию
make migrate-up # накатить миграции локально
make migrate-status # статус миграцийgo test ./...Покрыты: конфигурация, разбор и арифметика дат MM-YYYY, бизнес-правила сервиса
(валидация периода, нормализация пагинации), HTTP-слой (коды ответов,
валидация запросов, маршрутизация /subscriptions/cost мимо /subscriptions/:id)
и in-memory репозиторий, включая расчёт стоимости и конкурентный доступ.
Сквозная проверка поднятого сервиса (33 проверки: CRUDL, фильтры, расчёт
стоимости, коды ошибок, swagger) — scripts/smoke.sh. Работает одинаково для
обоих хранилищ:
make build && make smokecmd/ точка входа, сборка зависимостей, graceful shutdown
docs/ сгенерированная swagger-документация
internal/config/ конфигурация из окружения (cleanenv)
internal/domain/ доменные типы: Subscription, MonthYear, параметры выборки
internal/model/ представление в хранилище и доменные ошибки
internal/handler/ HTTP-слой на fiber: роуты, DTO, валидация, логирование
internal/service/ бизнес-логика и интерфейс репозитория
internal/repository/ реализации репозитория: postgres и local
migration/ sql-миграции goose и скрипт управления
scripts/ сквозная проверка API на поднятом сервисе