feat: add full pay path
This commit is contained in:
@@ -1,141 +1,103 @@
|
||||
import json
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from urllib.parse import parse_qs
|
||||
import aiohttp
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from ulid import ULID
|
||||
from src.application.commands import CreateOrderCommand
|
||||
from src.application.commands import CreatePaymentCommand
|
||||
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.application.domain.enums import OrderStatus
|
||||
from src.presentation.decorators import require_access_token, csrf_protect
|
||||
from src.presentation.dependencies.commands import get_create_order_command, get_create_payment_command
|
||||
from src.presentation.dependencies.logger import get_logger
|
||||
from src.presentation.schemas.order import CreateOrder
|
||||
from src.infrastructure.config import settings
|
||||
|
||||
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||
|
||||
order_router = APIRouter(prefix='/order', tags=['orders'])
|
||||
|
||||
ITPAY_API_BASE = 'https://api.gw.itpay.ru'
|
||||
HARDCODED_USDT_TO_RUB = Decimal('10')
|
||||
HARDCODED_GAS_RUB = Decimal('5')
|
||||
HARDCODED_OUR_COMMISSION_RUB = Decimal('5')
|
||||
HARDCODED_ITPAY_TEST_AMOUNT_RUB = Decimal('20.00')
|
||||
|
||||
|
||||
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),
|
||||
payment_data: CreateOrder,
|
||||
#auth: AuthContext = Depends(require_access_token),
|
||||
command: CreateOrderCommand = Depends(get_create_order_command),
|
||||
logger: ILogger = Depends(get_logger),
|
||||
) -> ORJSONResponse:
|
||||
amount_rub = _amount_rub_for_itpay(body.amount_usdt)
|
||||
if (os.getenv('ITPAY_TEST_FORCE_20_RUB') or '').strip() == '1':
|
||||
amount_rub = HARDCODED_ITPAY_TEST_AMOUNT_RUB
|
||||
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',
|
||||
}
|
||||
logger.info(json.dumps({
|
||||
'event': 'itpay_payment_create_request',
|
||||
'client_payment_id': client_payment_id,
|
||||
'amount_usdt': str(body.amount_usdt),
|
||||
'amount_rub': amount_str,
|
||||
'url': url,
|
||||
'payload': payload,
|
||||
}, ensure_ascii=False, default=str))
|
||||
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}
|
||||
logger.info(json.dumps({
|
||||
'event': 'itpay_payment_create_response',
|
||||
'client_payment_id': client_payment_id,
|
||||
'status': resp.status,
|
||||
'response': response_json,
|
||||
}, ensure_ascii=False, default=str))
|
||||
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),
|
||||
},
|
||||
}
|
||||
#o = await command(payment_data, auth.user_id)
|
||||
o = await command(payment_data, '01KPKAFN6J1NJBY15DX8JE2QYB')
|
||||
itpay_error = o.status in (
|
||||
OrderStatus.CANCELLED,
|
||||
OrderStatus.REJECTED,
|
||||
OrderStatus.ERROR,
|
||||
)
|
||||
|
||||
http_code = 409 if itpay_error else 201
|
||||
content: dict = {
|
||||
'status_code': http_code,
|
||||
'order': {
|
||||
'id': o.id,
|
||||
'created_at': o.created_at.isoformat() if o.created_at is not None else None,
|
||||
'updated_at': o.updated_at.isoformat() if o.updated_at is not None else None,
|
||||
'user_id': o.user_id,
|
||||
'usdt_amount': str(o.usdt_amount) if o.usdt_amount is not None else None,
|
||||
'usdt_exchange_rate': str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
||||
'gas_fee': str(o.gas_fee) if o.gas_fee is not None else None,
|
||||
'total_price': str(o.total_price) if o.total_price is not None else None,
|
||||
'service_fee': str(o.service_fee) if o.service_fee is not None else None,
|
||||
'status': o.status.value if o.status is not None else None,
|
||||
'client_payment_id': o.client_payment_id,
|
||||
'itpay_payment_qr_url_desktop': o.itpay_payment_qr_url_desktop,
|
||||
'itpay_payment_qr_url_android': o.itpay_payment_qr_url_android,
|
||||
'itpay_payment_qr_url_ios': o.itpay_payment_qr_url_ios,
|
||||
'itpay_payment_qr_image_desktop': o.itpay_payment_qr_image_desktop,
|
||||
'itpay_payment_qr_image_android': o.itpay_payment_qr_image_android,
|
||||
'itpay_payment_qr_image_ios': o.itpay_payment_qr_image_ios,
|
||||
'itpay_id': o.itpay_id,
|
||||
'itpay_qr_id': o.itpay_qr_id,
|
||||
'itpay_amount': str(o.itpay_amount) if o.itpay_amount is not None else None,
|
||||
'itpay_created_at': o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
|
||||
}
|
||||
}
|
||||
log_ids = {
|
||||
'event': 'order_create_itpay_failed' if itpay_error else 'order_created',
|
||||
'order_id': o.id,
|
||||
'user_id': o.user_id,
|
||||
'client_payment_id': o.client_payment_id,
|
||||
'itpay_id': o.itpay_id,
|
||||
'order_status': o.status.value if o.status is not None else None,
|
||||
}
|
||||
logger.info(orjson.dumps(log_ids, default=str).decode())
|
||||
return ORJSONResponse(content=content, status_code=http_code)
|
||||
|
||||
|
||||
@order_router.post('/webhook/itpay')
|
||||
async def itpay_webhook(request: Request, logger: ILogger = Depends(get_logger)) -> ORJSONResponse:
|
||||
async def itpay_webhook(
|
||||
request: Request,
|
||||
payment_command: CreatePaymentCommand = Depends(get_create_payment_command),
|
||||
logger: ILogger = Depends(get_logger)
|
||||
) -> ORJSONResponse:
|
||||
raw = await request.body()
|
||||
ct = (request.headers.get('content-type') or '').lower()
|
||||
logger.info(json.dumps({
|
||||
'event': 'itpay_webhook_received',
|
||||
'method': request.method,
|
||||
'url': str(request.url),
|
||||
'content_type': ct,
|
||||
'body_size': len(raw),
|
||||
}, ensure_ascii=False, default=str))
|
||||
if 'application/json' in ct:
|
||||
try:
|
||||
parsed = json.loads(raw.decode('utf-8'))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
parsed = raw.decode('utf-8', errors='replace')
|
||||
payload = orjson.loads(raw)
|
||||
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()}
|
||||
payload = {k: (vals[0] if len(vals) == 1 else vals) for k, vals in qs.items()}
|
||||
else:
|
||||
parsed = raw.decode('utf-8', errors='replace')
|
||||
headers = {k: v for k, v in request.headers.items() if k.lower() not in {'authorization', 'cookie'}}
|
||||
payload = orjson.loads(raw)
|
||||
data = payload.get('data') if isinstance(payload.get('data'), dict) else {}
|
||||
status = str(data.get('status') or '').strip().lower()
|
||||
log_payload = {
|
||||
'event': 'itpay_webhook_payload',
|
||||
'method': request.method,
|
||||
'url': str(request.url),
|
||||
'headers': headers,
|
||||
'body': parsed,
|
||||
'event': 'itpay_webhook_received',
|
||||
'webhook_id': payload.get('id'),
|
||||
'webhook_type': payload.get('type'),
|
||||
'payment_id': data.get('id'),
|
||||
'client_payment_id': data.get('client_payment_id'),
|
||||
'payment_status': status,
|
||||
}
|
||||
logger.info(json.dumps(log_payload, ensure_ascii=False, default=str))
|
||||
logger.info(orjson.dumps(log_payload, default=str).decode())
|
||||
if status == 'completed':
|
||||
payment = ItpayPaymentData.model_validate(data)
|
||||
await payment_command(payment)
|
||||
return ORJSONResponse(content={'status': 0})
|
||||
|
||||
Reference in New Issue
Block a user