feat: add admin orders management
- Add admin API endpoints for order management - Add orders page with vendor selector and filtering - Add order schemas for admin operations - Support order status tracking and management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -366,5 +366,251 @@ class OrderService:
|
||||
raise ValidationException(f"Failed to update order: {str(e)}")
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Admin Methods (cross-vendor)
|
||||
# =========================================================================
|
||||
|
||||
def get_all_orders_admin(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
vendor_id: int | None = None,
|
||||
status: str | None = None,
|
||||
channel: str | None = None,
|
||||
search: str | None = None,
|
||||
) -> tuple[list[dict], int]:
|
||||
"""
|
||||
Get orders across all vendors for admin.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
skip: Pagination offset
|
||||
limit: Pagination limit
|
||||
vendor_id: Filter by vendor
|
||||
status: Filter by status
|
||||
channel: Filter by channel
|
||||
search: Search by order number or customer
|
||||
|
||||
Returns:
|
||||
Tuple of (orders with vendor info, total_count)
|
||||
"""
|
||||
from models.database.customer import Customer
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
query = db.query(Order).join(Vendor).join(Customer)
|
||||
|
||||
if vendor_id:
|
||||
query = query.filter(Order.vendor_id == vendor_id)
|
||||
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
|
||||
if channel:
|
||||
query = query.filter(Order.channel == channel)
|
||||
|
||||
if search:
|
||||
search_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(Order.order_number.ilike(search_term))
|
||||
| (Customer.email.ilike(search_term))
|
||||
| (Customer.first_name.ilike(search_term))
|
||||
| (Customer.last_name.ilike(search_term))
|
||||
)
|
||||
|
||||
# Order by most recent first
|
||||
query = query.order_by(Order.created_at.desc())
|
||||
|
||||
total = query.count()
|
||||
orders = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Build response with vendor/customer info
|
||||
result = []
|
||||
for order in orders:
|
||||
item_count = len(order.items) if order.items else 0
|
||||
customer_name = None
|
||||
customer_email = None
|
||||
|
||||
if order.customer:
|
||||
customer_name = (
|
||||
f"{order.customer.first_name} {order.customer.last_name}".strip()
|
||||
)
|
||||
customer_email = order.customer.email
|
||||
|
||||
result.append(
|
||||
{
|
||||
"id": order.id,
|
||||
"vendor_id": order.vendor_id,
|
||||
"vendor_name": order.vendor.name if order.vendor else None,
|
||||
"vendor_code": order.vendor.vendor_code if order.vendor else None,
|
||||
"customer_id": order.customer_id,
|
||||
"customer_name": customer_name,
|
||||
"customer_email": customer_email,
|
||||
"order_number": order.order_number,
|
||||
"channel": order.channel,
|
||||
"status": order.status,
|
||||
"subtotal": order.subtotal,
|
||||
"tax_amount": order.tax_amount,
|
||||
"shipping_amount": order.shipping_amount,
|
||||
"discount_amount": order.discount_amount,
|
||||
"total_amount": order.total_amount,
|
||||
"currency": order.currency,
|
||||
"shipping_method": order.shipping_method,
|
||||
"tracking_number": order.tracking_number,
|
||||
"item_count": item_count,
|
||||
"created_at": order.created_at,
|
||||
"updated_at": order.updated_at,
|
||||
"paid_at": order.paid_at,
|
||||
"shipped_at": order.shipped_at,
|
||||
"delivered_at": order.delivered_at,
|
||||
"cancelled_at": order.cancelled_at,
|
||||
}
|
||||
)
|
||||
|
||||
return result, total
|
||||
|
||||
def get_order_stats_admin(self, db: Session) -> dict:
|
||||
"""Get platform-wide order statistics."""
|
||||
from sqlalchemy import func
|
||||
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
# Get status counts
|
||||
status_counts = (
|
||||
db.query(Order.status, func.count(Order.id)).group_by(Order.status).all()
|
||||
)
|
||||
|
||||
stats = {
|
||||
"total_orders": 0,
|
||||
"pending_orders": 0,
|
||||
"processing_orders": 0,
|
||||
"shipped_orders": 0,
|
||||
"delivered_orders": 0,
|
||||
"cancelled_orders": 0,
|
||||
"refunded_orders": 0,
|
||||
"total_revenue": 0.0,
|
||||
"vendors_with_orders": 0,
|
||||
}
|
||||
|
||||
for status, count in status_counts:
|
||||
stats["total_orders"] += count
|
||||
key = f"{status}_orders"
|
||||
if key in stats:
|
||||
stats[key] = count
|
||||
|
||||
# Get total revenue (from delivered orders only)
|
||||
revenue = (
|
||||
db.query(func.sum(Order.total_amount))
|
||||
.filter(Order.status == "delivered")
|
||||
.scalar()
|
||||
)
|
||||
stats["total_revenue"] = float(revenue) if revenue else 0.0
|
||||
|
||||
# Count vendors with orders
|
||||
vendors_count = (
|
||||
db.query(func.count(func.distinct(Order.vendor_id))).scalar() or 0
|
||||
)
|
||||
stats["vendors_with_orders"] = vendors_count
|
||||
|
||||
return stats
|
||||
|
||||
def get_vendors_with_orders_admin(self, db: Session) -> list[dict]:
|
||||
"""Get list of vendors that have orders."""
|
||||
from sqlalchemy import func
|
||||
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
results = (
|
||||
db.query(
|
||||
Vendor.id,
|
||||
Vendor.name,
|
||||
Vendor.vendor_code,
|
||||
func.count(Order.id).label("order_count"),
|
||||
)
|
||||
.join(Order)
|
||||
.group_by(Vendor.id, Vendor.name, Vendor.vendor_code)
|
||||
.order_by(Vendor.name)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.name,
|
||||
"vendor_code": r.vendor_code,
|
||||
"order_count": r.order_count,
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
|
||||
def get_order_by_id_admin(self, db: Session, order_id: int) -> Order:
|
||||
"""Get order by ID without vendor scope (admin only)."""
|
||||
order = db.query(Order).filter(Order.id == order_id).first()
|
||||
|
||||
if not order:
|
||||
raise OrderNotFoundException(str(order_id))
|
||||
|
||||
return order
|
||||
|
||||
def update_order_status_admin(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
status: str,
|
||||
tracking_number: str | None = None,
|
||||
reason: str | None = None,
|
||||
) -> Order:
|
||||
"""
|
||||
Update order status as admin.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
order_id: Order ID
|
||||
status: New status
|
||||
tracking_number: Optional tracking number
|
||||
reason: Optional reason for change
|
||||
|
||||
Returns:
|
||||
Updated Order object
|
||||
"""
|
||||
order = self.get_order_by_id_admin(db, order_id)
|
||||
|
||||
# Update status with timestamps
|
||||
old_status = order.status
|
||||
order.status = status
|
||||
|
||||
now = datetime.now(UTC)
|
||||
if status == "shipped" and not order.shipped_at:
|
||||
order.shipped_at = now
|
||||
elif status == "delivered" and not order.delivered_at:
|
||||
order.delivered_at = now
|
||||
elif status == "cancelled" and not order.cancelled_at:
|
||||
order.cancelled_at = now
|
||||
|
||||
# Update tracking number
|
||||
if tracking_number:
|
||||
order.tracking_number = tracking_number
|
||||
|
||||
# Add reason to internal notes if provided
|
||||
if reason:
|
||||
note = f"[{now.isoformat()}] Status changed from {old_status} to {status}: {reason}"
|
||||
if order.internal_notes:
|
||||
order.internal_notes = f"{order.internal_notes}\n{note}"
|
||||
else:
|
||||
order.internal_notes = note
|
||||
|
||||
order.updated_at = now
|
||||
db.flush()
|
||||
db.refresh(order)
|
||||
|
||||
logger.info(
|
||||
f"Admin updated order {order.order_number}: "
|
||||
f"{old_status} -> {status}"
|
||||
f"{f' (reason: {reason})' if reason else ''}"
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
# Create service instance
|
||||
order_service = OrderService()
|
||||
|
||||
Reference in New Issue
Block a user