feat: add adaptive fee
This commit is contained in:
@@ -7,6 +7,7 @@ from src.application.domain.exceptions.forbidden_exception import ForbiddenExcep
|
||||
from src.application.domain.exceptions.internal_server_exception import InternalServerException
|
||||
from src.application.domain.exceptions.jwt_exception import JwtException
|
||||
from src.application.domain.exceptions.not_found_exception import NotFoundException
|
||||
from src.application.domain.exceptions.order_total_out_of_range_exception import OrderTotalOutOfRangeException
|
||||
from src.application.domain.exceptions.payment_metadata_exception import PaymentMetadataException
|
||||
from src.application.domain.exceptions.payment_provider_exception import PaymentProviderException
|
||||
from src.application.domain.exceptions.price_changed_exception import PriceChangedException
|
||||
@@ -26,6 +27,7 @@ __all__ = [
|
||||
'InternalServerException',
|
||||
'JwtException',
|
||||
'NotFoundException',
|
||||
'OrderTotalOutOfRangeException',
|
||||
'PaymentMetadataException',
|
||||
'PaymentProviderException',
|
||||
'PriceChangedException',
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.bad_request_exception import BadRequestException
|
||||
|
||||
|
||||
class OrderTotalOutOfRangeException(BadRequestException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Order total is outside allowed range',
|
||||
headers: Mapping[str,str] | None = None,
|
||||
) -> None:
|
||||
super().__init__(message=message, headers=headers)
|
||||
@@ -1,10 +1,35 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime,timezone
|
||||
from decimal import Decimal,ROUND_UP
|
||||
from src.application.contracts import ICache,ILogger
|
||||
from src.application.domain.exceptions import ServiceUnavailableException
|
||||
from src.infrastructure.config import settings
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal, ROUND_UP
|
||||
from src.application.contracts import ICache, ILogger
|
||||
from src.application.domain.exceptions import OrderTotalOutOfRangeException, ServiceUnavailableException
|
||||
|
||||
|
||||
_FEE_TIERS: tuple[tuple[Decimal, Decimal, Decimal, bool, bool], ...] = (
|
||||
(Decimal('0.08'), Decimal('5000'), Decimal('30000'), True, True),
|
||||
(Decimal('0.06'), Decimal('30000'), Decimal('100000'), False, True),
|
||||
(Decimal('0.04'), Decimal('100000'), 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)
|
||||
@@ -25,8 +50,8 @@ class PaymentQuoteService:
|
||||
|
||||
|
||||
async def get_quote(self, usdt_amount: Decimal) -> PaymentQuote:
|
||||
rate_raw = await self._remote_cache.hget('tradex:rub:rate','value')
|
||||
gas_raw = await self._remote_cache.hget('gwei:eth:last','normal_rub')
|
||||
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:
|
||||
@@ -39,8 +64,41 @@ class PaymentQuoteService:
|
||||
|
||||
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)
|
||||
service_fee = (usdt_amount * usdt_exchange_rate * settings.PAYMENT_SERVICE_FEE_RATE).quantize(Decimal('0.01'))
|
||||
total_price = (usdt_amount * usdt_exchange_rate + service_fee + gas_fee).quantize(Decimal('0.01'))
|
||||
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:
|
||||
total_if_lowest_fee = (
|
||||
base_rub
|
||||
+ (base_rub * Decimal('0.04')).quantize(Decimal('0.01'))
|
||||
+ gas_fee
|
||||
).quantize(Decimal('0.01'))
|
||||
if total_if_lowest_fee > Decimal('600000'):
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total exceeds maximum allowed amount',
|
||||
)
|
||||
total_if_highest_fee = (
|
||||
base_rub
|
||||
+ (base_rub * Decimal('0.08')).quantize(Decimal('0.01'))
|
||||
+ gas_fee
|
||||
).quantize(Decimal('0.01'))
|
||||
if total_if_highest_fee < Decimal('5000'):
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total is below minimum allowed amount',
|
||||
)
|
||||
raise OrderTotalOutOfRangeException()
|
||||
|
||||
return PaymentQuote(
|
||||
usdt_amount=usdt_amount,
|
||||
@@ -48,6 +106,6 @@ class PaymentQuoteService:
|
||||
gas_fee=gas_fee,
|
||||
service_fee=service_fee,
|
||||
total_price=total_price,
|
||||
service_fee_rate=settings.PAYMENT_SERVICE_FEE_RATE,
|
||||
service_fee_rate=chosen_rate,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from typing import List, Literal
|
||||
import os
|
||||
@@ -95,8 +94,6 @@ class Settings(BaseSettings):
|
||||
ITPAY_PUBLIC_ID: str
|
||||
ITPAY_API_SECRET: str
|
||||
|
||||
PAYMENT_SERVICE_FEE_RATE: Decimal = Field(gt=0)
|
||||
|
||||
CLOUD_KASSIR_PUBLIC_ID: str = ''
|
||||
CLOUD_KASSIR_API_SECRET: str = ''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user