152 lines
6.1 KiB
Python
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')
|