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

152 lines
6.1 KiB
Python

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