feat: add full pay path

This commit is contained in:
2026-05-01 13:10:13 +03:00
parent d1ac7e8e84
commit bf68aca4fa
53 changed files with 1436 additions and 334 deletions

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from ulid import ULID
import aiohttp
from aiohttp import BasicAuth, ClientTimeout
from src.application.contracts import IReceipt
from src.application.domain.exceptions import ApplicationException
class ClaudeKassirClient(IReceipt):
def __init__(
self,
*,
public_id: str,
api_secret: str,
inn: str,
api_base_url: str = 'https://api.cloudpayments.ru',
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,
phone: str | None = None,
customer_inn: 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}'
fee_description = f'Агентское вознаграждение за исполнение поручения по заявке №{order_id}'
payload: dict[str, Any] = {
'Inn': self._inn,
'Type': 'Income',
'InvoiceId': order_id,
'AccountId': user_id,
'Description': description,
'CustomerReceipt': {
'Items': [
{
'label': description,
'price': float(principal),
'quantity': 1.00,
'amount': float(principal),
'vat': 0,
'method': 4,
'object': 4,
'measurement_unit': 'шт',
'agent_info': {
'type': 2,
},
'supplier_info': {
'name': 'Принципал (физическое лицо)',
'inn': '',
'phones': [],
},
},
{
'label': fee_description,
'price': float(fee),
'quantity': 1.00,
'amount': float(fee),
'vat': 0,
'method': 4,
'object': 1,
'measurement_unit': 'шт',
},
],
'taxationSystem': 2,
'email': email,
'phone': phone,
'customerInn': customer_inn,
'agentSign': 2,
'amounts': {
'electronic': float(total),
'advancePayment': 0.00,
'credit': 0.00,
'provision': 0.00,
},
},
'Email': email,
'SuccessUrl': success_url or self._success_url,
'FailUrl': fail_url or self._fail_url,
}
if phone is None:
payload['CustomerReceipt'].pop('phone')
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:
body = await resp.json(content_type=None)
if resp.status >= 400:
raise ApplicationException(status_code=502, message='Receipt provider error')
if body.get('Success') is False:
raise ApplicationException(status_code=409, message=str(body.get('Message') or 'Receipt provider rejected receipt'))
return body
except ApplicationException:
raise
except aiohttp.ClientError:
raise ApplicationException(status_code=502, message='Receipt provider unreachable')