Spaces:
Sleeping
Sleeping
| from typing import Optional | |
| from fastapi import APIRouter, Depends, HTTPException, Query, status | |
| from sqlalchemy import or_, desc, asc, func, text, case, cast, String | |
| from sqlalchemy.orm import Session, joinedload | |
| from datetime import datetime, timezone | |
| from app.db.base import get_db | |
| from app.models.product import Product, ProductImage, Category | |
| from app.schemas.product import ( | |
| ProductCreate, | |
| ProductUpdate, | |
| ProductListItem, | |
| ProductDetail, | |
| ProductImageCreate, | |
| PaginatedResponse, | |
| ) | |
| from app.schemas.auth import AuthResponse | |
| from app.core.security import get_current_user | |
| from app.core.logging import log_audit_event | |
| router = APIRouter(prefix="/products", tags=["Products"]) | |
| async def list_products( | |
| search: Optional[str] = Query(None), | |
| brand: Optional[str] = Query(None), | |
| category_id: Optional[int] = Query(None), | |
| min_price: Optional[float] = Query(None), | |
| max_price: Optional[float] = Query(None), | |
| is_featured: Optional[bool] = Query(None), | |
| has_discount: Optional[bool] = Query(None), | |
| sort_by: str = Query("created_at", regex="^(created_at|price|rating|name_en)$"), | |
| sort_order: str = Query("desc", regex="^(asc|desc)$"), | |
| page: int = Query(1, ge=1), | |
| page_size: int = Query(12, ge=1, le=2000), | |
| limit: Optional[int] = Query(None, ge=1, le=2000), | |
| specs_filter: Optional[str] = Query(None, description="JSON string of filters, e.g. {'RAM': ['8GB']}"), | |
| db: Session = Depends(get_db), | |
| ): | |
| # Use limit if provided, otherwise use page_size | |
| effective_page_size = limit if limit is not None else page_size | |
| from app.models.settings import StoreSettings | |
| settings = db.query(StoreSettings).first() | |
| global_discount = settings.global_discount if settings else 0 | |
| multiplier = (1 - global_discount / 100) | |
| query = db.query(Product).filter( | |
| Product.is_active == True, # noqa: E712 | |
| Product.deleted_at == None # noqa: E711 | |
| ) | |
| # Search (with Arabic Normalization: أ, إ, آ -> ا | ة -> ه | ى -> ي) | |
| if search: | |
| search_term = f"%{search}%" | |
| # Search (with Arabic Normalization: أ, إ, آ -> ا | ة -> ه | ى -> ي | ignore Tashkeel) | |
| def normalize_arabic(column): | |
| # 1-7. Previous normalization | |
| col = func.replace(column, 'أ', 'ا') | |
| col = func.replace(col, 'إ', 'ا') | |
| col = func.replace(col, 'آ', 'ا') | |
| col = func.replace(col, 'ة', 'ه') | |
| col = func.replace(col, 'ى', 'ي') | |
| col = func.replace(col, 'ؤ', 'و') | |
| col = func.replace(col, 'ئ', 'ي') | |
| # 8. Ignore Tashkeel/Diacritics (Fatha, Damma, Kesra, Shadda, etc) | |
| tashkeel = ['ً', 'ٌ', 'ٍ', 'َ', 'ُ', 'ِ', 'ّ', 'ْ', 'ـ'] | |
| for char in tashkeel: | |
| col = func.replace(col, char, '') | |
| return col | |
| # Normalize the user's search term in python | |
| n_search = search.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي').replace('ؤ', 'و').replace('ئ', 'ي') | |
| # Remove Tashkeel from search term too | |
| for char in ['ً', 'ٌ', 'ٍ', 'َ', 'ُ', 'ِ', 'ّ', 'ْ', 'ـ']: | |
| n_search = n_search.replace(char, '') | |
| n_search_term = f"%{n_search}%" | |
| query = query.filter( | |
| or_( | |
| Product.name_en.ilike(search_term), | |
| normalize_arabic(Product.name_ar).ilike(n_search_term), | |
| Product.description_en.ilike(search_term), | |
| normalize_arabic(Product.description_ar).ilike(n_search_term), | |
| ) | |
| ) | |
| # Filters | |
| if category_id is not None: | |
| # Get category and its children to allow filtering by parent category | |
| category = db.query(Category).filter(Category.id == category_id).first() | |
| if category: | |
| subcats = db.query(Category.id).filter(Category.parent_id == category_id).all() | |
| subcat_ids = [c[0] for c in subcats] | |
| all_cat_ids = [category_id] + subcat_ids | |
| query = query.filter(Product.category_id.in_(all_cat_ids)) | |
| if brand: | |
| brands = [b.strip() for b in brand.split(',') if b.strip()] | |
| brand_conditions = [] | |
| for b in brands: | |
| brand_search = f"%{b.lower()}%" | |
| brand_conditions.append( | |
| or_( | |
| cast(Product.specs['brand_name'], String).ilike(brand_search), | |
| cast(Product.specs['brand'], String).ilike(brand_search), | |
| cast(Product.specs['details_en']['Brand Name'], String).ilike(brand_search), | |
| cast(Product.specs['details_en']['Brand'], String).ilike(brand_search) | |
| ) | |
| ) | |
| if brand_conditions: | |
| query = query.filter(or_(*brand_conditions)) | |
| if specs_filter: | |
| import json | |
| try: | |
| filters = json.loads(specs_filter) | |
| if isinstance(filters, dict): | |
| for key, values in filters.items(): | |
| if values and isinstance(values, list): | |
| # Postgres JSONB extraction | |
| query = query.filter( | |
| or_( | |
| cast(Product.specs[key], String).in_(values), | |
| cast(Product.specs['details_en'][key], String).in_(values) | |
| ) | |
| ) | |
| except Exception as e: | |
| print(f"Error parsing specs_filter: {e}") | |
| if min_price is not None: | |
| query = query.filter(Product.price >= min_price) | |
| if max_price is not None: | |
| query = query.filter(Product.price <= max_price) | |
| if is_featured is not None: | |
| query = query.filter(Product.is_featured == is_featured) | |
| if has_discount is True: | |
| query = query.filter(Product.compare_price > Product.price) | |
| elif has_discount is False: | |
| query = query.filter(or_(Product.compare_price == None, Product.compare_price <= Product.price)) | |
| # Count | |
| total = query.count() | |
| # Sort | |
| from sqlalchemy import case | |
| # Custom Hybrid Sorting Logic: | |
| # 1. New Products (ID > 3708) first, sorted by ID DESC | |
| # 2. Original Catalog (ID <= 3708) next, sorted by ID ASC | |
| query = query.order_by( | |
| case( | |
| (Product.id > 3708, 0), | |
| else_=1 | |
| ), | |
| Product.id.asc() | |
| ) | |
| # Pagination | |
| offset = (page - 1) * effective_page_size | |
| products = ( | |
| query.options(joinedload(Product.category), joinedload(Product.images)) | |
| .offset(offset) | |
| .limit(effective_page_size) | |
| .all() | |
| ) | |
| # Map to response | |
| items = [] | |
| for p in products: | |
| primary_image = None | |
| if p.images: | |
| sorted_imgs = sorted(p.images, key=lambda x: x.sort_order) | |
| primary_image = sorted_imgs[0].image_url if sorted_imgs else None | |
| # Pricing for combined discount display: | |
| # price = original_price * multiplier | |
| # compare_price = original_compare (raw) | |
| original_price = p.price | |
| original_compare = p.compare_price if p.compare_price else p.price | |
| item = ProductListItem( | |
| id=p.id, | |
| slug=p.slug, | |
| name_ar=p.name_ar, | |
| name_en=p.name_en, | |
| price=original_price * multiplier, | |
| compare_price=original_compare if (multiplier < 1.0 or p.compare_price) else None, | |
| stock=p.stock, | |
| category_id=p.category_id, | |
| category=p.category, | |
| rating=p.rating, | |
| rating_count=p.rating_count, | |
| is_featured=p.is_featured, | |
| image_url=primary_image, | |
| created_at=p.created_at, | |
| quality_score=p.quality_score or 100, | |
| deleted_at=p.deleted_at, | |
| ) | |
| items.append(item.model_dump()) | |
| # Serialize dates | |
| for item in items: | |
| if isinstance(item.get("created_at"), datetime): | |
| item["created_at"] = item["created_at"].isoformat() | |
| if item.get("deleted_at") and isinstance(item["deleted_at"], datetime): | |
| item["deleted_at"] = item["deleted_at"].isoformat() | |
| total_pages = (total + page_size - 1) // page_size | |
| return PaginatedResponse( | |
| isSuccess=True, | |
| value={ | |
| "items": items, | |
| "total": total, | |
| "page": page, | |
| "page_size": page_size, | |
| "total_pages": total_pages, | |
| }, | |
| statusCode=200, | |
| ) | |
| async def list_deleted_products( | |
| page: int = Query(1, ge=1), | |
| page_size: int = Query(12, ge=1, le=48), | |
| db: Session = Depends(get_db), | |
| user_id: int = Depends(get_current_user), | |
| ): | |
| query = db.query(Product).filter(Product.deleted_at != None) # noqa: E711 | |
| total = query.count() | |
| offset = (page - 1) * page_size | |
| products = ( | |
| query.options(joinedload(Product.category), joinedload(Product.images)) | |
| .order_by(desc(Product.deleted_at)) | |
| .offset(offset) | |
| .limit(page_size) | |
| .all() | |
| ) | |
| items = [] | |
| for p in products: | |
| item = ProductDetail.model_validate(p).model_dump() | |
| if isinstance(item.get("created_at"), datetime): | |
| item["created_at"] = item["created_at"].isoformat() | |
| if item.get("deleted_at") and isinstance(item["deleted_at"], datetime): | |
| item["deleted_at"] = item["deleted_at"].isoformat() | |
| items.append(item) | |
| total_pages = (total + page_size - 1) // page_size | |
| return PaginatedResponse( | |
| isSuccess=True, | |
| value={ | |
| "items": items, | |
| "total": total, | |
| "page": page, | |
| "page_size": page_size, | |
| "total_pages": total_pages, | |
| }, | |
| statusCode=200, | |
| ) | |
| async def get_filters(category_id: Optional[int] = Query(None), db: Session = Depends(get_db)): | |
| """ | |
| Returns filter metadata for a category (price range, brands, dynamic attributes). | |
| Supports parent categories by aggregating children. If category_id is None, returns global filters. | |
| """ | |
| all_cat_ids = None | |
| cat_name_en = "" | |
| if category_id is not None: | |
| category = db.query(Category).filter(Category.id == category_id).first() | |
| if not category: | |
| raise HTTPException(status_code=404, detail="Category not found") | |
| subcats = db.query(Category.id).filter(Category.parent_id == category_id).all() | |
| all_cat_ids = [category_id] + [c[0] for c in subcats] | |
| cat_name_en = (category.name_en or "").lower() | |
| base_filter = [Product.is_active == True, Product.deleted_at == None] | |
| if all_cat_ids is not None: | |
| base_filter.append(Product.category_id.in_(all_cat_ids)) | |
| # 1. Price Range | |
| price_stats = db.query( | |
| func.min(Product.price).label("min_p"), | |
| func.max(Product.price).label("max_p") | |
| ).filter(*base_filter).first() | |
| # 2. Brands | |
| brands_q1 = db.query(func.distinct(cast(Product.specs['brand_name'], String))).filter(*base_filter) | |
| brands_q2 = db.query(func.distinct(cast(Product.specs['details_en']['Brand Name'], String))).filter(*base_filter) | |
| brands = sorted(list(set([b[0] for b in brands_q1.all() if b[0]] + [b[0] for b in brands_q2.all() if b[0]]))) | |
| # 3. Dynamic Attributes per subcategory | |
| attr_mappings = [] | |
| if cat_name_en: | |
| # ── Electronics ── | |
| if any(k in cat_name_en for k in ["mobile", "phone", "smartphone", "جوالات"]): | |
| attr_mappings = [ | |
| {"key": "RAM", "label_ar": "الرام", "label_en": "RAM"}, | |
| {"key": "Internal Memory", "label_ar": "المساحة الداخلية", "label_en": "Storage"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Screen Size", "label_ar": "حجم الشاشة", "label_en": "Screen Size"}, | |
| {"key": "Network Type", "label_ar": "نوع الشبكة", "label_en": "Network"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["laptop", "computer", "لابتوب"]): | |
| attr_mappings = [ | |
| {"key": "Processor", "label_ar": "المعالج", "label_en": "Processor"}, | |
| {"key": "RAM", "label_ar": "الرام", "label_en": "RAM"}, | |
| {"key": "Hard Drive Capacity", "label_ar": "سعة التخزين", "label_en": "Storage"}, | |
| {"key": "Screen Size", "label_ar": "حجم الشاشة", "label_en": "Screen Size"}, | |
| {"key": "Graphics Card", "label_ar": "كرت الشاشة", "label_en": "Graphics Card"}, | |
| {"key": "Operating System", "label_ar": "نظام التشغيل", "label_en": "OS"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["tablet", "لوحي"]): | |
| attr_mappings = [ | |
| {"key": "RAM", "label_ar": "الرام", "label_en": "RAM"}, | |
| {"key": "Internal Memory", "label_ar": "المساحة", "label_en": "Storage"}, | |
| {"key": "Screen Size", "label_ar": "حجم الشاشة", "label_en": "Screen Size"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["tv", "television", "تلفزيون"]): | |
| attr_mappings = [ | |
| {"key": "Screen Size", "label_ar": "حجم الشاشة", "label_en": "Screen Size"}, | |
| {"key": "Resolution Type", "label_ar": "الدقة", "label_en": "Resolution"}, | |
| {"key": "Smart TV", "label_ar": "تلفزيون ذكي", "label_en": "Smart TV"}, | |
| {"key": "Display Type", "label_ar": "نوع الشاشة", "label_en": "Display Type"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["camera", "كاميرا"]): | |
| attr_mappings = [ | |
| {"key": "Resolution", "label_ar": "الدقة", "label_en": "Resolution"}, | |
| {"key": "Type", "label_ar": "النوع", "label_en": "Type"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["printer", "طابع"]): | |
| attr_mappings = [ | |
| {"key": "Print Technology", "label_ar": "تقنية الطباعة", "label_en": "Print Tech"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Connectivity", "label_ar": "الاتصال", "label_en": "Connectivity"}, | |
| ] | |
| # ── Accessories ── | |
| elif any(k in cat_name_en for k in ["audio", "headphone", "سماعات", "صوت"]): | |
| attr_mappings = [ | |
| {"key": "Connection Type", "label_ar": "نوع الاتصال", "label_en": "Connection"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Type", "label_ar": "النوع", "label_en": "Type"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["watch", "ساعات"]): | |
| attr_mappings = [ | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Band Material", "label_ar": "مادة السوار", "label_en": "Band Material"}, | |
| {"key": "Display Type", "label_ar": "نوع الشاشة", "label_en": "Display"}, | |
| ] | |
| # ── Gaming ── | |
| elif any(k in cat_name_en for k in ["gaming", "console", "ألعاب"]): | |
| attr_mappings = [ | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Storage Capacity", "label_ar": "سعة التخزين", "label_en": "Storage"}, | |
| {"key": "Type", "label_ar": "النوع", "label_en": "Type"}, | |
| ] | |
| # ── Home Appliances ── | |
| elif any(k in cat_name_en for k in ["refrigerator", "ثلاج"]): | |
| attr_mappings = [ | |
| {"key": "Capacity", "label_ar": "السعة", "label_en": "Capacity"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Door Type", "label_ar": "نوع الباب", "label_en": "Door Type"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["wash", "غسال"]): | |
| attr_mappings = [ | |
| {"key": "Capacity", "label_ar": "السعة", "label_en": "Capacity (kg)"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Load Type", "label_ar": "نوع التحميل", "label_en": "Load Type"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["conditioner", "مكيف"]): | |
| attr_mappings = [ | |
| {"key": "Capacity", "label_ar": "السعة (BTU)", "label_en": "Capacity (BTU)"}, | |
| {"key": "Type", "label_ar": "النوع", "label_en": "Type"}, | |
| {"key": "Energy Rating", "label_ar": "كفاءة الطاقة", "label_en": "Energy Rating"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["small appliance", "منزلية صغيرة"]): | |
| attr_mappings = [ | |
| {"key": "Type", "label_ar": "النوع", "label_en": "Type"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Wattage", "label_ar": "القدرة", "label_en": "Wattage"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["large appliance", "منزلية كبيرة"]): | |
| attr_mappings = [ | |
| {"key": "Capacity", "label_ar": "السعة", "label_en": "Capacity"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Type", "label_ar": "النوع", "label_en": "Type"}, | |
| ] | |
| # ── Parent categories (aggregate) ── | |
| elif any(k in cat_name_en for k in ["electronic", "إلكتروني"]): | |
| attr_mappings = [ | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["accessor", "ملحقات"]): | |
| attr_mappings = [ | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| {"key": "Connection Type", "label_ar": "نوع الاتصال", "label_en": "Connection"}, | |
| ] | |
| elif any(k in cat_name_en for k in ["home appliance", "المنزلية"]): | |
| attr_mappings = [ | |
| {"key": "Capacity", "label_ar": "السعة", "label_en": "Capacity"}, | |
| {"key": "Color", "label_ar": "اللون", "label_en": "Color"}, | |
| ] | |
| # 4. Resolve attribute values from DB | |
| attributes = [] | |
| for attr in attr_mappings: | |
| key = attr["key"] | |
| # Try direct spec key first, then details_en | |
| values_query = db.query(func.distinct(cast(Product.specs[key], String))).filter( | |
| *base_filter | |
| ).all() | |
| vals = [v[0] for v in values_query if v[0]] | |
| if not vals: | |
| values_query = db.query(func.distinct(cast(Product.specs['details_en'][key], String))).filter( | |
| *base_filter | |
| ).all() | |
| vals = [v[0] for v in values_query if v[0]] | |
| if vals: | |
| attributes.append({ | |
| "key": key, | |
| "label_ar": attr["label_ar"], | |
| "label_en": attr["label_en"], | |
| "options": sorted(list(set(vals))) | |
| }) | |
| return PaginatedResponse( | |
| isSuccess=True, | |
| value={ | |
| "min_price": float(price_stats.min_p) if price_stats and price_stats.min_p else 0, | |
| "max_price": float(price_stats.max_p) if price_stats and price_stats.max_p else 10000, | |
| "brands": brands, | |
| "attributes": attributes | |
| }, | |
| statusCode=200 | |
| ) | |
| async def get_product(identifier: str, raw: bool = False, db: Session = Depends(get_db)): | |
| query = db.query(Product).options(joinedload(Product.category), joinedload(Product.images)) | |
| if identifier.isdigit(): | |
| product = query.filter(Product.id == int(identifier)).first() | |
| # If not found by ID, maybe the slug itself is numeric? | |
| if not product: | |
| product = query.filter(Product.slug == identifier).first() | |
| else: | |
| product = query.filter(Product.slug == identifier).first() | |
| if not product: | |
| raise HTTPException(status_code=404, detail="Product not found") | |
| from app.models.settings import StoreSettings | |
| settings = db.query(StoreSettings).first() | |
| global_discount = settings.global_discount if settings else 0 | |
| multiplier = (1 - global_discount / 100) | |
| detail = ProductDetail.model_validate(product).model_dump() | |
| # Only apply discount if NOT raw | |
| if not raw: | |
| # Normalize pricing for combined discount display: | |
| # price = original_price * multiplier | |
| # compare_price = original_compare (raw) | |
| original_price = detail["price"] | |
| original_compare = detail.get("compare_price") if detail.get("compare_price") else original_price | |
| detail["price"] = original_price * multiplier | |
| if multiplier < 1.0 or detail.get("compare_price"): | |
| detail["compare_price"] = original_compare | |
| # Apply to variants in specs | |
| if detail.get("specs") and "variants" in detail["specs"]: | |
| for v in detail["specs"]["variants"]: | |
| if v.get("price"): | |
| v_original_price = v["price"] | |
| v["price"] = v_original_price * multiplier | |
| # Optional: Could add compare_price to variants too, but mostly price is enough | |
| # as the main product's compare_price is often used for the range. | |
| if v.get("price_modifier"): | |
| v["price_modifier"] *= multiplier | |
| return PaginatedResponse( | |
| isSuccess=True, | |
| value=detail, | |
| statusCode=200, | |
| ) | |
| async def create_product( | |
| request: ProductCreate, | |
| user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| if not request.images or len(request.images) == 0: | |
| raise HTTPException(status_code=400, detail="Main image is required / صورة رئيسية مطلوبة") | |
| product = Product( | |
| name_ar=request.name_ar, | |
| name_en=request.name_en, | |
| description_ar=request.description_ar, | |
| description_en=request.description_en, | |
| price=request.price, | |
| compare_price=request.compare_price, | |
| stock=request.stock, | |
| category_id=request.category_id, | |
| is_featured=request.is_featured, | |
| is_active=request.is_active, | |
| specs=request.specs, | |
| ) | |
| db.add(product) | |
| db.flush() | |
| # Add images | |
| for img in request.images: | |
| db_img = ProductImage( | |
| product_id=product.id, | |
| image_url=img.image_url, | |
| alt_text=img.alt_text, | |
| sort_order=img.sort_order, | |
| ) | |
| db.add(db_img) | |
| db.commit() | |
| db.refresh(product) | |
| return AuthResponse( | |
| isSuccess=True, | |
| value={"product_id": product.id}, | |
| statusCode=201, | |
| ) | |
| async def update_product( | |
| product_id: int, | |
| request: ProductUpdate, | |
| user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| product = db.query(Product).filter(Product.id == product_id).first() | |
| if not product or product.deleted_at is not None: | |
| raise HTTPException(status_code=404, detail="Product not found") | |
| update_data = request.model_dump(exclude_unset=True) | |
| for field, value in update_data.items(): | |
| setattr(product, field, value) | |
| db.commit() | |
| return AuthResponse( | |
| isSuccess=True, | |
| value={"product_id": product.id}, | |
| statusCode=200, | |
| ) | |
| async def delete_product( | |
| product_id: int, | |
| user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| product = db.query(Product).filter(Product.id == product_id).first() | |
| if not product or product.deleted_at is not None: | |
| raise HTTPException(status_code=404, detail="Product not found") | |
| # Soft Delete | |
| product.deleted_at = datetime.now(timezone.utc) | |
| product.deleted_by = user_id | |
| product.deletion_reason = "Manual deletion by admin" | |
| db.commit() | |
| return AuthResponse( | |
| isSuccess=True, | |
| value={"deleted": True, "soft_delete": True}, | |
| statusCode=200, | |
| ) | |
| async def hard_delete_product( | |
| product_id: int, | |
| user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_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 AuthResponse( | |
| isSuccess=True, | |
| value={"deleted": True, "hard_delete": True}, | |
| statusCode=200, | |
| ) | |
| async def restore_product( | |
| product_id: int, | |
| user_id: int = Depends(get_current_user), | |
| db: Session = Depends(get_db), | |
| ): | |
| product = db.query(Product).filter(Product.id == product_id).first() | |
| if not product or product.deleted_at is None: | |
| raise HTTPException(status_code=404, detail="Product not found or not deleted") | |
| product.deleted_at = None | |
| product.deleted_by = None | |
| product.deletion_reason = None | |
| db.commit() | |
| return AuthResponse( | |
| isSuccess=True, | |
| value={"restored": True}, | |
| statusCode=200, | |
| ) | |
| # End of products.py | |