115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
import json
|
|
from decimal import Decimal
|
|
from urllib.parse import parse_qs
|
|
import aiohttp
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import ORJSONResponse
|
|
from ulid import ULID
|
|
from src.application.contracts import ILogger
|
|
from src.application.domain.dto import AuthContext
|
|
from src.application.domain.exceptions import ApplicationException
|
|
from src.presentation.decorators import csrf_protect, require_access_token
|
|
from src.presentation.dependencies.logger import get_logger
|
|
from src.presentation.schemas.order import CreateOrder
|
|
from src.infrastructure.config import settings
|
|
|
|
|
|
order_router = APIRouter(prefix='/order', tags=['orders'])
|
|
|
|
ITPAY_API_BASE = 'https://api.gw.itpay.ru'
|
|
HARDCODED_USDT_TO_RUB = Decimal('100')
|
|
HARDCODED_GAS_RUB = Decimal('15')
|
|
HARDCODED_OUR_COMMISSION_RUB = Decimal('25')
|
|
|
|
|
|
def _amount_rub_for_itpay(amount_usdt: Decimal) -> Decimal:
|
|
return (amount_usdt * HARDCODED_USDT_TO_RUB + HARDCODED_GAS_RUB + HARDCODED_OUR_COMMISSION_RUB).quantize(Decimal('0.01'))
|
|
|
|
|
|
|
|
@order_router.post('/create')
|
|
#@csrf_protect()
|
|
async def create_order(
|
|
request: Request,
|
|
body: CreateOrder,
|
|
#auth: AuthContext = Depends(require_access_token),
|
|
logger: ILogger = Depends(get_logger),
|
|
) -> ORJSONResponse:
|
|
amount_rub = _amount_rub_for_itpay(body.amount_usdt)
|
|
amount_str = str(amount_rub)
|
|
client_payment_id = str(ULID())
|
|
payload = {
|
|
'amount': amount_str,
|
|
'client_payment_id': client_payment_id,
|
|
'description': f'USDT {body.amount_usdt}',
|
|
'metadata': {
|
|
'user_id': '01KPSYW27JZ26HBDR3QS5J6VMS',
|
|
'amount_usdt': str(body.amount_usdt),
|
|
'rate': str(HARDCODED_USDT_TO_RUB),
|
|
'gas_rub': str(HARDCODED_GAS_RUB),
|
|
'commission_rub': str(HARDCODED_OUR_COMMISSION_RUB),
|
|
},
|
|
}
|
|
url = f'{ITPAY_API_BASE}/v1/payments'
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
}
|
|
try:
|
|
timeout = aiohttp.ClientTimeout(total=30)
|
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
auth = aiohttp.BasicAuth(settings.ITPAY_PUBLIC_ID, settings.ITPAY_API_SECRET)
|
|
async with session.post(url, json=payload, headers=headers, auth=auth) as resp:
|
|
response_text = await resp.text()
|
|
try:
|
|
response_json = json.loads(response_text)
|
|
except json.JSONDecodeError:
|
|
response_json = {'raw': response_text}
|
|
if resp.status >= 400:
|
|
logger.warning(f'itpay payments POST {resp.status} {response_text}')
|
|
raise ApplicationException(status_code=502, message='Payment provider error')
|
|
except ApplicationException:
|
|
raise
|
|
except aiohttp.ClientError as e:
|
|
logger.error(str(e))
|
|
raise ApplicationException(status_code=502, message='Payment provider unreachable')
|
|
return ORJSONResponse(
|
|
content={
|
|
'itpay': response_json,
|
|
'client_payment_id': client_payment_id,
|
|
'amount_usdt': str(body.amount_usdt),
|
|
'amount_rub': amount_str,
|
|
'hardcoded': {
|
|
'usdt_to_rub': str(HARDCODED_USDT_TO_RUB),
|
|
'gas_rub': str(HARDCODED_GAS_RUB),
|
|
'commission_rub': str(HARDCODED_OUR_COMMISSION_RUB),
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
|
|
@order_router.post('/webhook/itpay')
|
|
async def itpay_webhook(request: Request, logger: ILogger = Depends(get_logger)) -> ORJSONResponse:
|
|
raw = await request.body()
|
|
ct = (request.headers.get('content-type') or '').lower()
|
|
if 'application/json' in ct:
|
|
try:
|
|
parsed = json.loads(raw.decode('utf-8'))
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
parsed = raw.decode('utf-8', errors='replace')
|
|
elif 'application/x-www-form-urlencoded' in ct:
|
|
decoded = raw.decode('utf-8', errors='replace')
|
|
qs = parse_qs(decoded, keep_blank_values=True)
|
|
parsed = {k: (vals[0] if len(vals) == 1 else vals) for k, vals in qs.items()}
|
|
else:
|
|
parsed = raw.decode('utf-8', errors='replace')
|
|
log_payload = {
|
|
'method': request.method,
|
|
'url': str(request.url),
|
|
'headers': {k: v for k, v in request.headers.items()},
|
|
'body': parsed,
|
|
}
|
|
logger.info(json.dumps(log_payload, ensure_ascii=False, default=str))
|
|
return ORJSONResponse(content={'status': 0})
|