Files
pay-service/src/infrastructure/itpay/client.py

124 lines
6.3 KiB
Python

import orjson
from dataclasses import replace
from datetime import datetime, timezone
from decimal import Decimal,InvalidOperation
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,PaymentProviderException
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 PaymentProviderException(message='Payment provider error')
body_raw = response_json.get('data')
body = body_raw if isinstance(body_raw, dict) else response_json
status = str(body.get('status') or '').strip().lower()
itpay_id = str(body.get('id') or '').strip()
if not status or not itpay_id:
raise PaymentProviderException(message='Payment provider response invalid')
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.get('qrc_id') or '').strip()
if not qrc_id:
raise PaymentProviderException(message='Payment provider response invalid')
itpay_amount = Decimal(str(body.get('amount')))
created_norm = str(body.get('created') or '').replace('Z', '+00:00')
itpay_created_at = datetime.fromisoformat(created_norm)
payment_qr_urls = body.get('payment_qr_urls')
if isinstance(payment_qr_urls, str):
payment_qr_urls = orjson.loads(payment_qr_urls)
payment_qr_images = body.get('payment_qr_images')
if isinstance(payment_qr_images, str):
payment_qr_images = orjson.loads(payment_qr_images)
if not isinstance(payment_qr_urls,dict) or not isinstance(payment_qr_images,dict):
raise PaymentProviderException(message='Payment provider response invalid')
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 (TimeoutError,aiohttp.ClientError):
raise PaymentProviderException(message='Payment provider unreachable')
except (KeyError,ValueError,TypeError,InvalidOperation,orjson.JSONDecodeError) as exception:
raise PaymentProviderException(message='Payment provider response invalid') from exception