major refactoring adding vendor and customer features
This commit is contained in:
@@ -53,7 +53,7 @@ def get_user_vendor(
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(vendor_code)
|
||||
|
||||
if current_user.role != "admin" and vendor.owner_id != current_user.id:
|
||||
if current_user.role != "admin" and vendor.owner_user_id != current_user.id:
|
||||
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
|
||||
|
||||
return vendor
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
# app/api/main.py
|
||||
"""Summary description ....
|
||||
"""
|
||||
API router configuration for multi-tenant ecommerce platform.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- ....
|
||||
- ....
|
||||
- ....
|
||||
This module provides:
|
||||
- API version 1 route aggregation
|
||||
- Route organization by user type (admin, vendor, public)
|
||||
- Proper route prefixing and tagging
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import admin, auth, marketplace, vendor, stats, stock
|
||||
from app.api.v1 import admin, vendor, public
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Include all route modules
|
||||
api_router.include_router(admin.router, tags=["admin"])
|
||||
api_router.include_router(auth.router, tags=["authentication"])
|
||||
api_router.include_router(marketplace.router, tags=["marketplace"])
|
||||
api_router.include_router(vendor.router, tags=["vendor"])
|
||||
api_router.include_router(stats.router, tags=["statistics"])
|
||||
api_router.include_router(stock.router, tags=["stock"])
|
||||
# ============================================================================
|
||||
# ADMIN ROUTES (Platform-level management)
|
||||
# Prefix: /api/v1/admin
|
||||
# ============================================================================
|
||||
|
||||
api_router.include_router(
|
||||
admin.router,
|
||||
prefix="/v1/admin",
|
||||
tags=["admin"]
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR ROUTES (Vendor-scoped operations)
|
||||
# Prefix: /api/v1/vendor
|
||||
# ============================================================================
|
||||
|
||||
api_router.include_router(
|
||||
vendor.router,
|
||||
prefix="/v1/vendor",
|
||||
tags=["vendor"]
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# PUBLIC/CUSTOMER ROUTES (Customer-facing)
|
||||
# Prefix: /api/v1/public
|
||||
# ============================================================================
|
||||
|
||||
api_router.include_router(
|
||||
public.router,
|
||||
prefix="/v1/public",
|
||||
tags=["public"]
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# app/api/v1/__init__.py
|
||||
"""
|
||||
API Version 1 - All endpoints
|
||||
"""
|
||||
|
||||
from . import admin, vendor, public
|
||||
|
||||
__all__ = ["admin", "vendor", "public"]
|
||||
@@ -1,125 +0,0 @@
|
||||
# app/api/v1/admin.py
|
||||
"""
|
||||
Admin endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- User management (view, toggle status)
|
||||
- Vendor management (view, verify, toggle status)
|
||||
- Marketplace import job monitoring
|
||||
- Admin dashboard statistics
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_user
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from models.schemas.auth import UserResponse
|
||||
from models.schemas.marketplace_import_job import MarketplaceImportJobResponse
|
||||
from models.schemas.vendor import VendorListResponse
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/admin/users", response_model=List[UserResponse])
|
||||
def get_all_users(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all users (Admin only)."""
|
||||
users = admin_service.get_all_users(db=db, skip=skip, limit=limit)
|
||||
return [UserResponse.model_validate(user) for user in users]
|
||||
|
||||
|
||||
@router.put("/admin/users/{user_id}/status")
|
||||
def toggle_user_status(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Toggle user active status (Admin only)."""
|
||||
user, message = admin_service.toggle_user_status(db, user_id, current_admin.id)
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.get("/admin/vendors", response_model=VendorListResponse)
|
||||
def get_all_vendors_admin(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all vendors with admin view (Admin only)."""
|
||||
vendors, total = admin_service.get_all_vendors(db=db, skip=skip, limit=limit)
|
||||
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.put("/admin/vendors/{vendor_id}/verify")
|
||||
def verify_vendor(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Verify/unverify vendor (Admin only)."""
|
||||
vendor, message = admin_service.verify_vendor(db, vendor_id)
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.put("/admin/vendors/{vendor_id}/status")
|
||||
def toggle_vendor_status(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Toggle vendor active status (Admin only)."""
|
||||
vendor, message = admin_service.toggle_vendor_status(db, vendor_id)
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/admin/marketplace-import-jobs", response_model=List[MarketplaceImportJobResponse]
|
||||
)
|
||||
def get_all_marketplace_import_jobs(
|
||||
marketplace: Optional[str] = Query(None),
|
||||
vendor_name: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all marketplace import jobs (Admin only)."""
|
||||
return admin_service.get_marketplace_import_jobs(
|
||||
db=db,
|
||||
marketplace=marketplace,
|
||||
vendor_name=vendor_name,
|
||||
status=status,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/admin/stats/users")
|
||||
def get_user_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get user statistics for admin dashboard (Admin only)."""
|
||||
return admin_service.get_user_statistics(db)
|
||||
|
||||
|
||||
@router.get("/admin/stats/vendors")
|
||||
def get_vendor_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get vendor statistics for admin dashboard (Admin only)."""
|
||||
return admin_service.get_vendor_statistics(db)
|
||||
17
app/api/v1/admin/__init__.py
Normal file
17
app/api/v1/admin/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Admin API endpoints.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import auth, vendors, dashboard, users
|
||||
|
||||
# Create admin router
|
||||
router = APIRouter()
|
||||
|
||||
# Include all admin sub-routers
|
||||
router.include_router(auth.router, tags=["admin-auth"])
|
||||
router.include_router(vendors.router, tags=["admin-vendors"])
|
||||
router.include_router(dashboard.router, tags=["admin-dashboard"])
|
||||
router.include_router(users.router, tags=["admin-users"])
|
||||
|
||||
__all__ = ["router"]
|
||||
58
app/api/v1/admin/auth.py
Normal file
58
app/api/v1/admin/auth.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# app/api/v1/admin/auth.py
|
||||
"""
|
||||
Admin authentication endpoints.
|
||||
|
||||
This module provides:
|
||||
- Admin user login
|
||||
- Admin token validation
|
||||
- Admin-specific authentication logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.auth_service import auth_service
|
||||
from app.exceptions import InvalidCredentialsException
|
||||
from models.schemas.auth import LoginResponse, UserLogin
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Admin login endpoint.
|
||||
|
||||
Only allows users with 'admin' role to login.
|
||||
Returns JWT token for authenticated admin users.
|
||||
"""
|
||||
# Authenticate user
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
|
||||
# Verify user is admin
|
||||
if login_result["user"].role != "admin":
|
||||
logger.warning(f"Non-admin user attempted admin login: {user_credentials.username}")
|
||||
raise InvalidCredentialsException("Admin access required")
|
||||
|
||||
logger.info(f"Admin login successful: {login_result['user'].username}")
|
||||
|
||||
return LoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
expires_in=login_result["token_data"]["expires_in"],
|
||||
user=login_result["user"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def admin_logout():
|
||||
"""
|
||||
Admin logout endpoint.
|
||||
|
||||
Client should remove token from storage.
|
||||
Server-side token invalidation can be implemented here if needed.
|
||||
"""
|
||||
return {"message": "Logged out successfully"}
|
||||
90
app/api/v1/admin/dashboard.py
Normal file
90
app/api/v1/admin/dashboard.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# app/api/v1/admin/dashboard.py
|
||||
"""
|
||||
Admin dashboard and statistics endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_user
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from app.services.stats_service import stats_service
|
||||
from models.database.user import User
|
||||
from models.schemas.stats import MarketplaceStatsResponse, StatsResponse
|
||||
|
||||
router = APIRouter(prefix="/dashboard")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_admin_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get admin dashboard with platform statistics (Admin only)."""
|
||||
return {
|
||||
"platform": {
|
||||
"name": "Multi-Tenant Ecommerce Platform",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"users": admin_service.get_user_statistics(db),
|
||||
"vendors": admin_service.get_vendor_statistics(db),
|
||||
"recent_vendors": admin_service.get_recent_vendors(db, limit=5),
|
||||
"recent_imports": admin_service.get_recent_import_jobs(db, limit=10),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats", response_model=StatsResponse)
|
||||
def get_comprehensive_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get comprehensive platform statistics (Admin only)."""
|
||||
stats_data = stats_service.get_comprehensive_stats(db=db)
|
||||
|
||||
return StatsResponse(
|
||||
total_products=stats_data["total_products"],
|
||||
unique_brands=stats_data["unique_brands"],
|
||||
unique_categories=stats_data["unique_categories"],
|
||||
unique_marketplaces=stats_data["unique_marketplaces"],
|
||||
unique_vendors=stats_data["unique_vendors"],
|
||||
total_inventory_entries=stats_data["total_inventory_entries"],
|
||||
total_inventory_quantity=stats_data["total_inventory_quantity"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse])
|
||||
def get_marketplace_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get statistics broken down by marketplace (Admin only)."""
|
||||
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)
|
||||
|
||||
return [
|
||||
MarketplaceStatsResponse(
|
||||
marketplace=stat["marketplace"],
|
||||
total_products=stat["total_products"],
|
||||
unique_vendors=stat["unique_vendors"],
|
||||
unique_brands=stat["unique_brands"],
|
||||
)
|
||||
for stat in marketplace_stats
|
||||
]
|
||||
|
||||
|
||||
@router.get("/stats/platform")
|
||||
def get_platform_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get comprehensive platform statistics (Admin only)."""
|
||||
return {
|
||||
"users": admin_service.get_user_statistics(db),
|
||||
"vendors": admin_service.get_vendor_statistics(db),
|
||||
"products": admin_service.get_product_statistics(db),
|
||||
"orders": admin_service.get_order_statistics(db),
|
||||
"imports": admin_service.get_import_statistics(db),
|
||||
}
|
||||
49
app/api/v1/admin/marketplace.py
Normal file
49
app/api/v1/admin/marketplace.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# app/api/v1/admin/marketplace.py
|
||||
"""
|
||||
Marketplace import job monitoring endpoints for admin.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_user
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from models.schemas.marketplace_import_job import MarketplaceImportJobResponse
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/marketplace-import-jobs")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("", response_model=List[MarketplaceImportJobResponse])
|
||||
def get_all_marketplace_import_jobs(
|
||||
marketplace: Optional[str] = Query(None),
|
||||
vendor_name: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all marketplace import jobs (Admin only)."""
|
||||
return admin_service.get_marketplace_import_jobs(
|
||||
db=db,
|
||||
marketplace=marketplace,
|
||||
vendor_name=vendor_name,
|
||||
status=status,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def get_import_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get marketplace import statistics (Admin only)."""
|
||||
return admin_service.get_import_statistics(db)
|
||||
51
app/api/v1/admin/users.py
Normal file
51
app/api/v1/admin/users.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# app/api/v1/admin/users.py
|
||||
"""
|
||||
User management endpoints for admin.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_user
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from models.schemas.auth import UserResponse
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/users")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("", response_model=List[UserResponse])
|
||||
def get_all_users(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all users (Admin only)."""
|
||||
users = admin_service.get_all_users(db=db, skip=skip, limit=limit)
|
||||
return [UserResponse.model_validate(user) for user in users]
|
||||
|
||||
|
||||
@router.put("/{user_id}/status")
|
||||
def toggle_user_status(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Toggle user active status (Admin only)."""
|
||||
user, message = admin_service.toggle_user_status(db, user_id, current_admin.id)
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def get_user_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get user statistics for admin dashboard (Admin only)."""
|
||||
return admin_service.get_user_statistics(db)
|
||||
143
app/api/v1/admin/vendors.py
Normal file
143
app/api/v1/admin/vendors.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# app/api/v1/admin/vendors.py
|
||||
"""
|
||||
Vendor management endpoints for admin.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_user
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from models.schemas.vendor import VendorListResponse, VendorResponse, VendorCreate
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/vendors")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("", response_model=VendorResponse)
|
||||
def create_vendor_with_owner(
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
Create a new vendor with owner user account (Admin only).
|
||||
|
||||
This endpoint:
|
||||
1. Creates a new vendor
|
||||
2. Creates an owner user account for the vendor
|
||||
3. Sets up default roles (Owner, Manager, Editor, Viewer)
|
||||
4. Sends welcome email to vendor owner (if email service configured)
|
||||
|
||||
Returns the created vendor with owner information.
|
||||
"""
|
||||
vendor, owner_user, temp_password = admin_service.create_vendor_with_owner(
|
||||
db=db,
|
||||
vendor_data=vendor_data
|
||||
)
|
||||
|
||||
return {
|
||||
**VendorResponse.model_validate(vendor).model_dump(),
|
||||
"owner_email": owner_user.email,
|
||||
"owner_username": owner_user.username,
|
||||
"temporary_password": temp_password, # Only shown once!
|
||||
"login_url": f"{vendor.subdomain}.platform.com/vendor/login" if vendor.subdomain else None
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=VendorListResponse)
|
||||
def get_all_vendors_admin(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None, description="Search by name or vendor code"),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_verified: Optional[bool] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get all vendors with admin view (Admin only)."""
|
||||
vendors, total = admin_service.get_all_vendors(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
search=search,
|
||||
is_active=is_active,
|
||||
is_verified=is_verified
|
||||
)
|
||||
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}", response_model=VendorResponse)
|
||||
def get_vendor_details(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get detailed vendor information (Admin only)."""
|
||||
vendor = admin_service.get_vendor_by_id(db, vendor_id)
|
||||
return VendorResponse.model_validate(vendor)
|
||||
|
||||
|
||||
@router.put("/{vendor_id}/verify")
|
||||
def verify_vendor(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Verify/unverify vendor (Admin only)."""
|
||||
vendor, message = admin_service.verify_vendor(db, vendor_id)
|
||||
return {"message": message, "vendor": VendorResponse.model_validate(vendor)}
|
||||
|
||||
|
||||
@router.put("/{vendor_id}/status")
|
||||
def toggle_vendor_status(
|
||||
vendor_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Toggle vendor active status (Admin only)."""
|
||||
vendor, message = admin_service.toggle_vendor_status(db, vendor_id)
|
||||
return {"message": message, "vendor": VendorResponse.model_validate(vendor)}
|
||||
|
||||
|
||||
@router.delete("/{vendor_id}")
|
||||
def delete_vendor(
|
||||
vendor_id: int,
|
||||
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""
|
||||
Delete vendor and all associated data (Admin only).
|
||||
|
||||
WARNING: This is destructive and will delete:
|
||||
- Vendor account
|
||||
- All products
|
||||
- All orders
|
||||
- All customers
|
||||
- All team members
|
||||
|
||||
Requires confirmation parameter.
|
||||
"""
|
||||
if not confirm:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Deletion requires confirmation parameter: confirm=true"
|
||||
)
|
||||
|
||||
message = admin_service.delete_vendor(db, vendor_id)
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.get("/stats/vendors")
|
||||
def get_vendor_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_user),
|
||||
):
|
||||
"""Get vendor statistics for admin dashboard (Admin only)."""
|
||||
return admin_service.get_vendor_statistics(db)
|
||||
@@ -1,50 +0,0 @@
|
||||
# app/api/v1/auth.py
|
||||
"""
|
||||
Authentication endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- User registration and validation
|
||||
- User authentication and JWT token generation
|
||||
- Current user information retrieval
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.services.auth_service import auth_service
|
||||
from models.schemas.auth import (LoginResponse, UserLogin, UserRegister,
|
||||
UserResponse)
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/auth/register", response_model=UserResponse)
|
||||
def register_user(user_data: UserRegister, db: Session = Depends(get_db)):
|
||||
"""Register a new user."""
|
||||
user = auth_service.register_user(db=db, user_data=user_data)
|
||||
return UserResponse.model_validate(user)
|
||||
|
||||
|
||||
@router.post("/auth/login", response_model=LoginResponse)
|
||||
def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
"""Login user and return JWT token."""
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
|
||||
return LoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
expires_in=login_result["token_data"]["expires_in"],
|
||||
user=UserResponse.model_validate(login_result["user"]),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/auth/me", response_model=UserResponse)
|
||||
def get_current_user_info(current_user: User = Depends(get_current_user)):
|
||||
"""Get current user information."""
|
||||
return UserResponse.model_validate(current_user)
|
||||
@@ -1,264 +0,0 @@
|
||||
# app/api/v1/marketplace_products.py
|
||||
"""
|
||||
MarketplaceProduct endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- MarketplaceProduct CRUD operations with marketplace support
|
||||
- Advanced product filtering and search
|
||||
- MarketplaceProduct export functionality
|
||||
- MarketplaceProduct import from marketplace CSV files
|
||||
- Import job management and monitoring
|
||||
- Import statistics and job cancellation
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.services.marketplace_import_job_service import marketplace_import_job_service
|
||||
from app.services.marketplace_product_service import marketplace_product_service
|
||||
from app.tasks.background_tasks import process_marketplace_import
|
||||
from middleware.decorators import rate_limit
|
||||
from models.schemas.marketplace_import_job import (MarketplaceImportJobResponse,
|
||||
MarketplaceImportJobRequest)
|
||||
from models.schemas.marketplace_product import (MarketplaceProductCreate, MarketplaceProductDetailResponse,
|
||||
MarketplaceProductListResponse, MarketplaceProductResponse,
|
||||
MarketplaceProductUpdate)
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PRODUCT ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/marketplace/product/export-csv")
|
||||
async def export_csv(
|
||||
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
||||
vendor_name: Optional[str] = Query(None, description="Filter by vendor name"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Export products as CSV with streaming and marketplace filtering (Protected)."""
|
||||
|
||||
def generate_csv():
|
||||
return marketplace_product_service.generate_csv_export(
|
||||
db=db, marketplace=marketplace, vendor_name=vendor_name
|
||||
)
|
||||
|
||||
filename = "marketplace_products_export"
|
||||
if marketplace:
|
||||
filename += f"_{marketplace}"
|
||||
if vendor_name:
|
||||
filename += f"_{vendor_name}"
|
||||
filename += ".csv"
|
||||
|
||||
return StreamingResponse(
|
||||
generate_csv(),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/marketplace/product", response_model=MarketplaceProductListResponse)
|
||||
def get_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
brand: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
availability: Optional[str] = Query(None),
|
||||
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
||||
vendor_name: Optional[str] = Query(None, description="Filter by vendor name"),
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get products with advanced filtering including marketplace and vendor (Protected)."""
|
||||
products, total = marketplace_product_service.get_products_with_filters(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
brand=brand,
|
||||
category=category,
|
||||
availability=availability,
|
||||
marketplace=marketplace,
|
||||
vendor_name=vendor_name,
|
||||
search=search,
|
||||
)
|
||||
|
||||
return MarketplaceProductListResponse(
|
||||
products=products, total=total, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.post("/marketplace/product", response_model=MarketplaceProductResponse)
|
||||
def create_product(
|
||||
product: MarketplaceProductCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new product with validation and marketplace support (Protected)."""
|
||||
logger.info(f"Starting product creation for ID: {product.marketplace_product_id}")
|
||||
|
||||
db_product = marketplace_product_service.create_product(db, product)
|
||||
logger.info("MarketplaceProduct created successfully")
|
||||
|
||||
return db_product
|
||||
|
||||
|
||||
@router.get("/marketplace/product/{marketplace_product_id}", response_model=MarketplaceProductDetailResponse)
|
||||
def get_product(
|
||||
marketplace_product_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get product with stock information (Protected)."""
|
||||
product = marketplace_product_service.get_product_by_id_or_raise(db, marketplace_product_id)
|
||||
|
||||
# Get stock information if GTIN exists
|
||||
stock_info = None
|
||||
if product.gtin:
|
||||
stock_info = marketplace_product_service.get_stock_info(db, product.gtin)
|
||||
|
||||
return MarketplaceProductDetailResponse(product=product, stock_info=stock_info)
|
||||
|
||||
|
||||
@router.put("/marketplace/product/{marketplace_product_id}", response_model=MarketplaceProductResponse)
|
||||
def update_product(
|
||||
marketplace_product_id: str,
|
||||
product_update: MarketplaceProductUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update product with validation and marketplace support (Protected)."""
|
||||
updated_product = marketplace_product_service.update_product(db, marketplace_product_id, product_update)
|
||||
return updated_product
|
||||
|
||||
|
||||
@router.delete("/marketplace/product/{marketplace_product_id}")
|
||||
def delete_product(
|
||||
marketplace_product_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete product and associated stock (Protected)."""
|
||||
marketplace_product_service.delete_product(db, marketplace_product_id)
|
||||
return {"message": "MarketplaceProduct and associated stock deleted successfully"}
|
||||
|
||||
# ============================================================================
|
||||
# IMPORT JOB ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/marketplace/import-product", response_model=MarketplaceImportJobResponse)
|
||||
@rate_limit(max_requests=10, window_seconds=3600) # Limit marketplace imports
|
||||
async def import_products_from_marketplace(
|
||||
request: MarketplaceImportJobRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Import products from marketplace CSV with background processing (Protected)."""
|
||||
logger.info(
|
||||
f"Starting marketplace import: {request.marketplace} -> {request.vendor_code} by user {current_user.username}"
|
||||
)
|
||||
|
||||
# Create import job through service
|
||||
import_job = marketplace_import_job_service.create_import_job(db, request, current_user)
|
||||
|
||||
# Process in background
|
||||
background_tasks.add_task(
|
||||
process_marketplace_import,
|
||||
import_job.id,
|
||||
request.url,
|
||||
request.marketplace,
|
||||
request.vendor_code,
|
||||
request.batch_size or 1000,
|
||||
)
|
||||
|
||||
return MarketplaceImportJobResponse(
|
||||
job_id=import_job.id,
|
||||
status="pending",
|
||||
marketplace=request.marketplace,
|
||||
vendor_code=request.vendor_code,
|
||||
vendor_id=import_job.vendor_id,
|
||||
vendor_name=import_job.vendor_name,
|
||||
message=f"Marketplace import started from {request.marketplace}. Check status with "
|
||||
f"/import-status/{import_job.id}",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/marketplace/import-status/{job_id}", response_model=MarketplaceImportJobResponse
|
||||
)
|
||||
def get_marketplace_import_status(
|
||||
job_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get status of marketplace import job (Protected)."""
|
||||
job = marketplace_import_job_service.get_import_job_by_id(db, job_id, current_user)
|
||||
return marketplace_import_job_service.convert_to_response_model(job)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/marketplace/import-jobs", response_model=List[MarketplaceImportJobResponse]
|
||||
)
|
||||
def get_marketplace_import_jobs(
|
||||
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
||||
vendor_name: Optional[str] = Query(None, description="Filter by vendor name"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get marketplace import jobs with filtering (Protected)."""
|
||||
jobs = marketplace_import_job_service.get_import_jobs(
|
||||
db=db,
|
||||
user=current_user,
|
||||
marketplace=marketplace,
|
||||
vendor_name=vendor_name,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return [marketplace_import_job_service.convert_to_response_model(job) for job in jobs]
|
||||
|
||||
|
||||
@router.get("/marketplace/marketplace-import-stats")
|
||||
def get_marketplace_import_stats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get statistics about marketplace import jobs (Protected)."""
|
||||
return marketplace_import_job_service.get_job_stats(db, current_user)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/marketplace/import-jobs/{job_id}/cancel",
|
||||
response_model=MarketplaceImportJobResponse,
|
||||
)
|
||||
def cancel_marketplace_import_job(
|
||||
job_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Cancel a pending or running marketplace import job (Protected)."""
|
||||
job = marketplace_import_job_service.cancel_import_job(db, job_id, current_user)
|
||||
return marketplace_import_job_service.convert_to_response_model(job)
|
||||
|
||||
|
||||
@router.delete("/marketplace/import-jobs/{job_id}")
|
||||
def delete_marketplace_import_job(
|
||||
job_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a completed marketplace import job (Protected)."""
|
||||
marketplace_import_job_service.delete_import_job(db, job_id, current_user)
|
||||
return {"message": "Marketplace import job deleted successfully"}
|
||||
18
app/api/v1/public/__init__.py
Normal file
18
app/api/v1/public/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# app/api/v1/public/__init__.py
|
||||
"""
|
||||
Public API endpoints (customer-facing).
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from .vendors import auth, products, cart, orders
|
||||
|
||||
# Create public router
|
||||
router = APIRouter()
|
||||
|
||||
# Include all public sub-routers
|
||||
router.include_router(auth.router, prefix="/vendors", tags=["public-auth"])
|
||||
router.include_router(products.router, prefix="/vendors", tags=["public-products"])
|
||||
router.include_router(cart.router, prefix="/vendors", tags=["public-cart"])
|
||||
router.include_router(orders.router, prefix="/vendors", tags=["public-orders"])
|
||||
|
||||
__all__ = ["router"]
|
||||
2
app/api/v1/public/vendors/__init__.py
vendored
Normal file
2
app/api/v1/public/vendors/__init__.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# app/api/v1/public/vendors/__init__.py
|
||||
"""Vendor-specific public API endpoints"""
|
||||
175
app/api/v1/public/vendors/auth.py
vendored
Normal file
175
app/api/v1/public/vendors/auth.py
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
# app/api/v1/public/vendors/auth.py
|
||||
"""
|
||||
Customer authentication endpoints (public-facing).
|
||||
|
||||
This module provides:
|
||||
- Customer registration (vendor-scoped)
|
||||
- Customer login (vendor-scoped)
|
||||
- Customer password reset
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.customer_service import customer_service
|
||||
from app.exceptions import VendorNotFoundException
|
||||
from models.schemas.auth import LoginResponse, UserLogin
|
||||
from models.schemas.customer import CustomerRegister, CustomerResponse
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/customers/register", response_model=CustomerResponse)
|
||||
def register_customer(
|
||||
vendor_id: int,
|
||||
customer_data: CustomerRegister,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Register a new customer for a specific vendor.
|
||||
|
||||
Customer accounts are vendor-scoped - each vendor has independent customers.
|
||||
Same email can be used for different vendors.
|
||||
"""
|
||||
# Verify vendor exists and is active
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Create customer account
|
||||
customer = customer_service.register_customer(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
customer_data=customer_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"New customer registered: {customer.email} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return CustomerResponse.model_validate(customer)
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/customers/login", response_model=LoginResponse)
|
||||
def customer_login(
|
||||
vendor_id: int,
|
||||
user_credentials: UserLogin,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Customer login for a specific vendor.
|
||||
|
||||
Authenticates customer and returns JWT token.
|
||||
Customer must belong to the specified vendor.
|
||||
"""
|
||||
# Verify vendor exists and is active
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Authenticate customer
|
||||
login_result = customer_service.login_customer(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
credentials=user_credentials
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Customer login successful: {login_result['customer'].email} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return LoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
expires_in=login_result["token_data"]["expires_in"],
|
||||
user=login_result["customer"], # Return customer as user
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/customers/logout")
|
||||
def customer_logout(vendor_id: int):
|
||||
"""
|
||||
Customer logout.
|
||||
|
||||
Client should remove token from storage.
|
||||
"""
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/customers/forgot-password")
|
||||
def forgot_password(
|
||||
vendor_id: int,
|
||||
email: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Request password reset for customer.
|
||||
|
||||
Sends password reset email to customer if account exists.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# TODO: Implement password reset logic
|
||||
# - Generate reset token
|
||||
# - Send email with reset link
|
||||
# - Store token in database
|
||||
|
||||
logger.info(f"Password reset requested for {email} in vendor {vendor.vendor_code}")
|
||||
|
||||
return {
|
||||
"message": "If an account exists, a password reset link has been sent",
|
||||
"email": email
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/customers/reset-password")
|
||||
def reset_password(
|
||||
vendor_id: int,
|
||||
token: str,
|
||||
new_password: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Reset customer password using reset token.
|
||||
|
||||
Validates token and updates password.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# TODO: Implement password reset logic
|
||||
# - Validate reset token
|
||||
# - Check token expiration
|
||||
# - Update password
|
||||
# - Invalidate token
|
||||
|
||||
logger.info(f"Password reset completed for vendor {vendor.vendor_code}")
|
||||
|
||||
return {"message": "Password reset successful"}
|
||||
164
app/api/v1/public/vendors/cart.py
vendored
Normal file
164
app/api/v1/public/vendors/cart.py
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
# app/api/v1/public/vendors/cart.py
|
||||
"""
|
||||
Shopping cart endpoints (customer-facing).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Path, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.cart_service import cart_service
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AddToCartRequest(BaseModel):
|
||||
"""Request model for adding to cart."""
|
||||
product_id: int = Field(..., description="Product ID to add")
|
||||
quantity: int = Field(1, ge=1, description="Quantity to add")
|
||||
|
||||
|
||||
class UpdateCartItemRequest(BaseModel):
|
||||
"""Request model for updating cart item."""
|
||||
quantity: int = Field(..., ge=1, description="New quantity")
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/cart/{session_id}")
|
||||
def get_cart(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
session_id: str = Path(..., description="Session ID"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get shopping cart contents.
|
||||
|
||||
No authentication required - uses session ID.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
cart = cart_service.get_cart(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
return cart
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/cart/{session_id}/items")
|
||||
def add_to_cart(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
session_id: str = Path(..., description="Session ID"),
|
||||
cart_data: AddToCartRequest = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Add product to cart.
|
||||
|
||||
No authentication required - uses session ID.
|
||||
"""
|
||||
# Verify vendor
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
result = cart_service.add_to_cart(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
session_id=session_id,
|
||||
product_id=cart_data.product_id,
|
||||
quantity=cart_data.quantity
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/{vendor_id}/cart/{session_id}/items/{product_id}")
|
||||
def update_cart_item(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
session_id: str = Path(..., description="Session ID"),
|
||||
product_id: int = Path(..., description="Product ID"),
|
||||
cart_data: UpdateCartItemRequest = Body(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update cart item quantity."""
|
||||
# Verify vendor
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
result = cart_service.update_cart_item(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
session_id=session_id,
|
||||
product_id=product_id,
|
||||
quantity=cart_data.quantity
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{vendor_id}/cart/{session_id}/items/{product_id}")
|
||||
def remove_from_cart(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
session_id: str = Path(..., description="Session ID"),
|
||||
product_id: int = Path(..., description="Product ID"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove item from cart."""
|
||||
# Verify vendor
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
result = cart_service.remove_from_cart(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
session_id=session_id,
|
||||
product_id=product_id
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{vendor_id}/cart/{session_id}")
|
||||
def clear_cart(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
session_id: str = Path(..., description="Session ID"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Clear all items from cart."""
|
||||
result = cart_service.clear_cart(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
return result
|
||||
163
app/api/v1/public/vendors/orders.py
vendored
Normal file
163
app/api/v1/public/vendors/orders.py
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
# app/api/v1/public/vendors/orders.py
|
||||
"""
|
||||
Customer order endpoints (public-facing).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.order_service import order_service
|
||||
from app.services.customer_service import customer_service
|
||||
from models.schemas.order import (
|
||||
OrderCreate,
|
||||
OrderResponse,
|
||||
OrderDetailResponse,
|
||||
OrderListResponse
|
||||
)
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.customer import Customer
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_current_customer(
|
||||
vendor_id: int,
|
||||
customer_id: int,
|
||||
db: Session
|
||||
) -> Customer:
|
||||
"""Helper to get and verify customer."""
|
||||
customer = customer_service.get_customer(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
customer_id=customer_id
|
||||
)
|
||||
return customer
|
||||
|
||||
|
||||
@router.post("/{vendor_id}/orders", response_model=OrderResponse)
|
||||
def place_order(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
order_data: OrderCreate = ...,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Place a new order.
|
||||
|
||||
Customer must be authenticated to place an order.
|
||||
This endpoint creates an order from the customer's cart.
|
||||
"""
|
||||
# Verify vendor exists and is active
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Create order
|
||||
order = order_service.create_order(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
order_data=order_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Order {order.order_number} placed for vendor {vendor.vendor_code}, "
|
||||
f"total: €{order.total_amount:.2f}"
|
||||
)
|
||||
|
||||
# TODO: Update customer stats
|
||||
# TODO: Clear cart
|
||||
# TODO: Send order confirmation email
|
||||
|
||||
return OrderResponse.model_validate(order)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/customers/{customer_id}/orders", response_model=OrderListResponse)
|
||||
def get_customer_orders(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
customer_id: int = Path(..., description="Customer ID"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get order history for customer.
|
||||
|
||||
Returns all orders placed by the authenticated customer.
|
||||
"""
|
||||
# Verify vendor
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Verify customer belongs to vendor
|
||||
customer = get_current_customer(vendor_id, customer_id, db)
|
||||
|
||||
# Get orders
|
||||
orders, total = order_service.get_customer_orders(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
customer_id=customer_id,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return OrderListResponse(
|
||||
orders=[OrderResponse.model_validate(o) for o in orders],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/customers/{customer_id}/orders/{order_id}", response_model=OrderDetailResponse)
|
||||
def get_customer_order_details(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
customer_id: int = Path(..., description="Customer ID"),
|
||||
order_id: int = Path(..., description="Order ID"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed order information for customer.
|
||||
|
||||
Customer can only view their own orders.
|
||||
"""
|
||||
# Verify vendor
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Verify customer
|
||||
customer = get_current_customer(vendor_id, customer_id, db)
|
||||
|
||||
# Get order
|
||||
order = order_service.get_order(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
order_id=order_id
|
||||
)
|
||||
|
||||
# Verify order belongs to customer
|
||||
if order.customer_id != customer_id:
|
||||
from app.exceptions import OrderNotFoundException
|
||||
raise OrderNotFoundException(str(order_id))
|
||||
|
||||
return OrderDetailResponse.model_validate(order)
|
||||
138
app/api/v1/public/vendors/products.py
vendored
Normal file
138
app/api/v1/public/vendors/products.py
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
# app/api/v1/public/vendors/products.py
|
||||
"""
|
||||
Public product catalog endpoints (customer-facing).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.product_service import product_service
|
||||
from models.schemas.product import ProductResponse, ProductDetailResponse, ProductListResponse
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/products", response_model=ProductListResponse)
|
||||
def get_public_product_catalog(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None, description="Search products by name"),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get public product catalog for a vendor.
|
||||
|
||||
Only returns active products visible to customers.
|
||||
No authentication required.
|
||||
"""
|
||||
# Verify vendor exists and is active
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# Get only active products for public view
|
||||
products, total = product_service.get_vendor_products(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
is_active=True, # Only show active products to customers
|
||||
is_featured=is_featured
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=[ProductResponse.model_validate(p) for p in products],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/products/{product_id}", response_model=ProductDetailResponse)
|
||||
def get_public_product_details(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
product_id: int = Path(..., description="Product ID"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed product information for customers.
|
||||
|
||||
No authentication required.
|
||||
"""
|
||||
# Verify vendor exists and is active
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
product = product_service.get_product(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
product_id=product_id
|
||||
)
|
||||
|
||||
# Check if product is active
|
||||
if not product.is_active:
|
||||
from app.exceptions import ProductNotActiveException
|
||||
raise ProductNotActiveException(str(product_id))
|
||||
|
||||
return ProductDetailResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.get("/{vendor_id}/products/search")
|
||||
def search_products(
|
||||
vendor_id: int = Path(..., description="Vendor ID"),
|
||||
q: str = Query(..., min_length=1, description="Search query"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Search products in vendor catalog.
|
||||
|
||||
Searches in product names, descriptions, and SKUs.
|
||||
No authentication required.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
vendor = db.query(Vendor).filter(
|
||||
Vendor.id == vendor_id,
|
||||
Vendor.is_active == True
|
||||
).first()
|
||||
|
||||
if not vendor:
|
||||
from app.exceptions import VendorNotFoundException
|
||||
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
||||
|
||||
# TODO: Implement search functionality
|
||||
# For now, return filtered products
|
||||
products, total = product_service.get_vendor_products(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=[ProductResponse.model_validate(p) for p in products],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
@@ -1,111 +0,0 @@
|
||||
# app/api/v1/stats.py
|
||||
"""
|
||||
Statistics endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Comprehensive system statistics
|
||||
- Marketplace-specific analytics
|
||||
- Performance metrics and data insights
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.services.stats_service import stats_service
|
||||
from models.schemas.stats import MarketplaceStatsResponse, StatsResponse
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=StatsResponse)
|
||||
def get_stats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get comprehensive statistics with marketplace data (Protected)."""
|
||||
stats_data = stats_service.get_comprehensive_stats(db=db)
|
||||
|
||||
return StatsResponse(
|
||||
total_products=stats_data["total_products"],
|
||||
unique_brands=stats_data["unique_brands"],
|
||||
unique_categories=stats_data["unique_categories"],
|
||||
unique_marketplaces=stats_data["unique_marketplaces"],
|
||||
unique_vendors=stats_data["unique_vendors"],
|
||||
total_stock_entries=stats_data["total_stock_entries"],
|
||||
total_inventory_quantity=stats_data["total_inventory_quantity"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse])
|
||||
def get_marketplace_stats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get statistics broken down by marketplace (Protected)."""
|
||||
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)
|
||||
|
||||
# app/api/v1/stats.py
|
||||
"""
|
||||
Statistics endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Comprehensive system statistics
|
||||
- Marketplace-specific analytics
|
||||
- Performance metrics and data insights
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.services.stats_service import stats_service
|
||||
from models.schemas.stats import MarketplaceStatsResponse, StatsResponse
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=StatsResponse)
|
||||
def get_stats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get comprehensive statistics with marketplace data (Protected)."""
|
||||
stats_data = stats_service.get_comprehensive_stats(db=db)
|
||||
|
||||
return StatsResponse(
|
||||
total_products=stats_data["total_products"],
|
||||
unique_brands=stats_data["unique_brands"],
|
||||
unique_categories=stats_data["unique_categories"],
|
||||
unique_marketplaces=stats_data["unique_marketplaces"],
|
||||
unique_vendors=stats_data["unique_vendors"],
|
||||
total_stock_entries=stats_data["total_stock_entries"],
|
||||
total_inventory_quantity=stats_data["total_inventory_quantity"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats/marketplace", response_model=List[MarketplaceStatsResponse])
|
||||
def get_marketplace_stats(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get statistics broken down by marketplace (Protected)."""
|
||||
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)
|
||||
|
||||
return [
|
||||
MarketplaceStatsResponse(
|
||||
marketplace=stat["marketplace"],
|
||||
total_products=stat["total_products"],
|
||||
unique_vendors=stat["unique_vendors"],
|
||||
unique_brands=stat["unique_brands"],
|
||||
)
|
||||
for stat in marketplace_stats
|
||||
]
|
||||
@@ -1,112 +0,0 @@
|
||||
# app/api/v1/stock.py
|
||||
"""
|
||||
Stock endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Stock quantity management (set, add, remove)
|
||||
- Stock information retrieval and filtering
|
||||
- Location-based stock tracking
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.services.stock_service import stock_service
|
||||
from models.schemas.stock import (StockAdd, StockCreate, StockResponse,
|
||||
StockSummaryResponse, StockUpdate)
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/stock", response_model=StockResponse)
|
||||
def set_stock(
|
||||
stock: StockCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Set exact stock quantity for a GTIN at a specific location (replaces existing quantity)."""
|
||||
return stock_service.set_stock(db, stock)
|
||||
|
||||
|
||||
@router.post("/stock/add", response_model=StockResponse)
|
||||
def add_stock(
|
||||
stock: StockAdd,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Add quantity to existing stock for a GTIN at a specific location (adds to existing quantity)."""
|
||||
return stock_service.add_stock(db, stock)
|
||||
|
||||
|
||||
@router.post("/stock/remove", response_model=StockResponse)
|
||||
def remove_stock(
|
||||
stock: StockAdd,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Remove quantity from existing stock for a GTIN at a specific location."""
|
||||
return stock_service.remove_stock(db, stock)
|
||||
|
||||
|
||||
@router.get("/stock/{gtin}", response_model=StockSummaryResponse)
|
||||
def get_stock_by_gtin(
|
||||
gtin: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get all stock locations and total quantity for a specific GTIN."""
|
||||
return stock_service.get_stock_by_gtin(db, gtin)
|
||||
|
||||
|
||||
@router.get("/stock/{gtin}/total")
|
||||
def get_total_stock(
|
||||
gtin: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get total quantity in stock for a specific GTIN."""
|
||||
return stock_service.get_total_stock(db, gtin)
|
||||
|
||||
|
||||
@router.get("/stock", response_model=List[StockResponse])
|
||||
def get_all_stock(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
location: Optional[str] = Query(None, description="Filter by location"),
|
||||
gtin: Optional[str] = Query(None, description="Filter by GTIN"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get all stock entries with optional filtering."""
|
||||
return stock_service.get_all_stock(
|
||||
db=db, skip=skip, limit=limit, location=location, gtin=gtin
|
||||
)
|
||||
|
||||
|
||||
@router.put("/stock/{stock_id}", response_model=StockResponse)
|
||||
def update_stock(
|
||||
stock_id: int,
|
||||
stock_update: StockUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update stock quantity for a specific stock entry."""
|
||||
return stock_service.update_stock(db, stock_id, stock_update)
|
||||
|
||||
|
||||
@router.delete("/stock/{stock_id}")
|
||||
def delete_stock(
|
||||
stock_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a stock entry."""
|
||||
stock_service.delete_stock(db, stock_id)
|
||||
return {"message": "Stock entry deleted successfully"}
|
||||
@@ -1,137 +0,0 @@
|
||||
# app/api/v1/vendor.py
|
||||
"""
|
||||
Vendor endpoints - simplified with service-level exception handling.
|
||||
|
||||
This module provides classes and functions for:
|
||||
- Vendor CRUD operations and management
|
||||
- Vendor product catalog management
|
||||
- Vendor filtering and verification
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user, get_user_vendor
|
||||
from app.core.database import get_db
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.schemas.vendor import (VendorCreate, VendorListResponse, VendorResponse)
|
||||
from models.schemas.product import (ProductCreate,ProductResponse)
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/vendor", response_model=VendorResponse)
|
||||
def create_vendor(
|
||||
vendor_data: VendorCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new vendor (Protected)."""
|
||||
vendor = vendor_service.create_vendor(
|
||||
db=db, vendor_data=vendor_data, current_user=current_user
|
||||
)
|
||||
return VendorResponse.model_validate(vendor)
|
||||
|
||||
|
||||
@router.get("/vendor", response_model=VendorListResponse)
|
||||
def get_vendors(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
active_only: bool = Query(True),
|
||||
verified_only: bool = Query(False),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get vendors with filtering (Protected)."""
|
||||
vendors, total = vendor_service.get_vendors(
|
||||
db=db,
|
||||
current_user=current_user,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
active_only=active_only,
|
||||
verified_only=verified_only,
|
||||
)
|
||||
|
||||
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}", response_model=VendorResponse)
|
||||
def get_vendor(
|
||||
vendor_code: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get vendor details (Protected)."""
|
||||
vendor = vendor_service.get_vendor_by_code(
|
||||
db=db, vendor_code=vendor_code, current_user=current_user
|
||||
)
|
||||
return VendorResponse.model_validate(vendor)
|
||||
|
||||
|
||||
@router.post("/vendor/{vendor_code}/products", response_model=ProductResponse)
|
||||
def add_product_to_catalog(
|
||||
vendor_code: str,
|
||||
product: ProductCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Add existing product to vendor catalog with vendor -specific settings (Protected)."""
|
||||
# Get and verify vendor (using existing dependency)
|
||||
vendor = get_user_vendor(vendor_code, current_user, db)
|
||||
|
||||
# Add product to vendor
|
||||
new_product = vendor_service.add_product_to_catalog(
|
||||
db=db, vendor=vendor, product=product
|
||||
)
|
||||
|
||||
# Return with product details
|
||||
response = ProductResponse.model_validate(new_product)
|
||||
response.marketplace_product = new_product.marketplace_product
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/vendor/{vendor_code}/products")
|
||||
def get_products(
|
||||
vendor_code: str,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
active_only: bool = Query(True),
|
||||
featured_only: bool = Query(False),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get products in vendor catalog (Protected)."""
|
||||
# Get vendor
|
||||
vendor = vendor_service.get_vendor_by_code(
|
||||
db=db, vendor_code=vendor_code, current_user=current_user
|
||||
)
|
||||
|
||||
# Get vendor products
|
||||
vendor_products, total = vendor_service.get_products(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
current_user=current_user,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
active_only=active_only,
|
||||
featured_only=featured_only,
|
||||
)
|
||||
|
||||
# Format response
|
||||
products = []
|
||||
for vp in vendor_products:
|
||||
product_response = ProductResponse.model_validate(vp)
|
||||
product_response.marketplace_product = vp.marketplace_product
|
||||
products.append(product_response)
|
||||
|
||||
return {
|
||||
"products": products,
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"vendor": VendorResponse.model_validate(vendor),
|
||||
}
|
||||
21
app/api/v1/vendor/__init__.py
vendored
Normal file
21
app/api/v1/vendor/__init__.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# app/api/v1/vendor/__init__.py
|
||||
"""
|
||||
Vendor API endpoints.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import auth, dashboard, products, orders, marketplace, inventory, vendor
|
||||
|
||||
# Create vendor router
|
||||
router = APIRouter()
|
||||
|
||||
# Include all vendor sub-routers
|
||||
router.include_router(auth.router, tags=["vendor-auth"])
|
||||
router.include_router(dashboard.router, tags=["vendor-dashboard"])
|
||||
router.include_router(products.router, tags=["vendor-products"])
|
||||
router.include_router(orders.router, tags=["vendor-orders"])
|
||||
router.include_router(marketplace.router, tags=["vendor-marketplace"])
|
||||
router.include_router(inventory.router, tags=["vendor-inventory"])
|
||||
router.include_router(vendor.router, tags=["vendor-management"])
|
||||
|
||||
__all__ = ["router"]
|
||||
83
app/api/v1/vendor/auth.py
vendored
Normal file
83
app/api/v1/vendor/auth.py
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
# app/api/v1/vendor/auth.py
|
||||
"""
|
||||
Vendor team authentication endpoints.
|
||||
|
||||
This module provides:
|
||||
- Vendor team member login
|
||||
- Vendor owner login
|
||||
- Vendor-scoped authentication
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.auth_service import auth_service
|
||||
from app.exceptions import InvalidCredentialsException
|
||||
from middleware.vendor_context import get_current_vendor
|
||||
from models.schemas.auth import LoginResponse, UserLogin
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
def vendor_login(
|
||||
user_credentials: UserLogin,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Vendor team member login.
|
||||
|
||||
Authenticates users who are part of a vendor team.
|
||||
Validates against vendor context if available.
|
||||
"""
|
||||
# Authenticate user
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
user = login_result["user"]
|
||||
|
||||
# Prevent admin users from using vendor login
|
||||
if user.role == "admin":
|
||||
logger.warning(f"Admin user attempted vendor login: {user.username}")
|
||||
raise InvalidCredentialsException("Please use admin portal to login")
|
||||
|
||||
# Optional: Validate user belongs to current vendor context
|
||||
vendor = get_current_vendor(request)
|
||||
if vendor:
|
||||
# Check if user is vendor owner or team member
|
||||
is_owner = any(v.id == vendor.id for v in user.owned_vendors)
|
||||
is_team_member = any(
|
||||
vm.vendor_id == vendor.id and vm.is_active
|
||||
for vm in user.vendor_memberships
|
||||
)
|
||||
|
||||
if not (is_owner or is_team_member):
|
||||
logger.warning(
|
||||
f"User {user.username} attempted login to vendor {vendor.vendor_code} "
|
||||
f"but is not authorized"
|
||||
)
|
||||
raise InvalidCredentialsException(
|
||||
"You do not have access to this vendor"
|
||||
)
|
||||
|
||||
logger.info(f"Vendor team login successful: {user.username}")
|
||||
|
||||
return LoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
expires_in=login_result["token_data"]["expires_in"],
|
||||
user=login_result["user"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def vendor_logout():
|
||||
"""
|
||||
Vendor team member logout.
|
||||
|
||||
Client should remove token from storage.
|
||||
"""
|
||||
return {"message": "Logged out successfully"}
|
||||
62
app/api/v1/vendor/dashboard.py
vendored
Normal file
62
app/api/v1/vendor/dashboard.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# app/api/v1/vendor/dashboard.py
|
||||
"""
|
||||
Vendor dashboard and statistics endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.stats_service import stats_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/dashboard")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def get_vendor_dashboard_stats(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor-specific dashboard statistics.
|
||||
|
||||
Returns statistics for the current vendor only:
|
||||
- Total products in catalog
|
||||
- Total orders
|
||||
- Total customers
|
||||
- Revenue metrics
|
||||
"""
|
||||
# Get vendor-scoped statistics
|
||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor.id)
|
||||
|
||||
return {
|
||||
"vendor": {
|
||||
"id": vendor.id,
|
||||
"name": vendor.name,
|
||||
"vendor_code": vendor.vendor_code,
|
||||
},
|
||||
"products": {
|
||||
"total": stats_data.get("total_products", 0),
|
||||
"active": stats_data.get("active_products", 0),
|
||||
},
|
||||
"orders": {
|
||||
"total": stats_data.get("total_orders", 0),
|
||||
"pending": stats_data.get("pending_orders", 0),
|
||||
"completed": stats_data.get("completed_orders", 0),
|
||||
},
|
||||
"customers": {
|
||||
"total": stats_data.get("total_customers", 0),
|
||||
"active": stats_data.get("active_customers", 0),
|
||||
},
|
||||
"revenue": {
|
||||
"total": stats_data.get("total_revenue", 0),
|
||||
"this_month": stats_data.get("revenue_this_month", 0),
|
||||
}
|
||||
}
|
||||
141
app/api/v1/vendor/inventory.py
vendored
Normal file
141
app/api/v1/vendor/inventory.py
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
# app/api/v1/vendor/inventory.py
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.inventory_service import inventory_service
|
||||
from models.schemas.inventory import (
|
||||
InventoryCreate,
|
||||
InventoryAdjust,
|
||||
InventoryUpdate,
|
||||
InventoryReserve,
|
||||
InventoryResponse,
|
||||
ProductInventorySummary,
|
||||
InventoryListResponse
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/inventory/set", response_model=InventoryResponse)
|
||||
def set_inventory(
|
||||
inventory: InventoryCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
return inventory_service.set_inventory(db, vendor.id, inventory)
|
||||
|
||||
|
||||
@router.post("/inventory/adjust", response_model=InventoryResponse)
|
||||
def adjust_inventory(
|
||||
adjustment: InventoryAdjust,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Adjust inventory (positive to add, negative to remove)."""
|
||||
return inventory_service.adjust_inventory(db, vendor.id, adjustment)
|
||||
|
||||
|
||||
@router.post("/inventory/reserve", response_model=InventoryResponse)
|
||||
def reserve_inventory(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Reserve inventory for an order."""
|
||||
return inventory_service.reserve_inventory(db, vendor.id, reservation)
|
||||
|
||||
|
||||
@router.post("/inventory/release", response_model=InventoryResponse)
|
||||
def release_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Release reserved inventory (cancel order)."""
|
||||
return inventory_service.release_reservation(db, vendor.id, reservation)
|
||||
|
||||
|
||||
@router.post("/inventory/fulfill", response_model=InventoryResponse)
|
||||
def fulfill_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Fulfill reservation (complete order, remove from stock)."""
|
||||
return inventory_service.fulfill_reservation(db, vendor.id, reservation)
|
||||
|
||||
|
||||
@router.get("/inventory/product/{product_id}", response_model=ProductInventorySummary)
|
||||
def get_product_inventory(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get inventory summary for a product."""
|
||||
return inventory_service.get_product_inventory(db, vendor.id, product_id)
|
||||
|
||||
|
||||
@router.get("/inventory", response_model=InventoryListResponse)
|
||||
def get_vendor_inventory(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
location: Optional[str] = Query(None),
|
||||
low_stock: Optional[int] = Query(None, ge=0),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all inventory for vendor."""
|
||||
inventories = inventory_service.get_vendor_inventory(
|
||||
db, vendor.id, skip, limit, location, low_stock
|
||||
)
|
||||
|
||||
# Get total count
|
||||
total = len(inventories) # You might want a separate count query for large datasets
|
||||
|
||||
return InventoryListResponse(
|
||||
inventories=inventories,
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.put("/inventory/{inventory_id}", response_model=InventoryResponse)
|
||||
def update_inventory(
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update inventory entry."""
|
||||
return inventory_service.update_inventory(db, vendor.id, inventory_id, inventory_update)
|
||||
|
||||
|
||||
@router.delete("/inventory/{inventory_id}")
|
||||
def delete_inventory(
|
||||
inventory_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete inventory entry."""
|
||||
inventory_service.delete_inventory(db, vendor.id, inventory_id)
|
||||
return {"message": "Inventory deleted successfully"}
|
||||
115
app/api/v1/vendor/marketplace.py
vendored
Normal file
115
app/api/v1/vendor/marketplace.py
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
# app/api/v1/vendor/marketplace.py # Note: Should be under /vendor/ route
|
||||
"""
|
||||
Marketplace import endpoints for vendors.
|
||||
Vendor context is automatically injected by middleware.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context # IMPORTANT
|
||||
from app.services.marketplace_import_job_service import marketplace_import_job_service
|
||||
from app.tasks.background_tasks import process_marketplace_import
|
||||
from middleware.decorators import rate_limit
|
||||
from models.schemas.marketplace_import_job import (
|
||||
MarketplaceImportJobResponse,
|
||||
MarketplaceImportJobRequest
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/import", response_model=MarketplaceImportJobResponse)
|
||||
@rate_limit(max_requests=10, window_seconds=3600)
|
||||
async def import_products_from_marketplace(
|
||||
request: MarketplaceImportJobRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ADDED: Vendor from middleware
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Import products from marketplace CSV with background processing (Protected)."""
|
||||
logger.info(
|
||||
f"Starting marketplace import: {request.marketplace} for vendor {vendor.vendor_code} "
|
||||
f"by user {current_user.username}"
|
||||
)
|
||||
|
||||
# Create import job (vendor comes from middleware)
|
||||
import_job = marketplace_import_job_service.create_import_job(
|
||||
db, request, vendor, current_user
|
||||
)
|
||||
|
||||
# Process in background
|
||||
background_tasks.add_task(
|
||||
process_marketplace_import,
|
||||
import_job.id,
|
||||
request.source_url, # FIXED: was request.url
|
||||
request.marketplace,
|
||||
vendor.id, # Pass vendor_id instead of vendor_code
|
||||
request.batch_size or 1000,
|
||||
)
|
||||
|
||||
return MarketplaceImportJobResponse(
|
||||
job_id=import_job.id,
|
||||
status="pending",
|
||||
marketplace=request.marketplace,
|
||||
vendor_id=import_job.vendor_id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
vendor_name=vendor.name, # FIXED: from vendor object
|
||||
source_url=request.source_url,
|
||||
message=f"Marketplace import started from {request.marketplace}. "
|
||||
f"Check status with /import-status/{import_job.id}",
|
||||
imported=0,
|
||||
updated=0,
|
||||
total_processed=0,
|
||||
error_count=0,
|
||||
created_at=import_job.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/imports/{job_id}", response_model=MarketplaceImportJobResponse)
|
||||
def get_marketplace_import_status(
|
||||
job_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get status of marketplace import job (Protected)."""
|
||||
job = marketplace_import_job_service.get_import_job_by_id(db, job_id, current_user)
|
||||
|
||||
# Verify job belongs to current vendor
|
||||
if job.vendor_id != vendor.id:
|
||||
from app.exceptions import UnauthorizedVendorAccessException
|
||||
raise UnauthorizedVendorAccessException(vendor.vendor_code, current_user.id)
|
||||
|
||||
return marketplace_import_job_service.convert_to_response_model(job)
|
||||
|
||||
|
||||
@router.get("/imports", response_model=List[MarketplaceImportJobResponse])
|
||||
def get_marketplace_import_jobs(
|
||||
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get marketplace import jobs for current vendor (Protected)."""
|
||||
jobs = marketplace_import_job_service.get_import_jobs(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
user=current_user,
|
||||
marketplace=marketplace,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return [marketplace_import_job_service.convert_to_response_model(job) for job in jobs]
|
||||
111
app/api/v1/vendor/orders.py
vendored
Normal file
111
app/api/v1/vendor/orders.py
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
# app/api/v1/vendor/orders.py
|
||||
"""
|
||||
Vendor order management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.order_service import order_service
|
||||
from models.schemas.order import (
|
||||
OrderResponse,
|
||||
OrderDetailResponse,
|
||||
OrderListResponse,
|
||||
OrderUpdate
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/orders")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("", response_model=OrderListResponse)
|
||||
def get_vendor_orders(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
status: Optional[str] = Query(None, description="Filter by order status"),
|
||||
customer_id: Optional[int] = Query(None, description="Filter by customer"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all orders for vendor.
|
||||
|
||||
Supports filtering by:
|
||||
- status: Order status (pending, processing, shipped, delivered, cancelled)
|
||||
- customer_id: Filter orders from specific customer
|
||||
"""
|
||||
orders, total = order_service.get_vendor_orders(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
status=status,
|
||||
customer_id=customer_id
|
||||
)
|
||||
|
||||
return OrderListResponse(
|
||||
orders=[OrderResponse.model_validate(o) for o in orders],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{order_id}", response_model=OrderDetailResponse)
|
||||
def get_order_details(
|
||||
order_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get detailed order information including items and addresses."""
|
||||
order = order_service.get_order(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
order_id=order_id
|
||||
)
|
||||
|
||||
return OrderDetailResponse.model_validate(order)
|
||||
|
||||
|
||||
@router.put("/{order_id}/status", response_model=OrderResponse)
|
||||
def update_order_status(
|
||||
order_id: int,
|
||||
order_update: OrderUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update order status and tracking information.
|
||||
|
||||
Valid statuses:
|
||||
- pending: Order placed, awaiting processing
|
||||
- processing: Order being prepared
|
||||
- shipped: Order shipped to customer
|
||||
- delivered: Order delivered
|
||||
- cancelled: Order cancelled
|
||||
- refunded: Order refunded
|
||||
"""
|
||||
order = order_service.update_order_status(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
order_id=order_id,
|
||||
order_update=order_update
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Order {order.order_number} status updated to {order.status} "
|
||||
f"by user {current_user.username}"
|
||||
)
|
||||
|
||||
return OrderResponse.model_validate(order)
|
||||
227
app/api/v1/vendor/products.py
vendored
Normal file
227
app/api/v1/vendor/products.py
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
# app/api/v1/vendor/products.py
|
||||
"""
|
||||
Vendor product catalog management endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.product_service import product_service
|
||||
from models.schemas.product import (
|
||||
ProductCreate,
|
||||
ProductUpdate,
|
||||
ProductResponse,
|
||||
ProductDetailResponse,
|
||||
ProductListResponse
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/products")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("", response_model=ProductListResponse)
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all products in vendor catalog.
|
||||
|
||||
Supports filtering by:
|
||||
- is_active: Filter active/inactive products
|
||||
- is_featured: Filter featured products
|
||||
"""
|
||||
products, total = product_service.get_vendor_products(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
is_active=is_active,
|
||||
is_featured=is_featured
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=[ProductResponse.model_validate(p) for p in products],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=ProductDetailResponse)
|
||||
def get_product_details(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get detailed product information including inventory."""
|
||||
product = product_service.get_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_id=product_id
|
||||
)
|
||||
|
||||
return ProductDetailResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.post("", response_model=ProductResponse)
|
||||
def add_product_to_catalog(
|
||||
product_data: ProductCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Add a product from marketplace to vendor catalog.
|
||||
|
||||
This publishes a MarketplaceProduct to the vendor's public catalog.
|
||||
"""
|
||||
product = product_service.create_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_data=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product.id} added to catalog by user {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
product = product_service.update_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_id=product_id,
|
||||
product_update=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product_id} updated by user {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
product_service.delete_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_id=product_id
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product_id} removed from catalog by user {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return {"message": f"Product {product_id} removed from catalog"}
|
||||
|
||||
|
||||
@router.post("/from-import/{marketplace_product_id}", response_model=ProductResponse)
|
||||
def publish_from_marketplace(
|
||||
marketplace_product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Publish a marketplace product to vendor catalog.
|
||||
|
||||
Shortcut endpoint for publishing directly from marketplace import.
|
||||
"""
|
||||
product_data = ProductCreate(
|
||||
marketplace_product_id=marketplace_product_id,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
product = product_service.create_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
product_data=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Marketplace product {marketplace_product_id} published to catalog "
|
||||
f"by user {current_user.username} for vendor {vendor.vendor_code}"
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.put("/{product_id}/toggle-active")
|
||||
def toggle_product_active(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product active status."""
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
|
||||
product.is_active = not product.is_active
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
status = "activated" if product.is_active else "deactivated"
|
||||
logger.info(f"Product {product_id} {status} for vendor {vendor.vendor_code}")
|
||||
|
||||
return {
|
||||
"message": f"Product {status}",
|
||||
"is_active": product.is_active
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{product_id}/toggle-featured")
|
||||
def toggle_product_featured(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product featured status."""
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
|
||||
product.is_featured = not product.is_featured
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
status = "featured" if product.is_featured else "unfeatured"
|
||||
logger.info(f"Product {product_id} {status} for vendor {vendor.vendor_code}")
|
||||
|
||||
return {
|
||||
"message": f"Product {status}",
|
||||
"is_featured": product.is_featured
|
||||
}
|
||||
330
app/api/v1/vendor/vendor.py
vendored
Normal file
330
app/api/v1/vendor/vendor.py
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
# app/api/v1/vendor/vendor.py
|
||||
"""
|
||||
Vendor management endpoints for vendor-scoped operations.
|
||||
|
||||
This module provides:
|
||||
- Vendor profile management
|
||||
- Vendor settings configuration
|
||||
- Vendor team member management
|
||||
- Vendor dashboard statistics
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.services.team_service import team_service
|
||||
from models.schemas.vendor import VendorUpdate, VendorResponse
|
||||
from models.schemas.product import ProductResponse, ProductListResponse
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR PROFILE ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/profile", response_model=VendorResponse)
|
||||
def get_vendor_profile(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current vendor profile information."""
|
||||
return vendor
|
||||
|
||||
|
||||
@router.put("/profile", response_model=VendorResponse)
|
||||
def update_vendor_profile(
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor profile information."""
|
||||
# Verify user has permission to update vendor
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
return vendor_service.update_vendor(db, vendor.id, vendor_update)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR SETTINGS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/settings")
|
||||
def get_vendor_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor settings and configuration."""
|
||||
return {
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
"name": vendor.name,
|
||||
"contact_email": vendor.contact_email,
|
||||
"contact_phone": vendor.contact_phone,
|
||||
"website": vendor.website,
|
||||
"business_address": vendor.business_address,
|
||||
"tax_number": vendor.tax_number,
|
||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||
"theme_config": vendor.theme_config,
|
||||
"is_active": vendor.is_active,
|
||||
"is_verified": vendor.is_verified,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/settings/marketplace")
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
# Verify permissions
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# Update Letzshop URLs
|
||||
if "letzshop_csv_url_fr" in marketplace_config:
|
||||
vendor.letzshop_csv_url_fr = marketplace_config["letzshop_csv_url_fr"]
|
||||
if "letzshop_csv_url_en" in marketplace_config:
|
||||
vendor.letzshop_csv_url_en = marketplace_config["letzshop_csv_url_en"]
|
||||
if "letzshop_csv_url_de" in marketplace_config:
|
||||
vendor.letzshop_csv_url_de = marketplace_config["letzshop_csv_url_de"]
|
||||
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return {
|
||||
"message": "Marketplace settings updated successfully",
|
||||
"letzshop_csv_url_fr": vendor.letzshop_csv_url_fr,
|
||||
"letzshop_csv_url_en": vendor.letzshop_csv_url_en,
|
||||
"letzshop_csv_url_de": vendor.letzshop_csv_url_de,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/settings/theme")
|
||||
def update_theme_settings(
|
||||
theme_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor theme configuration."""
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
vendor.theme_config = theme_config
|
||||
db.commit()
|
||||
db.refresh(vendor)
|
||||
|
||||
return {
|
||||
"message": "Theme settings updated successfully",
|
||||
"theme_config": vendor.theme_config,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR CATALOG ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/products", response_model=ProductListResponse)
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all products in vendor catalog."""
|
||||
products, total = vendor_service.get_products(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
current_user=current_user,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
active_only=is_active,
|
||||
featured_only=is_featured,
|
||||
)
|
||||
|
||||
return ProductListResponse(
|
||||
products=products,
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
|
||||
@router.post("/products", response_model=ProductResponse)
|
||||
def add_product_to_catalog(
|
||||
product_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Add a product from marketplace to vendor catalog."""
|
||||
from models.schemas.product import ProductCreate
|
||||
|
||||
product_create = ProductCreate(**product_data)
|
||||
return vendor_service.add_product_to_catalog(db, vendor, product_create)
|
||||
|
||||
|
||||
@router.get("/products/{product_id}", response_model=ProductResponse)
|
||||
def get_vendor_product(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get a specific product from vendor catalog."""
|
||||
from app.services.product_service import product_service
|
||||
return product_service.get_product(db, vendor.id, product_id)
|
||||
|
||||
|
||||
@router.put("/products/{product_id}", response_model=ProductResponse)
|
||||
def update_vendor_product(
|
||||
product_id: int,
|
||||
product_update: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
from app.services.product_service import product_service
|
||||
from models.schemas.product import ProductUpdate
|
||||
|
||||
product_update_schema = ProductUpdate(**product_update)
|
||||
return product_service.update_product(db, vendor.id, product_id, product_update_schema)
|
||||
|
||||
|
||||
@router.delete("/products/{product_id}")
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
from app.services.product_service import product_service
|
||||
product_service.delete_product(db, vendor.id, product_id)
|
||||
return {"message": "Product removed from catalog successfully"}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR TEAM ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/team/members")
|
||||
def get_team_members(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all team members for vendor."""
|
||||
return team_service.get_team_members(db, vendor.id, current_user)
|
||||
|
||||
|
||||
@router.post("/team/invite")
|
||||
def invite_team_member(
|
||||
invitation_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Invite a new team member."""
|
||||
return team_service.invite_team_member(db, vendor.id, invitation_data, current_user)
|
||||
|
||||
|
||||
@router.put("/team/members/{user_id}")
|
||||
def update_team_member(
|
||||
user_id: int,
|
||||
update_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update team member role or status."""
|
||||
return team_service.update_team_member(db, vendor.id, user_id, update_data, current_user)
|
||||
|
||||
|
||||
@router.delete("/team/members/{user_id}")
|
||||
def remove_team_member(
|
||||
user_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove team member from vendor."""
|
||||
team_service.remove_team_member(db, vendor.id, user_id, current_user)
|
||||
return {"message": "Team member removed successfully"}
|
||||
|
||||
|
||||
@router.get("/team/roles")
|
||||
def get_team_roles(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get available roles for vendor team."""
|
||||
return team_service.get_vendor_roles(db, vendor.id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR DASHBOARD & STATISTICS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/dashboard")
|
||||
def get_vendor_dashboard(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor dashboard statistics."""
|
||||
from app.services.stats_service import stats_service
|
||||
|
||||
return {
|
||||
"vendor": {
|
||||
"code": vendor.vendor_code,
|
||||
"name": vendor.name,
|
||||
"subdomain": vendor.subdomain,
|
||||
"is_verified": vendor.is_verified,
|
||||
},
|
||||
"stats": stats_service.get_vendor_stats(db, vendor.id),
|
||||
"recent_imports": [], # TODO: Implement
|
||||
"recent_orders": [], # TODO: Implement
|
||||
"low_stock_products": [], # TODO: Implement
|
||||
}
|
||||
|
||||
|
||||
@router.get("/analytics")
|
||||
def get_vendor_analytics(
|
||||
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics data."""
|
||||
from app.services.stats_service import stats_service
|
||||
|
||||
return stats_service.get_vendor_analytics(db, vendor.id, period)
|
||||
Reference in New Issue
Block a user