feat: add full pay path
This commit is contained in:
109
src/infrastructure/itpay/client.py
Normal file
109
src/infrastructure/itpay/client.py
Normal file
@@ -0,0 +1,109 @@
|
||||
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) -> 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] = {
|
||||
'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,
|
||||
'amount': amount_str,
|
||||
}
|
||||
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')
|
||||
Reference in New Issue
Block a user