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')