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.internal_server_exception import InternalServerException
|
||||||
from src.application.domain.exceptions.jwt_exception import JwtException
|
from src.application.domain.exceptions.jwt_exception import JwtException
|
||||||
from src.application.domain.exceptions.not_found_exception import NotFoundException
|
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_metadata_exception import PaymentMetadataException
|
||||||
from src.application.domain.exceptions.payment_provider_exception import PaymentProviderException
|
from src.application.domain.exceptions.payment_provider_exception import PaymentProviderException
|
||||||
from src.application.domain.exceptions.price_changed_exception import PriceChangedException
|
from src.application.domain.exceptions.price_changed_exception import PriceChangedException
|
||||||
@@ -26,6 +27,7 @@ __all__ = [
|
|||||||
'InternalServerException',
|
'InternalServerException',
|
||||||
'JwtException',
|
'JwtException',
|
||||||
'NotFoundException',
|
'NotFoundException',
|
||||||
|
'OrderTotalOutOfRangeException',
|
||||||
'PaymentMetadataException',
|
'PaymentMetadataException',
|
||||||
'PaymentProviderException',
|
'PaymentProviderException',
|
||||||
'PriceChangedException',
|
'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)
|
||||||
@@ -3,8 +3,33 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from decimal import Decimal, ROUND_UP
|
from decimal import Decimal, ROUND_UP
|
||||||
from src.application.contracts import ICache, ILogger
|
from src.application.contracts import ICache, ILogger
|
||||||
from src.application.domain.exceptions import ServiceUnavailableException
|
from src.application.domain.exceptions import OrderTotalOutOfRangeException, ServiceUnavailableException
|
||||||
from src.infrastructure.config import settings
|
|
||||||
|
|
||||||
|
_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)
|
@dataclass(slots=True)
|
||||||
@@ -39,8 +64,41 @@ class PaymentQuoteService:
|
|||||||
|
|
||||||
gas_fee = Decimal(gas_raw).quantize(Decimal('0.00'), rounding=ROUND_UP)
|
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)
|
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'))
|
base_rub = usdt_amount * usdt_exchange_rate
|
||||||
total_price = (usdt_amount * usdt_exchange_rate + service_fee + gas_fee).quantize(Decimal('0.01'))
|
|
||||||
|
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(
|
return PaymentQuote(
|
||||||
usdt_amount=usdt_amount,
|
usdt_amount=usdt_amount,
|
||||||
@@ -48,6 +106,6 @@ class PaymentQuoteService:
|
|||||||
gas_fee=gas_fee,
|
gas_fee=gas_fee,
|
||||||
service_fee=service_fee,
|
service_fee=service_fee,
|
||||||
total_price=total_price,
|
total_price=total_price,
|
||||||
service_fee_rate=settings.PAYMENT_SERVICE_FEE_RATE,
|
service_fee_rate=chosen_rate,
|
||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import List, Literal
|
from typing import List, Literal
|
||||||
import os
|
import os
|
||||||
@@ -95,8 +94,6 @@ class Settings(BaseSettings):
|
|||||||
ITPAY_PUBLIC_ID: str
|
ITPAY_PUBLIC_ID: str
|
||||||
ITPAY_API_SECRET: str
|
ITPAY_API_SECRET: str
|
||||||
|
|
||||||
PAYMENT_SERVICE_FEE_RATE: Decimal = Field(gt=0)
|
|
||||||
|
|
||||||
CLOUD_KASSIR_PUBLIC_ID: str = ''
|
CLOUD_KASSIR_PUBLIC_ID: str = ''
|
||||||
CLOUD_KASSIR_API_SECRET: str = ''
|
CLOUD_KASSIR_API_SECRET: str = ''
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user