1475 lines
56 KiB
Python
1475 lines
56 KiB
Python
from fastapi import FastAPI, APIRouter, HTTPException, Depends, status, Query, Response
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from fastapi.responses import StreamingResponse
|
|
from dotenv import load_dotenv
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_, or_, desc, asc
|
|
from sqlalchemy.orm import selectinload
|
|
import os
|
|
import logging
|
|
from pathlib import Path
|
|
from pydantic import BaseModel, Field, EmailStr, ConfigDict
|
|
from typing import List, Optional, Dict, Any
|
|
import uuid
|
|
from datetime import datetime, timezone, timedelta
|
|
import bcrypt
|
|
import jwt
|
|
import io
|
|
import csv
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import letter, A4
|
|
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import inch
|
|
|
|
from database import get_db, init_db, AsyncSessionLocal
|
|
from models import (
|
|
User, Product, Service, CartItem, Order, OrderItem, OrderStatusHistory,
|
|
Review, Booking, Contact, InventoryLog, Category, SalesReport,
|
|
OrderStatus, UserRole, Base
|
|
)
|
|
|
|
ROOT_DIR = Path(__file__).parent
|
|
load_dotenv(ROOT_DIR / '.env')
|
|
|
|
# JWT Configuration
|
|
SECRET_KEY = os.environ.get('JWT_SECRET', 'techzone-super-secret-key-2024-production')
|
|
ALGORITHM = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_HOURS = 24
|
|
|
|
# Create the main app
|
|
app = FastAPI(title="TechZone API", version="2.0.0")
|
|
|
|
# Create a router with the /api prefix
|
|
api_router = APIRouter(prefix="/api")
|
|
|
|
security = HTTPBearer()
|
|
|
|
# ================== PYDANTIC MODELS ==================
|
|
|
|
class UserCreate(BaseModel):
|
|
email: EmailStr
|
|
name: str
|
|
password: str
|
|
|
|
class UserLogin(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
class TokenResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
user: dict
|
|
|
|
class ProductCreate(BaseModel):
|
|
name: str
|
|
description: str
|
|
price: float
|
|
category: str
|
|
image_url: str
|
|
stock: int = 10
|
|
low_stock_threshold: int = 5
|
|
brand: str = ""
|
|
specs: dict = {}
|
|
|
|
class ProductUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
price: Optional[float] = None
|
|
category: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
stock: Optional[int] = None
|
|
low_stock_threshold: Optional[int] = None
|
|
brand: Optional[str] = None
|
|
specs: Optional[dict] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
class ServiceCreate(BaseModel):
|
|
name: str
|
|
description: str
|
|
price: float
|
|
duration: str
|
|
image_url: str
|
|
category: str
|
|
|
|
class ServiceUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
price: Optional[float] = None
|
|
duration: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
category: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
class CartItemCreate(BaseModel):
|
|
product_id: str
|
|
quantity: int = 1
|
|
|
|
class OrderCreate(BaseModel):
|
|
shipping_address: dict = {}
|
|
notes: str = ""
|
|
|
|
class OrderStatusUpdate(BaseModel):
|
|
status: str
|
|
notes: str = ""
|
|
tracking_number: Optional[str] = None
|
|
|
|
class ReviewCreate(BaseModel):
|
|
product_id: Optional[str] = None
|
|
service_id: Optional[str] = None
|
|
rating: int
|
|
title: str = ""
|
|
comment: str = ""
|
|
|
|
class BookingCreate(BaseModel):
|
|
service_id: str
|
|
name: str
|
|
email: EmailStr
|
|
phone: str
|
|
preferred_date: str
|
|
notes: str = ""
|
|
|
|
class ContactCreate(BaseModel):
|
|
name: str
|
|
email: EmailStr
|
|
subject: str
|
|
message: str
|
|
|
|
class InventoryAdjust(BaseModel):
|
|
quantity_change: int
|
|
notes: str = ""
|
|
|
|
# ================== HELPERS ==================
|
|
|
|
def hash_password(password: str) -> str:
|
|
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
|
|
|
def verify_password(password: str, hashed: str) -> bool:
|
|
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
|
|
|
def create_access_token(data: dict) -> str:
|
|
to_encode = data.copy()
|
|
expire = datetime.now(timezone.utc) + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
|
|
to_encode.update({"exp": expire})
|
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
|
|
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db)):
|
|
try:
|
|
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
|
|
user_id = payload.get("sub")
|
|
if user_id is None:
|
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
user = result.scalar_one_or_none()
|
|
if user is None:
|
|
raise HTTPException(status_code=401, detail="User not found")
|
|
return user
|
|
except jwt.ExpiredSignatureError:
|
|
raise HTTPException(status_code=401, detail="Token expired")
|
|
except jwt.InvalidTokenError:
|
|
raise HTTPException(status_code=401, detail="Invalid token")
|
|
|
|
async def get_admin_user(user: User = Depends(get_current_user)):
|
|
if user.role != UserRole.ADMIN:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
return user
|
|
|
|
async def get_optional_user(credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)), db: AsyncSession = Depends(get_db)):
|
|
if credentials is None:
|
|
return None
|
|
try:
|
|
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
|
|
user_id = payload.get("sub")
|
|
if user_id:
|
|
result = await db.execute(select(User).where(User.id == user_id))
|
|
return result.scalar_one_or_none()
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
def user_to_dict(user: User) -> dict:
|
|
return {
|
|
"id": user.id,
|
|
"email": user.email,
|
|
"name": user.name,
|
|
"role": user.role.value if user.role else "user",
|
|
"created_at": user.created_at.isoformat() if user.created_at else None
|
|
}
|
|
|
|
def product_to_dict(product: Product, include_reviews: bool = False) -> dict:
|
|
data = {
|
|
"id": product.id,
|
|
"name": product.name,
|
|
"description": product.description,
|
|
"price": product.price,
|
|
"category": product.category,
|
|
"image_url": product.image_url,
|
|
"stock": product.stock,
|
|
"low_stock_threshold": product.low_stock_threshold,
|
|
"brand": product.brand,
|
|
"specs": product.specs or {},
|
|
"is_active": product.is_active,
|
|
"created_at": product.created_at.isoformat() if product.created_at else None
|
|
}
|
|
if include_reviews and product.reviews:
|
|
data["reviews"] = [review_to_dict(r) for r in product.reviews]
|
|
data["average_rating"] = sum(r.rating for r in product.reviews) / len(product.reviews) if product.reviews else 0
|
|
data["review_count"] = len(product.reviews)
|
|
return data
|
|
|
|
def service_to_dict(service: Service, include_reviews: bool = False) -> dict:
|
|
data = {
|
|
"id": service.id,
|
|
"name": service.name,
|
|
"description": service.description,
|
|
"price": service.price,
|
|
"duration": service.duration,
|
|
"image_url": service.image_url,
|
|
"category": service.category,
|
|
"is_active": service.is_active,
|
|
"created_at": service.created_at.isoformat() if service.created_at else None
|
|
}
|
|
if include_reviews and service.reviews:
|
|
data["reviews"] = [review_to_dict(r) for r in service.reviews]
|
|
data["average_rating"] = sum(r.rating for r in service.reviews) / len(service.reviews) if service.reviews else 0
|
|
data["review_count"] = len(service.reviews)
|
|
return data
|
|
|
|
def order_to_dict(order: Order) -> dict:
|
|
return {
|
|
"id": order.id,
|
|
"user_id": order.user_id,
|
|
"status": order.status.value if order.status else "pending",
|
|
"subtotal": order.subtotal,
|
|
"tax": order.tax,
|
|
"shipping": order.shipping,
|
|
"total": order.total,
|
|
"shipping_address": order.shipping_address or {},
|
|
"notes": order.notes,
|
|
"tracking_number": order.tracking_number,
|
|
"created_at": order.created_at.isoformat() if order.created_at else None,
|
|
"updated_at": order.updated_at.isoformat() if order.updated_at else None,
|
|
"items": [order_item_to_dict(item) for item in order.items] if order.items else [],
|
|
"status_history": [status_history_to_dict(h) for h in order.status_history] if order.status_history else []
|
|
}
|
|
|
|
def order_item_to_dict(item: OrderItem) -> dict:
|
|
return {
|
|
"id": item.id,
|
|
"product_id": item.product_id,
|
|
"product_name": item.product_name,
|
|
"product_image": item.product_image,
|
|
"quantity": item.quantity,
|
|
"price": item.price
|
|
}
|
|
|
|
def status_history_to_dict(history: OrderStatusHistory) -> dict:
|
|
return {
|
|
"id": history.id,
|
|
"status": history.status.value if history.status else None,
|
|
"notes": history.notes,
|
|
"created_at": history.created_at.isoformat() if history.created_at else None
|
|
}
|
|
|
|
def review_to_dict(review: Review) -> dict:
|
|
return {
|
|
"id": review.id,
|
|
"user_id": review.user_id,
|
|
"user_name": review.user.name if review.user else "Anonymous",
|
|
"product_id": review.product_id,
|
|
"service_id": review.service_id,
|
|
"rating": review.rating,
|
|
"title": review.title,
|
|
"comment": review.comment,
|
|
"is_verified_purchase": review.is_verified_purchase,
|
|
"created_at": review.created_at.isoformat() if review.created_at else None
|
|
}
|
|
|
|
def booking_to_dict(booking: Booking) -> dict:
|
|
return {
|
|
"id": booking.id,
|
|
"service_id": booking.service_id,
|
|
"service_name": booking.service_name,
|
|
"name": booking.name,
|
|
"email": booking.email,
|
|
"phone": booking.phone,
|
|
"preferred_date": booking.preferred_date,
|
|
"notes": booking.notes,
|
|
"status": booking.status,
|
|
"created_at": booking.created_at.isoformat() if booking.created_at else None
|
|
}
|
|
|
|
# ================== AUTH ROUTES ==================
|
|
|
|
@api_router.post("/auth/register", response_model=TokenResponse)
|
|
async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(User).where(User.email == user_data.email))
|
|
if result.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="Email already registered")
|
|
|
|
user = User(
|
|
email=user_data.email,
|
|
name=user_data.name,
|
|
password=hash_password(user_data.password),
|
|
role=UserRole.USER
|
|
)
|
|
db.add(user)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
|
|
token = create_access_token({"sub": user.id})
|
|
return TokenResponse(access_token=token, user=user_to_dict(user))
|
|
|
|
@api_router.post("/auth/login", response_model=TokenResponse)
|
|
async def login(credentials: UserLogin, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(User).where(User.email == credentials.email))
|
|
user = result.scalar_one_or_none()
|
|
if not user or not verify_password(credentials.password, user.password):
|
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
|
|
token = create_access_token({"sub": user.id})
|
|
return TokenResponse(access_token=token, user=user_to_dict(user))
|
|
|
|
@api_router.get("/auth/me")
|
|
async def get_me(user: User = Depends(get_current_user)):
|
|
return user_to_dict(user)
|
|
|
|
# ================== PRODUCTS ROUTES ==================
|
|
|
|
@api_router.get("/products")
|
|
async def get_products(
|
|
category: Optional[str] = None,
|
|
search: Optional[str] = None,
|
|
min_price: Optional[float] = None,
|
|
max_price: Optional[float] = None,
|
|
in_stock: Optional[bool] = None,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
query = select(Product).where(Product.is_active == True)
|
|
|
|
if category and category != "all":
|
|
query = query.where(Product.category == category)
|
|
if search:
|
|
query = query.where(
|
|
or_(
|
|
Product.name.ilike(f"%{search}%"),
|
|
Product.description.ilike(f"%{search}%"),
|
|
Product.brand.ilike(f"%{search}%")
|
|
)
|
|
)
|
|
if min_price is not None:
|
|
query = query.where(Product.price >= min_price)
|
|
if max_price is not None:
|
|
query = query.where(Product.price <= max_price)
|
|
if in_stock:
|
|
query = query.where(Product.stock > 0)
|
|
|
|
query = query.options(selectinload(Product.reviews).selectinload(Review.user))
|
|
result = await db.execute(query)
|
|
products = result.scalars().all()
|
|
return [product_to_dict(p, include_reviews=True) for p in products]
|
|
|
|
@api_router.get("/products/{product_id}")
|
|
async def get_product(product_id: str, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Product)
|
|
.where(Product.id == product_id)
|
|
.options(selectinload(Product.reviews).selectinload(Review.user))
|
|
)
|
|
product = result.scalar_one_or_none()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
return product_to_dict(product, include_reviews=True)
|
|
|
|
@api_router.get("/products/categories/list")
|
|
async def get_product_categories(db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Product.category).distinct())
|
|
categories = [row[0] for row in result.fetchall()]
|
|
return categories
|
|
|
|
# ================== SERVICES ROUTES ==================
|
|
|
|
@api_router.get("/services")
|
|
async def get_services(category: Optional[str] = None, db: AsyncSession = Depends(get_db)):
|
|
query = select(Service).where(Service.is_active == True)
|
|
if category and category != "all":
|
|
query = query.where(Service.category == category)
|
|
query = query.options(selectinload(Service.reviews).selectinload(Review.user))
|
|
result = await db.execute(query)
|
|
services = result.scalars().all()
|
|
return [service_to_dict(s, include_reviews=True) for s in services]
|
|
|
|
@api_router.get("/services/{service_id}")
|
|
async def get_service(service_id: str, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Service)
|
|
.where(Service.id == service_id)
|
|
.options(selectinload(Service.reviews).selectinload(Review.user))
|
|
)
|
|
service = result.scalar_one_or_none()
|
|
if not service:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
return service_to_dict(service, include_reviews=True)
|
|
|
|
@api_router.post("/services/book")
|
|
async def book_service(
|
|
booking_data: BookingCreate,
|
|
user: Optional[User] = Depends(get_optional_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
result = await db.execute(select(Service).where(Service.id == booking_data.service_id))
|
|
service = result.scalar_one_or_none()
|
|
if not service:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
|
|
booking = Booking(
|
|
service_id=booking_data.service_id,
|
|
user_id=user.id if user else None,
|
|
name=booking_data.name,
|
|
email=booking_data.email,
|
|
phone=booking_data.phone,
|
|
preferred_date=booking_data.preferred_date,
|
|
notes=booking_data.notes,
|
|
service_name=service.name
|
|
)
|
|
db.add(booking)
|
|
await db.commit()
|
|
return {"message": "Booking created successfully", "booking_id": booking.id}
|
|
|
|
# ================== CART ROUTES ==================
|
|
|
|
@api_router.get("/cart")
|
|
async def get_cart(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(CartItem)
|
|
.where(CartItem.user_id == user.id)
|
|
.options(selectinload(CartItem.product))
|
|
)
|
|
cart_items = result.scalars().all()
|
|
return [{
|
|
"id": item.id,
|
|
"product_id": item.product_id,
|
|
"quantity": item.quantity,
|
|
"product": product_to_dict(item.product) if item.product else None
|
|
} for item in cart_items]
|
|
|
|
@api_router.post("/cart/add")
|
|
async def add_to_cart(item: CartItemCreate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Product).where(Product.id == item.product_id))
|
|
product = result.scalar_one_or_none()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
result = await db.execute(
|
|
select(CartItem).where(
|
|
and_(CartItem.user_id == user.id, CartItem.product_id == item.product_id)
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
existing.quantity += item.quantity
|
|
else:
|
|
cart_item = CartItem(user_id=user.id, product_id=item.product_id, quantity=item.quantity)
|
|
db.add(cart_item)
|
|
|
|
await db.commit()
|
|
return {"message": "Item added to cart"}
|
|
|
|
@api_router.put("/cart/{item_id}")
|
|
async def update_cart_item(item_id: str, quantity: int = Query(...), user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(CartItem).where(and_(CartItem.id == item_id, CartItem.user_id == user.id))
|
|
)
|
|
item = result.scalar_one_or_none()
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Cart item not found")
|
|
|
|
if quantity <= 0:
|
|
await db.delete(item)
|
|
else:
|
|
item.quantity = quantity
|
|
|
|
await db.commit()
|
|
return {"message": "Cart updated"}
|
|
|
|
@api_router.delete("/cart/{item_id}")
|
|
async def remove_from_cart(item_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(CartItem).where(and_(CartItem.id == item_id, CartItem.user_id == user.id))
|
|
)
|
|
item = result.scalar_one_or_none()
|
|
if item:
|
|
await db.delete(item)
|
|
await db.commit()
|
|
return {"message": "Item removed from cart"}
|
|
|
|
@api_router.delete("/cart")
|
|
async def clear_cart(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
await db.execute(
|
|
CartItem.__table__.delete().where(CartItem.user_id == user.id)
|
|
)
|
|
await db.commit()
|
|
return {"message": "Cart cleared"}
|
|
|
|
# ================== ORDERS ROUTES ==================
|
|
|
|
@api_router.post("/orders")
|
|
async def create_order(order_data: OrderCreate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
# Get cart items
|
|
result = await db.execute(
|
|
select(CartItem)
|
|
.where(CartItem.user_id == user.id)
|
|
.options(selectinload(CartItem.product))
|
|
)
|
|
cart_items = result.scalars().all()
|
|
|
|
if not cart_items:
|
|
raise HTTPException(status_code=400, detail="Cart is empty")
|
|
|
|
# Calculate totals
|
|
subtotal = sum(item.product.price * item.quantity for item in cart_items)
|
|
tax = subtotal * 0.08
|
|
shipping = 0 if subtotal > 100 else 9.99
|
|
total = subtotal + tax + shipping
|
|
|
|
# Create order
|
|
order = Order(
|
|
user_id=user.id,
|
|
status=OrderStatus.PENDING,
|
|
subtotal=subtotal,
|
|
tax=tax,
|
|
shipping=shipping,
|
|
total=total,
|
|
shipping_address=order_data.shipping_address,
|
|
notes=order_data.notes
|
|
)
|
|
db.add(order)
|
|
await db.flush()
|
|
|
|
# Create order items and update inventory
|
|
for cart_item in cart_items:
|
|
product = cart_item.product
|
|
order_item = OrderItem(
|
|
order_id=order.id,
|
|
product_id=product.id,
|
|
quantity=cart_item.quantity,
|
|
price=product.price,
|
|
product_name=product.name,
|
|
product_image=product.image_url
|
|
)
|
|
db.add(order_item)
|
|
|
|
# Update stock
|
|
previous_stock = product.stock
|
|
product.stock = max(0, product.stock - cart_item.quantity)
|
|
|
|
# Log inventory change
|
|
inv_log = InventoryLog(
|
|
product_id=product.id,
|
|
action="sale",
|
|
quantity_change=-cart_item.quantity,
|
|
previous_stock=previous_stock,
|
|
new_stock=product.stock,
|
|
notes=f"Order {order.id}",
|
|
created_by=user.id
|
|
)
|
|
db.add(inv_log)
|
|
|
|
# Add status history
|
|
status_history = OrderStatusHistory(
|
|
order_id=order.id,
|
|
status=OrderStatus.PENDING,
|
|
notes="Order placed",
|
|
created_by=user.id
|
|
)
|
|
db.add(status_history)
|
|
|
|
# Clear cart
|
|
await db.execute(CartItem.__table__.delete().where(CartItem.user_id == user.id))
|
|
|
|
await db.commit()
|
|
await db.refresh(order)
|
|
|
|
return {"message": "Order created successfully", "order_id": order.id}
|
|
|
|
@api_router.get("/orders")
|
|
async def get_orders(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Order)
|
|
.where(Order.user_id == user.id)
|
|
.options(selectinload(Order.items), selectinload(Order.status_history))
|
|
.order_by(desc(Order.created_at))
|
|
)
|
|
orders = result.scalars().all()
|
|
return [order_to_dict(o) for o in orders]
|
|
|
|
@api_router.get("/orders/{order_id}")
|
|
async def get_order(order_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Order)
|
|
.where(and_(Order.id == order_id, Order.user_id == user.id))
|
|
.options(selectinload(Order.items), selectinload(Order.status_history))
|
|
)
|
|
order = result.scalar_one_or_none()
|
|
if not order:
|
|
raise HTTPException(status_code=404, detail="Order not found")
|
|
return order_to_dict(order)
|
|
|
|
# ================== REVIEWS ROUTES ==================
|
|
|
|
@api_router.post("/reviews")
|
|
async def create_review(review_data: ReviewCreate, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
if not review_data.product_id and not review_data.service_id:
|
|
raise HTTPException(status_code=400, detail="Product or service ID required")
|
|
|
|
if review_data.rating < 1 or review_data.rating > 5:
|
|
raise HTTPException(status_code=400, detail="Rating must be between 1 and 5")
|
|
|
|
# Check for verified purchase
|
|
is_verified = False
|
|
if review_data.product_id:
|
|
result = await db.execute(
|
|
select(OrderItem)
|
|
.join(Order)
|
|
.where(
|
|
and_(
|
|
Order.user_id == user.id,
|
|
OrderItem.product_id == review_data.product_id,
|
|
Order.status.in_([OrderStatus.DELIVERED, OrderStatus.SHIPPED])
|
|
)
|
|
)
|
|
)
|
|
if result.scalar_one_or_none():
|
|
is_verified = True
|
|
|
|
review = Review(
|
|
user_id=user.id,
|
|
product_id=review_data.product_id,
|
|
service_id=review_data.service_id,
|
|
rating=review_data.rating,
|
|
title=review_data.title,
|
|
comment=review_data.comment,
|
|
is_verified_purchase=is_verified
|
|
)
|
|
db.add(review)
|
|
await db.commit()
|
|
await db.refresh(review)
|
|
|
|
return {"message": "Review submitted successfully", "review_id": review.id}
|
|
|
|
@api_router.get("/reviews/product/{product_id}")
|
|
async def get_product_reviews(product_id: str, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Review)
|
|
.where(and_(Review.product_id == product_id, Review.is_approved == True))
|
|
.options(selectinload(Review.user))
|
|
.order_by(desc(Review.created_at))
|
|
)
|
|
reviews = result.scalars().all()
|
|
return [review_to_dict(r) for r in reviews]
|
|
|
|
@api_router.get("/reviews/service/{service_id}")
|
|
async def get_service_reviews(service_id: str, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Review)
|
|
.where(and_(Review.service_id == service_id, Review.is_approved == True))
|
|
.options(selectinload(Review.user))
|
|
.order_by(desc(Review.created_at))
|
|
)
|
|
reviews = result.scalars().all()
|
|
return [review_to_dict(r) for r in reviews]
|
|
|
|
# ================== CONTACT ROUTES ==================
|
|
|
|
@api_router.post("/contact")
|
|
async def submit_contact(contact_data: ContactCreate, db: AsyncSession = Depends(get_db)):
|
|
contact = Contact(
|
|
name=contact_data.name,
|
|
email=contact_data.email,
|
|
subject=contact_data.subject,
|
|
message=contact_data.message
|
|
)
|
|
db.add(contact)
|
|
await db.commit()
|
|
return {"message": "Message sent successfully", "id": contact.id}
|
|
|
|
# ================== ADMIN ROUTES ==================
|
|
|
|
# Admin - Dashboard Stats
|
|
@api_router.get("/admin/dashboard")
|
|
async def get_admin_dashboard(user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
today = datetime.now(timezone.utc).date()
|
|
month_ago = today - timedelta(days=30)
|
|
|
|
# Total counts
|
|
products_count = await db.execute(select(func.count(Product.id)))
|
|
services_count = await db.execute(select(func.count(Service.id)))
|
|
users_count = await db.execute(select(func.count(User.id)))
|
|
orders_count = await db.execute(select(func.count(Order.id)))
|
|
|
|
# Revenue
|
|
total_revenue = await db.execute(select(func.sum(Order.total)))
|
|
monthly_revenue = await db.execute(
|
|
select(func.sum(Order.total))
|
|
.where(Order.created_at >= datetime.combine(month_ago, datetime.min.time()))
|
|
)
|
|
|
|
# Today's stats
|
|
today_orders = await db.execute(
|
|
select(func.count(Order.id))
|
|
.where(func.date(Order.created_at) == today)
|
|
)
|
|
today_revenue = await db.execute(
|
|
select(func.sum(Order.total))
|
|
.where(func.date(Order.created_at) == today)
|
|
)
|
|
|
|
# Low stock products - don't use relationships
|
|
low_stock = await db.execute(
|
|
select(Product)
|
|
.where(Product.stock <= Product.low_stock_threshold)
|
|
.where(Product.is_active == True)
|
|
)
|
|
low_stock_list = low_stock.scalars().all()
|
|
low_stock_products = [{
|
|
"id": p.id,
|
|
"name": p.name,
|
|
"stock": p.stock,
|
|
"low_stock_threshold": p.low_stock_threshold,
|
|
"category": p.category
|
|
} for p in low_stock_list]
|
|
|
|
# Recent orders - load items explicitly
|
|
recent_orders_result = await db.execute(
|
|
select(Order)
|
|
.options(selectinload(Order.items))
|
|
.order_by(desc(Order.created_at))
|
|
.limit(10)
|
|
)
|
|
recent_orders_list = recent_orders_result.scalars().all()
|
|
|
|
# Pending bookings
|
|
pending_bookings = await db.execute(
|
|
select(func.count(Booking.id))
|
|
.where(Booking.status == "pending")
|
|
)
|
|
|
|
recent_orders_data = []
|
|
for o in recent_orders_list:
|
|
recent_orders_data.append({
|
|
"id": o.id,
|
|
"status": o.status.value if o.status else "pending",
|
|
"total": o.total,
|
|
"created_at": o.created_at.isoformat() if o.created_at else None,
|
|
"items": [{"id": i.id, "product_name": i.product_name, "quantity": i.quantity} for i in o.items] if o.items else []
|
|
})
|
|
|
|
return {
|
|
"stats": {
|
|
"total_products": products_count.scalar() or 0,
|
|
"total_services": services_count.scalar() or 0,
|
|
"total_users": users_count.scalar() or 0,
|
|
"total_orders": orders_count.scalar() or 0,
|
|
"total_revenue": total_revenue.scalar() or 0,
|
|
"monthly_revenue": monthly_revenue.scalar() or 0,
|
|
"today_orders": today_orders.scalar() or 0,
|
|
"today_revenue": today_revenue.scalar() or 0,
|
|
"pending_bookings": pending_bookings.scalar() or 0
|
|
},
|
|
"low_stock_products": low_stock_products,
|
|
"recent_orders": recent_orders_data
|
|
}
|
|
|
|
return {
|
|
"stats": {
|
|
"total_products": products_count.scalar() or 0,
|
|
"total_services": services_count.scalar() or 0,
|
|
"total_users": users_count.scalar() or 0,
|
|
"total_orders": orders_count.scalar() or 0,
|
|
"total_revenue": total_revenue.scalar() or 0,
|
|
"monthly_revenue": monthly_revenue.scalar() or 0,
|
|
"today_orders": today_orders.scalar() or 0,
|
|
"today_revenue": today_revenue.scalar() or 0,
|
|
"pending_bookings": pending_bookings.scalar() or 0
|
|
},
|
|
"low_stock_products": low_stock_products,
|
|
"recent_orders": recent_orders_data
|
|
}
|
|
|
|
# Admin - Products CRUD
|
|
@api_router.get("/admin/products")
|
|
async def admin_get_products(
|
|
include_inactive: bool = False,
|
|
user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
query = select(Product)
|
|
if not include_inactive:
|
|
query = query.where(Product.is_active == True)
|
|
query = query.order_by(desc(Product.created_at))
|
|
result = await db.execute(query)
|
|
products = result.scalars().all()
|
|
return [product_to_dict(p) for p in products]
|
|
|
|
@api_router.post("/admin/products")
|
|
async def admin_create_product(product_data: ProductCreate, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
product = Product(**product_data.model_dump())
|
|
db.add(product)
|
|
await db.commit()
|
|
await db.refresh(product)
|
|
|
|
# Log inventory
|
|
inv_log = InventoryLog(
|
|
product_id=product.id,
|
|
action="add",
|
|
quantity_change=product.stock,
|
|
previous_stock=0,
|
|
new_stock=product.stock,
|
|
notes="Initial stock",
|
|
created_by=user.id
|
|
)
|
|
db.add(inv_log)
|
|
await db.commit()
|
|
|
|
return product_to_dict(product)
|
|
|
|
@api_router.put("/admin/products/{product_id}")
|
|
async def admin_update_product(product_id: str, product_data: ProductUpdate, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Product).where(Product.id == product_id))
|
|
product = result.scalar_one_or_none()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
update_data = product_data.model_dump(exclude_unset=True)
|
|
|
|
# Track stock changes
|
|
if "stock" in update_data and update_data["stock"] != product.stock:
|
|
inv_log = InventoryLog(
|
|
product_id=product.id,
|
|
action="adjust",
|
|
quantity_change=update_data["stock"] - product.stock,
|
|
previous_stock=product.stock,
|
|
new_stock=update_data["stock"],
|
|
notes="Manual adjustment",
|
|
created_by=user.id
|
|
)
|
|
db.add(inv_log)
|
|
|
|
for key, value in update_data.items():
|
|
setattr(product, key, value)
|
|
|
|
await db.commit()
|
|
await db.refresh(product)
|
|
return product_to_dict(product)
|
|
|
|
@api_router.delete("/admin/products/{product_id}")
|
|
async def admin_delete_product(product_id: str, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Product).where(Product.id == product_id))
|
|
product = result.scalar_one_or_none()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
product.is_active = False
|
|
await db.commit()
|
|
return {"message": "Product deleted"}
|
|
|
|
# Admin - Services CRUD
|
|
@api_router.get("/admin/services")
|
|
async def admin_get_services(include_inactive: bool = False, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
query = select(Service)
|
|
if not include_inactive:
|
|
query = query.where(Service.is_active == True)
|
|
query = query.order_by(desc(Service.created_at))
|
|
result = await db.execute(query)
|
|
services = result.scalars().all()
|
|
return [service_to_dict(s) for s in services]
|
|
|
|
@api_router.post("/admin/services")
|
|
async def admin_create_service(service_data: ServiceCreate, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
service = Service(**service_data.model_dump())
|
|
db.add(service)
|
|
await db.commit()
|
|
await db.refresh(service)
|
|
return service_to_dict(service)
|
|
|
|
@api_router.put("/admin/services/{service_id}")
|
|
async def admin_update_service(service_id: str, service_data: ServiceUpdate, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Service).where(Service.id == service_id))
|
|
service = result.scalar_one_or_none()
|
|
if not service:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
|
|
update_data = service_data.model_dump(exclude_unset=True)
|
|
for key, value in update_data.items():
|
|
setattr(service, key, value)
|
|
|
|
await db.commit()
|
|
await db.refresh(service)
|
|
return service_to_dict(service)
|
|
|
|
@api_router.delete("/admin/services/{service_id}")
|
|
async def admin_delete_service(service_id: str, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Service).where(Service.id == service_id))
|
|
service = result.scalar_one_or_none()
|
|
if not service:
|
|
raise HTTPException(status_code=404, detail="Service not found")
|
|
|
|
service.is_active = False
|
|
await db.commit()
|
|
return {"message": "Service deleted"}
|
|
|
|
# Admin - Orders Management
|
|
@api_router.get("/admin/orders")
|
|
async def admin_get_orders(
|
|
status: Optional[str] = None,
|
|
limit: int = 50,
|
|
user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
query = select(Order).options(selectinload(Order.items), selectinload(Order.status_history), selectinload(Order.user))
|
|
if status:
|
|
query = query.where(Order.status == OrderStatus(status))
|
|
query = query.order_by(desc(Order.created_at)).limit(limit)
|
|
result = await db.execute(query)
|
|
orders = result.scalars().all()
|
|
return [{
|
|
**order_to_dict(o),
|
|
"user_name": o.user.name if o.user else "Unknown",
|
|
"user_email": o.user.email if o.user else "Unknown"
|
|
} for o in orders]
|
|
|
|
@api_router.put("/admin/orders/{order_id}/status")
|
|
async def admin_update_order_status(order_id: str, status_data: OrderStatusUpdate, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Order).where(Order.id == order_id).options(selectinload(Order.items))
|
|
)
|
|
order = result.scalar_one_or_none()
|
|
if not order:
|
|
raise HTTPException(status_code=404, detail="Order not found")
|
|
|
|
new_status = OrderStatus(status_data.status)
|
|
order.status = new_status
|
|
if status_data.tracking_number:
|
|
order.tracking_number = status_data.tracking_number
|
|
|
|
# Handle refunds - restore stock
|
|
if new_status == OrderStatus.REFUNDED:
|
|
for item in order.items:
|
|
result = await db.execute(select(Product).where(Product.id == item.product_id))
|
|
product = result.scalar_one_or_none()
|
|
if product:
|
|
previous_stock = product.stock
|
|
product.stock += item.quantity
|
|
inv_log = InventoryLog(
|
|
product_id=product.id,
|
|
action="refund",
|
|
quantity_change=item.quantity,
|
|
previous_stock=previous_stock,
|
|
new_stock=product.stock,
|
|
notes=f"Refund for order {order_id}",
|
|
created_by=user.id
|
|
)
|
|
db.add(inv_log)
|
|
|
|
# Add status history
|
|
status_history = OrderStatusHistory(
|
|
order_id=order.id,
|
|
status=new_status,
|
|
notes=status_data.notes,
|
|
created_by=user.id
|
|
)
|
|
db.add(status_history)
|
|
|
|
await db.commit()
|
|
return {"message": "Order status updated"}
|
|
|
|
# Admin - Inventory Management
|
|
@api_router.get("/admin/inventory")
|
|
async def admin_get_inventory(user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(Product)
|
|
.where(Product.is_active == True)
|
|
.order_by(Product.stock)
|
|
)
|
|
products = result.scalars().all()
|
|
return [{
|
|
**product_to_dict(p),
|
|
"is_low_stock": p.stock <= p.low_stock_threshold
|
|
} for p in products]
|
|
|
|
@api_router.post("/admin/inventory/{product_id}/adjust")
|
|
async def admin_adjust_inventory(product_id: str, adjustment: InventoryAdjust, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Product).where(Product.id == product_id))
|
|
product = result.scalar_one_or_none()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="Product not found")
|
|
|
|
previous_stock = product.stock
|
|
product.stock = max(0, product.stock + adjustment.quantity_change)
|
|
|
|
inv_log = InventoryLog(
|
|
product_id=product.id,
|
|
action="adjust" if adjustment.quantity_change >= 0 else "remove",
|
|
quantity_change=adjustment.quantity_change,
|
|
previous_stock=previous_stock,
|
|
new_stock=product.stock,
|
|
notes=adjustment.notes,
|
|
created_by=user.id
|
|
)
|
|
db.add(inv_log)
|
|
await db.commit()
|
|
|
|
return {"message": "Inventory adjusted", "new_stock": product.stock}
|
|
|
|
@api_router.get("/admin/inventory/{product_id}/logs")
|
|
async def admin_get_inventory_logs(product_id: str, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(InventoryLog)
|
|
.where(InventoryLog.product_id == product_id)
|
|
.order_by(desc(InventoryLog.created_at))
|
|
.limit(50)
|
|
)
|
|
logs = result.scalars().all()
|
|
return [{
|
|
"id": log.id,
|
|
"action": log.action,
|
|
"quantity_change": log.quantity_change,
|
|
"previous_stock": log.previous_stock,
|
|
"new_stock": log.new_stock,
|
|
"notes": log.notes,
|
|
"created_at": log.created_at.isoformat() if log.created_at else None
|
|
} for log in logs]
|
|
|
|
# Admin - Bookings Management
|
|
@api_router.get("/admin/bookings")
|
|
async def admin_get_bookings(status: Optional[str] = None, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
query = select(Booking).options(selectinload(Booking.service))
|
|
if status:
|
|
query = query.where(Booking.status == status)
|
|
query = query.order_by(desc(Booking.created_at))
|
|
result = await db.execute(query)
|
|
bookings = result.scalars().all()
|
|
return [booking_to_dict(b) for b in bookings]
|
|
|
|
@api_router.put("/admin/bookings/{booking_id}/status")
|
|
async def admin_update_booking_status(booking_id: str, status: str, user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Booking).where(Booking.id == booking_id))
|
|
booking = result.scalar_one_or_none()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail="Booking not found")
|
|
|
|
booking.status = status
|
|
await db.commit()
|
|
return {"message": "Booking status updated"}
|
|
|
|
# Admin - Users Management
|
|
@api_router.get("/admin/users")
|
|
async def admin_get_users(user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(User).order_by(desc(User.created_at)))
|
|
users = result.scalars().all()
|
|
return [user_to_dict(u) for u in users]
|
|
|
|
# Admin - Reports
|
|
@api_router.get("/admin/reports/sales")
|
|
async def admin_get_sales_report(
|
|
period: str = "daily", # daily, weekly, monthly
|
|
start_date: Optional[str] = None,
|
|
end_date: Optional[str] = None,
|
|
user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
now = datetime.now(timezone.utc)
|
|
|
|
if start_date:
|
|
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
|
else:
|
|
if period == "daily":
|
|
start = now - timedelta(days=30)
|
|
elif period == "weekly":
|
|
start = now - timedelta(weeks=12)
|
|
else:
|
|
start = now - timedelta(days=365)
|
|
|
|
if end_date:
|
|
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
|
else:
|
|
end = now
|
|
|
|
# Get orders in date range
|
|
result = await db.execute(
|
|
select(Order)
|
|
.where(and_(Order.created_at >= start, Order.created_at <= end))
|
|
.options(selectinload(Order.items))
|
|
.order_by(Order.created_at)
|
|
)
|
|
orders = result.scalars().all()
|
|
|
|
# Get bookings in date range
|
|
bookings_result = await db.execute(
|
|
select(Booking)
|
|
.where(and_(Booking.created_at >= start, Booking.created_at <= end))
|
|
)
|
|
bookings = bookings_result.scalars().all()
|
|
|
|
# Aggregate by period
|
|
report_data = {}
|
|
for order in orders:
|
|
if period == "daily":
|
|
key = order.created_at.strftime("%Y-%m-%d")
|
|
elif period == "weekly":
|
|
key = order.created_at.strftime("%Y-W%W")
|
|
else:
|
|
key = order.created_at.strftime("%Y-%m")
|
|
|
|
if key not in report_data:
|
|
report_data[key] = {
|
|
"period": key,
|
|
"orders": 0,
|
|
"revenue": 0,
|
|
"products_sold": 0,
|
|
"order_statuses": {}
|
|
}
|
|
|
|
report_data[key]["orders"] += 1
|
|
report_data[key]["revenue"] += order.total
|
|
report_data[key]["products_sold"] += sum(item.quantity for item in order.items)
|
|
|
|
status = order.status.value if order.status else "unknown"
|
|
report_data[key]["order_statuses"][status] = report_data[key]["order_statuses"].get(status, 0) + 1
|
|
|
|
# Add booking counts
|
|
for booking in bookings:
|
|
if period == "daily":
|
|
key = booking.created_at.strftime("%Y-%m-%d")
|
|
elif period == "weekly":
|
|
key = booking.created_at.strftime("%Y-W%W")
|
|
else:
|
|
key = booking.created_at.strftime("%Y-%m")
|
|
|
|
if key not in report_data:
|
|
report_data[key] = {
|
|
"period": key,
|
|
"orders": 0,
|
|
"revenue": 0,
|
|
"products_sold": 0,
|
|
"order_statuses": {}
|
|
}
|
|
|
|
report_data[key]["services_booked"] = report_data[key].get("services_booked", 0) + 1
|
|
|
|
# Calculate totals
|
|
total_orders = len(orders)
|
|
total_revenue = sum(o.total for o in orders)
|
|
total_products = sum(sum(item.quantity for item in o.items) for o in orders)
|
|
total_bookings = len(bookings)
|
|
|
|
return {
|
|
"period": period,
|
|
"start_date": start.isoformat(),
|
|
"end_date": end.isoformat(),
|
|
"summary": {
|
|
"total_orders": total_orders,
|
|
"total_revenue": total_revenue,
|
|
"total_products_sold": total_products,
|
|
"total_services_booked": total_bookings,
|
|
"average_order_value": total_revenue / total_orders if total_orders > 0 else 0
|
|
},
|
|
"data": list(report_data.values())
|
|
}
|
|
|
|
# Admin - Export Reports
|
|
@api_router.get("/admin/reports/export/csv")
|
|
async def admin_export_csv(
|
|
report_type: str = "sales", # sales, inventory, orders
|
|
period: str = "monthly",
|
|
user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
if report_type == "sales":
|
|
if period == "daily":
|
|
start = now - timedelta(days=30)
|
|
elif period == "weekly":
|
|
start = now - timedelta(weeks=12)
|
|
else:
|
|
start = now - timedelta(days=365)
|
|
|
|
result = await db.execute(
|
|
select(Order)
|
|
.where(Order.created_at >= start)
|
|
.options(selectinload(Order.items), selectinload(Order.user))
|
|
.order_by(Order.created_at)
|
|
)
|
|
orders = result.scalars().all()
|
|
|
|
writer.writerow(["Date", "Order ID", "Customer", "Items", "Subtotal", "Tax", "Shipping", "Total", "Status"])
|
|
for order in orders:
|
|
writer.writerow([
|
|
order.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
order.id,
|
|
order.user.name if order.user else "Guest",
|
|
sum(item.quantity for item in order.items),
|
|
f"${order.subtotal:.2f}",
|
|
f"${order.tax:.2f}",
|
|
f"${order.shipping:.2f}",
|
|
f"${order.total:.2f}",
|
|
order.status.value if order.status else "unknown"
|
|
])
|
|
|
|
elif report_type == "inventory":
|
|
result = await db.execute(select(Product).where(Product.is_active == True))
|
|
products = result.scalars().all()
|
|
|
|
writer.writerow(["Product ID", "Name", "Category", "Brand", "Price", "Stock", "Low Stock Threshold", "Status"])
|
|
for product in products:
|
|
writer.writerow([
|
|
product.id,
|
|
product.name,
|
|
product.category,
|
|
product.brand,
|
|
f"${product.price:.2f}",
|
|
product.stock,
|
|
product.low_stock_threshold,
|
|
"Low Stock" if product.stock <= product.low_stock_threshold else "In Stock"
|
|
])
|
|
|
|
elif report_type == "orders":
|
|
result = await db.execute(
|
|
select(Order)
|
|
.options(selectinload(Order.items), selectinload(Order.user))
|
|
.order_by(desc(Order.created_at))
|
|
.limit(500)
|
|
)
|
|
orders = result.scalars().all()
|
|
|
|
writer.writerow(["Order ID", "Date", "Customer", "Email", "Items", "Total", "Status", "Tracking"])
|
|
for order in orders:
|
|
writer.writerow([
|
|
order.id,
|
|
order.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
order.user.name if order.user else "Guest",
|
|
order.user.email if order.user else "",
|
|
sum(item.quantity for item in order.items),
|
|
f"${order.total:.2f}",
|
|
order.status.value if order.status else "unknown",
|
|
order.tracking_number or ""
|
|
])
|
|
|
|
output.seek(0)
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": f"attachment; filename={report_type}_report_{now.strftime('%Y%m%d')}.csv"}
|
|
)
|
|
|
|
@api_router.get("/admin/reports/export/pdf")
|
|
async def admin_export_pdf(
|
|
report_type: str = "sales",
|
|
period: str = "monthly",
|
|
user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
buffer = io.BytesIO()
|
|
doc = SimpleDocTemplate(buffer, pagesize=A4)
|
|
styles = getSampleStyleSheet()
|
|
elements = []
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
# Title
|
|
title_style = ParagraphStyle(
|
|
'CustomTitle',
|
|
parent=styles['Heading1'],
|
|
fontSize=24,
|
|
spaceAfter=30
|
|
)
|
|
elements.append(Paragraph(f"TechZone {report_type.title()} Report", title_style))
|
|
elements.append(Paragraph(f"Generated: {now.strftime('%Y-%m-%d %H:%M')}", styles['Normal']))
|
|
elements.append(Spacer(1, 20))
|
|
|
|
if report_type == "sales":
|
|
if period == "daily":
|
|
start = now - timedelta(days=30)
|
|
elif period == "weekly":
|
|
start = now - timedelta(weeks=12)
|
|
else:
|
|
start = now - timedelta(days=365)
|
|
|
|
result = await db.execute(
|
|
select(Order)
|
|
.where(Order.created_at >= start)
|
|
.options(selectinload(Order.items))
|
|
)
|
|
orders = result.scalars().all()
|
|
|
|
# Summary
|
|
total_orders = len(orders)
|
|
total_revenue = sum(o.total for o in orders)
|
|
total_products = sum(sum(item.quantity for item in o.items) for o in orders)
|
|
|
|
elements.append(Paragraph("Summary", styles['Heading2']))
|
|
summary_data = [
|
|
["Metric", "Value"],
|
|
["Total Orders", str(total_orders)],
|
|
["Total Revenue", f"${total_revenue:.2f}"],
|
|
["Products Sold", str(total_products)],
|
|
["Average Order Value", f"${total_revenue/total_orders:.2f}" if total_orders > 0 else "$0.00"]
|
|
]
|
|
summary_table = Table(summary_data, colWidths=[3*inch, 2*inch])
|
|
summary_table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, 0), 12),
|
|
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
|
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
|
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
|
]))
|
|
elements.append(summary_table)
|
|
elements.append(Spacer(1, 20))
|
|
|
|
# Orders table
|
|
elements.append(Paragraph("Recent Orders", styles['Heading2']))
|
|
orders_data = [["Date", "Order ID", "Items", "Total", "Status"]]
|
|
for order in orders[:50]:
|
|
orders_data.append([
|
|
order.created_at.strftime("%Y-%m-%d"),
|
|
order.id[:8] + "...",
|
|
str(sum(item.quantity for item in order.items)),
|
|
f"${order.total:.2f}",
|
|
order.status.value if order.status else "unknown"
|
|
])
|
|
|
|
orders_table = Table(orders_data, colWidths=[1.2*inch, 1.2*inch, 0.8*inch, 1*inch, 1*inch])
|
|
orders_table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 9),
|
|
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
|
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
|
|
]))
|
|
elements.append(orders_table)
|
|
|
|
elif report_type == "inventory":
|
|
result = await db.execute(select(Product).where(Product.is_active == True).order_by(Product.stock))
|
|
products = result.scalars().all()
|
|
|
|
elements.append(Paragraph("Inventory Status", styles['Heading2']))
|
|
inv_data = [["Product", "Category", "Price", "Stock", "Status"]]
|
|
for product in products:
|
|
status = "LOW STOCK" if product.stock <= product.low_stock_threshold else "In Stock"
|
|
inv_data.append([
|
|
product.name[:30] + "..." if len(product.name) > 30 else product.name,
|
|
product.category,
|
|
f"${product.price:.2f}",
|
|
str(product.stock),
|
|
status
|
|
])
|
|
|
|
inv_table = Table(inv_data, colWidths=[2*inch, 1*inch, 0.8*inch, 0.6*inch, 0.8*inch])
|
|
inv_table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 8),
|
|
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
|
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
|
|
]))
|
|
elements.append(inv_table)
|
|
|
|
doc.build(elements)
|
|
buffer.seek(0)
|
|
|
|
return StreamingResponse(
|
|
buffer,
|
|
media_type="application/pdf",
|
|
headers={"Content-Disposition": f"attachment; filename={report_type}_report_{now.strftime('%Y%m%d')}.pdf"}
|
|
)
|
|
|
|
# ================== SEED DATA ==================
|
|
|
|
@api_router.post("/seed")
|
|
async def seed_data(db: AsyncSession = Depends(get_db)):
|
|
# Check if data exists
|
|
result = await db.execute(select(func.count(Product.id)))
|
|
if result.scalar() > 0:
|
|
return {"message": "Data already seeded"}
|
|
|
|
# Create admin user
|
|
admin = User(
|
|
email="admin@techzone.com",
|
|
name="Admin",
|
|
password=hash_password("admin123"),
|
|
role=UserRole.ADMIN
|
|
)
|
|
db.add(admin)
|
|
|
|
# Create products
|
|
products = [
|
|
Product(name="MacBook Pro 16\"", description="Powerful laptop with M3 Pro chip, 18GB RAM, 512GB SSD.", price=2499.99, category="laptops", image_url="https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=800", stock=15, brand="Apple", specs={"processor": "M3 Pro", "ram": "18GB", "storage": "512GB SSD"}),
|
|
Product(name="Dell XPS 15", description="Ultra-thin laptop with Intel Core i7, 16GB RAM, stunning OLED display.", price=1799.99, category="laptops", image_url="https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=800", stock=20, brand="Dell", specs={"processor": "Intel i7", "ram": "16GB", "storage": "512GB SSD"}),
|
|
Product(name="iPhone 15 Pro Max", description="Latest iPhone with titanium design, A17 Pro chip, 48MP camera.", price=1199.99, category="phones", image_url="https://images.unsplash.com/photo-1695048133142-1a20484d2569?w=800", stock=30, brand="Apple", specs={"display": "6.7\" OLED", "camera": "48MP", "storage": "256GB"}),
|
|
Product(name="Samsung Galaxy S24 Ultra", description="Premium Android phone with S Pen, 200MP camera, AI features.", price=1299.99, category="phones", image_url="https://images.unsplash.com/photo-1610945265064-0e34e5519bbf?w=800", stock=25, brand="Samsung", specs={"display": "6.8\" AMOLED", "camera": "200MP", "storage": "512GB"}),
|
|
Product(name="Sony WH-1000XM5", description="Industry-leading noise cancellation, 30-hour battery.", price=349.99, category="accessories", image_url="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800", stock=40, brand="Sony", specs={"type": "Over-ear", "battery": "30 hours"}),
|
|
Product(name="iPad Pro 12.9\"", description="Powerful tablet with M2 chip, Liquid Retina XDR display.", price=1099.99, category="tablets", image_url="https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=800", stock=18, brand="Apple", specs={"processor": "M2", "display": "12.9\" XDR"}),
|
|
Product(name="Apple Watch Ultra 2", description="Rugged smartwatch with titanium case, GPS, 36-hour battery.", price=799.99, category="wearables", image_url="https://images.unsplash.com/photo-1434493789847-2f02dc6ca35d?w=800", stock=22, brand="Apple", specs={"display": "49mm", "battery": "36 hours"}),
|
|
Product(name="Logitech MX Master 3S", description="Premium wireless mouse with 8K DPI sensor, silent clicks.", price=99.99, category="accessories", image_url="https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=800", stock=50, brand="Logitech", specs={"sensor": "8K DPI", "battery": "70 days"}),
|
|
]
|
|
for p in products:
|
|
db.add(p)
|
|
|
|
# Create services
|
|
services = [
|
|
Service(name="Screen Repair", description="Professional screen replacement for phones, tablets, and laptops.", price=149.99, duration="1-2 hours", image_url="https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=800", category="repair"),
|
|
Service(name="Battery Replacement", description="Restore your device's battery life with genuine replacement.", price=79.99, duration="30-60 mins", image_url="https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5?w=800", category="repair"),
|
|
Service(name="Data Recovery", description="Professional data recovery from damaged drives.", price=199.99, duration="2-5 days", image_url="https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800", category="data"),
|
|
Service(name="Virus Removal", description="Complete malware and virus removal with system optimization.", price=89.99, duration="1-3 hours", image_url="https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800", category="software"),
|
|
Service(name="Hardware Upgrade", description="Upgrade your RAM, SSD, or other components.", price=49.99, duration="1-2 hours", image_url="https://images.unsplash.com/photo-1591799265444-d66432b91588?w=800", category="upgrade"),
|
|
Service(name="Device Setup", description="Complete setup service for new devices including data transfer.", price=59.99, duration="1-2 hours", image_url="https://images.unsplash.com/photo-1531297484001-80022131f5a1?w=800", category="setup"),
|
|
]
|
|
for s in services:
|
|
db.add(s)
|
|
|
|
await db.commit()
|
|
return {"message": "Data seeded successfully"}
|
|
|
|
# ================== ROOT ==================
|
|
|
|
@api_router.get("/")
|
|
async def root():
|
|
return {"message": "TechZone API is running", "version": "2.0.0"}
|
|
|
|
# Include the router
|
|
app.include_router(api_router)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_credentials=True,
|
|
allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','),
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
await init_db()
|
|
logger.info("Database initialized")
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
pass
|