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})