116 lines
5.5 KiB
Python
116 lines
5.5 KiB
Python
import orjson
|
|
from dataclasses import replace
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
import aiohttp
|
|
from aiohttp import BasicAuth, ClientTimeout
|
|
from src.application.contracts.i_itpay_service import IItPayService
|
|
from src.application.domain.entities.order import OrderEntity
|
|
from src.application.domain.enums import OrderStatus
|
|
from src.application.domain.exceptions import ApplicationException
|
|
|
|
|
|
class ItPayClient(IItPayService):
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
public_id: str,
|
|
api_secret: str,
|
|
api_base_url: str = 'https://api.gw.itpay.ru',
|
|
timeout_seconds: float = 30,
|
|
) -> None:
|
|
self._api_base_url = api_base_url.rstrip('/')
|
|
self._public_id = public_id
|
|
self._api_secret = api_secret
|
|
self._timeout = ClientTimeout(total=timeout_seconds)
|
|
|
|
async def create_payment(self, order: OrderEntity, trace_id: str) -> OrderEntity:
|
|
total = order.total_price if order.total_price is not None else Decimal('0')
|
|
amount = total if isinstance(total, Decimal) else Decimal(str(total))
|
|
amount_str = str(amount.quantize(Decimal('0.01')))
|
|
metadata: dict[str,Any] = {
|
|
'trace_id': trace_id,
|
|
'order_id': order.id,
|
|
'user_id': order.user_id,
|
|
'usdt_amount': str(order.usdt_amount) if order.usdt_amount is not None else None,
|
|
'usdt_exchange_rate': str(order.usdt_exchange_rate) if order.usdt_exchange_rate is not None else None,
|
|
'gas_fee': str(order.gas_fee) if order.gas_fee is not None else None,
|
|
'service_fee': str(order.service_fee) if order.service_fee is not None else None,
|
|
'agent_fee': str(order.service_fee) if order.service_fee is not None else None,
|
|
'amount': amount_str,
|
|
'total_amount': amount_str,
|
|
}
|
|
if order.total_price is not None and order.service_fee is not None:
|
|
principal = (Decimal(str(order.total_price)) - Decimal(str(order.service_fee))).quantize(Decimal('0.01'))
|
|
metadata['principal_amount'] = str(principal)
|
|
metadata = {k:v for k,v in metadata.items() if v is not None and v != ''}
|
|
payload: dict[str, Any] = {
|
|
'amount': amount_str,
|
|
'client_payment_id': order.client_payment_id or '',
|
|
'method': 'sbp',
|
|
'description': 'CFU',
|
|
'metadata': metadata,
|
|
}
|
|
url = f'{self._api_base_url}/v1/payments'
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
}
|
|
try:
|
|
async with aiohttp.ClientSession(timeout=self._timeout) as session:
|
|
auth = BasicAuth(self._public_id, self._api_secret)
|
|
async with session.post(url, json=payload, headers=headers, auth=auth) as resp:
|
|
response_text = await resp.text()
|
|
try:
|
|
response_json: dict[str, Any] = orjson.loads(response_text)
|
|
except orjson.JSONDecodeError:
|
|
response_json = {'raw': response_text}
|
|
if resp.status >= 400:
|
|
raise ApplicationException(status_code=502, message='Payment provider error')
|
|
body_raw = response_json.get('data')
|
|
body = body_raw if isinstance(body_raw, dict) else response_json
|
|
|
|
status = str(body['status']).strip().lower()
|
|
itpay_id = str(body['id'])
|
|
|
|
if status == 'cancelled':
|
|
return replace(order, status=OrderStatus.CANCELLED, itpay_id=itpay_id)
|
|
if status == 'rejected':
|
|
return replace(order, status=OrderStatus.REJECTED, itpay_id=itpay_id)
|
|
if status == 'error':
|
|
return replace(order, status=OrderStatus.ERROR, itpay_id=itpay_id)
|
|
|
|
qrc_id = str(body['qrc_id'])
|
|
itpay_amount = Decimal(str(body['amount']))
|
|
|
|
created_norm = str(body['created']).replace('Z', '+00:00')
|
|
itpay_created_at = datetime.fromisoformat(created_norm)
|
|
|
|
payment_qr_urls = body['payment_qr_urls']
|
|
if isinstance(payment_qr_urls, str):
|
|
payment_qr_urls = orjson.loads(payment_qr_urls)
|
|
|
|
payment_qr_images = body['payment_qr_images']
|
|
if isinstance(payment_qr_images, str):
|
|
payment_qr_images = orjson.loads(payment_qr_images)
|
|
|
|
return replace(
|
|
order,
|
|
itpay_id=itpay_id,
|
|
itpay_qr_id=qrc_id,
|
|
itpay_created_at=itpay_created_at,
|
|
itpay_amount=itpay_amount,
|
|
itpay_payment_qr_url_android=str(payment_qr_urls['android']),
|
|
itpay_payment_qr_url_ios=str(payment_qr_urls['ios']),
|
|
itpay_payment_qr_url_desktop=str(payment_qr_urls['desktop']),
|
|
itpay_payment_qr_image_android=str(payment_qr_images['android']),
|
|
itpay_payment_qr_image_ios=str(payment_qr_images['ios']),
|
|
itpay_payment_qr_image_desktop=str(payment_qr_images['desktop']),
|
|
)
|
|
except ApplicationException:
|
|
raise
|
|
except aiohttp.ClientError:
|
|
raise ApplicationException(status_code=502, message='Payment provider unreachable')
|