major refactoring adding vendor and customer features
This commit is contained in:
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