- jwt логин по email/password
get /api/auth/users/me- кампании и промокоды с фильтрами
- активация промокода с проверкой дат, активности и лимитов
- разделение прав user/admin
- история изменений промокода
- снапшот данных промокода в активации
- демо данные через миграции
- docker compose, dockerfile, makefile
- pytest тесты на happy path и негативные сценарии
- python
- fastapi
- pydantic
- uvicorn
- postgresql
- sqlalchemy
- alembic
- pytest
app/
api/
core/
models/
schemas/
services/
alembic/
tests/
README.md
Makefile
Dockerfile
docker-compose.yml
можно взять из .env.example
DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/promocodes
SECRET_KEY=misha-privet
ACCESS_TOKEN_EXPIRE_MINUTES=120
JWT_ALGORITHM=HS256
MOSCOW_TIMEZONE=Europe/Moscowmake install
cp .env.example .env
make migrate
make runсервис будет доступен на http://localhost:8000
docs swagger будут на http://localhost:8000/docs
в swagger authorize можно сразу ввести логин и пароль
username= email пользователяpassword= пароль
docker compose up --buildпри старте контейнера приложения автоматически выполняется alembic upgrade head, затем поднимается uvicorn
make test
make up
make down
make logsдемо данные создаются миграцией 0001_initial
учетные записи
- admin
- email:
admin@example.com - password:
admin123
- email:
- user
- email:
user@example.com - password:
user123
- email:
демо сущности
- активная кампания
welcome campaign - истекшая кампания
old campaign - общий промокод
WELCOME100 - персональный промокод
PERSONAL500дляuser@example.com - отключенный промокод
PAUSED50
POST /api/auth/jwt/loginPOST /api/auth/tokenдля swagger authorizeGET /api/auth/users/me
POST /api/promo-campaignsтолько adminPATCH /api/promo-campaigns/{campaign_id}только adminGET /api/promo-campaigns
POST /api/promosтолько adminPATCH /api/promos/{promo_id}только adminPOST /api/promos/{promo_id}/disableтолько adminGET /api/promosGET /api/promos/{promo_id}POST /api/promos/{promo_id}/activateGET /api/promos/activations/myGET /api/promos/activationsтолько admin
is_active
promo_typeis_activecampaign_id
curl -X POST http://localhost:8000/api/auth/jwt/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"admin123"}'curl http://localhost:8000/api/auth/users/me \
-H 'Authorization: Bearer <access_token>'curl -X POST http://localhost:8000/api/promos/<promo_id>/activate \
-H 'Authorization: Bearer <access_token>'все доменные ошибки отдаются в едином формате
{
"error": {
"code": "promo_per_user_limit_exceeded",
"message": "достигнут лимит активаций промокода на пользователя",
"context": {
"promo_id": "...",
"per_user_limit": 1
}
}
}для ошибок валидации возвращается validation_error и список проблемных полей в context.fields
make testпокрыты ключевые сценарии
- успешный логин
- невалидный логин
meс валидным и невалидным токеном- видимость доступных промокодов
- запрет доступа к чужому персональному промокоду
- успешная активация
- превышение
per_user_limit - активация в истекшей кампании
- создание и отключение промокода админом
- запрет менять критичные поля после активации
- невалидные даты кампании
- история изменений промокода
- залогиниться под
admin@example.com / admin123и убедиться, что приходитaccess_token - залогиниться под
user@example.com / user123и вызватьGET /api/auth/users/me - под пользователем вызвать
GET /api/promo-campaignsи убедиться, что видна только активная доступная кампания - под пользователем вызвать
GET /api/promosи убедиться, что видны только общий активный промокод и персональный промокод этого пользователя - под пользователем активировать
WELCOME100и проверить запись вGET /api/promos/activations/my - повторно активировать
WELCOME100тем же пользователем и получитьpromo_per_user_limit_exceeded - под другим пользователем попробовать активировать персональный промокод
PERSONAL500и получитьpromo_for_another_user - под пользователем попробовать активировать промокод из истекшей кампании и получить
campaign_expired - под админом создать новую кампанию и новый промокод, затем получить их в списках
- под админом отключить промокод через
POST /api/promos/{promo_id}/disableи убедиться, чтоis_active=false - сначала активировать промокод, затем под админом попробовать изменить
promo_typeилиtarget_user_idи получитьpromo_immutable_after_activation - под админом открыть
GET /api/promos/{promo_id}и убедиться, что в ответе естьhistory
кампания — это контейнер для группы промокодов
у кампании есть имя, флаг активности и окно действия по датам
промокод можно активировать только если одновременно выполняются условия по кампании и по самому промокоду
generic
- общий промокод
- доступен всем пользователям, если проходит фильтры активности, дат и лимитов
- не должен иметь
target_user_id
personal
- персональный промокод
- доступен только одному конкретному пользователю
- обязан иметь
target_user_id - другой пользователь не может его просматривать как доступный и не может активировать
при активации проверяются
- активность кампании
- окно действия кампании
- активность промокода
- окно действия промокода
- принадлежность персонального промокода нужному пользователю
- общий лимит
max_activations - лимит на пользователя
per_user_limit
активация всегда привязывается к текущему пользователю из bearer токена
если после активации админ изменит описание, код или бонусные баллы промокода, старая активация не должна переписаться задним числом
поэтому в PromoActivation сохраняются отдельные snapshot поля
applied_bonus_pointspromo_code_snapshotpromo_description_snapshotpromo_type_snapshotcampaign_name_snapshot
это гарантирует консистентную историю и делает аудит предсказуемым