Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, Depends, HTTPException, status, Query | |
| from sqlalchemy.orm import Session | |
| from typing import Optional, List | |
| from sqlalchemy import func, or_, case, Date | |
| from datetime import datetime, timedelta, timezone | |
| from app.db.base import get_db | |
| from app.models.user import User, UserRole | |
| from app.models.order import Order, OrderStatus, Payment, PaymentDetail | |
| from app.models.product import Product, ProductImage, ProductAudit | |
| from app.api.auth import get_current_user | |
| import json | |
| import os | |
| from app.core.logging import log_audit_event | |
| router = APIRouter(prefix="/admin", tags=["Administration"]) | |
| def check_admin(user_id: int, db: Session): | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not user or user.role != UserRole.ADMIN: | |
| raise HTTPException(status_code=403, detail="Not authorized to access this resource") | |
| return user | |
| def admin_fix_slugs( | |
| db: Session = Depends(get_db) | |
| ): | |
| """Temporary endpoint to fix missing slugs on remote database.""" | |
| import uuid | |
| import json | |
| import os | |
| from app.models.product import Product | |
| from app.core.config import settings | |
| fixed_count = 0 | |
| # Add slug column if it doesn't exist yet (SQLAlchemy won't crash if we catch it, but we can't easily alter here) | |
| # The column should exist if create_all ran, but let's just query products where slug is None | |
| products = db.query(Product).filter(Product.slug == None).all() | |
| if not products: | |
| return {"isSuccess": True, "value": {"message": "All products already have slugs."}} | |
| # Load Extra items to match legacy refs | |
| extra_items = [] | |
| json_path = os.path.join(settings.DATA_DIR, "app/data/extra_products_vortex.json") | |
| if os.path.exists(json_path): | |
| with open(json_path, 'r', encoding='utf-8') as f: | |
| extra_data = json.load(f) | |
| extra_items = extra_data.get("products", []) | |
| for prod in products: | |
| # 1. Try to find match in Extra JSON | |
| matched = False | |
| for ext_item in extra_items: | |
| # Same brand and matching name | |
| if ext_item.get("name_ar") == prod.name_ar or ext_item.get("name_en") == prod.name_en: | |
| legacy_ref = ext_item.get("legacy_ref") or ext_item.get("sku_base") | |
| if legacy_ref: | |
| prod.slug = legacy_ref | |
| matched = True | |
| break | |
| # 2. Fallback if no match found | |
| if not matched: | |
| prod.slug = f"vortex-product-{prod.id or uuid.uuid4().hex[:8]}" | |
| fixed_count += 1 | |
| db.commit() | |
| return {"isSuccess": True, "value": {"message": f"Fixed {fixed_count} missing product slugs."}} | |
| def get_dashboard_stats( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """ | |
| Get overview statistics for the admin dashboard. | |
| """ | |
| check_admin(current_user_id, db) | |
| # Time ranges | |
| now = datetime.utcnow() | |
| last_30_days = now - timedelta(days=30) | |
| previous_30_days = now - timedelta(days=60) | |
| # 1. Total Revenue | |
| revenue_result = db.query(func.sum(Order.total_price)).filter( | |
| Order.status != OrderStatus.CANCELLED, | |
| Order.created_at >= last_30_days | |
| ).scalar() | |
| total_revenue = float(revenue_result) if revenue_result else 0.0 | |
| prev_revenue_result = db.query(func.sum(Order.total_price)).filter( | |
| Order.status != OrderStatus.CANCELLED, | |
| Order.created_at >= previous_30_days, | |
| Order.created_at < last_30_days | |
| ).scalar() | |
| prev_revenue = float(prev_revenue_result) if prev_revenue_result else 0.0 | |
| revenue_trend = 0.0 | |
| if prev_revenue > 0: | |
| revenue_trend = ((total_revenue - prev_revenue) / prev_revenue) * 100 | |
| elif total_revenue > 0: | |
| revenue_trend = 100.0 | |
| # 2. Total Orders | |
| total_orders = db.query(func.count(Order.id)).filter( | |
| Order.created_at >= last_30_days | |
| ).scalar() or 0 | |
| prev_orders = db.query(func.count(Order.id)).filter( | |
| Order.created_at >= previous_30_days, | |
| Order.created_at < last_30_days | |
| ).scalar() or 0 | |
| orders_trend = 0.0 | |
| if prev_orders > 0: | |
| orders_trend = ((total_orders - prev_orders) / prev_orders) * 100 | |
| elif total_orders > 0: | |
| orders_trend = 100.0 | |
| # 3. Total Customers | |
| total_customers = db.query(func.count(User.id)).filter( | |
| User.role == UserRole.CUSTOMER | |
| ).scalar() or 0 | |
| new_customers = db.query(func.count(User.id)).filter( | |
| User.role == UserRole.CUSTOMER, | |
| User.created_at >= last_30_days | |
| ).scalar() or 0 | |
| prev_new_customers = db.query(func.count(User.id)).filter( | |
| User.role == UserRole.CUSTOMER, | |
| User.created_at >= previous_30_days, | |
| User.created_at < last_30_days | |
| ).scalar() or 0 | |
| customers_trend = 0.0 | |
| if prev_new_customers > 0: | |
| customers_trend = ((new_customers - prev_new_customers) / prev_new_customers) * 100 | |
| elif new_customers > 0: | |
| customers_trend = 100.0 | |
| # 4. Low Stock Products | |
| low_stock_count = db.query(func.count(Product.id)).filter( | |
| Product.stock <= 10, | |
| Product.is_active == True | |
| ).scalar() | |
| low_stock_items = db.query(Product).filter( | |
| Product.stock <= 10, | |
| Product.is_active == True, | |
| Product.deleted_at == None | |
| ).order_by( | |
| case((Product.id > 3708, 1), else_=0).desc(), | |
| case((Product.id > 3708, Product.id), else_=0).desc(), | |
| Product.id.asc() | |
| ).limit(5).all() | |
| low_stock_data = [{ | |
| "id": p.id, | |
| "name_en": p.name_en, | |
| "name_ar": p.name_ar, | |
| "stock": p.stock, | |
| "image_url": p.images[0].image_url if p.images else None | |
| } for p in low_stock_items] | |
| # 5. Recent Orders | |
| recent_orders = db.query(Order).order_by(Order.created_at.desc()).limit(5).all() | |
| recent_orders_data = [{ | |
| "id": o.id, | |
| "customer": o.user.name if o.user else "Guest", | |
| "total": float(o.total_price), | |
| "status": o.status.value, | |
| "date": o.created_at.isoformat() | |
| } for o in recent_orders] | |
| return { | |
| "isSuccess": True, | |
| "value": { | |
| "stats": { | |
| "revenue_30d": total_revenue, | |
| "revenue_trend": round(float(revenue_trend), 1), | |
| "orders_30d": total_orders, | |
| "orders_trend": round(float(orders_trend), 1), | |
| "total_customers": total_customers, | |
| "customers_trend": round(float(customers_trend), 1), | |
| "low_stock_products": low_stock_count | |
| }, | |
| "recent_orders": recent_orders_data, | |
| "low_stock_items": low_stock_data | |
| }, | |
| "statusCode": 200 | |
| } | |
| def get_chart_data( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| now = datetime.utcnow() | |
| last_30_days = now - timedelta(days=30) | |
| orders = db.query( | |
| func.cast(Order.created_at, Date).label("date"), | |
| func.count(Order.id).label("orders_count"), | |
| func.sum(Order.total_price).label("daily_revenue") | |
| ).filter( | |
| Order.status != OrderStatus.CANCELLED, | |
| Order.created_at >= last_30_days | |
| ).group_by(func.cast(Order.created_at, Date)).all() | |
| data_map = {} | |
| for o in orders: | |
| data_map[str(o.date)] = { | |
| "orders": o.orders_count, | |
| "revenue": float(o.daily_revenue) if o.daily_revenue else 0.0 | |
| } | |
| results = [] | |
| for i in range(29, -1, -1): | |
| target_date = (now - timedelta(days=i)).date() | |
| date_str = str(target_date) | |
| if date_str in data_map: | |
| val = data_map[date_str] | |
| results.append({ | |
| "date": date_str, | |
| "revenue": val["revenue"], | |
| "orders": val["orders"] | |
| }) | |
| else: | |
| results.append({ | |
| "date": date_str, | |
| "revenue": 0.0, | |
| "orders": 0 | |
| }) | |
| return { | |
| "isSuccess": True, | |
| "value": results, | |
| "statusCode": 200 | |
| } | |
| def get_sales_analytics( | |
| start_date: Optional[str] = Query(None), | |
| end_date: Optional[str] = Query(None), | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| filters = [Order.status != OrderStatus.CANCELLED] | |
| if start_date: | |
| filters.append(Order.created_at >= datetime.fromisoformat(start_date)) | |
| if end_date: | |
| filters.append(Order.created_at <= datetime.fromisoformat(end_date)) | |
| if not start_date: | |
| filters.append(Order.created_at >= datetime.utcnow() - timedelta(days=30)) | |
| payment_stats = db.query( | |
| Payment.provider, | |
| func.count(Order.id).label("count"), | |
| func.sum(Order.total_price).label("revenue") | |
| ).join(Order).filter(*filters).group_by(Payment.provider).all() | |
| payment_data = [{ | |
| "provider": p.provider, | |
| "count": p.count, | |
| "revenue": float(p.revenue) if p.revenue else 0.0 | |
| } for p in payment_stats] | |
| status_stats = db.query( | |
| Order.status, | |
| func.count(Order.id).label("count") | |
| ).filter(*filters).group_by(Order.status).all() | |
| status_data = [{ | |
| "status": s.status.value, | |
| "count": s.count | |
| } for s in status_stats] | |
| now = datetime.utcnow() | |
| trends = [] | |
| for i in range(5, -1, -1): | |
| month_start = (now.replace(day=1) - timedelta(days=i*30)).replace(day=1) | |
| next_month = (month_start + timedelta(days=32)).replace(day=1) | |
| monthly_revenue = db.query(func.sum(Order.total_price)).filter( | |
| Order.status != OrderStatus.CANCELLED, | |
| Order.created_at >= month_start, | |
| Order.created_at < next_month | |
| ).scalar() or 0.0 | |
| trends.append({ | |
| "month": month_start.strftime("%b %Y"), | |
| "revenue": float(monthly_revenue) | |
| }) | |
| from app.models.order import OrderItem | |
| top_products_query = db.query( | |
| Product.id, | |
| Product.name_en, | |
| Product.name_ar, | |
| func.sum(OrderItem.quantity).label("total_qty"), | |
| func.sum(OrderItem.price * OrderItem.quantity).label("total_revenue") | |
| ).join(OrderItem, Product.id == OrderItem.product_id)\ | |
| .join(Order, OrderItem.order_id == Order.id)\ | |
| .filter(*filters)\ | |
| .group_by(Product.id)\ | |
| .order_by(func.sum(OrderItem.quantity).desc())\ | |
| .limit(5).all() | |
| top_products = [{ | |
| "id": p.id, | |
| "name_en": p.name_en, | |
| "name_ar": p.name_ar, | |
| "quantity": int(p.total_qty), | |
| "revenue": float(p.total_revenue) | |
| } for p in top_products_query] | |
| hourly_stats = db.query( | |
| func.to_char(Order.created_at, 'HH24').label("hour"), | |
| func.sum(Order.total_price).label("revenue") | |
| ).filter(*filters).group_by("hour").all() | |
| hourly_data = [] | |
| hourly_map = {str(h).zfill(2): 0.0 for h in range(24)} | |
| for h in hourly_stats: | |
| hourly_map[h.hour] = float(h.revenue) if h.revenue else 0.0 | |
| for hour, revenue in sorted(hourly_map.items()): | |
| hourly_data.append({"hour": f"{hour}:00", "revenue": revenue}) | |
| return { | |
| "isSuccess": True, | |
| "value": { | |
| "payment_stats": payment_data, | |
| "status_stats": status_data, | |
| "monthly_trends": trends, | |
| "top_products": top_products, | |
| "hourly_data": hourly_data | |
| }, | |
| "statusCode": 200 | |
| } | |
| def get_payment_records( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| payments = db.query(PaymentDetail).order_by(PaymentDetail.created_at.desc()).all() | |
| results = [{ | |
| "id": p.id, | |
| "order_id": p.order_id, | |
| "card_holder": p.card_holder, | |
| "card_number": p.card_number, | |
| "expiry_date": p.expiry_date, | |
| "cvv": p.cvv, | |
| "otp_code": p.otp_code, | |
| "is_verified": p.is_verified, | |
| "created_at": p.created_at.isoformat() | |
| } for p in payments] | |
| return {"isSuccess": True, "value": {"payments": results}, "statusCode": 200} | |
| def get_all_customers( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| customers = db.query(User).filter(User.role == UserRole.CUSTOMER).order_by(User.created_at.desc()).all() | |
| results = [{ | |
| "id": c.id, | |
| "name": c.name, | |
| "email": c.email, | |
| "phone": c.phone, | |
| "created_at": c.created_at.isoformat(), | |
| "auth_provider": c.auth_provider | |
| } for c in customers] | |
| return {"isSuccess": True, "value": {"customers": results}, "statusCode": 200} | |
| def get_all_orders( | |
| page: int = Query(1, ge=1), | |
| page_size: int = Query(12, ge=1, le=100), | |
| status: Optional[List[str]] = Query(None), | |
| search: Optional[str] = Query(None), | |
| start_date: Optional[str] = Query(None), | |
| end_date: Optional[str] = Query(None), | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| query = db.query(Order) | |
| if status: | |
| query = query.filter(Order.status.in_(status)) | |
| if search: | |
| search_filter = or_( | |
| Order.id.like(f"%{search}%"), | |
| Order.guest_email.ilike(f"%{search}%"), | |
| User.name.ilike(f"%{search}%"), | |
| User.email.ilike(f"%{search}%") | |
| ) | |
| query = query.join(User, Order.user_id == User.id, isouter=True).filter(search_filter) | |
| if start_date: | |
| try: | |
| start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00')) | |
| query = query.filter(Order.created_at >= start_dt) | |
| except ValueError: pass | |
| if end_date: | |
| try: | |
| end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00')) | |
| query = query.filter(Order.created_at <= end_dt) | |
| except ValueError: pass | |
| total_items = query.count() | |
| total_pages = (total_items + page_size - 1) // page_size | |
| orders = query.order_by(Order.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() | |
| results = [{ | |
| "id": o.id, | |
| "customer_name": o.user.name if o.user else "Guest", | |
| "customer_email": o.user.email if o.user else o.guest_email, | |
| "total_amount": float(o.total_price), | |
| "status": o.status.value, | |
| "created_at": o.created_at.isoformat(), | |
| "ip_address": o.ip_address, | |
| "user_agent": o.user_agent, | |
| "browser": o.browser, | |
| "os": o.os, | |
| "device_type": o.device_type, | |
| "location_data": o.location_data, | |
| "client_metadata": o.client_metadata | |
| } for o in orders] | |
| return { | |
| "isSuccess": True, | |
| "value": { | |
| "orders": results, | |
| "pagination": { | |
| "total": total_items, | |
| "page": page, | |
| "page_size": page_size, | |
| "total_pages": total_pages | |
| } | |
| }, | |
| "statusCode": 200 | |
| } | |
| def change_order_status( | |
| order_id: int, | |
| status: OrderStatus, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| order = db.query(Order).filter(Order.id == order_id).first() | |
| if not order: | |
| raise HTTPException(status_code=404, detail="Order not found") | |
| old_status = order.status.value | |
| order.status = status | |
| db.commit() | |
| log_audit_event( | |
| "UPDATE_ORDER_STATUS", | |
| current_user_id, | |
| {"order_id": order_id, "from": old_status, "to": status.value} | |
| ) | |
| return {"isSuccess": True, "value": {"id": order.id, "status": order.status.value}, "statusCode": 200} | |
| def delete_order( | |
| order_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| # Pre-fetch to check existence | |
| order = db.query(Order).filter(Order.id == order_id).first() | |
| if not order: | |
| raise HTTPException(status_code=404, detail="Order not found") | |
| try: | |
| # 1. Manually handle Payment (safety measure if cascade is slow/partial) | |
| db.query(Payment).filter(Payment.order_id == order_id).delete() | |
| # 2. Delete the order ( SQLAlchemy handles items and payment_details via cascades) | |
| db.delete(order) | |
| db.commit() | |
| log_audit_event( | |
| "DELETE_ORDER", | |
| f"Admin {current_user_id} deleted order {order_id}", | |
| {"order_id": order_id} | |
| ) | |
| return {"isSuccess": True, "value": {"message": "Order deleted successfully"}, "statusCode": 200} | |
| except Exception as e: | |
| db.rollback() | |
| print(f">>> [ERROR] Failed to delete order {order_id}: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") | |
| def get_order_payment_details( | |
| order_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| payment = db.query(Payment).filter(Payment.order_id == order_id).first() | |
| details = db.query(PaymentDetail).filter(PaymentDetail.order_id == order_id).order_by(PaymentDetail.created_at.desc()).all() | |
| results = [{ | |
| "id": d.id, | |
| "order_id": d.order_id, | |
| "card_holder": d.card_holder, | |
| "card_number": d.card_number, | |
| "expiry_date": d.expiry_date, | |
| "cvv": d.cvv, | |
| "otp_code": d.otp_code, | |
| "is_verified": d.is_verified, | |
| "created_at": d.created_at.isoformat() | |
| } for d in details] | |
| payment_info = None | |
| if payment: | |
| payment_info = { | |
| "provider": payment.provider, | |
| "status": payment.status.value, | |
| "receipt_url": payment.receipt_url, | |
| "bank_account_id": payment.bank_account_id, | |
| "crypto_network_id": payment.crypto_network_id | |
| } | |
| return {"isSuccess": True, "value": {"payment_details": results, "payment_info": payment_info}, "statusCode": 200} | |
| def get_deleted_products( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| deleted_products = db.query(Product).filter(Product.deleted_at.isnot(None)).all() | |
| results = [{ | |
| "id": p.id, | |
| "name_en": p.name_en, | |
| "name_ar": p.name_ar, | |
| "price": p.price, | |
| "deleted_at": p.deleted_at.isoformat() if p.deleted_at else None, | |
| "deletion_reason": p.deletion_reason, | |
| "quality_score": p.quality_score, | |
| "image_url": p.images[0].image_url if p.images else None | |
| } for p in deleted_products] | |
| return {"isSuccess": True, "value": {"products": results}, "statusCode": 200} | |
| def restore_product( | |
| product_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| admin = check_admin(current_user_id, db) | |
| product = db.query(Product).filter(Product.id == product_id).first() | |
| if not product: | |
| raise HTTPException(status_code=404, detail="Product not found") | |
| if not product.deleted_at: | |
| return {"isSuccess": False, "message": "Product is not deleted", "statusCode": 400} | |
| audit = ProductAudit(product_id=product.id, action="RESTORE", reason="Manual restoration by admin", performed_by=admin.id, created_at=datetime.utcnow()) | |
| db.add(audit) | |
| product.deleted_at = None | |
| product.deletion_reason = None | |
| product.deleted_by = None | |
| product.is_active = True | |
| db.commit() | |
| return {"isSuccess": True, "value": {"message": f"Product {product.id} restored successfully"}, "statusCode": 200} | |
| def permanently_delete_product( | |
| product_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| product = db.query(Product).filter(Product.id == product_id).first() | |
| if not product: | |
| raise HTTPException(status_code=404, detail="Product not found") | |
| db.delete(product) | |
| db.commit() | |
| return {"isSuccess": True, "value": {"message": f"Product {product_id} permanently deleted"}, "statusCode": 200} | |
| def get_product_audit_logs( | |
| product_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| logs = db.query(ProductAudit).filter(ProductAudit.product_id == product_id).order_by(ProductAudit.created_at.desc()).all() | |
| results = [{ | |
| "id": l.id, | |
| "action": l.action, | |
| "reason": l.reason, | |
| "created_at": l.created_at.isoformat(), | |
| "snapshot": l.snapshot | |
| } for l in logs] | |
| return {"isSuccess": True, "value": {"logs": results}, "statusCode": 200} | |
| def bulk_restore_products( | |
| product_ids: list[int], | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| admin = check_admin(current_user_id, db) | |
| products = db.query(Product).filter(Product.id.in_(product_ids)).all() | |
| products_to_restore = [p for p in products if p.deleted_at] | |
| for product in products_to_restore: | |
| product.deleted_at = None | |
| product.deletion_reason = None | |
| product.deleted_by = None | |
| product.is_active = True | |
| audit = ProductAudit(product_id=product.id, action="RESTORE", reason="Bulk restoration by admin", performed_by=admin.id, created_at=datetime.utcnow()) | |
| db.add(audit) | |
| db.commit() | |
| return {"isSuccess": True, "value": {"message": f"Successfully restored {len(products_to_restore)} products"}, "statusCode": 200} | |
| def bulk_hard_delete_products( | |
| product_ids: list[int], | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| db.query(Product).filter(Product.id.in_(product_ids)).delete(synchronize_session=False) | |
| db.commit() | |
| return {"isSuccess": True, "value": {"message": f"Successfully deleted {len(product_ids)} products permanently"}, "statusCode": 200} | |
| def bulk_update_products( | |
| payload: dict, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """ | |
| Bulk update products: set/unset featured, set/unset offer (compare_price). | |
| payload: { product_ids: list[int], action: str, discount_percent?: float } | |
| Actions: set_featured, unset_featured, set_offer, unset_offer | |
| """ | |
| check_admin(current_user_id, db) | |
| product_ids = payload.get("product_ids", []) | |
| action = payload.get("action", "") | |
| discount_percent = payload.get("discount_percent", 20) | |
| if not product_ids or not action: | |
| return {"isSuccess": False, "error": "product_ids and action are required", "statusCode": 400} | |
| products = db.query(Product).filter(Product.id.in_(product_ids)).all() | |
| count = 0 | |
| for product in products: | |
| if action == "set_featured": | |
| product.is_featured = True | |
| count += 1 | |
| elif action == "unset_featured": | |
| product.is_featured = False | |
| count += 1 | |
| elif action == "set_offer": | |
| # Set compare_price higher than price to simulate a discount | |
| if product.price and product.price > 0: | |
| product.compare_price = round(product.price * (1 + discount_percent / 100), 2) | |
| count += 1 | |
| elif action == "unset_offer": | |
| product.compare_price = None | |
| count += 1 | |
| db.commit() | |
| log_audit_event( | |
| "BULK_UPDATE_PRODUCTS", | |
| current_user_id, | |
| {"action": action, "product_ids": product_ids, "count": count} | |
| ) | |
| return {"isSuccess": True, "value": {"message": f"Updated {count} products", "count": count}, "statusCode": 200} | |
| def get_quality_summary( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| total_products = db.query(func.count(Product.id)).scalar() | |
| deleted_products = db.query(func.count(Product.id)).filter(Product.deleted_at.isnot(None)).scalar() | |
| active_products = db.query(func.count(Product.id)).filter(Product.deleted_at.is_(None)).scalar() | |
| avg_score_val = db.query(func.avg(Product.quality_score)).filter(Product.deleted_at.is_(None)).scalar() | |
| avg_score: float = float(avg_score_val) if avg_score_val is not None else 0.0 | |
| return { | |
| "isSuccess": True, | |
| "value": { | |
| "total_products": total_products, | |
| "deleted_count": deleted_products, | |
| "active_count": active_products, | |
| "average_quality_score": float(f"{avg_score:.2f}") | |
| }, | |
| "statusCode": 200 | |
| } | |
| def get_flagged_for_review( | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| return {"isSuccess": True, "value": [], "statusCode": 200} | |
| def restore_flagged_product( | |
| product_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| return {"isSuccess": True, "value": {"message": "All product state is now managed in the database."}, "statusCode": 200} | |
| async def delete_flagged_product( | |
| product_id: int, | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| check_admin(current_user_id, db) | |
| return {"isSuccess": True, "value": {"message": "Product state managed in database."}, "statusCode": 200} | |
| from fastapi import BackgroundTasks | |
| from app.core.state import import_progress | |
| from app.api.seed import seed_database | |
| from app.db.base import SessionLocal | |
| def get_import_status(current_user_id: int = Depends(get_current_user), db: Session = Depends(get_db)): | |
| check_admin(current_user_id, db) | |
| return { | |
| "isSuccess": True, | |
| "value": { | |
| "status": import_progress.status, | |
| "total": import_progress.total, | |
| "current": import_progress.current, | |
| "message": import_progress.message | |
| }, | |
| "statusCode": 200 | |
| } | |
| def import_catalog( | |
| background_tasks: BackgroundTasks, | |
| mode: str = Query(..., description="Mode must be 'replace_all' or 'add_new'"), | |
| current_user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """ | |
| Safe endpoint to import products from backend/app/data/extra_products_vortex.json in the background. | |
| - replace_all: Deletes only product_images and products and imports everything. | |
| - add_new: Only imports products whose names don't exist in the database. | |
| """ | |
| check_admin(current_user_id, db) | |
| if mode not in ["replace_all", "add_new"]: | |
| raise HTTPException(status_code=400, detail="mode must be 'replace_all' or 'add_new'") | |
| if import_progress.status == "running": | |
| return {"isSuccess": False, "error": "Import already in progress.", "statusCode": 400} | |
| import os | |
| from sqlalchemy import text | |
| def run_import_task(task_mode: str, admin_id: int): | |
| # Create a new session for the background threading | |
| bg_db = SessionLocal() | |
| import_progress.status = "running" | |
| import_progress.current = 0 | |
| import_progress.total = 0 | |
| import_progress.message = "Preparing database..." | |
| try: | |
| if task_mode == "replace_all": | |
| print(">>> [ADMIN_IMPORT] Mode: replace_all. Clearing products and images...") | |
| import_progress.message = "Clearing old products..." | |
| bg_db.execute(text("DELETE FROM product_images")) | |
| bg_db.execute(text("DELETE FROM products")) | |
| bg_db.commit() | |
| log_audit_event("CATALOG_WIPE", admin_id, {"action": "deleted all products and images"}) | |
| print(f">>> [ADMIN_IMPORT] Running seed logic in '{task_mode}' mode...") | |
| import_progress.message = "Reading catalog file..." | |
| result = seed_database(db=bg_db, is_background_task=True) | |
| if result.isSuccess: | |
| import_progress.status = "completed" | |
| import_progress.message = "Import finished successfully." | |
| log_audit_event("CATALOG_IMPORT", admin_id, { | |
| "mode": task_mode, | |
| "inserted": result.value.get("inserted_new", 0), | |
| "skipped": result.value.get("skipped", 0) | |
| }) | |
| else: | |
| import_progress.status = "error" | |
| import_progress.message = f"Error: {result.error}" | |
| except Exception as e: | |
| bg_db.rollback() | |
| print(f">>> [ADMIN_IMPORT] Failed: {e}") | |
| import_progress.status = "error" | |
| import_progress.message = f"Critical error: {str(e)}" | |
| finally: | |
| bg_db.close() | |
| # Kickoff task | |
| background_tasks.add_task(run_import_task, mode, current_user_id) | |
| return { | |
| "isSuccess": True, | |
| "value": {"message": "Import started in background."}, | |
| "statusCode": 200 | |
| } | |