Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9dc2b22
docs(product): update product use case stories and add SKU existence …
babakjahan Nov 3, 2025
9114408
test(e2e): add global_setup with clear_database and logger
babakjahan Nov 4, 2025
e917108
users: fix responses
babakjahan Nov 4, 2025
ebed680
Remove obsolete function
babakjahan Nov 4, 2025
591d9f6
add pagination schema
babakjahan Nov 4, 2025
e87c88a
impr:fix product crud
babakjahan Nov 4, 2025
5df1967
fix typo
babakjahan Nov 4, 2025
f0c12f2
change name
babakjahan Nov 4, 2025
096cace
preapare test structure for pagination
babakjahan Nov 4, 2025
871f7e8
product pagination with test
babakjahan Nov 4, 2025
1df3215
Initial plan
Copilot Nov 4, 2025
ee7dccf
Initial plan
Copilot Nov 4, 2025
29139d1
Update datetime usage to use datetime.UTC for Python 3.13+
Copilot Nov 4, 2025
344fa46
Fix capitalization: Change 'True wireless' to 'true wireless' for con…
Copilot Nov 4, 2025
02e025c
Merge pull request #37 from itanc-com/copilot/sub-pr-36
babakjahan Nov 4, 2025
1ff44cb
Merge pull request #38 from itanc-com/copilot/sub-pr-36-again
babakjahan Nov 4, 2025
70ecd3e
Initial plan
Copilot Nov 4, 2025
0be0c63
Remove redundant check before DELETE in sqlite_sequence
Copilot Nov 4, 2025
0a11604
Merge pull request #39 from itanc-com/copilot/sub-pr-36-another-one
babakjahan Nov 5, 2025
c589a18
Update app/modules/product/usecases/delete.py
babakjahan Nov 5, 2025
9bb2c5f
Initial plan
Copilot Nov 5, 2025
8b452a7
Merge pull request #40 from itanc-com/copilot/sub-pr-36-yet-again
babakjahan Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/common/http_response/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime

from pydantic import BaseModel
from pydantic import BaseModel, Field


class BaseResponse(BaseModel):
Expand All @@ -16,5 +16,5 @@ class BaseResponse(BaseModel):

status: int
message: str
timestamp: datetime
path: str
timestamp: datetime = Field(default_factory=lambda: datetime.now(datetime.UTC))
path: str = Field(default="/")
99 changes: 0 additions & 99 deletions app/common/http_response/doc_reponses.py

This file was deleted.

9 changes: 0 additions & 9 deletions app/common/http_response/success_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,3 @@ def to_json_response(self, request: Request) -> JSONResponse:
status_code=self.status_code,
content=model.model_dump(mode="json"),
)


#! TODO: Remove this function and use the to_json_response method instead
def success_response_builder(result: SuccessResult[T], request: Request) -> JSONResponse:
model = result.to_response_model(path=request.url.path)
return JSONResponse(
status_code=result.status_code,
content=model.model_dump(mode="json"),
)
Empty file added app/common/schemas/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions app/common/schemas/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic import BaseModel, Field


class PaginationInfo(BaseModel):
"""
Standard pagination information for list responses.
Reusable across all modules (products, users, categories, orders, etc.).
"""

page: int = Field(..., ge=1, description="Current page number", example=1)
limit: int = Field(..., ge=1, le=100, description="Items per page", example=10)
total_items: int = Field(..., ge=0, description="Total number of items", example=21)
total_pages: int = Field(..., ge=0, description="Total number of pages", example=3)
has_next: bool = Field(default=False, description="Whether there is a next page", example=True)
has_prev: bool = Field(default=False, description="Whether there is a previous page", example=False)
2 changes: 1 addition & 1 deletion app/core/logger/get_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ServiceName(StrEnum):
AUTH_SERVICE = "auth_service"
API_ROUTERS = "api_routers"
REDIS_SERVICE = "redis_service"
TEST_SERVICE = "test_service"
E2E_TEST_SERVICE = "e2e_test_service"
# Add more services as needed


Expand Down
12 changes: 6 additions & 6 deletions app/modules/auth/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from fastapi.params import Depends
from fastapi.security import OAuth2PasswordRequestForm

from app.common.http_response.doc_reponses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.doc_responses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.success_response import SuccessCodes, SuccessResponse
from app.common.http_response.success_result import SuccessResult, success_response_builder
from app.common.http_response.success_result import SuccessResult
from app.modules.user.depends import get_user_repository
from app.modules.user.models import UserRole
from app.modules.user.repository_interface import UserRepositoryInterface
Expand Down Expand Up @@ -72,7 +72,7 @@ async def auth_get_token(
data=tokens,
)

return success_response_builder(result, request)
return result.to_json_response(request)


