major refactoring adding vendor and customer features

This commit is contained in:
2025-10-11 09:09:25 +02:00
parent f569995883
commit dd16198276
126 changed files with 15109 additions and 3747 deletions

View File

@@ -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

View File

@@ -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"]
)

View File

@@ -0,0 +1,8 @@
# app/api/v1/__init__.py
"""
API Version 1 - All endpoints
"""
from . import admin, vendor, public
__all__ = ["admin", "vendor", "public"]

View File

@@ -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)

View 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
View 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"}

View 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),
}

View 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
View 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
View 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)

View File

@@ -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)

View File

@@ -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"}

View 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
View 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
View 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
View 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
View 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
View 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
)

View File

@@ -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
]

View File

@@ -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"}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)