feat: add mpre endpoints
This commit is contained in:
@@ -19,6 +19,16 @@ class IOrderRepository(ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_by_id_for_user(self,*,order_id: str,user_id: str) -> OrderEntity | None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def list_by_user_id(self,*,user_id: str,limit: int,offset: int) -> list[OrderEntity]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_after_itpay_payment_created(self,order: OrderEntity) -> OrderEntity:
|
async def update_after_itpay_payment_created(self,order: OrderEntity) -> OrderEntity:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from abc import ABC,abstractmethod
|
from abc import ABC,abstractmethod
|
||||||
|
from src.application.domain.entities import PaymentEntity
|
||||||
|
from src.application.domain.enums import PaymentStatus
|
||||||
|
|
||||||
|
|
||||||
class IPaymentRepository(ABC):
|
class IPaymentRepository(ABC):
|
||||||
@@ -12,7 +14,22 @@ class IPaymentRepository(ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def update_status(self,*,order_id:str,status:PaymentStatus) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_receipt(self,*,order_id:str,receipt_cloudekassir_id:str|None,receipt_cloudekassir_link:str|None) -> None:
|
async def update_receipt(self,*,order_id:str,receipt_cloudekassir_id:str|None,receipt_cloudekassir_link:str|None) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_by_order_id(self,order_id:str) -> PaymentEntity | None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def list_by_user_id(self,*,user_id:str,limit:int,offset:int) -> list[PaymentEntity]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +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
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import ILogger,IReceipt
|
from src.application.contracts import ILogger,IReceipt
|
||||||
|
from src.application.domain.enums import PaymentStatus
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
@@ -62,18 +63,26 @@ class CreateCryptoTransferCompletedCommand:
|
|||||||
if principal_amount < 0:
|
if principal_amount < 0:
|
||||||
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
|
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
|
||||||
|
|
||||||
receipt_response = await self._receipt.create_receipt(
|
try:
|
||||||
order_id=order_id,
|
receipt_response = await self._receipt.create_receipt(
|
||||||
user_id=user_id,
|
order_id=order_id,
|
||||||
email=email,
|
user_id=user_id,
|
||||||
total_amount=total_amount,
|
email=email,
|
||||||
principal_amount=principal_amount,
|
total_amount=total_amount,
|
||||||
service_fee=service_fee,
|
principal_amount=principal_amount,
|
||||||
customer_info=customer_info,
|
service_fee=service_fee,
|
||||||
customer_inn=customer_inn,
|
customer_info=customer_info,
|
||||||
customer_birthday=customer_birthday,
|
customer_inn=customer_inn,
|
||||||
request_id=self._logger.get_trace_id(),
|
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')
|
receipt_model = receipt_response.get('Model')
|
||||||
if not isinstance(receipt_model, dict):
|
if not isinstance(receipt_model, dict):
|
||||||
receipt_model = {}
|
receipt_model = {}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from decimal import Decimal, ROUND_UP
|
from decimal import Decimal
|
||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
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.contracts import IItPayService
|
||||||
from src.application.domain.entities.order import OrderEntity
|
from src.application.domain.entities.order import OrderEntity
|
||||||
from src.application.domain.enums import OrderStatus
|
from src.application.domain.enums import OrderStatus
|
||||||
from src.application.domain.exceptions import ApplicationException,ServiceUnavailableException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.application.services import PaymentQuoteService
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
from src.presentation.schemas.order import CreateOrder
|
from src.presentation.schemas.order import CreateOrder
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ class CreateOrderCommand:
|
|||||||
*,
|
*,
|
||||||
unit_of_work: IUnitOfWork,
|
unit_of_work: IUnitOfWork,
|
||||||
logger: ILogger,
|
logger: ILogger,
|
||||||
remote_cache: ICache,
|
payment_quote_service: PaymentQuoteService,
|
||||||
itpay_service: IItPayService,
|
itpay_service: IItPayService,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._unit_of_work = unit_of_work
|
self._unit_of_work = unit_of_work
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
self._remote_cache = remote_cache
|
self._payment_quote_service = payment_quote_service
|
||||||
self._itpay_service = itpay_service
|
self._itpay_service = itpay_service
|
||||||
|
|
||||||
|
|
||||||
@@ -33,22 +33,11 @@ class CreateOrderCommand:
|
|||||||
async def __call__(self, payment_data: CreateOrder, user_id: str) -> OrderEntity:
|
async def __call__(self, payment_data: CreateOrder, user_id: str) -> OrderEntity:
|
||||||
client_payment_id = str(ULID())
|
client_payment_id = str(ULID())
|
||||||
|
|
||||||
rate_raw = await self._remote_cache.hget('tradex:rub:rate','value')
|
quote = await self._payment_quote_service.get_quote(payment_data.usdt_amount)
|
||||||
gas_raw = await self._remote_cache.hget('gwei:eth:last','normal_rub')
|
actual_gas_fee = quote.gas_fee
|
||||||
self._logger.info(f'Actual market values: rate={rate_raw}, gas={gas_raw}')
|
actual_usdt_exchange_rate = quote.usdt_exchange_rate
|
||||||
|
actual_service_fee = quote.service_fee
|
||||||
if rate_raw is None:
|
actual_total_price = quote.total_price
|
||||||
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'))
|
|
||||||
if actual_total_price > payment_data.total_price * Decimal('1.01'):
|
if actual_total_price > payment_data.total_price * Decimal('1.01'):
|
||||||
self._logger.error('Price has changed, please refresh and try again')
|
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')
|
raise ApplicationException(status_code=409, message='Price has changed, please refresh and try again')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
from ulid import ULID
|
from ulid import ULID
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import ILogger,IQueueMessanger
|
from src.application.contracts import ILogger,IQueueMessanger
|
||||||
|
from src.application.domain.enums import PaymentStatus
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
@@ -52,3 +53,7 @@ class CreatePaymentCommand:
|
|||||||
correlation_id=message['trace_id'],
|
correlation_id=message['trace_id'],
|
||||||
headers={'trace_id': trace_id},
|
headers={'trace_id': trace_id},
|
||||||
)
|
)
|
||||||
|
await self._unit_of_work.payment_repository.update_status(
|
||||||
|
order_id=order_id,
|
||||||
|
status=PaymentStatus.WEB3_PROCESSING,
|
||||||
|
)
|
||||||
|
|||||||
111
src/application/commands/payment_read_commands.py
Normal file
111
src/application/commands/payment_read_commands.py
Normal 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)
|
||||||
@@ -7,6 +7,10 @@ from src.application.domain.enums import PaymentStatus
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class PaymentEntity:
|
class PaymentEntity:
|
||||||
|
id: str | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
user_id: str | None = None
|
user_id: str | None = None
|
||||||
|
|
||||||
order_id: str | None = None
|
order_id: str | None = None
|
||||||
@@ -16,7 +20,8 @@ class PaymentEntity:
|
|||||||
receipt_cloudekassir_link: str | None = None
|
receipt_cloudekassir_link: str | None = None
|
||||||
|
|
||||||
itpay_payment_id: str | None = None
|
itpay_payment_id: str | None = None
|
||||||
|
itpay_paid_amount: Decimal | None = None
|
||||||
transaction_id: str | None = None
|
transaction_id: str | None = None
|
||||||
web3_transaction_hash: str | None = None
|
web3_transaction_hash: str | None = None
|
||||||
paid_at: str | None = None
|
paid_at: datetime | None = None
|
||||||
expired_date: str | None = None
|
expired_date: datetime | None = None
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from enum import Enum
|
|||||||
class PaymentStatus(str,Enum):
|
class PaymentStatus(str,Enum):
|
||||||
PENDING='pending'
|
PENDING='pending'
|
||||||
MONEY_ACCEPTED='money_accepted'
|
MONEY_ACCEPTED='money_accepted'
|
||||||
|
WEB3_PROCESSING='web3_processing'
|
||||||
WEB3_HASH_ERROR='web3_hash_error'
|
WEB3_HASH_ERROR='web3_hash_error'
|
||||||
WEB3_BALANCE_PROBLEM='web3_balance_problem'
|
WEB3_BALANCE_PROBLEM='web3_balance_problem'
|
||||||
USDT_DELIVERED='usdt_delivered'
|
USDT_DELIVERED='usdt_delivered'
|
||||||
|
|||||||
4
src/application/services/__init__.py
Normal file
4
src/application/services/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from src.application.services.payment_quote_service import PaymentQuote,PaymentQuoteService
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['PaymentQuote', 'PaymentQuoteService']
|
||||||
53
src/application/services/payment_quote_service.py
Normal file
53
src/application/services/payment_quote_service.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class PaymentQuote:
|
||||||
|
usdt_amount: Decimal
|
||||||
|
usdt_exchange_rate: Decimal
|
||||||
|
gas_fee: Decimal
|
||||||
|
service_fee: Decimal
|
||||||
|
total_price: Decimal
|
||||||
|
service_fee_rate: Decimal
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentQuoteService:
|
||||||
|
def __init__(self, *, remote_cache: ICache, logger: ILogger):
|
||||||
|
self._remote_cache = remote_cache
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
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')
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
return PaymentQuote(
|
||||||
|
usdt_amount=usdt_amount,
|
||||||
|
usdt_exchange_rate=usdt_exchange_rate,
|
||||||
|
gas_fee=gas_fee,
|
||||||
|
service_fee=service_fee,
|
||||||
|
total_price=total_price,
|
||||||
|
service_fee_rate=settings.PAYMENT_SERVICE_FEE_RATE,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from datetime import datetime,timezone
|
from datetime import datetime,timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from sqlalchemy import select,update
|
from sqlalchemy import desc,select,update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from src.application.abstractions.repositories.i_order_repository import IOrderRepository
|
from src.application.abstractions.repositories.i_order_repository import IOrderRepository
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
@@ -85,6 +85,26 @@ class OrderRepository(IOrderRepository):
|
|||||||
return self._to_entity(model)
|
return self._to_entity(model)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_by_id_for_user(self,*,order_id: str,user_id: str) -> OrderEntity | None:
|
||||||
|
stmt=select(Order).where(Order.id==order_id,Order.user_id==user_id)
|
||||||
|
model=await self._session.scalar(stmt)
|
||||||
|
if model is None:
|
||||||
|
return None
|
||||||
|
return self._to_entity(model)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_by_user_id(self,*,user_id: str,limit: int,offset: int) -> list[OrderEntity]:
|
||||||
|
stmt=(
|
||||||
|
select(Order)
|
||||||
|
.where(Order.user_id==user_id)
|
||||||
|
.order_by(desc(Order.created_at))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
result=await self._session.scalars(stmt)
|
||||||
|
return [self._to_entity(model) for model in result.all()]
|
||||||
|
|
||||||
|
|
||||||
async def update_after_itpay_payment_created(self,order: OrderEntity) -> OrderEntity:
|
async def update_after_itpay_payment_created(self,order: OrderEntity) -> OrderEntity:
|
||||||
if not order.id:
|
if not order.id:
|
||||||
raise ValueError('OrderEntity.id is required')
|
raise ValueError('OrderEntity.id is required')
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from sqlalchemy import select
|
from sqlalchemy import desc,select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from src.application.abstractions.repositories.i_payment_repository import IPaymentRepository
|
from src.application.abstractions.repositories.i_payment_repository import IPaymentRepository
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
|
from src.application.domain.entities import PaymentEntity
|
||||||
from src.application.domain.enums import PaymentStatus
|
from src.application.domain.enums import PaymentStatus
|
||||||
from src.infrastructure.database.models.payment import Payment
|
from src.infrastructure.database.models.payment import Payment
|
||||||
|
|
||||||
@@ -16,6 +17,26 @@ class PaymentRepository(IPaymentRepository):
|
|||||||
self._logger=logger
|
self._logger=logger
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_entity(model: Payment) -> PaymentEntity:
|
||||||
|
return PaymentEntity(
|
||||||
|
id=model.id,
|
||||||
|
created_at=model.created_at,
|
||||||
|
updated_at=model.updated_at,
|
||||||
|
user_id=model.user_id,
|
||||||
|
order_id=model.order_id,
|
||||||
|
status=model.status,
|
||||||
|
receipt_cloudekassir_id=model.receipt_cloudekassir_id,
|
||||||
|
receipt_cloudekassir_link=model.receipt_cloudekassir_link,
|
||||||
|
itpay_payment_id=model.itpay_payment_id,
|
||||||
|
itpay_paid_amount=model.itpay_paid_amount,
|
||||||
|
transaction_id=model.transaction_id,
|
||||||
|
web3_transaction_hash=model.web3_transaction_hash,
|
||||||
|
paid_at=model.paid_at,
|
||||||
|
expired_date=model.expired_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_completed(self,*,user_id:str,order_id:str,itpay_payment_id:str,itpay_paid_amount:str|None,transaction_id:str|None,paid_at:str|None,expired_date:str|None) -> bool:
|
async def create_completed(self,*,user_id:str,order_id:str,itpay_payment_id:str,itpay_paid_amount:str|None,transaction_id:str|None,paid_at:str|None,expired_date:str|None) -> bool:
|
||||||
stmt=select(Payment).where(Payment.order_id==order_id)
|
stmt=select(Payment).where(Payment.order_id==order_id)
|
||||||
existing=await self._session.scalar(stmt)
|
existing=await self._session.scalar(stmt)
|
||||||
@@ -52,6 +73,16 @@ class PaymentRepository(IPaymentRepository):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def update_status(self,*,order_id:str,status:PaymentStatus) -> None:
|
||||||
|
stmt=select(Payment).where(Payment.order_id==order_id)
|
||||||
|
model=await self._session.scalar(stmt)
|
||||||
|
if model is None:
|
||||||
|
return
|
||||||
|
model.status=status
|
||||||
|
await self._session.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
async def update_receipt(self,*,order_id:str,receipt_cloudekassir_id:str|None,receipt_cloudekassir_link:str|None) -> None:
|
async def update_receipt(self,*,order_id:str,receipt_cloudekassir_id:str|None,receipt_cloudekassir_link:str|None) -> None:
|
||||||
stmt=select(Payment).where(Payment.order_id==order_id)
|
stmt=select(Payment).where(Payment.order_id==order_id)
|
||||||
model=await self._session.scalar(stmt)
|
model=await self._session.scalar(stmt)
|
||||||
@@ -63,3 +94,23 @@ class PaymentRepository(IPaymentRepository):
|
|||||||
await self._session.flush()
|
await self._session.flush()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def get_by_order_id(self,order_id:str) -> PaymentEntity | None:
|
||||||
|
stmt=select(Payment).where(Payment.order_id==order_id)
|
||||||
|
model=await self._session.scalar(stmt)
|
||||||
|
if model is None:
|
||||||
|
return None
|
||||||
|
return self._to_entity(model)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_by_user_id(self,*,user_id:str,limit:int,offset:int) -> list[PaymentEntity]:
|
||||||
|
stmt=(
|
||||||
|
select(Payment)
|
||||||
|
.where(Payment.user_id==user_id)
|
||||||
|
.order_by(desc(Payment.created_at))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
result=await self._session.scalars(stmt)
|
||||||
|
return [self._to_entity(model) for model in result.all()]
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from src.infrastructure.config import settings
|
|||||||
from src.presentation.handler import application_exception_handler, unhandled_exception_handler
|
from src.presentation.handler import application_exception_handler, unhandled_exception_handler
|
||||||
from src.presentation.messaging import crypto_transfer_router
|
from src.presentation.messaging import crypto_transfer_router
|
||||||
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
||||||
from src.presentation.routing import order_router
|
from src.presentation.routing import order_router,orders_router,payment_router,payments_router
|
||||||
|
|
||||||
security = HTTPBasic()
|
security = HTTPBasic()
|
||||||
|
|
||||||
@@ -74,6 +74,9 @@ app.add_exception_handler(ApplicationException, application_exception_handler)
|
|||||||
app.add_exception_handler(Exception, unhandled_exception_handler)
|
app.add_exception_handler(Exception, unhandled_exception_handler)
|
||||||
|
|
||||||
app.include_router(order_router)
|
app.include_router(order_router)
|
||||||
|
app.include_router(orders_router)
|
||||||
|
app.include_router(payment_router)
|
||||||
|
app.include_router(payments_router)
|
||||||
app.include_router(crypto_transfer_router)
|
app.include_router(crypto_transfer_router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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
|
from src.application.commands import CreateCryptoTransferCompletedCommand,CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,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.infrastructure.cloud_kassir import ClaudeKassirClient
|
from src.infrastructure.cloud_kassir import ClaudeKassirClient
|
||||||
from src.infrastructure.cloud_kassir.constants import CLOUD_KASSIR_API_BASE_URL,CLOUD_KASSIR_FAIL_URL,CLOUD_KASSIR_INN,CLOUD_KASSIR_SUCCESS_URL
|
from src.infrastructure.cloud_kassir.constants import CLOUD_KASSIR_API_BASE_URL,CLOUD_KASSIR_FAIL_URL,CLOUD_KASSIR_INN,CLOUD_KASSIR_SUCCESS_URL
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
@@ -28,14 +29,64 @@ def get_create_order_command(
|
|||||||
remote_cache: ICache = Depends(get_remote_cache),
|
remote_cache: ICache = Depends(get_remote_cache),
|
||||||
itpay_service: IItPayService = Depends(get_itpay_service),
|
itpay_service: IItPayService = Depends(get_itpay_service),
|
||||||
) -> CreateOrderCommand:
|
) -> CreateOrderCommand:
|
||||||
|
payment_quote_service = PaymentQuoteService(remote_cache=remote_cache,logger=logger)
|
||||||
return CreateOrderCommand(
|
return CreateOrderCommand(
|
||||||
unit_of_work=unit_of_work,
|
unit_of_work=unit_of_work,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
remote_cache=remote_cache,
|
payment_quote_service=payment_quote_service,
|
||||||
itpay_service=itpay_service,
|
itpay_service=itpay_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_quote_service(
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
remote_cache: ICache = Depends(get_remote_cache),
|
||||||
|
) -> PaymentQuoteService:
|
||||||
|
return PaymentQuoteService(remote_cache=remote_cache,logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_config_command(
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
payment_quote_service: PaymentQuoteService = Depends(get_payment_quote_service),
|
||||||
|
) -> GetPaymentConfigCommand:
|
||||||
|
return GetPaymentConfigCommand(payment_quote_service=payment_quote_service,logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_quote_command(
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
payment_quote_service: PaymentQuoteService = Depends(get_payment_quote_service),
|
||||||
|
) -> GetPaymentQuoteCommand:
|
||||||
|
return GetPaymentQuoteCommand(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),
|
||||||
|
) -> ListOrdersCommand:
|
||||||
|
return ListOrdersCommand(unit_of_work=unit_of_work,logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_list_payments_command(
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
) -> ListPaymentsCommand:
|
||||||
|
return ListPaymentsCommand(unit_of_work=unit_of_work,logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_order_command(
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
) -> GetOrderCommand:
|
||||||
|
return GetOrderCommand(unit_of_work=unit_of_work,logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
def get_order_status_command(
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
) -> GetOrderStatusCommand:
|
||||||
|
return GetOrderStatusCommand(unit_of_work=unit_of_work,logger=logger)
|
||||||
|
|
||||||
|
|
||||||
def get_create_payment_command(
|
def get_create_payment_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),
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from src.presentation.routing.order import order_router
|
from src.presentation.routing.order import order_router,orders_router,payment_router,payments_router
|
||||||
@@ -1,20 +1,155 @@
|
|||||||
|
import asyncio
|
||||||
|
from decimal import Decimal
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
import orjson
|
import orjson
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter,Depends,Query,Request,WebSocket,WebSocketDisconnect
|
||||||
from fastapi.responses import ORJSONResponse
|
from fastapi.responses import ORJSONResponse
|
||||||
from src.application.commands import CreateOrderCommand
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
from src.application.commands import CreatePaymentCommand
|
from ulid import ULID
|
||||||
from src.application.contracts import ILogger
|
from src.application.commands import CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,ListOrdersCommand,ListPaymentsCommand
|
||||||
from src.application.domain.dto import AuthContext
|
from src.application.contracts import IJwtService,ILogger
|
||||||
|
from src.application.domain.dto import AccessTokenPayload,AuthContext
|
||||||
|
from src.application.domain.entities import OrderEntity,PaymentEntity
|
||||||
from src.application.domain.enums import OrderStatus
|
from src.application.domain.enums import OrderStatus
|
||||||
from src.application.domain.exceptions import ConflictException
|
from src.application.domain.exceptions import ApplicationException,ConflictException
|
||||||
|
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.decorators import require_access_token, csrf_protect
|
||||||
from src.presentation.dependencies.commands import get_create_order_command, get_create_payment_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
|
||||||
from src.presentation.dependencies.logger import get_logger
|
from src.presentation.dependencies.logger import get_logger
|
||||||
from src.presentation.schemas.order import CreateOrder,CreateOrderResponse,ErrorResponse,OrderPaymentResponse
|
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.itpay_payment_models import ItpayPaymentData
|
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||||
|
|
||||||
order_router = APIRouter(prefix='/order', tags=['orders'])
|
order_router = APIRouter(prefix='/order', tags=['orders'])
|
||||||
|
orders_router = APIRouter(tags=['orders'])
|
||||||
|
payment_router = APIRouter(prefix='/payment', tags=['payments'])
|
||||||
|
payments_router = APIRouter(tags=['payments'])
|
||||||
|
|
||||||
|
|
||||||
|
def _payment_config_response(quote: PaymentQuote) -> PaymentConfigResponse:
|
||||||
|
return PaymentConfigResponse(
|
||||||
|
status_code=200,
|
||||||
|
usdt_exchange_rate=str(quote.usdt_exchange_rate),
|
||||||
|
gas_fee=str(quote.gas_fee),
|
||||||
|
service_fee_rate=str(quote.service_fee_rate),
|
||||||
|
one_usdt_service_fee=str(quote.service_fee),
|
||||||
|
one_usdt_total_price=str(quote.total_price),
|
||||||
|
created_at=quote.created_at.isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _payment_quote_response(quote: PaymentQuote) -> PaymentQuoteResponse:
|
||||||
|
return PaymentQuoteResponse(
|
||||||
|
status_code=200,
|
||||||
|
usdt_amount=str(quote.usdt_amount),
|
||||||
|
usdt_exchange_rate=str(quote.usdt_exchange_rate),
|
||||||
|
gas_fee=str(quote.gas_fee),
|
||||||
|
service_fee=str(quote.service_fee),
|
||||||
|
total_price=str(quote.total_price),
|
||||||
|
service_fee_rate=str(quote.service_fee_rate),
|
||||||
|
created_at=quote.created_at.isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _order_response(o: OrderEntity) -> OrderPaymentResponse:
|
||||||
|
return OrderPaymentResponse(
|
||||||
|
id=o.id,
|
||||||
|
created_at=o.created_at.isoformat() if o.created_at is not None else None,
|
||||||
|
updated_at=o.updated_at.isoformat() if o.updated_at is not None else None,
|
||||||
|
user_id=o.user_id,
|
||||||
|
usdt_amount=str(o.usdt_amount) if o.usdt_amount is not None else None,
|
||||||
|
usdt_exchange_rate=str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
||||||
|
gas_fee=str(o.gas_fee) if o.gas_fee is not None else None,
|
||||||
|
total_price=str(o.total_price) if o.total_price is not None else None,
|
||||||
|
service_fee=str(o.service_fee) if o.service_fee is not None else None,
|
||||||
|
status=o.status,
|
||||||
|
client_payment_id=o.client_payment_id,
|
||||||
|
itpay_payment_qr_url_desktop=o.itpay_payment_qr_url_desktop,
|
||||||
|
itpay_payment_qr_url_android=o.itpay_payment_qr_url_android,
|
||||||
|
itpay_payment_qr_url_ios=o.itpay_payment_qr_url_ios,
|
||||||
|
itpay_payment_qr_image_desktop=o.itpay_payment_qr_image_desktop,
|
||||||
|
itpay_payment_qr_image_android=o.itpay_payment_qr_image_android,
|
||||||
|
itpay_payment_qr_image_ios=o.itpay_payment_qr_image_ios,
|
||||||
|
itpay_id=o.itpay_id,
|
||||||
|
itpay_qr_id=o.itpay_qr_id,
|
||||||
|
itpay_amount=str(o.itpay_amount) if o.itpay_amount is not None else None,
|
||||||
|
itpay_created_at=o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _payment_response(payment: PaymentEntity) -> PaymentResponse:
|
||||||
|
return PaymentResponse(
|
||||||
|
id=payment.id,
|
||||||
|
created_at=payment.created_at.isoformat() if payment.created_at is not None else None,
|
||||||
|
updated_at=payment.updated_at.isoformat() if payment.updated_at is not None else None,
|
||||||
|
user_id=payment.user_id,
|
||||||
|
order_id=payment.order_id,
|
||||||
|
status=payment.status,
|
||||||
|
receipt_cloudekassir_id=payment.receipt_cloudekassir_id,
|
||||||
|
receipt_cloudekassir_link=payment.receipt_cloudekassir_link,
|
||||||
|
itpay_payment_id=payment.itpay_payment_id,
|
||||||
|
itpay_paid_amount=str(payment.itpay_paid_amount) if payment.itpay_paid_amount is not None else None,
|
||||||
|
transaction_id=payment.transaction_id,
|
||||||
|
web3_transaction_hash=payment.web3_transaction_hash,
|
||||||
|
paid_at=payment.paid_at.isoformat() if payment.paid_at is not None else None,
|
||||||
|
expired_date=payment.expired_date.isoformat() if payment.expired_date is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _order_detail_response(order: OrderEntity, payment: PaymentEntity | None) -> OrderDetailResponse:
|
||||||
|
return OrderDetailResponse(
|
||||||
|
status_code=200,
|
||||||
|
order=_order_response(order),
|
||||||
|
payment=_payment_response(payment) if payment is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _order_with_payment_response(order: OrderEntity, payment: PaymentEntity | None) -> OrderWithPaymentResponse:
|
||||||
|
return OrderWithPaymentResponse(
|
||||||
|
order=_order_response(order),
|
||||||
|
payment=_payment_response(payment) if payment is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _order_status_response(order: OrderEntity, payment: PaymentEntity | None) -> OrderStatusResponse:
|
||||||
|
return OrderStatusResponse(
|
||||||
|
status_code=200,
|
||||||
|
order_id=str(order.id),
|
||||||
|
order_status=order.status,
|
||||||
|
payment_status=payment.status if payment is not None else None,
|
||||||
|
receipt_cloudekassir_link=payment.receipt_cloudekassir_link if payment is not None else None,
|
||||||
|
web3_transaction_hash=payment.web3_transaction_hash if payment is not None else None,
|
||||||
|
updated_at=payment.updated_at.isoformat() if payment is not None and payment.updated_at is not None else order.updated_at.isoformat() if order.updated_at is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_websocket_access_token(websocket: WebSocket) -> str | None:
|
||||||
|
token = websocket.cookies.get('access_token')
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
auth = websocket.headers.get('Authorization')
|
||||||
|
if auth:
|
||||||
|
scheme,param = get_authorization_scheme_param(auth)
|
||||||
|
if scheme.lower() == 'bearer' and param:
|
||||||
|
return param
|
||||||
|
query_token = websocket.query_params.get('access_token') or websocket.query_params.get('token')
|
||||||
|
if query_token:
|
||||||
|
return str(query_token)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _websocket_auth_context(websocket: WebSocket, jwt_service: IJwtService) -> AuthContext | None:
|
||||||
|
token = _extract_websocket_access_token(websocket)
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
payload: AccessTokenPayload = await jwt_service.decode_access_token(token)
|
||||||
|
except ApplicationException:
|
||||||
|
return None
|
||||||
|
if payload.type != 'access':
|
||||||
|
return None
|
||||||
|
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)
|
||||||
|
|
||||||
|
|
||||||
@order_router.post(
|
@order_router.post(
|
||||||
@@ -59,33 +194,113 @@ async def create_order(
|
|||||||
raise ConflictException(message='Payment provider rejected order')
|
raise ConflictException(message='Payment provider rejected order')
|
||||||
content = CreateOrderResponse(
|
content = CreateOrderResponse(
|
||||||
status_code=201,
|
status_code=201,
|
||||||
order=OrderPaymentResponse(
|
order=_order_response(o),
|
||||||
id=o.id,
|
|
||||||
created_at=o.created_at.isoformat() if o.created_at is not None else None,
|
|
||||||
updated_at=o.updated_at.isoformat() if o.updated_at is not None else None,
|
|
||||||
user_id=o.user_id,
|
|
||||||
usdt_amount=str(o.usdt_amount) if o.usdt_amount is not None else None,
|
|
||||||
usdt_exchange_rate=str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
|
||||||
gas_fee=str(o.gas_fee) if o.gas_fee is not None else None,
|
|
||||||
total_price=str(o.total_price) if o.total_price is not None else None,
|
|
||||||
service_fee=str(o.service_fee) if o.service_fee is not None else None,
|
|
||||||
status=o.status,
|
|
||||||
client_payment_id=o.client_payment_id,
|
|
||||||
itpay_payment_qr_url_desktop=o.itpay_payment_qr_url_desktop,
|
|
||||||
itpay_payment_qr_url_android=o.itpay_payment_qr_url_android,
|
|
||||||
itpay_payment_qr_url_ios=o.itpay_payment_qr_url_ios,
|
|
||||||
itpay_payment_qr_image_desktop=o.itpay_payment_qr_image_desktop,
|
|
||||||
itpay_payment_qr_image_android=o.itpay_payment_qr_image_android,
|
|
||||||
itpay_payment_qr_image_ios=o.itpay_payment_qr_image_ios,
|
|
||||||
itpay_id=o.itpay_id,
|
|
||||||
itpay_qr_id=o.itpay_qr_id,
|
|
||||||
itpay_amount=str(o.itpay_amount) if o.itpay_amount is not None else None,
|
|
||||||
itpay_created_at=o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
@payment_router.get('/config', response_model=PaymentConfigResponse)
|
||||||
|
async def payment_config(
|
||||||
|
command: GetPaymentConfigCommand = Depends(get_payment_config_command),
|
||||||
|
) -> PaymentConfigResponse:
|
||||||
|
quote = await command()
|
||||||
|
return _payment_config_response(quote)
|
||||||
|
|
||||||
|
|
||||||
|
@payment_router.get('/quote', response_model=PaymentQuoteResponse)
|
||||||
|
async def payment_quote(
|
||||||
|
usdt_amount: Decimal = Query(gt=0, decimal_places=2, max_digits=20),
|
||||||
|
command: GetPaymentQuoteCommand = Depends(get_payment_quote_command),
|
||||||
|
) -> PaymentQuoteResponse:
|
||||||
|
quote = await command(usdt_amount)
|
||||||
|
return _payment_quote_response(quote)
|
||||||
|
|
||||||
|
|
||||||
|
@orders_router.get('/orders', response_model=OrdersResponse)
|
||||||
|
async def list_orders(
|
||||||
|
limit: int = Query(default=20, ge=1, le=100),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
auth: AuthContext = Depends(require_access_token),
|
||||||
|
command: ListOrdersCommand = Depends(get_list_orders_command),
|
||||||
|
) -> OrdersResponse:
|
||||||
|
orders = await command(user_id=auth.user_id,limit=limit,offset=offset)
|
||||||
|
items = [_order_with_payment_response(item.order,item.payment) for item in orders]
|
||||||
|
return OrdersResponse(status_code=200,orders=items,limit=limit,offset=offset)
|
||||||
|
|
||||||
|
|
||||||
|
@payments_router.get('/payments', response_model=PaymentsResponse)
|
||||||
|
async def list_payments(
|
||||||
|
limit: int = Query(default=20, ge=1, le=100),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
auth: AuthContext = Depends(require_access_token),
|
||||||
|
command: ListPaymentsCommand = Depends(get_list_payments_command),
|
||||||
|
) -> PaymentsResponse:
|
||||||
|
payments = await command(user_id=auth.user_id,limit=limit,offset=offset)
|
||||||
|
return PaymentsResponse(
|
||||||
|
status_code=200,
|
||||||
|
payments=[_payment_response(payment) for payment in payments],
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@order_router.get('/{order_id}/status', response_model=OrderStatusResponse)
|
||||||
|
async def order_status(
|
||||||
|
order_id: str,
|
||||||
|
auth: AuthContext = Depends(require_access_token),
|
||||||
|
command: GetOrderStatusCommand = Depends(get_order_status_command),
|
||||||
|
) -> OrderStatusResponse:
|
||||||
|
result = await command(order_id=order_id,user_id=auth.user_id)
|
||||||
|
return _order_status_response(result.order,result.payment)
|
||||||
|
|
||||||
|
|
||||||
|
@order_router.websocket('/{order_id}/events')
|
||||||
|
async def order_events(
|
||||||
|
websocket: WebSocket,
|
||||||
|
order_id: str,
|
||||||
|
command: GetOrderStatusCommand = Depends(get_order_status_command),
|
||||||
|
jwt_service: IJwtService = Depends(get_jwt_service),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> None:
|
||||||
|
trace_id = websocket.headers.get('X-Trace-ID') or websocket.headers.get('X-Request-ID') or str(ULID())
|
||||||
|
token = trace_id_var.set(trace_id)
|
||||||
|
try:
|
||||||
|
auth = await _websocket_auth_context(websocket, jwt_service)
|
||||||
|
if auth is None:
|
||||||
|
await websocket.close(code=1008)
|
||||||
|
return
|
||||||
|
await websocket.accept()
|
||||||
|
logger.info({'event':'order_events_connected','order_id':order_id,'user_id':auth.user_id})
|
||||||
|
last_payload: dict | None = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
result = await command(order_id=order_id,user_id=auth.user_id)
|
||||||
|
except ApplicationException as exception:
|
||||||
|
await websocket.send_json({'event':'order_events_error','detail':exception.message,'status_code':exception.status_code})
|
||||||
|
await websocket.close(code=1008)
|
||||||
|
return
|
||||||
|
status_payload = _order_status_response(result.order,result.payment).model_dump(mode='json')
|
||||||
|
payload = {'event':'order_status','data':status_payload}
|
||||||
|
if payload != last_payload:
|
||||||
|
await websocket.send_text(orjson.dumps(payload).decode())
|
||||||
|
last_payload = payload
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
logger.info({'event':'order_events_disconnected','order_id':order_id})
|
||||||
|
finally:
|
||||||
|
trace_id_var.reset(token)
|
||||||
|
|
||||||
|
|
||||||
|
@order_router.get('/{order_id}', response_model=OrderDetailResponse)
|
||||||
|
async def order_detail(
|
||||||
|
order_id: str,
|
||||||
|
auth: AuthContext = Depends(require_access_token),
|
||||||
|
command: GetOrderCommand = Depends(get_order_command),
|
||||||
|
) -> OrderDetailResponse:
|
||||||
|
result = await command(order_id=order_id,user_id=auth.user_id)
|
||||||
|
return _order_detail_response(result.order,result.payment)
|
||||||
|
|
||||||
|
|
||||||
@order_router.post('/webhook/itpay')
|
@order_router.post('/webhook/itpay')
|
||||||
async def itpay_webhook(
|
async def itpay_webhook(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pydantic import BaseModel,Field
|
from pydantic import BaseModel,Field
|
||||||
from src.application.domain.enums import OrderStatus
|
from src.application.domain.enums import OrderStatus,PaymentStatus
|
||||||
|
|
||||||
|
|
||||||
class CreateOrder(BaseModel):
|
class CreateOrder(BaseModel):
|
||||||
@@ -34,11 +34,82 @@ class OrderPaymentResponse(BaseModel):
|
|||||||
itpay_created_at: str | None = None
|
itpay_created_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentResponse(BaseModel):
|
||||||
|
id: str | None = None
|
||||||
|
created_at: str | None = None
|
||||||
|
updated_at: str | None = None
|
||||||
|
user_id: str | None = None
|
||||||
|
order_id: str | None = None
|
||||||
|
status: PaymentStatus | None = None
|
||||||
|
receipt_cloudekassir_id: str | None = None
|
||||||
|
receipt_cloudekassir_link: str | None = None
|
||||||
|
itpay_payment_id: str | None = None
|
||||||
|
itpay_paid_amount: str | None = None
|
||||||
|
transaction_id: str | None = None
|
||||||
|
web3_transaction_hash: str | None = None
|
||||||
|
paid_at: str | None = None
|
||||||
|
expired_date: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CreateOrderResponse(BaseModel):
|
class CreateOrderResponse(BaseModel):
|
||||||
status_code: int
|
status_code: int
|
||||||
order: OrderPaymentResponse
|
order: OrderPaymentResponse
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentConfigResponse(BaseModel):
|
||||||
|
status_code: int
|
||||||
|
usdt_exchange_rate: str
|
||||||
|
gas_fee: str
|
||||||
|
service_fee_rate: str
|
||||||
|
one_usdt_service_fee: str
|
||||||
|
one_usdt_total_price: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentQuoteResponse(BaseModel):
|
||||||
|
status_code: int
|
||||||
|
usdt_amount: str
|
||||||
|
usdt_exchange_rate: str
|
||||||
|
gas_fee: str
|
||||||
|
service_fee: str
|
||||||
|
total_price: str
|
||||||
|
service_fee_rate: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
|
class OrderWithPaymentResponse(BaseModel):
|
||||||
|
order: OrderPaymentResponse
|
||||||
|
payment: PaymentResponse | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class OrderDetailResponse(OrderWithPaymentResponse):
|
||||||
|
status_code: int
|
||||||
|
|
||||||
|
|
||||||
|
class OrdersResponse(BaseModel):
|
||||||
|
status_code: int
|
||||||
|
orders: list[OrderWithPaymentResponse]
|
||||||
|
limit: int
|
||||||
|
offset: int
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentsResponse(BaseModel):
|
||||||
|
status_code: int
|
||||||
|
payments: list[PaymentResponse]
|
||||||
|
limit: int
|
||||||
|
offset: int
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatusResponse(BaseModel):
|
||||||
|
status_code: int
|
||||||
|
order_id: str
|
||||||
|
order_status: OrderStatus | None = None
|
||||||
|
payment_status: PaymentStatus | None = None
|
||||||
|
receipt_cloudekassir_link: str | None = None
|
||||||
|
web3_transaction_hash: str | None = None
|
||||||
|
updated_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(BaseModel):
|
class ErrorResponse(BaseModel):
|
||||||
detail: str
|
detail: str
|
||||||
status_code: int
|
status_code: int
|
||||||
|
|||||||
Reference in New Issue
Block a user