feat: add rub quote
This commit is contained in:
@@ -25,4 +25,4 @@ ENV PATH="/app/.venv/bin:$PATH" \
|
|||||||
|
|
||||||
EXPOSE 8000
|
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_MODULE: "src.main:app"
|
||||||
APP_HOST: "0.0.0.0"
|
APP_HOST: "0.0.0.0"
|
||||||
APP_PORT: "8000"
|
APP_PORT: "8000"
|
||||||
APP_WORKERS: "1"
|
APP_WORKERS: "2"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
restart: no
|
restart: no
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from src.application.commands.create_order_command import CreateOrderCommand
|
from src.application.commands.create_order_command import CreateOrderCommand
|
||||||
from src.application.commands.create_payment_command import CreatePaymentCommand
|
from src.application.commands.create_payment_command import CreatePaymentCommand
|
||||||
from src.application.commands.create_crypto_transfer_completed_command import CreateCryptoTransferCompletedCommand
|
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
|
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:
|
class ListOrdersCommand:
|
||||||
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
|
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||||
self._unit_of_work = unit_of_work
|
self._unit_of_work = unit_of_work
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
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_DOWN, ROUND_UP
|
||||||
from src.application.contracts import ICache, ILogger
|
from src.application.contracts import ICache, ILogger
|
||||||
from src.application.domain.exceptions import OrderTotalOutOfRangeException, ServiceUnavailableException
|
from src.application.domain.exceptions import OrderTotalOutOfRangeException, ServiceUnavailableException
|
||||||
|
|
||||||
|
|
||||||
_MIN_USDT_AMOUNT: Decimal = Decimal('1')
|
_MIN_USDT_AMOUNT: Decimal = Decimal('1')
|
||||||
|
|
||||||
|
_MAX_TOTAL_RUB: Decimal = Decimal('600000')
|
||||||
|
|
||||||
_FEE_TIERS: tuple[tuple[Decimal, Decimal, Decimal, bool, bool], ...] = (
|
_FEE_TIERS: tuple[tuple[Decimal, Decimal, Decimal, bool, bool], ...] = (
|
||||||
(Decimal('0.08'), Decimal('0'), Decimal('30000'), True, True),
|
(Decimal('0.08'), Decimal('0'), Decimal('30000'), True, True),
|
||||||
(Decimal('0.06'), Decimal('30000'), Decimal('100000'), False, True),
|
(Decimal('0.06'), Decimal('30000'), Decimal('100000'), False, True),
|
||||||
@@ -51,12 +53,7 @@ class PaymentQuoteService:
|
|||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
|
||||||
|
|
||||||
async def get_quote(self, usdt_amount: Decimal) -> PaymentQuote:
|
async def _load_prices(self) -> tuple[Decimal, Decimal]:
|
||||||
if usdt_amount < _MIN_USDT_AMOUNT:
|
|
||||||
raise OrderTotalOutOfRangeException(
|
|
||||||
message='Order total is below minimum allowed amount',
|
|
||||||
)
|
|
||||||
|
|
||||||
rate_raw = await self._remote_cache.hget('tradex:rub:rate', 'value')
|
rate_raw = await self._remote_cache.hget('tradex:rub:rate', 'value')
|
||||||
gas_raw = await self._remote_cache.hget('gwei:eth:last', 'normal_rub')
|
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}')
|
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)
|
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)
|
||||||
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
|
chosen_rate: Decimal | None = None
|
||||||
service_fee: Decimal | None = None
|
service_fee: Decimal | None = None
|
||||||
total_price: Decimal | None = None
|
total_price: Decimal | None = None
|
||||||
@@ -87,9 +92,7 @@ class PaymentQuoteService:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if chosen_rate is None or service_fee is None or total_price is None:
|
if chosen_rate is None or service_fee is None or total_price is None:
|
||||||
raise OrderTotalOutOfRangeException(
|
return None
|
||||||
message='Order total exceeds maximum allowed amount',
|
|
||||||
)
|
|
||||||
|
|
||||||
return PaymentQuote(
|
return PaymentQuote(
|
||||||
usdt_amount=usdt_amount,
|
usdt_amount=usdt_amount,
|
||||||
@@ -100,3 +103,86 @@ class PaymentQuoteService:
|
|||||||
service_fee_rate=chosen_rate,
|
service_fee_rate=chosen_rate,
|
||||||
created_at=datetime.now(timezone.utc),
|
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 __future__ import annotations
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from src.application.abstractions import IUnitOfWork
|
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 import ICache,ILogger,IQueueMessanger,IReceipt
|
||||||
from src.application.contracts.i_itpay_service import IItPayService
|
from src.application.contracts.i_itpay_service import IItPayService
|
||||||
from src.application.services import PaymentQuoteService
|
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)
|
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(
|
def get_list_orders_command(
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
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.responses import ORJSONResponse
|
||||||
from fastapi.security.utils import get_authorization_scheme_param
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
from ulid import ULID
|
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.contracts import IJwtService,ILogger
|
||||||
from src.application.domain.dto import AccessTokenPayload,AuthContext
|
from src.application.domain.dto import AccessTokenPayload,AuthContext
|
||||||
from src.application.domain.entities import OrderEntity,PaymentEntity
|
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.application.services import PaymentQuote
|
||||||
from src.infrastructure.context_vars import trace_id_var
|
from src.infrastructure.context_vars import trace_id_var
|
||||||
from src.presentation.decorators import require_access_token, csrf_protect
|
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.logger import get_logger
|
||||||
from src.presentation.dependencies.security import get_jwt_service
|
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
|
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)
|
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_router.get(
|
||||||
'/orders',
|
'/orders',
|
||||||
response_model=OrdersResponse,
|
response_model=OrdersResponse,
|
||||||
|
|||||||
Reference in New Issue
Block a user