diff --git a/app/api/endpoints/cart.py b/app/api/endpoints/cart.py new file mode 100644 index 0000000..be8bb1b --- /dev/null +++ b/app/api/endpoints/cart.py @@ -0,0 +1,145 @@ +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app import crud, models, schemas +from app.api import deps + +router = APIRouter() + + +@router.get("/", response_model=schemas.Cart) +def read_user_cart( + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Get current user's cart with all items. + """ + cart = crud.cart.get_cart_with_items(db, user_id=current_user.id) + if not cart: + # Create a new cart for the user if it doesn't exist + cart = crud.cart.create(db, obj_in=schemas.CartCreate(user_id=current_user.id)) + return cart + + +@router.post("/items", response_model=schemas.CartItem) +def add_cart_item( + *, + db: Session = Depends(deps.get_db), + item_in: schemas.CartItemCreate, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Add item to cart. If item already exists, update quantity. + """ + # Verify the product exists and is active + product = crud.product.get(db, id=item_in.product_id) + if not product or not product.is_active: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found or inactive", + ) + + # Check if product is in stock + if product.stock < item_in.quantity: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Not enough stock. Available: {product.stock}", + ) + + # Get or create user's cart + cart = crud.cart.get_or_create_cart(db, user_id=current_user.id) + + # Add or update cart item + cart_item = crud.cart_item.create_or_update_cart_item( + db, cart_id=cart.id, product_id=item_in.product_id, quantity=item_in.quantity + ) + + return cart_item + + +@router.put("/items/{product_id}", response_model=schemas.CartItem) +def update_cart_item( + *, + db: Session = Depends(deps.get_db), + product_id: int, + item_in: schemas.CartItemUpdate, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Update cart item quantity. + """ + # Verify the product exists and is active + product = crud.product.get(db, id=product_id) + if not product or not product.is_active: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found or inactive", + ) + + # Check if product is in stock + if product.stock < item_in.quantity: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Not enough stock. Available: {product.stock}", + ) + + # Get user's cart + cart = crud.cart.get_by_user_id(db, user_id=current_user.id) + if not cart: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Cart not found", + ) + + # Update cart item + cart_item = crud.cart_item.create_or_update_cart_item( + db, cart_id=cart.id, product_id=product_id, quantity=item_in.quantity + ) + + return cart_item + + +@router.delete("/items/{product_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) +def remove_cart_item( + *, + db: Session = Depends(deps.get_db), + product_id: int, + current_user: models.User = Depends(deps.get_current_active_user), +) -> None: + """ + Remove item from cart. + """ + # Get user's cart + cart = crud.cart.get_by_user_id(db, user_id=current_user.id) + if not cart: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Cart not found", + ) + + # Remove cart item + crud.cart_item.remove_cart_item(db, cart_id=cart.id, product_id=product_id) + + +@router.delete("/", status_code=status.HTTP_204_NO_CONTENT, response_model=None) +def clear_cart( + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_active_user), +) -> None: + """ + Clear all items from cart. + """ + # Get user's cart + cart = crud.cart.get_by_user_id(db, user_id=current_user.id) + if not cart: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Cart not found", + ) + + # Clear cart + crud.cart_item.clear_cart(db, cart_id=cart.id) diff --git a/app/api/endpoints/categories.py b/app/api/endpoints/categories.py new file mode 100644 index 0000000..01e9b81 --- /dev/null +++ b/app/api/endpoints/categories.py @@ -0,0 +1,110 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app import crud, models, schemas +from app.api import deps + +router = APIRouter() + + +@router.get("/", response_model=List[schemas.Category]) +def read_categories( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, +) -> Any: + """ + Retrieve categories. + """ + categories = crud.category.get_multi(db, skip=skip, limit=limit) + return categories + + +@router.post("/", response_model=schemas.Category) +def create_category( + *, + db: Session = Depends(deps.get_db), + category_in: schemas.CategoryCreate, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> Any: + """ + Create new category (admin only). + """ + category = crud.category.get_by_name(db, name=category_in.name) + if category: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Category with this name already exists", + ) + category = crud.category.create(db, obj_in=category_in) + return category + + +@router.get("/{id}", response_model=schemas.Category) +def read_category( + *, + db: Session = Depends(deps.get_db), + id: int, +) -> Any: + """ + Get category by ID. + """ + category = crud.category.get(db, id=id) + if not category: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Category not found", + ) + return category + + +@router.put("/{id}", response_model=schemas.Category) +def update_category( + *, + db: Session = Depends(deps.get_db), + id: int, + category_in: schemas.CategoryUpdate, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> Any: + """ + Update a category (admin only). + """ + category = crud.category.get(db, id=id) + if not category: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Category not found", + ) + + # Check if name already exists (if updating name) + if category_in.name and category_in.name != category.name: + existing_category = crud.category.get_by_name(db, name=category_in.name) + if existing_category: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Category with this name already exists", + ) + + category = crud.category.update(db, db_obj=category, obj_in=category_in) + return category + + +@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) +def delete_category( + *, + db: Session = Depends(deps.get_db), + id: int, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> None: + """ + Delete a category (admin only). + """ + category = crud.category.get(db, id=id) + if not category: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Category not found", + ) + crud.category.remove(db, id=id) diff --git a/app/api/endpoints/orders.py b/app/api/endpoints/orders.py new file mode 100644 index 0000000..f5f3f13 --- /dev/null +++ b/app/api/endpoints/orders.py @@ -0,0 +1,172 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app import crud, models, schemas +from app.api import deps + +router = APIRouter() + + +@router.get("/", response_model=List[schemas.Order]) +def read_user_orders( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Retrieve current user's orders. + """ + orders = crud.order.get_orders_by_user( + db, user_id=current_user.id, skip=skip, limit=limit + ) + return orders + + +@router.post("/", response_model=schemas.Order) +def create_order( + *, + db: Session = Depends(deps.get_db), + order_in: schemas.OrderCreate, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Create a new order. + """ + # Validate order items + if not order_in.items or len(order_in.items) == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Order must contain at least one item", + ) + + # Check if products exist and are in stock + for item in order_in.items: + product = crud.product.get(db, id=item.product_id) + if not product or not product.is_active: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Product with id {item.product_id} not found or inactive", + ) + + if product.stock < item.quantity: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Not enough stock for product {product.name}. Available: {product.stock}", + ) + + # Create order + order = crud.order.create_with_items( + db, obj_in=order_in, user_id=current_user.id + ) + + # Update product stock + for item in order.items: + product = crud.product.get(db, id=item.product_id) + product.stock -= item.quantity + db.add(product) + + # Clear user's cart + cart = crud.cart.get_by_user_id(db, user_id=current_user.id) + if cart: + crud.cart_item.clear_cart(db, cart_id=cart.id) + + db.commit() + + return order + + +@router.get("/{order_id}", response_model=schemas.Order) +def read_order( + *, + db: Session = Depends(deps.get_db), + order_id: int, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Get order by ID. + """ + order = crud.order.get_order_with_items(db, order_id=order_id) + if not order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Order not found", + ) + + # Check if user is authorized to access this order + if order.user_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions", + ) + + return order + + +@router.put("/{order_id}", response_model=schemas.Order) +def update_order_status( + *, + db: Session = Depends(deps.get_db), + order_id: int, + order_in: schemas.OrderUpdate, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> Any: + """ + Update order status (admin only). + """ + order = crud.order.get(db, id=order_id) + if not order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Order not found", + ) + + order = crud.order.update(db, db_obj=order, obj_in=order_in) + return order + + +@router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) +def cancel_order( + *, + db: Session = Depends(deps.get_db), + order_id: int, + current_user: models.User = Depends(deps.get_current_active_user), +) -> None: + """ + Cancel an order. Only allowed for pending orders. + """ + order = crud.order.get_order_with_items(db, order_id=order_id) + if not order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Order not found", + ) + + # Check if user is authorized to cancel this order + if order.user_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions", + ) + + # Check if order can be cancelled + if order.status != models.OrderStatus.PENDING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only pending orders can be cancelled", + ) + + # Update order status to cancelled + order.status = models.OrderStatus.CANCELLED + + # Restore product stock + for item in order.items: + product = crud.product.get(db, id=item.product_id) + if product: + product.stock += item.quantity + db.add(product) + + db.add(order) + db.commit() diff --git a/app/api/endpoints/products.py b/app/api/endpoints/products.py new file mode 100644 index 0000000..eceb9d7 --- /dev/null +++ b/app/api/endpoints/products.py @@ -0,0 +1,108 @@ +from typing import Any, List, Optional + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app import crud, models, schemas +from app.api import deps + +router = APIRouter() + + +@router.get("/", response_model=List[schemas.Product]) +def read_products( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + name: Optional[str] = None, + category_id: Optional[int] = None, + min_price: Optional[float] = None, + max_price: Optional[float] = None, + in_stock: Optional[bool] = None, +) -> Any: + """ + Retrieve products with filtering options. + """ + filter_params = schemas.ProductFilterParams( + name=name, + category_id=category_id, + min_price=min_price, + max_price=max_price, + in_stock=in_stock, + ) + products = crud.product.search_products( + db, filter_params=filter_params, skip=skip, limit=limit + ) + return products + + +@router.post("/", response_model=schemas.Product) +def create_product( + *, + db: Session = Depends(deps.get_db), + product_in: schemas.ProductCreate, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> Any: + """ + Create new product (admin only). + """ + product = crud.product.create(db, obj_in=product_in) + return product + + +@router.get("/{id}", response_model=schemas.Product) +def read_product( + *, + db: Session = Depends(deps.get_db), + id: int, +) -> Any: + """ + Get product by ID. + """ + product = crud.product.get(db, id=id) + if not product: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found", + ) + return product + + +@router.put("/{id}", response_model=schemas.Product) +def update_product( + *, + db: Session = Depends(deps.get_db), + id: int, + product_in: schemas.ProductUpdate, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> Any: + """ + Update a product (admin only). + """ + product = crud.product.get(db, id=id) + if not product: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found", + ) + product = crud.product.update(db, db_obj=product, obj_in=product_in) + return product + + +@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) +def delete_product( + *, + db: Session = Depends(deps.get_db), + id: int, + current_user: models.User = Depends(deps.get_current_admin_user), +) -> None: + """ + Delete a product (admin only). + """ + product = crud.product.get(db, id=id) + if not product: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found", + ) + crud.product.remove(db, id=id) diff --git a/app/api/endpoints/reviews.py b/app/api/endpoints/reviews.py new file mode 100644 index 0000000..5640242 --- /dev/null +++ b/app/api/endpoints/reviews.py @@ -0,0 +1,124 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app import crud, models, schemas +from app.api import deps + +router = APIRouter() + + +@router.get("/product/{product_id}", response_model=List[schemas.Review]) +def read_product_reviews( + *, + db: Session = Depends(deps.get_db), + product_id: int, + skip: int = 0, + limit: int = 100, +) -> Any: + """ + Retrieve reviews for a specific product. + """ + # Check if product exists + product = crud.product.get(db, id=product_id) + if not product: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found", + ) + + reviews = crud.review.get_reviews_by_product( + db, product_id=product_id, skip=skip, limit=limit + ) + return reviews + + +@router.post("/", response_model=schemas.Review) +def create_review( + *, + db: Session = Depends(deps.get_db), + review_in: schemas.ReviewCreate, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Create a new review for a product. + """ + # Check if product exists + product = crud.product.get(db, id=review_in.product_id) + if not product: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found", + ) + + # Check if user has already reviewed this product + existing_review = crud.review.get_user_review_for_product( + db, user_id=current_user.id, product_id=review_in.product_id + ) + if existing_review: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="You have already reviewed this product", + ) + + review = crud.review.create_user_review( + db, obj_in=review_in, user_id=current_user.id + ) + return review + + +@router.put("/{review_id}", response_model=schemas.Review) +def update_review( + *, + db: Session = Depends(deps.get_db), + review_id: int, + review_in: schemas.ReviewUpdate, + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Update a review. + """ + review = crud.review.get(db, id=review_id) + if not review: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Review not found", + ) + + # Check if user is authorized to update this review + if review.user_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions", + ) + + review = crud.review.update(db, db_obj=review, obj_in=review_in) + return review + + +@router.delete("/{review_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None) +def delete_review( + *, + db: Session = Depends(deps.get_db), + review_id: int, + current_user: models.User = Depends(deps.get_current_active_user), +) -> None: + """ + Delete a review. + """ + review = crud.review.get(db, id=review_id) + if not review: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Review not found", + ) + + # Check if user is authorized to delete this review + if review.user_id != current_user.id and not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions", + ) + + crud.review.remove(db, id=review_id) diff --git a/app/api/router.py b/app/api/router.py index df276f5..8960e1c 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,9 +1,14 @@ from fastapi import APIRouter -from app.api.endpoints import auth, users +from app.api.endpoints import auth, cart, categories, orders, products, reviews, users api_router = APIRouter() -# Include authentication and user endpoints +# Include all API endpoints api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) api_router.include_router(users.router, prefix="/users", tags=["Users"]) +api_router.include_router(products.router, prefix="/products", tags=["Products"]) +api_router.include_router(categories.router, prefix="/categories", tags=["Categories"]) +api_router.include_router(cart.router, prefix="/cart", tags=["Shopping Cart"]) +api_router.include_router(orders.router, prefix="/orders", tags=["Orders"]) +api_router.include_router(reviews.router, prefix="/reviews", tags=["Reviews"]) diff --git a/app/crud/__init__.py b/app/crud/__init__.py index e96c50c..e4c07bd 100644 --- a/app/crud/__init__.py +++ b/app/crud/__init__.py @@ -1,3 +1,7 @@ +from app.crud.crud_cart import cart, cart_item +from app.crud.crud_order import order, order_item +from app.crud.crud_product import category, product +from app.crud.crud_review import review from app.crud.crud_user import user -__all__ = ["user"] +__all__ = ["user", "product", "category", "cart", "cart_item", "order", "order_item", "review"] diff --git a/app/crud/crud_cart.py b/app/crud/crud_cart.py new file mode 100644 index 0000000..6b44ba8 --- /dev/null +++ b/app/crud/crud_cart.py @@ -0,0 +1,65 @@ +from typing import Optional + +from sqlalchemy.orm import Session, joinedload + +from app.crud.base import CRUDBase +from app.models.cart import Cart, CartItem +from app.schemas.cart import CartCreate, CartItemCreate, CartItemUpdate + + +class CRUDCart(CRUDBase[Cart, CartCreate, CartCreate]): + def get_by_user_id(self, db: Session, *, user_id: int) -> Optional[Cart]: + return db.query(Cart).filter(Cart.user_id == user_id).first() + + def get_or_create_cart(self, db: Session, *, user_id: int) -> Cart: + cart = self.get_by_user_id(db, user_id=user_id) + if not cart: + cart = self.create(db, obj_in=CartCreate(user_id=user_id)) + return cart + + def get_cart_with_items(self, db: Session, *, user_id: int) -> Optional[Cart]: + return ( + db.query(Cart) + .filter(Cart.user_id == user_id) + .options(joinedload(Cart.items).joinedload(CartItem.product)) + .first() + ) + + +class CRUDCartItem(CRUDBase[CartItem, CartItemCreate, CartItemUpdate]): + def create_or_update_cart_item( + self, db: Session, *, cart_id: int, product_id: int, quantity: int + ) -> CartItem: + cart_item = ( + db.query(CartItem) + .filter(CartItem.cart_id == cart_id, CartItem.product_id == product_id) + .first() + ) + + if cart_item: + cart_item.quantity = quantity + db.add(cart_item) + db.commit() + db.refresh(cart_item) + return cart_item + else: + return self.create( + db, + obj_in=CartItemCreate(cart_id=cart_id, product_id=product_id, quantity=quantity) + ) + + def remove_cart_item( + self, db: Session, *, cart_id: int, product_id: int + ) -> None: + db.query(CartItem).filter( + CartItem.cart_id == cart_id, CartItem.product_id == product_id + ).delete() + db.commit() + + def clear_cart(self, db: Session, *, cart_id: int) -> None: + db.query(CartItem).filter(CartItem.cart_id == cart_id).delete() + db.commit() + + +cart = CRUDCart(Cart) +cart_item = CRUDCartItem(CartItem) diff --git a/app/crud/crud_order.py b/app/crud/crud_order.py new file mode 100644 index 0000000..09196a9 --- /dev/null +++ b/app/crud/crud_order.py @@ -0,0 +1,69 @@ +from typing import List, Optional + +from sqlalchemy.orm import Session, joinedload + +from app.crud.base import CRUDBase +from app.models.order import Order, OrderItem +from app.schemas.order import OrderCreate, OrderItemCreate, OrderUpdate + + +class CRUDOrder(CRUDBase[Order, OrderCreate, OrderUpdate]): + def get_orders_by_user( + self, db: Session, *, user_id: int, skip: int = 0, limit: int = 100 + ) -> List[Order]: + return ( + db.query(Order) + .filter(Order.user_id == user_id) + .order_by(Order.created_at.desc()) + .offset(skip) + .limit(limit) + .all() + ) + + def get_order_with_items(self, db: Session, *, order_id: int) -> Optional[Order]: + return ( + db.query(Order) + .filter(Order.id == order_id) + .options(joinedload(Order.items)) + .first() + ) + + def create_with_items( + self, db: Session, *, obj_in: OrderCreate, user_id: int + ) -> Order: + # Calculate the total amount + total_amount = sum(item.unit_price * item.quantity for item in obj_in.items) + + # Create order + db_obj = Order( + user_id=user_id, + status=obj_in.status, + total_amount=total_amount, + shipping_address=obj_in.shipping_address, + payment_details=obj_in.payment_details, + tracking_number=obj_in.tracking_number, + ) + db.add(db_obj) + db.flush() + + # Create order items + for item in obj_in.items: + order_item = OrderItem( + order_id=db_obj.id, + product_id=item.product_id, + quantity=item.quantity, + unit_price=item.unit_price, + ) + db.add(order_item) + + db.commit() + db.refresh(db_obj) + return db_obj + + +class CRUDOrderItem(CRUDBase[OrderItem, OrderItemCreate, OrderItemCreate]): + pass + + +order = CRUDOrder(Order) +order_item = CRUDOrderItem(OrderItem) diff --git a/app/crud/crud_product.py b/app/crud/crud_product.py new file mode 100644 index 0000000..03f0d42 --- /dev/null +++ b/app/crud/crud_product.py @@ -0,0 +1,53 @@ +from typing import List, Optional + +from sqlalchemy.orm import Session + +from app.crud.base import CRUDBase +from app.models.product import Category, Product +from app.schemas.product import ProductCreate, ProductFilterParams, ProductUpdate + + +class CRUDProduct(CRUDBase[Product, ProductCreate, ProductUpdate]): + def search_products( + self, db: Session, *, filter_params: ProductFilterParams, skip: int = 0, limit: int = 100 + ) -> List[Product]: + query = db.query(Product) + + # Apply filters + if filter_params.name: + query = query.filter(Product.name.ilike(f"%{filter_params.name}%")) + + if filter_params.category_id: + query = query.filter(Product.category_id == filter_params.category_id) + + if filter_params.min_price is not None: + query = query.filter(Product.price >= filter_params.min_price) + + if filter_params.max_price is not None: + query = query.filter(Product.price <= filter_params.max_price) + + if filter_params.in_stock is not None: + if filter_params.in_stock: + query = query.filter(Product.stock > 0) + else: + query = query.filter(Product.stock == 0) + + # Only return active products by default + query = query.filter(Product.is_active) + + # Order by newest first + query = query.order_by(Product.created_at.desc()) + + return query.offset(skip).limit(limit).all() + + def get_product_with_category(self, db: Session, product_id: int) -> Optional[Product]: + return db.query(Product).filter(Product.id == product_id).first() + + +class CRUDCategory(CRUDBase[Category, ProductCreate, ProductUpdate]): + def get_by_name(self, db: Session, *, name: str) -> Optional[Category]: + return db.query(Category).filter(Category.name == name).first() + + +product = CRUDProduct(Product) +category = CRUDCategory(Category) diff --git a/app/crud/crud_review.py b/app/crud/crud_review.py new file mode 100644 index 0000000..15ec36a --- /dev/null +++ b/app/crud/crud_review.py @@ -0,0 +1,59 @@ +from typing import List, Optional + +from sqlalchemy.orm import Session + +from app.crud.base import CRUDBase +from app.models.review import Review +from app.schemas.review import ReviewCreate, ReviewUpdate + + +class CRUDReview(CRUDBase[Review, ReviewCreate, ReviewUpdate]): + def get_reviews_by_product( + self, db: Session, *, product_id: int, skip: int = 0, limit: int = 100 + ) -> List[Review]: + return ( + db.query(Review) + .filter(Review.product_id == product_id) + .order_by(Review.created_at.desc()) + .offset(skip) + .limit(limit) + .all() + ) + + def get_reviews_by_user( + self, db: Session, *, user_id: int, skip: int = 0, limit: int = 100 + ) -> List[Review]: + return ( + db.query(Review) + .filter(Review.user_id == user_id) + .order_by(Review.created_at.desc()) + .offset(skip) + .limit(limit) + .all() + ) + + def get_user_review_for_product( + self, db: Session, *, user_id: int, product_id: int + ) -> Optional[Review]: + return ( + db.query(Review) + .filter(Review.user_id == user_id, Review.product_id == product_id) + .first() + ) + + def create_user_review( + self, db: Session, *, obj_in: ReviewCreate, user_id: int + ) -> Review: + db_obj = Review( + user_id=user_id, + product_id=obj_in.product_id, + rating=obj_in.rating, + comment=obj_in.comment, + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + +review = CRUDReview(Review) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 1705b60..ce406ba 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -1,5 +1,25 @@ # Import schemas for convenience +from app.schemas.cart import Cart, CartCreate, CartItem, CartItemCreate, CartItemUpdate +from app.schemas.order import Order, OrderCreate, OrderItem, OrderItemCreate, OrderUpdate +from app.schemas.product import ( + Category, + CategoryCreate, + CategoryUpdate, + Product, + ProductCreate, + ProductFilterParams, + ProductUpdate, +) +from app.schemas.review import Review, ReviewCreate, ReviewUpdate from app.schemas.token import Token, TokenPayload from app.schemas.user import User, UserCreate, UserInDB, UserUpdate -__all__ = ["Token", "TokenPayload", "User", "UserCreate", "UserInDB", "UserUpdate"] \ No newline at end of file +__all__ = [ + "Token", "TokenPayload", + "User", "UserCreate", "UserInDB", "UserUpdate", + "Product", "ProductCreate", "ProductUpdate", + "Category", "CategoryCreate", "CategoryUpdate", "ProductFilterParams", + "Order", "OrderCreate", "OrderUpdate", "OrderItem", "OrderItemCreate", + "Cart", "CartCreate", "CartItem", "CartItemCreate", "CartItemUpdate", + "Review", "ReviewCreate", "ReviewUpdate" +] diff --git a/app/schemas/cart.py b/app/schemas/cart.py new file mode 100644 index 0000000..7ca1dc8 --- /dev/null +++ b/app/schemas/cart.py @@ -0,0 +1,56 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + +from app.schemas.product import Product + + +# Cart Item schemas +class CartItemBase(BaseModel): + product_id: int + quantity: int = Field(1, gt=0) + + +class CartItemCreate(CartItemBase): + pass + + +class CartItemUpdate(BaseModel): + quantity: int = Field(..., gt=0) + + +class CartItemInDBBase(CartItemBase): + id: int + cart_id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class CartItem(CartItemInDBBase): + product: Optional[Product] = None + + +# Cart schemas +class CartBase(BaseModel): + user_id: int + + +class CartCreate(CartBase): + pass + + +class CartInDBBase(CartBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class Cart(CartInDBBase): + items: List[CartItem] = [] diff --git a/app/schemas/order.py b/app/schemas/order.py new file mode 100644 index 0000000..8ba5676 --- /dev/null +++ b/app/schemas/order.py @@ -0,0 +1,64 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + +from app.models.order import OrderStatus + + +# Order Item schemas +class OrderItemBase(BaseModel): + product_id: int + quantity: int = Field(..., gt=0) + unit_price: float = Field(..., gt=0) + + +class OrderItemCreate(OrderItemBase): + pass + + +class OrderItemInDBBase(OrderItemBase): + id: int + order_id: int + created_at: datetime + + class Config: + from_attributes = True + + +class OrderItem(OrderItemInDBBase): + pass + + +# Order schemas +class OrderBase(BaseModel): + status: OrderStatus = OrderStatus.PENDING + shipping_address: str + payment_details: Optional[str] = None + tracking_number: Optional[str] = None + + +class OrderCreate(OrderBase): + items: List[OrderItemCreate] + + +class OrderUpdate(BaseModel): + status: Optional[OrderStatus] = None + shipping_address: Optional[str] = None + payment_details: Optional[str] = None + tracking_number: Optional[str] = None + + +class OrderInDBBase(OrderBase): + id: int + user_id: int + total_amount: float + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class Order(OrderInDBBase): + items: List[OrderItem] diff --git a/app/schemas/product.py b/app/schemas/product.py new file mode 100644 index 0000000..4270556 --- /dev/null +++ b/app/schemas/product.py @@ -0,0 +1,75 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +# Category schemas +class CategoryBase(BaseModel): + name: str + description: Optional[str] = None + + +class CategoryCreate(CategoryBase): + pass + + +class CategoryUpdate(CategoryBase): + name: Optional[str] = None + + +class CategoryInDBBase(CategoryBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class Category(CategoryInDBBase): + pass + + +# Product schemas +class ProductBase(BaseModel): + name: str + description: Optional[str] = None + price: float = Field(..., gt=0) + stock: int = Field(0, ge=0) + image_url: Optional[str] = None + is_active: bool = True + category_id: Optional[int] = None + + +class ProductCreate(ProductBase): + pass + + +class ProductUpdate(ProductBase): + name: Optional[str] = None + price: Optional[float] = Field(None, gt=0) + stock: Optional[int] = Field(None, ge=0) + category_id: Optional[int] = None + + +class ProductInDBBase(ProductBase): + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class Product(ProductInDBBase): + category: Optional[Category] = None + + +# Product search and filtering +class ProductFilterParams(BaseModel): + name: Optional[str] = None + category_id: Optional[int] = None + min_price: Optional[float] = None + max_price: Optional[float] = None + in_stock: Optional[bool] = None diff --git a/app/schemas/review.py b/app/schemas/review.py new file mode 100644 index 0000000..3daca0e --- /dev/null +++ b/app/schemas/review.py @@ -0,0 +1,33 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +class ReviewBase(BaseModel): + product_id: int + rating: float = Field(..., ge=1, le=5) + comment: Optional[str] = None + + +class ReviewCreate(ReviewBase): + pass + + +class ReviewUpdate(BaseModel): + rating: Optional[float] = Field(None, ge=1, le=5) + comment: Optional[str] = None + + +class ReviewInDBBase(ReviewBase): + id: int + user_id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class Review(ReviewInDBBase): + pass diff --git a/migrations/versions/9a4f22e84e75_initial_migration.py b/migrations/versions/9a4f22e84e75_initial_migration.py index 5f2eacf..ab3b294 100644 --- a/migrations/versions/9a4f22e84e75_initial_migration.py +++ b/migrations/versions/9a4f22e84e75_initial_migration.py @@ -5,9 +5,8 @@ Revises: Create Date: 2023-10-30 00:00:00.000000 """ -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision = '9a4f22e84e75' @@ -149,29 +148,29 @@ def downgrade() -> None: # Drop all tables in reverse order op.drop_index(op.f('ix_reviews_id'), table_name='reviews') op.drop_table('reviews') - + op.drop_index(op.f('ix_order_items_id'), table_name='order_items') op.drop_table('order_items') - + op.drop_index(op.f('ix_orders_id'), table_name='orders') op.drop_table('orders') - + op.drop_index(op.f('ix_cart_items_id'), table_name='cart_items') op.drop_table('cart_items') - + op.drop_index(op.f('ix_carts_id'), table_name='carts') op.drop_table('carts') - + op.drop_index(op.f('ix_products_name'), table_name='products') op.drop_index(op.f('ix_products_id'), table_name='products') op.drop_table('products') - + op.drop_index(op.f('ix_categories_name'), table_name='categories') op.drop_index(op.f('ix_categories_id'), table_name='categories') op.drop_table('categories') - + op.drop_index(op.f('ix_users_full_name'), table_name='users') op.drop_index(op.f('ix_users_username'), table_name='users') op.drop_index(op.f('ix_users_email'), table_name='users') op.drop_index(op.f('ix_users_id'), table_name='users') - op.drop_table('users') \ No newline at end of file + op.drop_table('users')