feat: add view order history and filter by date
This commit is contained in:
@@ -10,6 +10,7 @@ from src.application.abstractions.repositories import (
|
|||||||
IPurchaseRequestRepository,
|
IPurchaseRequestRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
IWalletRepository,
|
IWalletRepository,
|
||||||
|
IOrderRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,4 +41,7 @@ class IUnitOfWork(Protocol):
|
|||||||
def purchase_request_repository(self) -> IPurchaseRequestRepository: ...
|
def purchase_request_repository(self) -> IPurchaseRequestRepository: ...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wallet_repository(self) -> IWalletRepository: ...
|
def wallet_repository(self) -> IWalletRepository: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def order_repository(self) -> IOrderRepository: ...
|
||||||
@@ -5,3 +5,4 @@ from src.application.abstractions.repositories.i_legal_entity_repository import
|
|||||||
from src.application.abstractions.repositories.i_organization_wallet_repository import IOrganizationWalletRepository
|
from src.application.abstractions.repositories.i_organization_wallet_repository import IOrganizationWalletRepository
|
||||||
from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository
|
from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository
|
||||||
from src.application.abstractions.repositories.i_wallets_repository import IWalletRepository
|
from src.application.abstractions.repositories.i_wallets_repository import IWalletRepository
|
||||||
|
from src.application.abstractions.repositories.i_order_repository import IOrderRepository
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from abc import ABC,abstractmethod
|
||||||
|
|
||||||
|
from src.application.domain.entities.order import OrderEntity
|
||||||
|
from src.application.domain.enums import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
|
class IOrderRepository(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
async def get_by_id(self,order_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
|
||||||
|
async def list_all(self,*,limit: int,offset: int, search: str | None = None) -> list[OrderEntity]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def list_by_date(self,*,year: int,month: int,limit: int,offset: int) -> list[OrderEntity]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def count_all(self,*,search: str | None = None) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def count_by_date(self,*,year: int,month: int | None = None) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def count_by_user_id(self,*,user_id: str) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -38,6 +38,12 @@ from src.application.commands.user_wallet_commands import (
|
|||||||
GetUserSecretKeysCommand,
|
GetUserSecretKeysCommand,
|
||||||
ListUserWalletsCommand,
|
ListUserWalletsCommand,
|
||||||
)
|
)
|
||||||
|
from src.application.commands.orders_commands import (
|
||||||
|
ListOrdersCommand,
|
||||||
|
ListUserOrdersCommand,
|
||||||
|
ListOrdersByDateCommand,
|
||||||
|
GetOrderCommand,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AdminLoginCommand',
|
'AdminLoginCommand',
|
||||||
@@ -68,4 +74,8 @@ __all__ = [
|
|||||||
'ListUserWalletsCommand',
|
'ListUserWalletsCommand',
|
||||||
'GetUserMnemonicCommand',
|
'GetUserMnemonicCommand',
|
||||||
'GetUserSecretKeysCommand',
|
'GetUserSecretKeysCommand',
|
||||||
|
'ListOrdersCommand',
|
||||||
|
'ListUserOrdersCommand',
|
||||||
|
'ListOrdersByDateCommand',
|
||||||
|
'GetOrderCommand',
|
||||||
]
|
]
|
||||||
|
|||||||
80
src/application/commands/orders_commands.py
Normal file
80
src/application/commands/orders_commands.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
from src.application.domain.exceptions import NotFoundException
|
||||||
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
|
||||||
|
|
||||||
|
class ListUserOrdersCommand:
|
||||||
|
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) -> tuple[list[OrderEntity], int]:
|
||||||
|
orders = await self._unit_of_work.order_repository.list_by_user_id(
|
||||||
|
user_id=user_id,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
total = await self._unit_of_work.order_repository.count_by_user_id(user_id=user_id)
|
||||||
|
return orders, total
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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, *, limit: int, offset: int, search: str | None = None) -> tuple[list[OrderEntity], int]:
|
||||||
|
orders = await self._unit_of_work.order_repository.list_all(
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
search=search
|
||||||
|
)
|
||||||
|
|
||||||
|
total = await self._unit_of_work.order_repository.count_all(search=search)
|
||||||
|
|
||||||
|
return orders, total
|
||||||
|
|
||||||
|
|
||||||
|
class ListOrdersByDateCommand:
|
||||||
|
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||||
|
self._unit_of_work = unit_of_work
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
|
||||||
|
@transactional
|
||||||
|
async def __call__(self, *, limit: int, offset: int, year: int, month: int | None = None) -> tuple[list[OrderEntity], int]:
|
||||||
|
orders = await self._unit_of_work.order_repository.list_by_date(
|
||||||
|
year=year,
|
||||||
|
month=month,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset
|
||||||
|
)
|
||||||
|
|
||||||
|
total = await self._unit_of_work.order_repository.count_by_date(year=year, month=month)
|
||||||
|
|
||||||
|
return orders, total
|
||||||
|
|
||||||
|
|
||||||
|
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) -> OrderEntity:
|
||||||
|
order = await self._unit_of_work.order_repository.get_by_id(order_id=order_id)
|
||||||
|
if order is None:
|
||||||
|
raise NotFoundException(message='Order not found')
|
||||||
|
|
||||||
|
return order
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from src.application.domain.entities.user import UserEntity
|
from src.application.domain.entities.user import UserEntity
|
||||||
from src.application.domain.entities.admin_user import AdminUserEntity
|
from src.application.domain.entities.admin_user import AdminUserEntity
|
||||||
from src.application.domain.entities.admin_session import AdminSessionEntity
|
from src.application.domain.entities.admin_session import AdminSessionEntity
|
||||||
|
from src.application.domain.entities.order import OrderEntity
|
||||||
|
|
||||||
__all__ = ['UserEntity', 'AdminUserEntity', 'AdminSessionEntity']
|
__all__ = ['UserEntity', 'AdminUserEntity', 'AdminSessionEntity', 'OrderEntity']
|
||||||
|
|||||||
36
src/application/domain/entities/order.py
Normal file
36
src/application/domain/entities/order.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from src.application.domain.enums import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class OrderEntity:
|
||||||
|
id: str | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
|
user_id: str | None = None
|
||||||
|
usdt_amount: Decimal | None = None
|
||||||
|
usdt_exchange_rate: Decimal | None = None
|
||||||
|
gas_fee: Decimal | None = None
|
||||||
|
total_price: Decimal | None = None
|
||||||
|
service_fee: Decimal | None = None
|
||||||
|
status: OrderStatus | None = None
|
||||||
|
|
||||||
|
client_payment_id: str | None = None
|
||||||
|
|
||||||
|
itpay_payment_qr_url_desktop: str | None = None
|
||||||
|
itpay_payment_qr_url_android: str | None = None
|
||||||
|
itpay_payment_qr_url_ios: str | None = None
|
||||||
|
|
||||||
|
itpay_payment_qr_image_desktop: str | None = None
|
||||||
|
itpay_payment_qr_image_android: str | None = None
|
||||||
|
itpay_payment_qr_image_ios: str | None = None
|
||||||
|
|
||||||
|
itpay_id: str | None = None
|
||||||
|
itpay_qr_id: str | None = None
|
||||||
|
itpay_amount: Decimal | None = None
|
||||||
|
itpay_created_at: datetime | None = None
|
||||||
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
from src.application.domain.enums.log_level import LogLevel
|
from src.application.domain.enums.log_level import LogLevel
|
||||||
from src.application.domain.enums.log_format import LogFormat
|
from src.application.domain.enums.log_format import LogFormat
|
||||||
|
from src.application.domain.enums.order_status import OrderStatus
|
||||||
9
src/application/domain/enums/order_status.py
Normal file
9
src/application/domain/enums/order_status.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatus(str, Enum):
|
||||||
|
PENDING = 'pending'
|
||||||
|
REJECTED = 'rejected'
|
||||||
|
COMPLETED = 'completed'
|
||||||
|
CANCELLED = 'cancelled'
|
||||||
|
ERROR = 'error'
|
||||||
@@ -6,6 +6,7 @@ from src.infrastructure.database.models.legal_entity import LegalEntityModel
|
|||||||
from src.infrastructure.database.models.organization_wallet import OrganizationWalletModel
|
from src.infrastructure.database.models.organization_wallet import OrganizationWalletModel
|
||||||
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
|
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
|
||||||
from src.infrastructure.database.models.wallets import WalletModel
|
from src.infrastructure.database.models.wallets import WalletModel
|
||||||
|
from src.infrastructure.database.models.order import OrderModel
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Base',
|
'Base',
|
||||||
@@ -16,4 +17,5 @@ __all__ = [
|
|||||||
'OrganizationWalletModel',
|
'OrganizationWalletModel',
|
||||||
'PurchaseRequestModel',
|
'PurchaseRequestModel',
|
||||||
'WalletModel',
|
'WalletModel',
|
||||||
|
'OrderModel',
|
||||||
]
|
]
|
||||||
|
|||||||
54
src/infrastructure/database/models/order.py
Normal file
54
src/infrastructure/database/models/order.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from sqlalchemy import DateTime, Enum as SAEnum, ForeignKey, Numeric, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from src.application.domain.enums import OrderStatus
|
||||||
|
from src.infrastructure.database.models.base import Base
|
||||||
|
from src.infrastructure.database.models.mixins import AuditTimestampsMixin, UlidPrimaryKeyMixin
|
||||||
|
|
||||||
|
|
||||||
|
class OrderModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin):
|
||||||
|
__tablename__ = 'orders'
|
||||||
|
|
||||||
|
user_id: Mapped[str] = mapped_column(
|
||||||
|
String(26),
|
||||||
|
ForeignKey('users.id', ondelete='RESTRICT'),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
usdt_amount: Mapped[Decimal] = mapped_column(Numeric(38, 2), nullable=False)
|
||||||
|
usdt_exchange_rate: Mapped[Decimal] = mapped_column(Numeric(38, 2), nullable=False)
|
||||||
|
gas_fee: Mapped[Decimal] = mapped_column(Numeric(38, 2), nullable=False)
|
||||||
|
total_price: Mapped[Decimal] = mapped_column(Numeric(38, 2), nullable=False)
|
||||||
|
service_fee: Mapped[Decimal] = mapped_column(Numeric(38, 2), nullable=False)
|
||||||
|
status: Mapped[OrderStatus] = mapped_column(
|
||||||
|
SAEnum(OrderStatus,name='order_status_enum',values_callable=lambda x:[e.value for e in x]),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
default=OrderStatus.PENDING,
|
||||||
|
)
|
||||||
|
|
||||||
|
client_payment_id: Mapped[str] = mapped_column(
|
||||||
|
String(26),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
itpay_payment_qr_url_desktop: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
itpay_payment_qr_url_android: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
itpay_payment_qr_url_ios: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
itpay_payment_qr_image_desktop: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
itpay_payment_qr_image_android: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
itpay_payment_qr_image_ios: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
itpay_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
itpay_qr_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||||
|
itpay_amount: Mapped[Decimal] = mapped_column(Numeric(38, 2), nullable=False)
|
||||||
|
itpay_created_at: Mapped[DateTime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ from src.infrastructure.database.repositories.legal_entity_repository import Leg
|
|||||||
from src.infrastructure.database.repositories.organization_wallet_repository import OrganizationWalletRepository
|
from src.infrastructure.database.repositories.organization_wallet_repository import OrganizationWalletRepository
|
||||||
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
|
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
|
||||||
from src.infrastructure.database.repositories.wallets_repository import WalletRepository
|
from src.infrastructure.database.repositories.wallets_repository import WalletRepository
|
||||||
|
from src.infrastructure.database.repositories.orders_repository import OrderRepository
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AdminUserRepository',
|
'AdminUserRepository',
|
||||||
@@ -14,4 +15,5 @@ __all__ = [
|
|||||||
'OrganizationWalletRepository',
|
'OrganizationWalletRepository',
|
||||||
'PurchaseRequestRepository',
|
'PurchaseRequestRepository',
|
||||||
'WalletRepository',
|
'WalletRepository',
|
||||||
|
'OrderRepository',
|
||||||
]
|
]
|
||||||
|
|||||||
151
src/infrastructure/database/repositories/orders_repository.py
Normal file
151
src/infrastructure/database/repositories/orders_repository.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import replace
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
from sqlalchemy import desc, select, or_, extract, func
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from src.application.abstractions.repositories.i_order_repository import IOrderRepository
|
||||||
|
from src.application.contracts import ILogger
|
||||||
|
from src.application.domain.entities.order import OrderEntity
|
||||||
|
from src.application.domain.enums import OrderStatus
|
||||||
|
from src.infrastructure.database.models.order import OrderModel
|
||||||
|
|
||||||
|
|
||||||
|
class OrderRepository(IOrderRepository):
|
||||||
|
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||||
|
self._session = session
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_entity(model: OrderModel) -> OrderEntity:
|
||||||
|
return OrderEntity(
|
||||||
|
id=model.id,
|
||||||
|
created_at=model.created_at,
|
||||||
|
updated_at=model.updated_at,
|
||||||
|
user_id=model.user_id,
|
||||||
|
usdt_amount=model.usdt_amount,
|
||||||
|
usdt_exchange_rate=model.usdt_exchange_rate,
|
||||||
|
gas_fee=model.gas_fee,
|
||||||
|
total_price=model.total_price,
|
||||||
|
service_fee=model.service_fee,
|
||||||
|
status=model.status,
|
||||||
|
client_payment_id=model.client_payment_id,
|
||||||
|
itpay_payment_qr_url_desktop=model.itpay_payment_qr_url_desktop,
|
||||||
|
itpay_payment_qr_url_android=model.itpay_payment_qr_url_android,
|
||||||
|
itpay_payment_qr_url_ios=model.itpay_payment_qr_url_ios,
|
||||||
|
itpay_payment_qr_image_desktop=model.itpay_payment_qr_image_desktop,
|
||||||
|
itpay_payment_qr_image_android=model.itpay_payment_qr_image_android,
|
||||||
|
itpay_payment_qr_image_ios=model.itpay_payment_qr_image_ios,
|
||||||
|
itpay_id=model.itpay_id,
|
||||||
|
itpay_qr_id=model.itpay_qr_id,
|
||||||
|
itpay_amount=model.itpay_amount,
|
||||||
|
itpay_created_at=model.itpay_created_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _search_filter(self, search: str | None):
|
||||||
|
if not search or not search.strip():
|
||||||
|
return None
|
||||||
|
|
||||||
|
pattern = f'%{search.strip()}%'
|
||||||
|
return or_(
|
||||||
|
OrderModel.id.ilike(pattern),
|
||||||
|
OrderModel.user_id.ilike(pattern),
|
||||||
|
OrderModel.client_payment_id.ilike(pattern),
|
||||||
|
OrderModel.itpay_id.ilike(pattern),
|
||||||
|
OrderModel.itpay_qr_id.ilike(pattern),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_by_id(self,order_id: str) -> OrderEntity | None:
|
||||||
|
stmt=select(OrderModel).where(OrderModel.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[OrderEntity]:
|
||||||
|
stmt=(
|
||||||
|
select(OrderModel)
|
||||||
|
.where(OrderModel.user_id==user_id)
|
||||||
|
.order_by(desc(OrderModel.created_at))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
result=await self._session.scalars(stmt)
|
||||||
|
return [self._to_entity(model) for model in result.all()]
|
||||||
|
|
||||||
|
|
||||||
|
async def list_all(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
limit: int,
|
||||||
|
offset: int,
|
||||||
|
search: str | None = None
|
||||||
|
) -> list[OrderEntity]:
|
||||||
|
stmt=(
|
||||||
|
select(OrderModel)
|
||||||
|
.order_by(desc(OrderModel.created_at))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
search_filter = self._search_filter(search)
|
||||||
|
if search_filter is not None:
|
||||||
|
stmt = stmt.where(search_filter)
|
||||||
|
res = await self._session.execute(stmt)
|
||||||
|
return [self._to_entity(model) for model in res.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
async def list_by_date(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
year: int,
|
||||||
|
month: int,
|
||||||
|
limit: int,
|
||||||
|
offset: int,
|
||||||
|
) -> list[OrderEntity]:
|
||||||
|
stmt=(
|
||||||
|
select(OrderModel)
|
||||||
|
.where(
|
||||||
|
extract("year", OrderModel.created_at) == year,
|
||||||
|
extract("month", OrderModel.created_at) == month,
|
||||||
|
)
|
||||||
|
.order_by(desc(OrderModel.created_at))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
res = await self._session.execute(stmt)
|
||||||
|
return [self._to_entity(model) for model in res.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
async def count_all(self,*,search: str | None = None) -> int:
|
||||||
|
stmt = select(func.count()).select_from(OrderModel)
|
||||||
|
search_filter = self._search_filter(search)
|
||||||
|
if search_filter is not None:
|
||||||
|
stmt = stmt.where(search_filter)
|
||||||
|
res = await self._session.execute(stmt)
|
||||||
|
return int(res.scalar_one())
|
||||||
|
|
||||||
|
|
||||||
|
async def count_by_date(self,*,year: int,month: int ) -> int:
|
||||||
|
stmt = select(func.count()).select_from(OrderModel)
|
||||||
|
stmt = stmt.where(
|
||||||
|
extract("year", OrderModel.created_at) == year,
|
||||||
|
extract("month", OrderModel.created_at) == month
|
||||||
|
)
|
||||||
|
res = await self._session.execute(stmt)
|
||||||
|
return int(res.scalar_one())
|
||||||
|
|
||||||
|
|
||||||
|
async def count_by_user_id(self,*,user_id: str) -> int:
|
||||||
|
stmt = select(func.count()).select_from(OrderModel).where(OrderModel.user_id == user_id)
|
||||||
|
res = await self._session.execute(stmt)
|
||||||
|
return int(res.scalar_one())
|
||||||
@@ -9,6 +9,7 @@ from src.application.abstractions.repositories import (
|
|||||||
IPurchaseRequestRepository,
|
IPurchaseRequestRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
IWalletRepository,
|
IWalletRepository,
|
||||||
|
IOrderRepository,
|
||||||
)
|
)
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.exceptions import RefreshConcurrentException
|
from src.application.domain.exceptions import RefreshConcurrentException
|
||||||
@@ -20,6 +21,7 @@ from src.infrastructure.database.repositories import (
|
|||||||
PurchaseRequestRepository,
|
PurchaseRequestRepository,
|
||||||
UserRepository,
|
UserRepository,
|
||||||
WalletRepository,
|
WalletRepository,
|
||||||
|
OrderRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ class UnitOfWork(IUnitOfWork):
|
|||||||
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
|
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
|
||||||
self._purchase_request_repository: IPurchaseRequestRepository | None = None
|
self._purchase_request_repository: IPurchaseRequestRepository | None = None
|
||||||
self._wallet_repository: IWalletRepository | None = None
|
self._wallet_repository: IWalletRepository | None = None
|
||||||
|
self._order_repository: IOrderRepository | None = None
|
||||||
self._logger: ILogger = logger
|
self._logger: ILogger = logger
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
@@ -103,3 +106,11 @@ class UnitOfWork(IUnitOfWork):
|
|||||||
session=self._session, logger=self._logger
|
session=self._session, logger=self._logger
|
||||||
)
|
)
|
||||||
return self._purchase_request_repository
|
return self._purchase_request_repository
|
||||||
|
|
||||||
|
@property
|
||||||
|
def order_repository(self) -> IOrderRepository:
|
||||||
|
if self._order_repository is None:
|
||||||
|
self._order_repository = OrderRepository(
|
||||||
|
session=self._session, logger=self._logger
|
||||||
|
)
|
||||||
|
return self._order_repository
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ from src.application.commands import (
|
|||||||
ListUserWalletsCommand,
|
ListUserWalletsCommand,
|
||||||
GetUserMnemonicCommand,
|
GetUserMnemonicCommand,
|
||||||
GetUserSecretKeysCommand,
|
GetUserSecretKeysCommand,
|
||||||
|
ListOrdersCommand,
|
||||||
|
ListUserOrdersCommand,
|
||||||
|
ListOrdersByDateCommand,
|
||||||
|
GetOrderCommand,
|
||||||
)
|
)
|
||||||
from src.application.contracts import IHashService, IJwtService, ILogger
|
from src.application.contracts import IHashService, IJwtService, ILogger
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
@@ -249,4 +253,28 @@ def get_get_user_secret_keys_command(
|
|||||||
uow: IUnitOfWork = Depends(get_unit_of_work),
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
) -> GetUserSecretKeysCommand:
|
) -> GetUserSecretKeysCommand:
|
||||||
return GetUserSecretKeysCommand(uow, logger)
|
return GetUserSecretKeysCommand(uow, logger)
|
||||||
|
|
||||||
|
def get_list_orders_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> ListOrdersCommand:
|
||||||
|
return ListOrdersCommand(uow, logger)
|
||||||
|
|
||||||
|
def get_list_user_orders_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> ListUserOrdersCommand:
|
||||||
|
return ListUserOrdersCommand(uow, logger)
|
||||||
|
|
||||||
|
def get_list_orders_by_date_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> ListOrdersByDateCommand:
|
||||||
|
return ListOrdersByDateCommand(uow, logger)
|
||||||
|
|
||||||
|
def get_get_order_command(
|
||||||
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
|
logger: ILogger = Depends(get_logger),
|
||||||
|
) -> GetOrderCommand:
|
||||||
|
return GetOrderCommand(uow, logger)
|
||||||
@@ -7,6 +7,7 @@ from src.presentation.routing.organizations import organizations_router
|
|||||||
from src.presentation.routing.purchase_requests import purchase_requests_router
|
from src.presentation.routing.purchase_requests import purchase_requests_router
|
||||||
from src.presentation.routing.users import users_router
|
from src.presentation.routing.users import users_router
|
||||||
from src.presentation.routing.search import search_router
|
from src.presentation.routing.search import search_router
|
||||||
|
from src.presentation.routing.orders import orders_router
|
||||||
|
|
||||||
v1_router = APIRouter(prefix='/v1')
|
v1_router = APIRouter(prefix='/v1')
|
||||||
v1_router.include_router(auth_router)
|
v1_router.include_router(auth_router)
|
||||||
@@ -15,4 +16,5 @@ v1_router.include_router(organizations_router)
|
|||||||
v1_router.include_router(documents_router)
|
v1_router.include_router(documents_router)
|
||||||
v1_router.include_router(purchase_requests_router)
|
v1_router.include_router(purchase_requests_router)
|
||||||
v1_router.include_router(users_router)
|
v1_router.include_router(users_router)
|
||||||
v1_router.include_router(search_router)
|
v1_router.include_router(search_router)
|
||||||
|
v1_router.include_router(orders_router)
|
||||||
85
src/presentation/routing/orders.py
Normal file
85
src/presentation/routing/orders.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Request, Query, status
|
||||||
|
|
||||||
|
from src.application.domain.dto import AdminAuthContext
|
||||||
|
from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role
|
||||||
|
from src.presentation.dependencies.commands import (
|
||||||
|
get_list_orders_command,
|
||||||
|
get_list_orders_by_date_command,
|
||||||
|
get_get_order_command,
|
||||||
|
)
|
||||||
|
from src.presentation.schemas.mappers import order_to_response
|
||||||
|
from src.presentation.schemas.order import (
|
||||||
|
OrdersResponse,
|
||||||
|
OrderResponse,
|
||||||
|
OrderDetailResponse,
|
||||||
|
)
|
||||||
|
from src.application.commands.orders_commands import (
|
||||||
|
ListOrdersCommand,
|
||||||
|
ListOrdersByDateCommand,
|
||||||
|
GetOrderCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
orders_router = APIRouter(prefix='/orders', tags=['orders'])
|
||||||
|
|
||||||
|
@orders_router.get('', response_model=OrdersResponse)
|
||||||
|
async def list_orders(
|
||||||
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
q: str | None = Query(default=None, min_length=1, max_length=255),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: ListOrdersCommand = Depends(get_list_orders_command),
|
||||||
|
):
|
||||||
|
items, total = await command(limit=limit, offset=offset, search=q)
|
||||||
|
return OrdersResponse(
|
||||||
|
items=[order_to_response(x) for x in items],
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@orders_router.get('/{order_id}', response_model=OrderDetailResponse)
|
||||||
|
async def get_order(
|
||||||
|
order_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrderCommand = Depends(get_get_order_command),
|
||||||
|
):
|
||||||
|
order = await command(order_id=order_id)
|
||||||
|
return OrderDetailResponse(
|
||||||
|
id=order.id,
|
||||||
|
created_at=order.created_at.isoformat() if order.created_at is not None else None,
|
||||||
|
updated_at=order.updated_at.isoformat() if order.updated_at is not None else None,
|
||||||
|
user_id=order.user_id,
|
||||||
|
usdt_amount=str(order.usdt_amount) if order.usdt_amount is not None else None,
|
||||||
|
usdt_exchange_rate=str(order.usdt_exchange_rate) if order.usdt_exchange_rate is not None else None,
|
||||||
|
gas_fee=str(order.gas_fee) if order.gas_fee is not None else None,
|
||||||
|
total_price=str(order.total_price) if order.total_price is not None else None,
|
||||||
|
service_fee=str(order.service_fee) if order.service_fee is not None else None,
|
||||||
|
status=order.status,
|
||||||
|
client_payment_id=order.client_payment_id,
|
||||||
|
itpay_payment_qr_url_desktop=order.itpay_payment_qr_url_desktop,
|
||||||
|
itpay_payment_qr_url_android=order.itpay_payment_qr_url_android,
|
||||||
|
itpay_payment_qr_url_ios=order.itpay_payment_qr_url_ios,
|
||||||
|
itpay_payment_qr_image_desktop=order.itpay_payment_qr_image_desktop,
|
||||||
|
itpay_payment_qr_image_android=order.itpay_payment_qr_image_android,
|
||||||
|
itpay_payment_qr_image_ios=order.itpay_payment_qr_image_ios,
|
||||||
|
itpay_id=order.itpay_id,
|
||||||
|
itpay_qr_id=order.itpay_qr_id,
|
||||||
|
itpay_amount=str(order.itpay_amount) if order.itpay_amount is not None else None,
|
||||||
|
itpay_created_at=order.itpay_created_at.isoformat() if order.itpay_created_at is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@orders_router.get('/filter/date', response_model=OrdersResponse)
|
||||||
|
async def list_orders_by_date(
|
||||||
|
year: int = Query(..., ge=2000, le=2100),
|
||||||
|
month: int = Query(..., ge=1, le=12),
|
||||||
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: ListOrdersByDateCommand = Depends(get_list_orders_by_date_command),
|
||||||
|
):
|
||||||
|
orders, total = await command(year=year, month=month, limit=limit, offset=offset)
|
||||||
|
return OrdersResponse(
|
||||||
|
items=[order_to_response(o) for o in orders],
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
@@ -11,6 +11,7 @@ from src.presentation.dependencies.commands import (
|
|||||||
get_list_user_wallets_command,
|
get_list_user_wallets_command,
|
||||||
get_get_user_mnemonic_command,
|
get_get_user_mnemonic_command,
|
||||||
get_get_user_secret_keys_command,
|
get_get_user_secret_keys_command,
|
||||||
|
get_list_user_orders_command,
|
||||||
)
|
)
|
||||||
from src.presentation.schemas.mappers import (
|
from src.presentation.schemas.mappers import (
|
||||||
mnemonic_to_response,
|
mnemonic_to_response,
|
||||||
@@ -18,6 +19,7 @@ from src.presentation.schemas.mappers import (
|
|||||||
party_search_to_response,
|
party_search_to_response,
|
||||||
secret_key_to_response,
|
secret_key_to_response,
|
||||||
wallet_to_response,
|
wallet_to_response,
|
||||||
|
order_to_response,
|
||||||
)
|
)
|
||||||
from src.presentation.schemas.user import (
|
from src.presentation.schemas.user import (
|
||||||
UserResponse,
|
UserResponse,
|
||||||
@@ -30,6 +32,9 @@ from src.presentation.schemas.entity import (
|
|||||||
SecretKeyResponse,
|
SecretKeyResponse,
|
||||||
WalletResponse,
|
WalletResponse,
|
||||||
)
|
)
|
||||||
|
from src.presentation.schemas.order import (
|
||||||
|
OrdersResponse,
|
||||||
|
)
|
||||||
from src.application.commands.set_password import SetPasswordCommand
|
from src.application.commands.set_password import SetPasswordCommand
|
||||||
from src.application.commands.user_commands import (
|
from src.application.commands.user_commands import (
|
||||||
ListUsersCommand,
|
ListUsersCommand,
|
||||||
@@ -40,8 +45,12 @@ from src.application.commands.user_wallet_commands import (
|
|||||||
GetUserMnemonicCommand,
|
GetUserMnemonicCommand,
|
||||||
GetUserSecretKeysCommand
|
GetUserSecretKeysCommand
|
||||||
)
|
)
|
||||||
|
from src.application.commands.orders_commands import (
|
||||||
|
ListUserOrdersCommand,
|
||||||
|
)
|
||||||
from src.presentation.schemas.password import SetPasswordRequest
|
from src.presentation.schemas.password import SetPasswordRequest
|
||||||
|
|
||||||
|
|
||||||
users_router = APIRouter(prefix='/users', tags=['users'])
|
users_router = APIRouter(prefix='/users', tags=['users'])
|
||||||
|
|
||||||
@users_router.get('', response_model=UserListResponse)
|
@users_router.get('', response_model=UserListResponse)
|
||||||
@@ -77,6 +86,21 @@ async def list_user_wallets(
|
|||||||
return [wallet_to_response(w) for w in wallets]
|
return [wallet_to_response(w) for w in wallets]
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.get('/{user_id}/orders', response_model=OrdersResponse)
|
||||||
|
async def list_user_orders(
|
||||||
|
user_id: str,
|
||||||
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: ListUserOrdersCommand = Depends(get_list_user_orders_command),
|
||||||
|
):
|
||||||
|
orders, total = await command(user_id=user_id, limit=limit, offset=offset)
|
||||||
|
return OrdersResponse(
|
||||||
|
items=[order_to_response(o) for o in orders],
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@users_router.get('/{user_id}/wallets/mnemonic', response_model=MnemonicResponse)
|
@users_router.get('/{user_id}/wallets/mnemonic', response_model=MnemonicResponse)
|
||||||
async def get_user_mnemonic(
|
async def get_user_mnemonic(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
@@ -125,4 +149,4 @@ async def set_password(
|
|||||||
command: SetPasswordCommand = Depends(get_set_password_command),
|
command: SetPasswordCommand = Depends(get_set_password_command),
|
||||||
):
|
):
|
||||||
await command(user_id=user_id, password=body.password)
|
await command(user_id=user_id, password=body.password)
|
||||||
return ORJSONResponse(content={'message': 'Password updated successfully'})
|
return ORJSONResponse(content={'message': 'Password updated successfully'})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from urllib import response
|
||||||
|
|
||||||
from src.application.domain.entities.organization import (
|
from src.application.domain.entities.organization import (
|
||||||
LegalEntityEntity,
|
LegalEntityEntity,
|
||||||
@@ -10,6 +11,9 @@ from src.application.domain.entities.organization import (
|
|||||||
from src.application.domain.entities.user import (
|
from src.application.domain.entities.user import (
|
||||||
UserEntity,
|
UserEntity,
|
||||||
)
|
)
|
||||||
|
from src.application.domain.entities.order import (
|
||||||
|
OrderEntity,
|
||||||
|
)
|
||||||
from src.presentation.schemas.user import (
|
from src.presentation.schemas.user import (
|
||||||
UserResponse,
|
UserResponse,
|
||||||
)
|
)
|
||||||
@@ -25,6 +29,9 @@ from src.presentation.schemas.entity import (
|
|||||||
SecretKeyResponse,
|
SecretKeyResponse,
|
||||||
WalletResponse,
|
WalletResponse,
|
||||||
)
|
)
|
||||||
|
from src.presentation.schemas.order import (
|
||||||
|
OrderResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def organization_to_response(entity: LegalEntityEntity) -> OrganizationResponse:
|
def organization_to_response(entity: LegalEntityEntity) -> OrganizationResponse:
|
||||||
@@ -156,4 +163,17 @@ def user_to_response(entity: UserEntity) -> UserResponse:
|
|||||||
created_at=entity.created_at.isoformat() if entity.created_at else None,
|
created_at=entity.created_at.isoformat() if entity.created_at else None,
|
||||||
updated_at=entity.updated_at.isoformat() if entity.updated_at else None,
|
updated_at=entity.updated_at.isoformat() if entity.updated_at else None,
|
||||||
kyc_verified_at=entity.kyc_verified_at.isoformat() if entity.kyc_verified_at else None,
|
kyc_verified_at=entity.kyc_verified_at.isoformat() if entity.kyc_verified_at else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def order_to_response(entity: OrderEntity) -> OrderResponse:
|
||||||
|
return OrderResponse(
|
||||||
|
order_id=entity.id,
|
||||||
|
order_status=entity.status,
|
||||||
|
usdt_amount=str(entity.usdt_amount),
|
||||||
|
usdt_exchange_rate=str(entity.usdt_exchange_rate) if entity.usdt_exchange_rate is not None else None,
|
||||||
|
gas_fee=str(entity.gas_fee) if entity.gas_fee is not None else None,
|
||||||
|
total_price=str(entity.total_price) if entity.total_price is not None else None,
|
||||||
|
service_fee=str(entity.service_fee) if entity.service_fee is not None else None,
|
||||||
|
updated_at=entity.updated_at.isoformat() if entity.updated_at else None,
|
||||||
|
)
|
||||||
|
|||||||
43
src/presentation/schemas/order.py
Normal file
43
src/presentation/schemas/order.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
from pydantic import BaseModel,Field
|
||||||
|
from src.application.domain.enums import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
|
class OrderDetailResponse(BaseModel):
|
||||||
|
id: str | None = None
|
||||||
|
created_at: str | None = None
|
||||||
|
updated_at: str | None = None
|
||||||
|
user_id: str | None = None
|
||||||
|
usdt_amount: str | None = None
|
||||||
|
usdt_exchange_rate: str | None = None
|
||||||
|
gas_fee: str | None = None
|
||||||
|
total_price: str | None = None
|
||||||
|
service_fee: str | None = None
|
||||||
|
status: OrderStatus | None = None
|
||||||
|
client_payment_id: str | None = None
|
||||||
|
itpay_payment_qr_url_desktop: str | None = None
|
||||||
|
itpay_payment_qr_url_android: str | None = None
|
||||||
|
itpay_payment_qr_url_ios: str | None = None
|
||||||
|
itpay_payment_qr_image_desktop: str | None = None
|
||||||
|
itpay_payment_qr_image_android: str | None = None
|
||||||
|
itpay_payment_qr_image_ios: str | None = None
|
||||||
|
itpay_id: str | None = None
|
||||||
|
itpay_qr_id: str | None = None
|
||||||
|
itpay_amount: str | None = None
|
||||||
|
itpay_created_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class OrderResponse(BaseModel):
|
||||||
|
order_id: str
|
||||||
|
order_status: OrderStatus | None = None
|
||||||
|
usdt_amount: str | None = None
|
||||||
|
usdt_exchange_rate: str | None = None
|
||||||
|
gas_fee: str | None = None
|
||||||
|
total_price: str | None = None
|
||||||
|
service_fee: str | None = None
|
||||||
|
updated_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class OrdersResponse(BaseModel):
|
||||||
|
items: list[OrderResponse]
|
||||||
|
total: int
|
||||||
Reference in New Issue
Block a user