""" @router InventoryRouter (Backend/app/api/v1/routers/InventoryRouter.py) @purpose Database-driven inventory management controller querying stock metrics from the append-only inventory ledger. """ from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session, selectinload from sqlalchemy import select, func from pydantic import BaseModel from typing import Optional from app.core.database.db_session import get_db from app.core.permissions.RoleChecker import RoleChecker from app.models.UserModel import User from app.models.ProductModel import Product, ProductVariant from app.services.InventoryService import get_available_stock_map, get_stock_metrics, record_ledger_entry from app.services.CatalogSearchService import sku_search_clause router = APIRouter(prefix="/api/v1/inventory", tags=["Inventory Ledger & Balances"]) class StockAdjustRequest(BaseModel): variant_id: str event_type: str # RECEIPT, DAMAGE, RETURN... qty: int notes: Optional[str] = None def clean_variant_suffix(attributes, product_name: str) -> str: clean_vals = [] seen = set() p_name_lower = product_name.lower() for attr in attributes: val = (getattr(attr, "attribute_value", "") or "").strip() if not val: continue val_lower = val.lower() # Skip numeric values (prices, internal counts, IDs) try: float(val) continue except ValueError: pass # Skip hyphenated slugs (e.g. apple-back-covers-starlight-white) if "-" in val and " " not in val and len(val) > 8: continue # Skip values already in product name if val_lower in p_name_lower: continue # Skip generic meta tags / regions / packaging if val_lower in ("accessories", "device only", "india", "taiwan", "china", "device, cable, manual", "accessories", "new", "refurbished"): continue if val_lower not in seen: seen.add(val_lower) clean_vals.append(val) return " / ".join(clean_vals[:3]) @router.get("/skus") def list_sellable_skus( page: int = Query(1, ge=1), limit: int = Query(50, ge=1, le=500), q: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), ): """ Paginated sellable product variants with live ledger quantities. Never loads the full catalog (safe at 500k+ SKUs). """ stmt = select(ProductVariant, Product).join(Product, Product.product_id == ProductVariant.product_id).options(selectinload(ProductVariant.attributes)) clause = sku_search_clause(q) if clause is not None: stmt = stmt.where(clause) total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar() or 0 rows = db.execute( stmt.order_by(Product.name.asc(), ProductVariant.sku.asc()) .offset((page - 1) * limit) .limit(limit) ).all() variant_ids = [variant.variant_id for variant, _product in rows] stock_map = get_available_stock_map(variant_ids, db) from app.services.InventoryService import get_pending_confirmation_units_map, get_confirmed_units_map pending_map = get_pending_confirmation_units_map(variant_ids, db) confirmed_map = get_confirmed_units_map(variant_ids, db) items = [] for variant, product in rows: available = stock_map.get(variant.variant_id, 0) # Build clean product name suffix specifying key variant attributes (e.g. Color, Storage, Model) variant_suffix = clean_variant_suffix(variant.attributes, product.name) display_name = f"{product.name} ({variant_suffix})" if variant_suffix else product.name items.append({ "variant_id": variant.variant_id, "product_id": product.product_id, "product_name": display_name, "sku": variant.sku, "barcode": variant.barcode, "price": float(variant.price), "cost_price": float(variant.cost_price), "available_stock": available, "pending_confirmation_units": pending_map.get(variant.variant_id, 0), "confirmed_units": confirmed_map.get(variant.variant_id, 0), "low_stock_threshold": variant.low_stock_threshold, "status": variant.status, "is_low": available <= (variant.low_stock_threshold or 0), }) return { "total": total, "page": page, "limit": limit, "items": items, } @router.get("/stock/{variant_id}") def get_variant_stock(variant_id: str, db: Session = Depends(get_db)): """ Get dynamic stock metrics (physical, reserved, available) for a specific product variant. """ return get_stock_metrics(variant_id, db) @router.post("/adjust") def adjust_inventory( payload: StockAdjustRequest, db: Session = Depends(get_db), current_user: User = Depends(RoleChecker(["Super Admin", "Admin"])), ): """ Manually append a stock adjustment entry in the inventory ledger. """ variant = db.execute( select(ProductVariant).where(ProductVariant.variant_id == payload.variant_id) ).scalar_one_or_none() if not variant: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Variant not found") entry = record_ledger_entry( variant_id=payload.variant_id, event_type=payload.event_type, qty=payload.qty, reference_id="MANUAL_ADJUST", db=db, notes=payload.notes ) from app.core.database.cache_manager import cache cache.invalidate_prefix("catalog:products:") cache.invalidate_prefix("catalog:product_detail:") return { "message": "Inventory ledger entry recorded successfully", "ledger_id": entry.ledger_id, "variant_id": entry.variant_id, "event_type": entry.event_type, "qty": entry.qty }