feat: add mpre endpoints

This commit is contained in:
2026-05-11 19:04:39 +03:00
parent 42fcfbff34
commit 489c9cb2da
18 changed files with 691 additions and 75 deletions

View File

@@ -1,3 +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.create_crypto_transfer_completed_command import CreateCryptoTransferCompletedCommand
from src.application.commands.payment_read_commands import GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,ListOrdersCommand,ListPaymentsCommand,OrderPaymentResult

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from decimal import Decimal
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ILogger,IReceipt
from src.application.domain.enums import PaymentStatus
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.decorators import transactional
@@ -62,18 +63,26 @@ class CreateCryptoTransferCompletedCommand:
if principal_amount < 0:
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
receipt_response = await self._receipt.create_receipt(
order_id=order_id,
user_id=user_id,
email=email,
total_amount=total_amount,
principal_amount=principal_amount,
service_fee=service_fee,
customer_info=customer_info,
customer_inn=customer_inn,
customer_birthday=customer_birthday,
request_id=self._logger.get_trace_id(),
)
try:
receipt_response = await self._receipt.create_receipt(
order_id=order_id,
user_id=user_id,
email=email,
total_amount=total_amount,
principal_amount=principal_amount,
service_fee=service_fee,
customer_info=customer_info,
customer_inn=customer_inn,
customer_birthday=customer_birthday,
request_id=self._logger.get_trace_id(),
)
except ApplicationException as exception:
self._logger.error({'event':'receipt_create_failed','order_id':order_id,'error':exception.message})
await self._unit_of_work.payment_repository.update_status(
order_id=order_id,
status=PaymentStatus.RECEIPT_ERROR,
)
return
receipt_model = receipt_response.get('Model')
if not isinstance(receipt_model, dict):
receipt_model = {}

View File

@@ -1,14 +1,14 @@
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal, ROUND_UP
from decimal import Decimal
from ulid import ULID
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ICache,ILogger
from src.application.contracts import ILogger
from src.application.contracts import IItPayService
from src.application.domain.entities.order import OrderEntity
from src.application.domain.enums import OrderStatus
from src.application.domain.exceptions import ApplicationException,ServiceUnavailableException
from src.infrastructure.config import settings
from src.application.domain.exceptions import ApplicationException
from src.application.services import PaymentQuoteService
from src.infrastructure.database.decorators import transactional
from src.presentation.schemas.order import CreateOrder
@@ -20,12 +20,12 @@ class CreateOrderCommand:
*,
unit_of_work: IUnitOfWork,
logger: ILogger,
remote_cache: ICache,
payment_quote_service: PaymentQuoteService,
itpay_service: IItPayService,
) -> None:
self._unit_of_work = unit_of_work
self._logger = logger
self._remote_cache = remote_cache
self._payment_quote_service = payment_quote_service
self._itpay_service = itpay_service
@@ -33,22 +33,11 @@ class CreateOrderCommand:
async def __call__(self, payment_data: CreateOrder, user_id: str) -> OrderEntity:
client_payment_id = str(ULID())
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')
actual_gas_fee = Decimal(gas_raw).quantize(Decimal('0.00'), rounding=ROUND_UP)
actual_usdt_exchange_rate = Decimal(rate_raw).quantize(Decimal('0.00'), rounding=ROUND_UP)
actual_service_fee = (payment_data.usdt_amount * actual_usdt_exchange_rate * settings.PAYMENT_SERVICE_FEE_RATE).quantize(Decimal('0.01'))
actual_total_price = (payment_data.usdt_amount * actual_usdt_exchange_rate + actual_service_fee + actual_gas_fee).quantize(Decimal('0.01'))
quote = await self._payment_quote_service.get_quote(payment_data.usdt_amount)
actual_gas_fee = quote.gas_fee
actual_usdt_exchange_rate = quote.usdt_exchange_rate
actual_service_fee = quote.service_fee
actual_total_price = quote.total_price
if actual_total_price > payment_data.total_price * Decimal('1.01'):
self._logger.error('Price has changed, please refresh and try again')
raise ApplicationException(status_code=409, message='Price has changed, please refresh and try again')

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from ulid import ULID
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ILogger,IQueueMessanger
from src.application.domain.enums import PaymentStatus
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.config import settings
from src.infrastructure.database.decorators import transactional
@@ -52,3 +53,7 @@ class CreatePaymentCommand:
correlation_id=message['trace_id'],
headers={'trace_id': trace_id},
)
await self._unit_of_work.payment_repository.update_status(
order_id=order_id,
status=PaymentStatus.WEB3_PROCESSING,
)

