Сервис реализует тестовое задание: принимает платеж, сохраняет его в PostgreSQL, публикует событие в RabbitMQ через outbox, обрабатывает платеж в consumer и отправляет webhook о результате.
- FastAPI + Pydantic v2
- SQLAlchemy 2.0 async
- PostgreSQL
- RabbitMQ + FastStream
- Alembic
- Docker Compose
- Idempotency-Key
- Outbox pattern
- Retry с экспоненциальной задержкой
- Dead Letter Queue
app/
api/
clients/
core/
db/
models/
repositories/
schemas/
services/
scripts/
alembic/
cp .env.example .env
docker compose up --buildСервисы:
- API:
http://localhost:8000 - Swagger:
http://localhost:8000/docs - RabbitMQ UI:
http://localhost:15672 - Webhook mock:
http://localhost:8010 - PostgreSQL:
localhost:5432
POST /api/v1/paymentsпринимает запрос иIdempotency-Key.- В одной транзакции сохраняются
paymentsи запись вoutbox. - API-фон публикует событие в RabbitMQ.
- Consumer получает сообщение из
payments.new. - Эмулируется платёжный шлюз: задержка 2-5 сек, 90% успеха.
- Статус платежа обновляется в БД.
- Отправляется webhook.
- Если webhook не ушёл, сообщение уходит в retry-очередь.
- После 3 неудач сообщение уходит в
payments.dlq.
payments.new— основная очередьpayments.retry.1— retry через 1 секундуpayments.retry.2— retry через 2 секундыpayments.retry.3— retry через 4 секундыpayments.dlq— окончательно упавшие сообщения
curl --request POST 'http://localhost:8000/api/v1/payments' \
--header 'Content-Type: application/json' \
--header 'X-API-Key: super-secret-key' \
--header 'Idempotency-Key: test-payment-1' \
--data '{
"amount": "1500.00",
"currency": "RUB",
"description": "Оплата заказа #42",
"metadata": {
"order_id": "42",
"customer_id": "100500"
},
"webhook_url": "https://example.com/webhook"
}'Ответ:
{
"payment_id": "3f27fdad-76ef-41d3-8eb0-32f2ea74bcfb",
"status": "pending",
"created_at": "2026-03-27T10:00:00.000000Z"
}curl --request GET 'http://localhost:8000/api/v1/payments/3f27fdad-76ef-41d3-8eb0-32f2ea74bcfb' \
--header 'X-API-Key: super-secret-key'Повторный POST с тем же Idempotency-Key вернёт уже созданный платёж, новый не создастся.
В docker-compose.yml добавлен сервис webhook-mock. Он принимает webhook внутри Docker-сети по адресу:
http://webhook-mock:8080/webhook
С хоста можно смотреть полученные события:
curl http://localhost:8010/eventsСбросить накопленные webhook:
curl -X DELETE http://localhost:8010/eventsНастроить временные ошибки webhook для проверки retry и DLQ:
curl -X POST http://localhost:8010/control \
-H 'Content-Type: application/json' \
-d '{"failures_remaining": 2, "status_code": 500}'E2E-проверка webhook:
cd ..
pytest -q -s -m webhookТесты показывают:
- успешную доставку webhook с первой попытки;
- успешную доставку после retry;
- уход сообщения в DLQ после исчерпания retry.
- Проект не копируется в Docker image, код подключается через volume.
- Запуск идёт через shell-скрипты из
docker-compose.yml. - Миграции выполняются перед стартом API.
- Топология RabbitMQ поднимается отдельным небольшим скриптом.