Initial commit

This commit is contained in:
2026-04-22 09:57:24 +03:00
commit 00e601c21a
81 changed files with 3552 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
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
order_router = APIRouter(prefix='/order', tags=['orders'])
ITPAY_API_BASE = 'https://api.gw.itpay.ru'
ITPAY_AUTHORIZATION = 'Token REPLACE_WITH_JWT_FROM_ITPAY_DASHBOARD'
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 = {
'Authorization': ITPAY_AUTHORIZATION,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
try:
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(url, json=payload, headers=headers) 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})