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