324 lines
10 KiB
Python

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"}