diff --git a/src/application/commands/orders_commands.py b/src/application/commands/orders_commands.py index d98b0b2..d39a12a 100644 --- a/src/application/commands/orders_commands.py +++ b/src/application/commands/orders_commands.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import datetime, timezone from decimal import Decimal from src.application.abstractions import IUnitOfWork from src.application.contracts import ILogger @@ -7,6 +8,15 @@ from src.application.domain.exceptions import NotFoundException from src.infrastructure.database.decorators import transactional +def _parse_cursor(cursor: str | None) -> datetime | None: + if not cursor: + return None + parsed = datetime.fromisoformat(cursor.replace('Z','+00:00')) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=timezone.utc) + return parsed + + class ListUserOrdersCommand: def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): self._unit_of_work = unit_of_work @@ -14,11 +24,12 @@ class ListUserOrdersCommand: @transactional - async def __call__(self, *, user_id: str, limit: int, offset: int) -> tuple[list[OrderEntity], int]: + async def __call__(self, *, user_id: str, limit: int, offset: int, cursor: str | None = None) -> tuple[list[OrderEntity], int]: orders = await self._unit_of_work.order_repository.list_by_user_id( user_id=user_id, limit=limit, offset=offset, + cursor_created_at=_parse_cursor(cursor), ) total = await self._unit_of_work.order_repository.count_all(search=user_id) return orders, total @@ -32,11 +43,12 @@ class ListOrdersCommand: @transactional - async def __call__(self, *, limit: int, offset: int, search: str | None = None) -> tuple[list[OrderEntity], int]: + async def __call__(self, *, limit: int, offset: int, search: str | None = None, cursor: str | None = None) -> tuple[list[OrderEntity], int]: orders = await self._unit_of_work.order_repository.list_all( limit=limit, offset=offset, - search=search + search=search, + cursor_created_at=_parse_cursor(cursor), ) total = await self._unit_of_work.order_repository.count_all(search=search) @@ -57,13 +69,15 @@ class ListOrdersByDateCommand: limit: int, offset: int, year: int, - month: int | None = None + month: int | None = None, + cursor: str | None = None, ) -> tuple[list[OrderEntity], int, Decimal]: orders = await self._unit_of_work.order_repository.list_by_date( year=year, month=month, limit=limit, - offset=offset + offset=offset, + cursor_created_at=_parse_cursor(cursor), ) total = await self._unit_of_work.order_repository.count_by_month(year=year, month=month) diff --git a/src/application/commands/user_commands.py b/src/application/commands/user_commands.py index 64fd08f..3e2a6ea 100644 --- a/src/application/commands/user_commands.py +++ b/src/application/commands/user_commands.py @@ -1,11 +1,22 @@ from __future__ import annotations +from datetime import datetime, timezone + from src.application.abstractions import IUnitOfWork from src.application.contracts import ILogger from src.application.domain.entities.user import UserEntity from src.infrastructure.database.decorators import transactional +def _parse_cursor(cursor: str | None) -> datetime | None: + if not cursor: + return None + parsed = datetime.fromisoformat(cursor.replace('Z','+00:00')) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=timezone.utc) + return parsed + + class ListUsersCommand: def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): self._unit_of_work = unit_of_work @@ -18,11 +29,13 @@ class ListUsersCommand: limit: int = 50, offset: int = 0, search: str | None = None, + cursor: str | None = None, ) -> tuple[list[UserEntity], int]: items = await self._unit_of_work.user_repository.list_all( limit=limit, offset=offset, search=search, + cursor_created_at=_parse_cursor(cursor), ) total = await self._unit_of_work.user_repository.count_all(search=search) return items, total diff --git a/src/infrastructure/database/repositories/orders_repository.py b/src/infrastructure/database/repositories/orders_repository.py index 969678e..b746766 100644 --- a/src/infrastructure/database/repositories/orders_repository.py +++ b/src/infrastructure/database/repositories/orders_repository.py @@ -71,15 +71,19 @@ class OrderRepository(IOrderRepository): *, user_id: str, limit: int, - offset: int + offset: int, + cursor_created_at=None ) -> list[OrderEntity]: stmt=( select(OrderModel) .where(OrderModel.user_id==user_id) .order_by(desc(OrderModel.created_at)) .limit(limit) - .offset(offset) ) + if cursor_created_at is not None: + stmt = stmt.where(OrderModel.created_at < cursor_created_at) + else: + stmt = stmt.offset(offset) result=await self._session.scalars(stmt) return [self._to_entity(model) for model in result.all()] @@ -89,14 +93,18 @@ class OrderRepository(IOrderRepository): *, limit: int, offset: int, - search: str | None = None + search: str | None = None, + cursor_created_at=None ) -> list[OrderEntity]: stmt=( select(OrderModel) .order_by(desc(OrderModel.created_at)) .limit(limit) - .offset(offset) ) + if cursor_created_at is not None: + stmt = stmt.where(OrderModel.created_at < cursor_created_at) + else: + stmt = stmt.offset(offset) search_filter = self._search_filter(search) if search_filter is not None: stmt = stmt.where(search_filter) @@ -111,6 +119,7 @@ class OrderRepository(IOrderRepository): month: int, limit: int, offset: int, + cursor_created_at=None, ) -> list[OrderEntity]: stmt=( select(OrderModel) @@ -120,8 +129,11 @@ class OrderRepository(IOrderRepository): ) .order_by(desc(OrderModel.created_at)) .limit(limit) - .offset(offset) ) + if cursor_created_at is not None: + stmt = stmt.where(OrderModel.created_at < cursor_created_at) + else: + stmt = stmt.offset(offset) res = await self._session.execute(stmt) return [self._to_entity(model) for model in res.scalars().all()] diff --git a/src/infrastructure/database/repositories/user_repository.py b/src/infrastructure/database/repositories/user_repository.py index 032f0ad..97c8eb9 100644 --- a/src/infrastructure/database/repositories/user_repository.py +++ b/src/infrastructure/database/repositories/user_repository.py @@ -156,14 +156,18 @@ class UserRepository(IUserRepository): limit: int, offset: int, search: str | None = None, + cursor_created_at=None, ) -> list[UserEntity]: stmt = ( select(UserModel) .where(UserModel.is_deleted.is_(False)) .order_by(UserModel.created_at.desc()) .limit(limit) - .offset(offset) ) + if cursor_created_at is not None: + stmt = stmt.where(UserModel.created_at < cursor_created_at) + else: + stmt = stmt.offset(offset) search_filter = self._search_filter(search) if search_filter is not None: stmt = stmt.where(search_filter) diff --git a/src/presentation/routing/orders.py b/src/presentation/routing/orders.py index c11f499..77736ca 100644 --- a/src/presentation/routing/orders.py +++ b/src/presentation/routing/orders.py @@ -26,14 +26,16 @@ orders_router = APIRouter(prefix='/orders', tags=['orders']) async def list_orders( limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), + cursor: str | None = Query(default=None), 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) + items, total = await command(limit=limit, offset=offset, search=q, cursor=cursor) return OrdersResponse( items=[order_to_response(x) for x in items], total=total, + next_cursor=items[-1].created_at.isoformat() if items and items[-1].created_at else None, ) @@ -75,12 +77,14 @@ async def list_orders_by_date( month: int = Query(..., ge=1, le=12), limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), + cursor: str | None = Query(default=None), auth: AdminAuthContext = Depends(require_admin_access), command: ListOrdersByDateCommand = Depends(get_list_orders_by_date_command), ): - orders, total, summary = await command(year=year, month=month, limit=limit, offset=offset) + orders, total, summary = await command(year=year, month=month, limit=limit, offset=offset, cursor=cursor) return OrdersByDateResponse( items=[order_to_response(o) for o in orders], total=total, summed_service_fees=summary, + next_cursor=orders[-1].created_at.isoformat() if orders and orders[-1].created_at else None, ) diff --git a/src/presentation/routing/users.py b/src/presentation/routing/users.py index ab9e791..93e9181 100644 --- a/src/presentation/routing/users.py +++ b/src/presentation/routing/users.py @@ -54,14 +54,16 @@ users_router = APIRouter(prefix='/users', tags=['users']) async def list_users( limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), + cursor: str | None = Query(default=None), q: str | None = Query(default=None, min_length=1, max_length=255), auth: AdminAuthContext = Depends(require_admin_access), command: ListUsersCommand = Depends(get_list_users_command), ): - items, total = await command(limit=limit, offset=offset, search=q) + items, total = await command(limit=limit, offset=offset, search=q, cursor=cursor) return UserListResponse( items=[user_to_response(x) for x in items], total=total, + next_cursor=items[-1].created_at.isoformat() if items and items[-1].created_at else None, ) @users_router.get('/{user_id}', response_model=UserResponse) @@ -88,13 +90,15 @@ async def list_user_orders( user_id: str, limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), + cursor: str | None = Query(default=None), 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) + orders, total = await command(user_id=user_id, limit=limit, offset=offset, cursor=cursor) return OrdersResponse( items=[order_to_response(o) for o in orders], total=total, + next_cursor=orders[-1].created_at.isoformat() if orders and orders[-1].created_at else None, ) diff --git a/src/presentation/schemas/order.py b/src/presentation/schemas/order.py index e46d1fb..5deaa41 100644 --- a/src/presentation/schemas/order.py +++ b/src/presentation/schemas/order.py @@ -41,8 +41,10 @@ class OrderResponse(BaseModel): class OrdersResponse(BaseModel): items: list[OrderResponse] total: int + next_cursor: str | None = None class OrdersByDateResponse(BaseModel): items: list[OrderResponse] total: int - summed_service_fees: Decimal \ No newline at end of file + summed_service_fees: Decimal + next_cursor: str | None = None \ No newline at end of file diff --git a/src/presentation/schemas/user.py b/src/presentation/schemas/user.py index 02daa8b..1739a68 100644 --- a/src/presentation/schemas/user.py +++ b/src/presentation/schemas/user.py @@ -27,3 +27,4 @@ class UserResponse(BaseModel): class UserListResponse(BaseModel): items: list[UserResponse] total: int + next_cursor: str | None = None