324 lines
10 KiB
Python
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"}
|