Complete e-commerce API implementation with all required endpoints
This commit is contained in:
parent
fd7eca550f
commit
c6db3ebe3b
145
app/api/endpoints/cart.py
Normal file
145
app/api/endpoints/cart.py
Normal file
@ -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)
|
110
app/api/endpoints/categories.py
Normal file
110
app/api/endpoints/categories.py
Normal file
@ -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)
|
172
app/api/endpoints/orders.py
Normal file
172
app/api/endpoints/orders.py
Normal file
@ -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()
|
108
app/api/endpoints/products.py
Normal file
108
app/api/endpoints/products.py
Normal file
@ -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)
|
124
app/api/endpoints/reviews.py
Normal file
124
app/api/endpoints/reviews.py
Normal file
@ -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)
|
@ -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"])
|
||||
|
@ -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"]
|
||||
|
65
app/crud/crud_cart.py
Normal file
65
app/crud/crud_cart.py
Normal file
@ -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)
|
69
app/crud/crud_order.py
Normal file
69
app/crud/crud_order.py
Normal file
@ -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)
|
53
app/crud/crud_product.py
Normal file
53
app/crud/crud_product.py
Normal file
@ -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)
|
59
app/crud/crud_review.py
Normal file
59
app/crud/crud_review.py
Normal file
@ -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)
|
@ -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"]
|
||||
__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"
|
||||
]
|
||||
|
56
app/schemas/cart.py
Normal file
56
app/schemas/cart.py
Normal file
@ -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] = []
|
64
app/schemas/order.py
Normal file
64
app/schemas/order.py
Normal file
@ -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]
|
75
app/schemas/product.py
Normal file
75
app/schemas/product.py
Normal file
@ -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
|
33
app/schemas/review.py
Normal file
33
app/schemas/review.py
Normal file
@ -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
|
@ -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')
|
||||
op.drop_table('users')
|
||||
|
Loading…
x
Reference in New Issue
Block a user