diff --git a/src/application/abstractions/i_unit_of_work.py b/src/application/abstractions/i_unit_of_work.py index 8d0654b..7bea9bb 100644 --- a/src/application/abstractions/i_unit_of_work.py +++ b/src/application/abstractions/i_unit_of_work.py @@ -10,6 +10,7 @@ from src.application.abstractions.repositories import ( IPurchaseRequestRepository, IUserRepository, IWalletRepository, + IOrderRepository, ) @@ -40,4 +41,7 @@ class IUnitOfWork(Protocol): def purchase_request_repository(self) -> IPurchaseRequestRepository: ... @property - def wallet_repository(self) -> IWalletRepository: ... \ No newline at end of file + def wallet_repository(self) -> IWalletRepository: ... + + @property + def order_repository(self) -> IOrderRepository: ... \ No newline at end of file diff --git a/src/application/abstractions/repositories/__init__.py b/src/application/abstractions/repositories/__init__.py index de4af85..1822f34 100644 --- a/src/application/abstractions/repositories/__init__.py +++ b/src/application/abstractions/repositories/__init__.py @@ -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_purchase_request_repository import IPurchaseRequestRepository from src.application.abstractions.repositories.i_wallets_repository import IWalletRepository +from src.application.abstractions.repositories.i_order_repository import IOrderRepository diff --git a/src/application/abstractions/repositories/i_order_repository.py b/src/application/abstractions/repositories/i_order_repository.py new file mode 100644 index 0000000..f0a41bc --- /dev/null +++ b/src/application/abstractions/repositories/i_order_repository.py @@ -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 diff --git a/src/application/commands/__init__.py b/src/application/commands/__init__.py index db54da1..f15dccb 100644 --- a/src/application/commands/__init__.py +++ b/src/application/commands/__init__.py @@ -38,6 +38,12 @@ from src.application.commands.user_wallet_commands import ( GetUserSecretKeysCommand, ListUserWalletsCommand, ) +from src.application.commands.orders_commands import ( + ListOrdersCommand, + ListUserOrdersCommand, + ListOrdersByDateCommand, + GetOrderCommand, +) __all__ = [ 'AdminLoginCommand', @@ -68,4 +74,8 @@ __all__ = [ 'ListUserWalletsCommand', 'GetUserMnemonicCommand', 'GetUserSecretKeysCommand', + 'ListOrdersCommand', + 'ListUserOrdersCommand', + 'ListOrdersByDateCommand', + 'GetOrderCommand', ] diff --git a/src/application/commands/orders_commands.py b/src/application/commands/orders_commands.py new file mode 100644 index 0000000..0c4e53e --- /dev/null +++ b/src/application/commands/orders_commands.py @@ -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 diff --git a/src/application/domain/entities/__init__.py b/src/application/domain/entities/__init__.py index 5b00633..d81ec5c 100644 --- a/src/application/domain/entities/__init__.py +++ b/src/application/domain/entities/__init__.py @@ -1,5 +1,6 @@ from src.application.domain.entities.user import UserEntity from src.application.domain.entities.admin_user import AdminUserEntity 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'] diff --git a/src/application/domain/entities/order.py b/src/application/domain/entities/order.py new file mode 100644 index 0000000..d5e563e --- /dev/null +++ b/src/application/domain/entities/order.py @@ -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 + diff --git a/src/application/domain/enums/__init__.py b/src/application/domain/enums/__init__.py index f2785a9..9c98b6f 100644 --- a/src/application/domain/enums/__init__.py +++ b/src/application/domain/enums/__init__.py @@ -1,2 +1,3 @@ from src.application.domain.enums.log_level import LogLevel -from src.application.domain.enums.log_format import LogFormat \ No newline at end of file +from src.application.domain.enums.log_format import LogFormat +from src.application.domain.enums.order_status import OrderStatus \ No newline at end of file diff --git a/src/application/domain/enums/order_status.py b/src/application/domain/enums/order_status.py new file mode 100644 index 0000000..9c8bb54 --- /dev/null +++ b/src/application/domain/enums/order_status.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class OrderStatus(str, Enum): + PENDING = 'pending' + REJECTED = 'rejected' + COMPLETED = 'completed' + CANCELLED = 'cancelled' + ERROR = 'error' \ No newline at end of file diff --git a/src/infrastructure/database/models/__init__.py b/src/infrastructure/database/models/__init__.py index ef7b775..5223747 100644 --- a/src/infrastructure/database/models/__init__.py +++ b/src/infrastructure/database/models/__init__.py @@ -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.purchase_request import PurchaseRequestModel from src.infrastructure.database.models.wallets import WalletModel +from src.infrastructure.database.models.order import OrderModel __all__ = [ 'Base', @@ -16,4 +17,5 @@ __all__ = [ 'OrganizationWalletModel', 'PurchaseRequestModel', 'WalletModel', + 'OrderModel', ] diff --git a/src/infrastructure/database/models/order.py b/src/infrastructure/database/models/order.py new file mode 100644 index 0000000..de4a55a --- /dev/null +++ b/src/infrastructure/database/models/order.py @@ -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 + ) + diff --git a/src/infrastructure/database/repositories/__init__.py b/src/infrastructure/database/repositories/__init__.py index 3940d8c..f0eb41a 100644 --- a/src/infrastructure/database/repositories/__init__.py +++ b/src/infrastructure/database/repositories/__init__.py @@ -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.purchase_request_repository import PurchaseRequestRepository from src.infrastructure.database.repositories.wallets_repository import WalletRepository +from src.infrastructure.database.repositories.orders_repository import OrderRepository __all__ = [ 'AdminUserRepository', @@ -14,4 +15,5 @@ __all__ = [ 'OrganizationWalletRepository', 'PurchaseRequestRepository', 'WalletRepository', + 'OrderRepository', ] diff --git a/src/infrastructure/database/repositories/orders_repository.py b/src/infrastructure/database/repositories/orders_repository.py new file mode 100644 index 0000000..8f3030d --- /dev/null +++ b/src/infrastructure/database/repositories/orders_repository.py @@ -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()) \ No newline at end of file diff --git a/src/infrastructure/database/unit_of_work.py b/src/infrastructure/database/unit_of_work.py index 2049dae..7456d8c 100644 --- a/src/infrastructure/database/unit_of_work.py +++ b/src/infrastructure/database/unit_of_work.py @@ -9,6 +9,7 @@ from src.application.abstractions.repositories import ( IPurchaseRequestRepository, IUserRepository, IWalletRepository, + IOrderRepository, ) from src.application.contracts import ILogger from src.application.domain.exceptions import RefreshConcurrentException @@ -20,6 +21,7 @@ from src.infrastructure.database.repositories import ( PurchaseRequestRepository, UserRepository, WalletRepository, + OrderRepository, ) @@ -34,6 +36,7 @@ class UnitOfWork(IUnitOfWork): self._organization_wallet_repository: IOrganizationWalletRepository | None = None self._purchase_request_repository: IPurchaseRequestRepository | None = None self._wallet_repository: IWalletRepository | None = None + self._order_repository: IOrderRepository | None = None self._logger: ILogger = logger async def __aenter__(self): @@ -103,3 +106,11 @@ class UnitOfWork(IUnitOfWork): session=self._session, logger=self._logger ) 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 diff --git a/src/presentation/dependencies/commands.py b/src/presentation/dependencies/commands.py index 43d8188..c176018 100644 --- a/src/presentation/dependencies/commands.py +++ b/src/presentation/dependencies/commands.py @@ -32,6 +32,10 @@ from src.application.commands import ( ListUserWalletsCommand, GetUserMnemonicCommand, GetUserSecretKeysCommand, + ListOrdersCommand, + ListUserOrdersCommand, + ListOrdersByDateCommand, + GetOrderCommand, ) from src.application.contracts import IHashService, IJwtService, ILogger from src.infrastructure.config import settings @@ -249,4 +253,28 @@ def get_get_user_secret_keys_command( uow: IUnitOfWork = Depends(get_unit_of_work), logger: ILogger = Depends(get_logger), ) -> GetUserSecretKeysCommand: - return GetUserSecretKeysCommand(uow, logger) \ No newline at end of file + 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) \ No newline at end of file diff --git a/src/presentation/routing/__init__.py b/src/presentation/routing/__init__.py index f24abcb..92e60c4 100644 --- a/src/presentation/routing/__init__.py +++ b/src/presentation/routing/__init__.py @@ -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.users import users_router from src.presentation.routing.search import search_router +from src.presentation.routing.orders import orders_router v1_router = APIRouter(prefix='/v1') 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(purchase_requests_router) v1_router.include_router(users_router) -v1_router.include_router(search_router) \ No newline at end of file +v1_router.include_router(search_router) +v1_router.include_router(orders_router) \ No newline at end of file diff --git a/src/presentation/routing/orders.py b/src/presentation/routing/orders.py new file mode 100644 index 0000000..4f64045 --- /dev/null +++ b/src/presentation/routing/orders.py @@ -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, + ) diff --git a/src/presentation/routing/users.py b/src/presentation/routing/users.py index 82fb992..8e913ed 100644 --- a/src/presentation/routing/users.py +++ b/src/presentation/routing/users.py @@ -11,6 +11,7 @@ from src.presentation.dependencies.commands import ( get_list_user_wallets_command, get_get_user_mnemonic_command, get_get_user_secret_keys_command, + get_list_user_orders_command, ) from src.presentation.schemas.mappers import ( mnemonic_to_response, @@ -18,6 +19,7 @@ from src.presentation.schemas.mappers import ( party_search_to_response, secret_key_to_response, wallet_to_response, + order_to_response, ) from src.presentation.schemas.user import ( UserResponse, @@ -30,6 +32,9 @@ from src.presentation.schemas.entity import ( SecretKeyResponse, WalletResponse, ) +from src.presentation.schemas.order import ( + OrdersResponse, +) from src.application.commands.set_password import SetPasswordCommand from src.application.commands.user_commands import ( ListUsersCommand, @@ -40,8 +45,12 @@ from src.application.commands.user_wallet_commands import ( GetUserMnemonicCommand, GetUserSecretKeysCommand ) +from src.application.commands.orders_commands import ( + ListUserOrdersCommand, +) from src.presentation.schemas.password import SetPasswordRequest + users_router = APIRouter(prefix='/users', tags=['users']) @users_router.get('', response_model=UserListResponse) @@ -77,6 +86,21 @@ async def list_user_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) async def get_user_mnemonic( user_id: str, @@ -125,4 +149,4 @@ async def set_password( command: SetPasswordCommand = Depends(get_set_password_command), ): await command(user_id=user_id, password=body.password) - return ORJSONResponse(content={'message': 'Password updated successfully'}) \ No newline at end of file + return ORJSONResponse(content={'message': 'Password updated successfully'}) diff --git a/src/presentation/schemas/mappers.py b/src/presentation/schemas/mappers.py index 9fee498..5972f24 100644 --- a/src/presentation/schemas/mappers.py +++ b/src/presentation/schemas/mappers.py @@ -1,4 +1,5 @@ from __future__ import annotations +from urllib import response from src.application.domain.entities.organization import ( LegalEntityEntity, @@ -10,6 +11,9 @@ from src.application.domain.entities.organization import ( from src.application.domain.entities.user import ( UserEntity, ) +from src.application.domain.entities.order import ( + OrderEntity, +) from src.presentation.schemas.user import ( UserResponse, ) @@ -25,6 +29,9 @@ from src.presentation.schemas.entity import ( SecretKeyResponse, WalletResponse, ) +from src.presentation.schemas.order import ( + OrderResponse, +) 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, 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, - ) \ No newline at end of file + ) + + +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, + ) diff --git a/src/presentation/schemas/order.py b/src/presentation/schemas/order.py new file mode 100644 index 0000000..577493f --- /dev/null +++ b/src/presentation/schemas/order.py @@ -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 \ No newline at end of file