import json import logging import random import string from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import desc from sqlalchemy.orm import Session from app.core.database import get_db from app.dependencies.auth import get_current_active_user, get_current_admin from app.models.cart import CartItem from app.models.order import Order, OrderItem, OrderStatus from app.models.product import Product, ProductStatus from app.models.user import User, UserRole from app.schemas.order import ( Order as OrderSchema, ) from app.schemas.order import ( OrderCreate, OrderSummary, OrderUpdate, ) router = APIRouter() logger = logging.getLogger(__name__) def generate_order_number(): """Generate a unique order number.""" timestamp = datetime.now().strftime("%Y%m%d") random_chars = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) return f"ORD-{timestamp}-{random_chars}" @router.get("/", response_model=list[OrderSummary]) async def get_orders( skip: int = 0, limit: int = 100, status: OrderStatus | None = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Get all orders for the current user. Admins can filter by user ID. """ query = db.query(Order) # Filter by user ID (regular users can only see their own orders) if current_user.role != UserRole.ADMIN: query = query.filter(Order.user_id == current_user.id) # Filter by status if provided if status: query = query.filter(Order.status == status) # Apply pagination and order by newest first orders = query.order_by(desc(Order.created_at)).offset(skip).limit(limit).all() # Add item count to each order for order in orders: order.item_count = sum(item.quantity for item in order.items) return orders @router.get("/{order_id}", response_model=OrderSchema) async def get_order( order_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Get a specific order by ID. Regular users can only get their own orders. Admins can get any order. """ order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Order not found" ) # Check permissions if current_user.role != UserRole.ADMIN and order.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions to access this order" ) return order @router.post("/", response_model=OrderSchema) async def create_order( order_in: OrderCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Create a new order from the shopping cart. """ # Validate cart items if not order_in.cart_items: # If no cart items are specified, use all items from the user's cart cart_items = db.query(CartItem).filter(CartItem.user_id == current_user.id).all() if not cart_items: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Shopping cart is empty" ) cart_item_ids = [item.id for item in cart_items] else: # If specific cart items are specified, validate them cart_items = db.query(CartItem).filter( CartItem.id.in_(order_in.cart_items), CartItem.user_id == current_user.id ).all() if len(cart_items) != len(order_in.cart_items): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="One or more cart items not found" ) cart_item_ids = order_in.cart_items # Calculate totals subtotal = 0 tax_amount = 0 shipping_amount = 10.0 # Default shipping cost (should be calculated based on shipping method, weight, etc.) discount_amount = 0 # Use default user addresses if requested if order_in.use_default_addresses: shipping_address = { "first_name": current_user.first_name or "", "last_name": current_user.last_name or "", "address_line1": current_user.address_line1 or "", "address_line2": current_user.address_line2, "city": current_user.city or "", "state": current_user.state or "", "postal_code": current_user.postal_code or "", "country": current_user.country or "", "phone_number": current_user.phone_number, "email": current_user.email } billing_address = shipping_address else: shipping_address = order_in.shipping_address.dict() billing_address = order_in.billing_address.dict() if order_in.billing_address else shipping_address # Create order items and calculate totals order_items_data = [] for cart_item in cart_items: # Validate product availability and stock product = db.query(Product).filter(Product.id == cart_item.product_id).first() if not product: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Product not found for cart item {cart_item.id}" ) if product.status != ProductStatus.PUBLISHED: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Product '{product.name}' is not available for purchase" ) if product.stock_quantity < cart_item.quantity: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Not enough stock for product '{product.name}'. Available: {product.stock_quantity}" ) # Calculate item subtotal and tax item_unit_price = product.current_price item_subtotal = item_unit_price * cart_item.quantity item_tax = item_subtotal * (product.tax_rate / 100) # Create order item order_item_data = { "product_id": product.id, "quantity": cart_item.quantity, "unit_price": item_unit_price, "subtotal": item_subtotal, "tax_amount": item_tax, "product_name": product.name, "product_sku": product.sku, "product_options": cart_item.custom_properties } order_items_data.append(order_item_data) # Update totals subtotal += item_subtotal tax_amount += item_tax # Update product stock product.stock_quantity -= cart_item.quantity # Check if product is now out of stock if product.stock_quantity == 0: product.status = ProductStatus.OUT_OF_STOCK # Calculate final total total_amount = subtotal + tax_amount + shipping_amount - discount_amount # Create the order db_order = Order( user_id=current_user.id, order_number=generate_order_number(), status=OrderStatus.PENDING, total_amount=total_amount, subtotal=subtotal, tax_amount=tax_amount, shipping_amount=shipping_amount, discount_amount=discount_amount, shipping_method=order_in.shipping_method, shipping_address=json.dumps(shipping_address), billing_address=json.dumps(billing_address), notes=order_in.notes ) db.add(db_order) db.commit() db.refresh(db_order) # Create order items for item_data in order_items_data: db_order_item = OrderItem( order_id=db_order.id, **item_data ) db.add(db_order_item) # Remove cart items for cart_item_id in cart_item_ids: db.query(CartItem).filter(CartItem.id == cart_item_id).delete() db.commit() db.refresh(db_order) logger.info(f"Order created: {db_order.order_number} (ID: {db_order.id}) for user {current_user.email}") return db_order @router.put("/{order_id}", response_model=OrderSchema) async def update_order( order_id: str, order_in: OrderUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Update an order. Regular users can only update their own orders and with limited fields. Admins can update any order with all fields. """ order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Order not found" ) # Check permissions if current_user.role != UserRole.ADMIN and order.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions to update this order" ) # Regular users can only cancel pending orders if current_user.role != UserRole.ADMIN: if order.status != OrderStatus.PENDING: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Only pending orders can be updated" ) if order_in.status and order_in.status != OrderStatus.CANCELLED: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="You can only cancel an order" ) # Update order attributes for key, value in order_in.dict(exclude_unset=True).items(): setattr(order, key, value) db.commit() db.refresh(order) logger.info(f"Order updated: {order.order_number} (ID: {order.id})") return order @router.delete("/{order_id}", response_model=dict) async def delete_order( order_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin) ): """ Delete an order (admin only). """ order = db.query(Order).filter(Order.id == order_id).first() if not order: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Order not found" ) # Delete order items first (should happen automatically with cascade) db.query(OrderItem).filter(OrderItem.order_id == order_id).delete() # Delete the order db.delete(order) db.commit() logger.info(f"Order deleted: {order.order_number} (ID: {order.id})") return {"message": "Order successfully deleted"}