from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timezone from decimal import Decimal, ROUND_DOWN, ROUND_UP from src.application.contracts import ICache, ILogger from src.application.domain.exceptions import OrderTotalOutOfRangeException, ServiceUnavailableException _MIN_TOTAL_RUB: Decimal = Decimal('5000') _MAX_TOTAL_RUB: Decimal = Decimal('600000') _FEE_TIERS: tuple[tuple[Decimal, Decimal, Decimal, bool, bool], ...] = ( (Decimal('0.08'), Decimal('0'), Decimal('30000'), True, True), (Decimal('0.06'), Decimal('30000'), Decimal('100000'), False, True), (Decimal('0.04'), Decimal('30000'), Decimal('600000'), False, True), ) def _total_in_bracket( total: Decimal, lo: Decimal, hi: Decimal, *, lo_inclusive: bool, hi_inclusive: bool, ) -> bool: if lo_inclusive: ok_lo = total >= lo else: ok_lo = total > lo if hi_inclusive: ok_hi = total <= hi else: ok_hi = total < hi return ok_lo and ok_hi @dataclass(slots=True) class PaymentQuote: usdt_amount: Decimal usdt_exchange_rate: Decimal gas_fee: Decimal service_fee: Decimal total_price: Decimal service_fee_rate: Decimal created_at: datetime class PaymentQuoteService: def __init__(self, *, remote_cache: ICache, logger: ILogger): self._remote_cache = remote_cache self._logger = logger async def _load_prices(self) -> tuple[Decimal, Decimal]: rate_raw = await self._remote_cache.hget('tradex:rub:rate', 'value') gas_raw = await self._remote_cache.hget('gwei:eth:last', 'normal_rub') self._logger.info(f'Actual market values: rate={rate_raw}, gas={gas_raw}') if rate_raw is None: self._logger.error('Exchange rate unavailable') raise ServiceUnavailableException(message='Exchange rate unavailable') if gas_raw is None: self._logger.error('Exchange gas unavailable') raise ServiceUnavailableException(message='Exchange gas unavailable') gas_fee = Decimal(gas_raw).quantize(Decimal('0.00'), rounding=ROUND_UP) usdt_exchange_rate = Decimal(rate_raw).quantize(Decimal('0.00'), rounding=ROUND_UP) return usdt_exchange_rate, gas_fee def _compose_quote( self, usdt_amount: Decimal, usdt_exchange_rate: Decimal, gas_fee: Decimal, ) -> PaymentQuote | None: base_rub = usdt_amount * usdt_exchange_rate chosen_rate: Decimal | None = None service_fee: Decimal | None = None total_price: Decimal | None = None for fee_rate, lo, hi, li, ri in _FEE_TIERS: sf = (base_rub * fee_rate).quantize(Decimal('0.01')) tp = (base_rub + sf + gas_fee).quantize(Decimal('0.01')) if _total_in_bracket(tp, lo, hi, lo_inclusive=li, hi_inclusive=ri): chosen_rate = fee_rate service_fee = sf total_price = tp break if chosen_rate is None or service_fee is None or total_price is None: return None return PaymentQuote( usdt_amount=usdt_amount, usdt_exchange_rate=usdt_exchange_rate, gas_fee=gas_fee, service_fee=service_fee, total_price=total_price, service_fee_rate=chosen_rate, created_at=datetime.now(timezone.utc), ) async def get_reference_quote(self, usdt_amount: Decimal) -> PaymentQuote: if usdt_amount <= Decimal('0'): raise OrderTotalOutOfRangeException( message='Order total is below minimum allowed amount', ) usdt_exchange_rate, gas_fee = await self._load_prices() quote = self._compose_quote(usdt_amount, usdt_exchange_rate, gas_fee) if quote is None: raise OrderTotalOutOfRangeException( message='Order total exceeds maximum allowed amount', ) return quote async def get_quote(self, usdt_amount: Decimal) -> PaymentQuote: quote = await self.get_reference_quote(usdt_amount) if quote.total_price < _MIN_TOTAL_RUB: raise OrderTotalOutOfRangeException( message='Order total is below minimum allowed amount', ) return quote async def get_quote_from_total_rub(self, total_rub: Decimal) -> PaymentQuote: if total_rub < _MIN_TOTAL_RUB: raise OrderTotalOutOfRangeException( message='Order total is below minimum allowed amount', ) if total_rub > _MAX_TOTAL_RUB: raise OrderTotalOutOfRangeException( message='Order total exceeds maximum allowed amount', ) usdt_exchange_rate, gas_fee = await self._load_prices() u_budget = ((total_rub - gas_fee) / usdt_exchange_rate).quantize(Decimal('0.01'), rounding=ROUND_DOWN) u_cap = ((_MAX_TOTAL_RUB - gas_fee) / (usdt_exchange_rate * Decimal('1.04'))).quantize( Decimal('0.01'), rounding=ROUND_DOWN, ) u_upper = min(u_budget, u_cap) n_hi = int((u_upper * 100).to_integral_value(rounding=ROUND_DOWN)) if n_hi < 1: raise OrderTotalOutOfRangeException( message='Order total is below minimum allowed amount', ) n_lo = 1 best_cent: int | None = None while n_lo <= n_hi: mid = (n_lo + n_hi) // 2 u = Decimal(mid) / Decimal(100) quote = self._compose_quote(u, usdt_exchange_rate, gas_fee) if quote is None: n_hi = mid - 1 continue if quote.total_price <= total_rub: best_cent = mid n_lo = mid + 1 else: n_hi = mid - 1 if best_cent is None: raise OrderTotalOutOfRangeException( message='Order total is below minimum allowed amount', ) final = self._compose_quote( Decimal(best_cent) / Decimal(100), usdt_exchange_rate, gas_fee, ) if final is None: raise OrderTotalOutOfRangeException( message='Order total exceeds maximum allowed amount', ) return final