101 lines
3.5 KiB
Markdown
101 lines
3.5 KiB
Markdown
# Асинхронный сервис процессинга платежей
|
||
|
||
Микросервис принимает платежи по HTTP, атомарно сохраняет платёж и outbox-событие в PostgreSQL, публикует событие в RabbitMQ и асинхронно отправляет результат на webhook.
|
||
|
||
## Запуск
|
||
|
||
Требования: Docker с Compose plugin.
|
||
|
||
```bash
|
||
docker compose up --build
|
||
```
|
||
|
||
После запуска доступны:
|
||
|
||
- API и Swagger UI: <http://localhost:8000/docs>
|
||
- RabbitMQ Management: <http://localhost:15672> (`guest` / `guest`)
|
||
- API key по умолчанию: `local-secret`; для другого значения задайте `API_KEY`
|
||
|
||
Миграции выполняются контейнером `migrate` до запуска API и consumer.
|
||
|
||
## API
|
||
|
||
Создать платёж:
|
||
|
||
```bash
|
||
curl -i -X POST http://localhost:8000/api/v1/payments \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'X-API-Key: local-secret' \
|
||
-H 'Idempotency-Key: order-123-attempt-1' \
|
||
-d '{
|
||
"amount": "1250.50",
|
||
"currency": "RUB",
|
||
"description": "Заказ 123",
|
||
"metadata": {"order_id": 123},
|
||
"webhook_url": "https://example.com/payment-webhook"
|
||
}'
|
||
```
|
||
|
||
Ответ имеет статус `202 Accepted`:
|
||
|
||
```json
|
||
{
|
||
"payment_id": "8f814d58-8a52-493f-aafb-98b383aa0325",
|
||
"status": "pending",
|
||
"created_at": "2026-09-16T08:00:00Z"
|
||
}
|
||
```
|
||
|
||
Повторный запрос с тем же `Idempotency-Key` вернёт тот же платёж и не создаст новое outbox-событие.
|
||
|
||
Получить платёж:
|
||
|
||
```bash
|
||
curl http://localhost:8000/api/v1/payments/8f814d58-8a52-493f-aafb-98b383aa0325 \
|
||
-H 'X-API-Key: local-secret'
|
||
```
|
||
|
||
Webhook получает JSON следующего вида:
|
||
|
||
```json
|
||
{
|
||
"payment_id": "8f814d58-8a52-493f-aafb-98b383aa0325",
|
||
"amount": "1250.50",
|
||
"currency": "RUB",
|
||
"status": "succeeded",
|
||
"processed_at": "2026-09-16T08:00:04Z",
|
||
"metadata": {"order_id": 123}
|
||
}
|
||
```
|
||
|
||
## Надёжность
|
||
|
||
- `payments` и `outbox` записываются в одной транзакции.
|
||
- Publisher читает outbox через `FOR UPDATE SKIP LOCKED` и помечает событие после publisher confirm RabbitMQ.
|
||
- Доставка события имеет семантику at-least-once. Consumer идемпотентен по состоянию платежа.
|
||
- Обработка и webhook выполняются до трёх раз с задержками 1 и 2 секунды.
|
||
- После исчерпания попыток сообщение отклоняется без requeue и RabbitMQ переносит его через `payments.dlx` в `payments.dead`.
|
||
- Если процесс остановился после обновления статуса, но до webhook, повторная доставка продолжит только webhook.
|
||
|
||
## Локальная разработка
|
||
|
||
```bash
|
||
python -m venv .venv
|
||
. .venv/bin/activate
|
||
pip install -e '.[dev]'
|
||
cp .env.example .env
|
||
ruff check .
|
||
ty check
|
||
pytest
|
||
```
|
||
|
||
Для локального запуска без Docker измените в `.env` адреса PostgreSQL и RabbitMQ на `localhost`, затем выполните:
|
||
|
||
```bash
|
||
alembic upgrade head
|
||
uvicorn app.main:app --reload
|
||
faststream run app.consumer:app
|
||
```
|
||
|
||
Основные параметры эмуляции (`PROCESSING_*`), retry и polling outbox перечислены в `.env.example`.
|