View File

@@ -0,0 +1,111 @@
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ILogger
from src.application.domain.entities import OrderEntity,PaymentEntity
from src.application.domain.exceptions import NotFoundException
from src.application.services import PaymentQuote,PaymentQuoteService
from src.infrastructure.database.decorators import transactional
@dataclass(slots=True)
class OrderPaymentResult:
order: OrderEntity
payment: PaymentEntity | None
class GetPaymentConfigCommand:
def __init__(self, *, payment_quote_service: PaymentQuoteService, logger: ILogger):
self._payment_quote_service = payment_quote_service
self._logger = logger
async def __call__(self) -> PaymentQuote:
quote = await self._payment_quote_service.get_quote(Decimal('1.00'))
self._logger.info({'event':'payment_config_requested'})
return quote
class GetPaymentQuoteCommand:
def __init__(self, *, payment_quote_service: PaymentQuoteService, logger: ILogger):
self._payment_quote_service = payment_quote_service
self._logger = logger
async def __call__(self, usdt_amount: Decimal) -> PaymentQuote:
quote = await self._payment_quote_service.get_quote(usdt_amount)
self._logger.info({'event':'payment_quote_requested','usdt_amount':str(usdt_amount)})
return quote
class ListOrdersCommand:
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, *, user_id: str, limit: int, offset: int) -> list[OrderPaymentResult]:
orders = await self._unit_of_work.order_repository.list_by_user_id(
user_id=user_id,
limit=limit,
offset=offset,
)
items: list[OrderPaymentResult] = []
for order in orders:
if order.id is None:
continue
payment = await self._unit_of_work.payment_repository.get_by_order_id(order.id)
items.append(OrderPaymentResult(order=order,payment=payment))
self._logger.info({'event':'orders_list_requested','user_id':user_id,'limit':limit,'offset':offset})
return items
class ListPaymentsCommand:
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, *, user_id: str, limit: int, offset: int) -> list[PaymentEntity]:
payments = await self._unit_of_work.payment_repository.list_by_user_id(
user_id=user_id,
limit=limit,
offset=offset,
)
self._logger.info({'event':'payments_list_requested','user_id':user_id,'limit':limit,'offset':offset})
return payments
class GetOrderCommand:
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, *, order_id: str, user_id: str) -> OrderPaymentResult:
order = await self._unit_of_work.order_repository.get_by_id_for_user(order_id=order_id,user_id=user_id)
if order is None:
raise NotFoundException(message='Order not found')
payment = await self._unit_of_work.payment_repository.get_by_order_id(order_id)
self._logger.info({'event':'order_detail_requested','order_id':order_id,'user_id':user_id})
return OrderPaymentResult(order=order,payment=payment)
class GetOrderStatusCommand:
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, *, order_id: str, user_id: str) -> OrderPaymentResult:
order = await self._unit_of_work.order_repository.get_by_id_for_user(order_id=order_id,user_id=user_id)
if order is None:
raise NotFoundException(message='Order not found')
payment = await self._unit_of_work.payment_repository.get_by_order_id(order_id)
self._logger.info({'event':'order_status_requested','order_id':order_id,'user_id':user_id})
return OrderPaymentResult(order=order,payment=payment)