from __future__ import annotations from decimal import Decimal from typing import Any from ulid import ULID import aiohttp import orjson from aiohttp import BasicAuth, ClientTimeout from src.application.contracts import IReceipt from src.application.domain.exceptions import ApplicationException,ReceiptProviderException from src.infrastructure.cloud_kassir.constants import CLOUD_KASSIR_API_BASE_URL class ClaudeKassirClient(IReceipt): def __init__( self, *, public_id: str, api_secret: str, inn: str, api_base_url: str = CLOUD_KASSIR_API_BASE_URL, success_url: str | None = None, fail_url: str | None = None, timeout_seconds: float = 30, ) -> None: self._public_id = public_id self._api_secret = api_secret self._inn = inn self._api_base_url = api_base_url.rstrip('/') self._success_url = success_url self._fail_url = fail_url self._timeout = ClientTimeout(total=timeout_seconds) async def create_receipt( self, *, order_id: str, user_id: str, email: str, total_amount: Decimal, principal_amount: Decimal, service_fee: Decimal, customer_info: str = '', customer_inn: str = '', customer_birthday: str = '', success_url: str | None = None, fail_url: str | None = None, request_id: str | None = None, ) -> dict[str, Any]: total = total_amount.quantize(Decimal('0.01')) principal = principal_amount.quantize(Decimal('0.01')) fee = service_fee.quantize(Decimal('0.01')) description = f'Оплата по заявке №{order_id}' principal_description = f'Исполнение поручения принципала по заявке №{order_id}' fee_description = f'Агентское вознаграждение по заявке №{order_id}' payload: dict[str, Any] = { 'Inn': self._inn, 'Type': 'Income', 'InvoiceId': order_id, 'AccountId': user_id, 'Description': description, 'CustomerReceipt': { 'TaxationSystem': 2, 'Items': [ { 'label': principal_description, 'price': float(principal), 'quantity': 1, 'amount': float(principal), 'vat': 0, 'method': 4, 'object': 4, 'measurement_unit': 'шт', 'agentSign': 6, 'agentData': { 'agentOperationName': 'Исполнение поручения принципала', }, 'purveyorData': { 'name': 'Elcsa', 'inn': self._inn, }, }, { 'label': fee_description, 'price': float(fee), 'quantity': 1, 'amount': float(fee), 'vat': 0, 'method': 4, 'object': 11, 'measurement_unit': 'шт', }, ], 'amounts': { 'electronic': float(total), 'advancePayment': 0, 'credit': 0, 'provision': 0, }, 'email': email, 'customerInfo': customer_info, 'customerInn': customer_inn, 'customerBirthday': customer_birthday, }, 'Email': email, 'SuccessUrl': success_url or self._success_url, 'FailUrl': fail_url or self._fail_url, } if payload['SuccessUrl'] is None: payload.pop('SuccessUrl') if payload['FailUrl'] is None: payload.pop('FailUrl') url = f'{self._api_base_url}/kkt/receipt' headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Request-ID': request_id or str(ULID()), } 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: raw = (await resp.text()).strip() if not raw: raise ReceiptProviderException( message=f'Receipt provider empty response (HTTP {resp.status})', ) try: parsed: Any = orjson.loads(raw) except orjson.JSONDecodeError: preview = raw[:240].replace('\n', ' ') raise ReceiptProviderException( message=f'Receipt provider non-JSON response (HTTP {resp.status}): {preview}', ) if not isinstance(parsed, dict): raise ReceiptProviderException(message='Receipt provider invalid response') body = parsed if resp.status >= 400: raise ReceiptProviderException( message=str(body.get('Message') or 'Receipt provider error'), ) if body.get('Success') is False: raise ReceiptProviderException( status_code=409, message=str(body.get('Message') or 'Receipt provider rejected receipt'), ) return body except ApplicationException: raise except (TimeoutError,aiohttp.ClientError): raise ReceiptProviderException(message='Receipt provider unreachable')