feat: add rub quote
This commit is contained in:
@@ -25,4 +25,4 @@ ENV PATH="/app/.venv/bin:$PATH" \
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["sh", "-c", "granian --interface asgi ${APP_MODULE:-src.main:app} --host ${APP_HOST:-0.0.0.0} --port ${APP_PORT:-8000} --workers ${APP_WORKERS:-1} --loop uvloop"]
|
||||
CMD ["sh", "-c", "granian --interface asgi ${APP_MODULE:-src.main:app} --host ${APP_HOST:-0.0.0.0} --port ${APP_PORT:-8000} --workers ${APP_WORKERS:-2} --loop uvloop"]
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
APP_MODULE: "src.main:app"
|
||||
APP_HOST: "0.0.0.0"
|
||||
APP_PORT: "8000"
|
||||
APP_WORKERS: "1"
|
||||
APP_WORKERS: "2"
|
||||
env_file:
|
||||
- .env
|
||||
restart: no
|
||||
@@ -1,4 +1,4 @@
|
||||
from src.application.commands.create_order_command import CreateOrderCommand
|
||||
from src.application.commands.create_payment_command import CreatePaymentCommand
|
||||
from src.application.commands.create_crypto_transfer_completed_command import CreateCryptoTransferCompletedCommand
|
||||
from src.application.commands.payment_read_commands import GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,ListOrdersCommand,ListPaymentsCommand,OrderPaymentResult
|
||||
from src.application.commands.payment_read_commands import GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,GetPaymentQuoteFromRubCommand,ListOrdersCommand,ListPaymentsCommand,OrderPaymentResult
|
||||
@@ -39,6 +39,18 @@ class GetPaymentQuoteCommand:
|
||||
return quote
|
||||
|
||||
|
||||
class GetPaymentQuoteFromRubCommand:
|
||||
def __init__(self, *, payment_quote_service: PaymentQuoteService, logger: ILogger):
|
||||
self._payment_quote_service = payment_quote_service
|
||||
self._logger = logger
|
||||
|
||||
|
||||
async def __call__(self, total_rub: Decimal) -> PaymentQuote:
|
||||
quote = await self._payment_quote_service.get_quote_from_total_rub(total_rub)
|
||||
self._logger.info({'event':'payment_quote_from_rub_requested','total_rub':str(total_rub)})
|
||||
return quote
|
||||
|
||||
|
||||
class ListOrdersCommand:
|
||||
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||
self._unit_of_work = unit_of_work
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal, ROUND_UP
|
||||
from decimal import Decimal, ROUND_DOWN, ROUND_UP
|
||||
from src.application.contracts import ICache, ILogger
|
||||
from src.application.domain.exceptions import OrderTotalOutOfRangeException, ServiceUnavailableException
|
||||
|
||||
|
||||
_MIN_USDT_AMOUNT: Decimal = Decimal('1')
|
||||
|
||||
_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),
|
||||
@@ -51,12 +53,7 @@ class PaymentQuoteService:
|
||||
self._logger = logger
|
||||
|
||||
|
||||
async def get_quote(self, usdt_amount: Decimal) -> PaymentQuote:
|
||||
if usdt_amount < _MIN_USDT_AMOUNT:
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total is below minimum allowed amount',
|
||||
)
|
||||
|
||||
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}')
|
||||
@@ -71,8 +68,16 @@ 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)
|
||||
base_rub = usdt_amount * usdt_exchange_rate
|
||||
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
|
||||
@@ -87,9 +92,7 @@ class PaymentQuoteService:
|
||||
break
|
||||
|
||||
if chosen_rate is None or service_fee is None or total_price is None:
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total exceeds maximum allowed amount',
|
||||
)
|
||||
return None
|
||||
|
||||
return PaymentQuote(
|
||||
usdt_amount=usdt_amount,
|
||||
@@ -100,3 +103,86 @@ class PaymentQuoteService:
|
||||
service_fee_rate=chosen_rate,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
async def get_quote(self, usdt_amount: Decimal) -> PaymentQuote:
|
||||
if usdt_amount < _MIN_USDT_AMOUNT:
|
||||
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_from_total_rub(self, total_rub: Decimal) -> PaymentQuote:
|
||||
if total_rub <= Decimal('0'):
|
||||
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()
|
||||
|
||||
if total_rub <= gas_fee:
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total is below minimum allowed amount',
|
||||
)
|
||||
|
||||
min_quote = self._compose_quote(_MIN_USDT_AMOUNT, usdt_exchange_rate, gas_fee)
|
||||
if min_quote is None or min_quote.total_price > total_rub:
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total is below minimum allowed amount',
|
||||
)
|
||||
|
||||
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 < 100:
|
||||
raise OrderTotalOutOfRangeException(
|
||||
message='Order total is below minimum allowed amount',
|
||||
)
|
||||
|
||||
n_lo = 100
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from fastapi import Depends
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.commands import CreateCryptoTransferCompletedCommand,CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,ListOrdersCommand,ListPaymentsCommand
|
||||
from src.application.commands import CreateCryptoTransferCompletedCommand,CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,GetPaymentQuoteFromRubCommand,ListOrdersCommand,ListPaymentsCommand
|
||||
from src.application.contracts import ICache,ILogger,IQueueMessanger,IReceipt
|
||||
from src.application.contracts.i_itpay_service import IItPayService
|
||||
from src.application.services import PaymentQuoteService
|
||||
@@ -59,6 +59,13 @@ def get_payment_quote_command(
|
||||
return GetPaymentQuoteCommand(payment_quote_service=payment_quote_service,logger=logger)
|
||||
|
||||
|
||||
def get_payment_quote_from_rub_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
payment_quote_service: PaymentQuoteService = Depends(get_payment_quote_service),
|
||||
) -> GetPaymentQuoteFromRubCommand:
|
||||
return GetPaymentQuoteFromRubCommand(payment_quote_service=payment_quote_service,logger=logger)
|
||||
|
||||
|
||||
def get_list_orders_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter,Depends,Query,Request,WebSocket,WebSocketDisconnec
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from ulid import ULID
|
||||
from src.application.commands import CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,ListOrdersCommand,ListPaymentsCommand
|
||||
from src.application.commands import CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,GetPaymentQuoteFromRubCommand,ListOrdersCommand,ListPaymentsCommand
|
||||
from src.application.contracts import IJwtService,ILogger
|
||||
from src.application.domain.dto import AccessTokenPayload,AuthContext
|
||||
from src.application.domain.entities import OrderEntity,PaymentEntity
|
||||
@@ -15,7 +15,7 @@ from src.application.domain.exceptions import ApplicationException,ConflictExcep
|
||||
from src.application.services import PaymentQuote
|
||||
from src.infrastructure.context_vars import trace_id_var
|
||||
from src.presentation.decorators import require_access_token, csrf_protect
|
||||
from src.presentation.dependencies.commands import get_create_order_command,get_create_payment_command,get_list_orders_command,get_list_payments_command,get_order_command,get_order_status_command,get_payment_config_command,get_payment_quote_command
|
||||
from src.presentation.dependencies.commands import get_create_order_command,get_create_payment_command,get_list_orders_command,get_list_payments_command,get_order_command,get_order_status_command,get_payment_config_command,get_payment_quote_command,get_payment_quote_from_rub_command
|
||||
from src.presentation.dependencies.logger import get_logger
|
||||
from src.presentation.dependencies.security import get_jwt_service
|
||||
from src.presentation.schemas.order import CreateOrder,CreateOrderResponse,ErrorResponse,OrderDetailResponse,OrderPaymentResponse,OrdersResponse,OrderStatusResponse,OrderWithPaymentResponse,PaymentConfigResponse,PaymentQuoteResponse,PaymentResponse,PaymentsResponse
|
||||
@@ -231,6 +231,20 @@ async def payment_quote(
|
||||
return _payment_quote_response(quote)
|
||||
|
||||
|
||||
@payment_router.get(
|
||||
'/quote/rub',
|
||||
response_model=PaymentQuoteResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def payment_quote_from_rub(
|
||||
total_rub: Decimal = Query(gt=0, decimal_places=2, max_digits=20),
|
||||
command: GetPaymentQuoteFromRubCommand = Depends(get_payment_quote_from_rub_command),
|
||||
) -> PaymentQuoteResponse:
|
||||
quote = await command(total_rub)
|
||||
return _payment_quote_response(quote)
|
||||
|
||||
|
||||
@orders_router.get(
|
||||
'/orders',
|
||||
response_model=OrdersResponse,
|
||||
|
||||
Reference in New Issue
Block a user