@router.post(
Expand Down Expand Up @@ -143,7 +143,7 @@ async def auth_get_me(
status_code=status.HTTP_200_OK,
data=user,
)
return success_response_builder(result, request)
return result.to_json_response(request)


@router.post(
Expand Down Expand Up @@ -187,7 +187,7 @@ async def auth_refresh_tokens(
data=tokens,
)

return success_response_builder(result, request)
return result.to_json_response(request)


@router.get(
Expand Down Expand Up @@ -228,4 +228,4 @@ async def auth_verify_refresh_token(
data=user_read,
)

return success_response_builder(result, request)
return result.to_json_response(request)
2 changes: 1 addition & 1 deletion app/modules/cart/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from fastapi import APIRouter, Depends, Request, status

from app.common.http_response.doc_reponses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.doc_responses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.success_response import SuccessResponse
from app.common.http_response.success_result import SuccessCodes, SuccessResult
from app.modules.product.depends import get_product_repository
Expand Down
2 changes: 1 addition & 1 deletion app/modules/category/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import JSONResponse

from app.common.http_response.doc_reponses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.doc_responses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.success_response import SuccessCodes, SuccessResponse
from app.common.http_response.success_result import SuccessResult
from app.modules.category.depends import get_category_repository
Expand Down
2 changes: 1 addition & 1 deletion app/modules/category/routers/category_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import Depends, Request, status
from fastapi.responses import JSONResponse

from app.common.http_response.doc_reponses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.doc_responses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.success_response import SuccessCodes, SuccessResponse
from app.common.http_response.success_result import SuccessResult
from app.modules.category.depends import get_category_repository
Expand Down
2 changes: 1 addition & 1 deletion app/modules/category/routers/category_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.params import Query
from fastapi.responses import JSONResponse

from app.common.http_response.doc_reponses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.doc_responses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.success_response import SuccessCodes, SuccessResponse
from app.common.http_response.success_result import SuccessResult
from app.modules.category.depends import get_category_repository
Expand Down
15 changes: 9 additions & 6 deletions app/modules/category/routers/category_retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from fastapi import Depends, status
from fastapi.requests import Request
from fastapi.responses import JSONResponse

from app.common.http_response.doc_reponses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.doc_responses import ResponseErrorDoc, ResponseSuccessDoc
from app.common.http_response.success_response import SuccessCodes, SuccessResponse
from app.common.http_response.success_result import SuccessResult
from app.modules.category.depends import get_category_repository
from app.modules.category.repository_interface import CategoryRepositoryInterface
Expand All @@ -14,24 +16,25 @@

@router.get(
"/{category_id}",
response_model=CategoryRead,
response_model=SuccessResponse[CategoryRead],
responses={
**ResponseSuccessDoc.HTTP_200_OK("Category retrieved successfully", CategoryRead),
**ResponseSuccessDoc.HTTP_200_OK("Category fetched successfully", CategoryRead),
**ResponseErrorDoc.HTTP_404_NOT_FOUND("Category not found"),
**ResponseErrorDoc.HTTP_500_INTERNAL_SERVER_ERROR("Internal server error"),
},
)
async def category_get_by_id(
request: Request,
category_id: int,
category_repository: Annotated[CategoryRepositoryInterface, Depends(get_category_repository)],
) -> CategoryRead:
) -> JSONResponse:
category_by_id_usecase = CategoryGetById(category_repository)

category_read = await category_by_id_usecase.execute(category_id)

return SuccessResult[CategoryRead](
code=status.HTTP_200_OK,
message="Category retrieved successfully",
code=SuccessCodes.SUCCESS,
message="Category fetched successfully",
status_code=status.HTTP_200_OK,
data=category_read,
).to_json_response(request)
21 changes: 14 additions & 7 deletions app/modules/product/repository.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any

from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from .models import Product
Expand All @@ -17,7 +17,7 @@ async def create(self, product: Product) -> Product:

return product

async def update(self, product_id: int, updated_product: Product) -> Product | None:
async def update_by_id(self, product_id: int, updated_product: Product) -> Product | None:
product = await self.session.get(Product, product_id)
if not product:
return None
Expand All @@ -30,7 +30,7 @@ async def update(self, product_id: int, updated_product: Product) -> Product | N
await self.session.refresh(product)
return product

async def delete(self, product_id: int) -> Product | None:
async def delete_by_id(self, product_id: int) -> Product | None:
product = await self.session.get(Product, product_id)

if not product:
Expand All @@ -41,10 +41,7 @@ async def delete(self, product_id: int) -> Product | None:
return product

async def get_by_id(self, product_id: int) -> Product | None:
# More explicit query with all columns
stmt = select(Product).where(Product.id == product_id)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
return await self.session.get(Product, product_id)

async def list_all(self, category_id: int | None = None, skip: int = 0, limit: int = 10) -> list[Product]:
query = select(Product)
Expand All @@ -61,3 +58,13 @@ async def exists_by_field(self, field: str, value: Any) -> bool:
stmt = select(Product).where(getattr(Product, field) == value)
result = await self.session.execute(stmt)
return result.scalar_one_or_none() is not None

async def count_all(self, category_id: int | None = None) -> int:
"""Count total products with optional category filter."""
query = select(func.count(Product.id))

if category_id is not None:
query = query.where(Product.category_id == category_id)

result = await self.session.execute(query)
return result.scalar() or 0
8 changes: 6 additions & 2 deletions app/modules/product/repository_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ async def create(self, product: Product) -> Product:
pass

@abstractmethod
async def update(self, product_id: int, updated_product: Product) -> Product:
async def update_by_id(self, product_id: int, updated_product: Product) -> Product:
pass

@abstractmethod
async def delete(self, product_id: int) -> Product | None:
async def delete_by_id(self, product_id: int) -> Product | None:
pass

@abstractmethod
Expand All @@ -28,3 +28,7 @@ async def list_all(self, category_id: int | None = None, skip: int = 0, limit: i
@abstractmethod
async def exists_by_field(self, field: str, value: Any) -> bool:
pass

@abstractmethod
async def count_all(self, category_id: int | None = None) -> int:
pass
Loading